-
Notifications
You must be signed in to change notification settings - Fork 18
[SBA-06] Create initial UnifiedCredentialStore with password auth #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: auth-05-repos
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a strategy-based authentication architecture by implementing the UnifiedCredentialStore with support for password authentication. The changes lay the foundation for future passkey and session authentication features while maintaining backward compatibility with the existing LncCredentialStore.
Key Changes:
- Implements strategy pattern for authentication with
AuthStrategyinterface andPasswordStrategyimplementation - Adds
CredentialOrchestratorto manage credential store lifecycle and bridge between LNC and stores - Introduces
CredentialCachefor efficient in-memory credential management during sessions
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/stores/authStrategy.ts | Defines the AuthStrategy interface for pluggable authentication methods |
| lib/stores/credentialCache.ts | Implements in-memory credential caching with comprehensive helper methods |
| lib/stores/credentialCache.test.ts | Comprehensive test coverage for credential cache functionality |
| lib/stores/passwordStrategy.ts | Implements password-based authentication strategy using encryption service and repository |
| lib/stores/passwordStrategy.test.ts | Test coverage for password strategy implementation |
| lib/stores/strategyManager.ts | Manages registration, lookup, and coordination of authentication strategies |
| lib/stores/strategyManager.test.ts | Test coverage for strategy manager functionality |
| lib/stores/unifiedCredentialStore.ts | Main unified credential store implementing strategy-based authentication |
| lib/stores/unifiedCredentialStore.test.ts | Comprehensive test suite for unified credential store |
| lib/credentialOrchestrator.ts | Orchestrates credential management between LNC and credential stores |
| lib/credentialOrchestrator.test.ts | Test coverage for orchestrator including legacy and unified store paths |
| lib/lnc.ts | Updates LNC class to use orchestrator and adds new authentication methods |
| lib/lnc.test.ts | Adds integration tests for orchestrator and new authentication methods |
| lib/types/lnc.ts | Adds useUnifiedStore config flag and documents temporary nature |
| lib/index.ts | Exports new types and orchestrator for public API |
| demos/passkeys-demo/src/hooks/useLNC.ts | Updates demo to use new unified store authentication methods |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b3aedad to
29a1465
Compare
98a5f6e to
7246001
Compare
|
@jamaljsr, remember to re-request review from reviewers when ready |
jbrill
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Starting to dive into this PR, and the architecture is becoming clearer, but I'm concerned we might be over-abstracting.
Some points made by Claude:
The isUnlocked problem - this appears in 4 different interfaces, with each layer mostly delegating to the one below:
EncryptionService.isUnlocked // actual logic
CredentialRepository.isUnlocked // delegates to encryption
AuthStrategy.isUnlocked // delegates to repository
UnifiedCredentialStore._isUnlocked // duplicates state!The unlock() call stack - 6 layers deep:
LNC → Orchestrator → UnifiedCredentialStore → Strategy → Repository → EncryptionService
I'd love to hop on a call to understand the reasoning behind each layer. A few specific questions:
-
Why separate
EncryptionServicefromCredentialRepository? The encryption service'shasStoredData()always returnsfalsebecause "storage is at the repository layer" - if they're this tightly coupled, should they be one class? -
Do we need
StrategyManagernow? With only password auth,UnifiedCredentialStorecould hold the strategy directly and we add the manager when passkey/session land. -
Why does
UnifiedCredentialStoretrack_isUnlockedseparately instead of delegating tostrategy.isUnlocked?
Possible simplification (2 interfaces + 3 classes instead of 4 + 6):
interface CredentialStore { /* existing */ }
interface AuthStrategy {
readonly method: UnlockMethod;
isUnlocked: boolean;
hasStoredCredentials: boolean;
unlock(options: UnlockOptions): Promise<boolean>;
getCredential(key: string): Promise<string | undefined>;
setCredential(key: string, value: string): Promise<void>;
clear(): void;
}
// Single class handles encryption + localStorage
class PasswordStrategy implements AuthStrategy { }
// Uses strategy directly, no manager
class UnifiedCredentialStore implements CredentialStore {
private strategy: AuthStrategy;
}I may be missing context on why the extra layers are needed - happy to discuss!
| try { | ||
| // Persist all cached credentials to the active strategy | ||
| for (const key of [ | ||
| 'localKey', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are re-used/duplicated in loadCredentialsToCache. I think a better place might be a const declared at the top of the file?
| '[UnifiedCredentialStore] Failed to persist credentials:', | ||
| error | ||
| ); | ||
| throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we definitely want to throw here?
| /** | ||
| * Get supported unlock methods | ||
| */ | ||
| getSupportedUnlockMethods(): UnlockMethod[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit get supportedUnlockMethods
|
|
||
| /** | ||
| * Get authentication information | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: getter
| } | ||
|
|
||
| // | ||
| // Enhanced authentication methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead I think this comment could be modified from enhanced to something a bit clearer for future reviewers(?) just a thought
| } | ||
|
|
||
| get remoteKey(): string { | ||
| return this.credentialCache.get('remoteKey') || ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to return | undefined instead of blank strings in these methods?
| return undefined; | ||
| } | ||
|
|
||
| set password(_value: string | undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems weird to me. Do we need to implement all methods in the interface?
| /** | ||
| * Get all supported unlock methods | ||
| */ | ||
| getSupportedMethods(): UnlockMethod[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: getter
Summary
This PR introduces the
UnifiedCredentialStorewith a strategy-based authentication architecture, laying the foundation for upcoming passkey and session features. The key goal is to move away from the monolithic credential handling inLncCredentialStoretoward a pluggable system where different authentication methods (password, passkey, session) can be added without modifying core code.Why a strategy pattern? As we add passkey and session-based auth, we need a clean way to support multiple unlock methods. The strategy pattern lets each auth method encapsulate its own encryption/decryption logic while the
UnifiedCredentialStoreprovides a consistent interface to the rest of the library.Why
CredentialOrchestrator? The orchestrator serves as the bridge betweenLNCand the credential stores. It decides which store to use based on configuration and provides high-level methods (unlock,persistWithPassword,getAuthenticationInfo) that work regardless of which underlying store is active. This keepsLNCfocused on connection management while the orchestrator handles auth complexity.The PR also updates the
passkeys-demoapp to use the new unified store, enabling end-to-end testing of password authentication with the new architecture.Screenshots
Technical Notes
CredentialCache: An in-memory key-value store for credentials during a session. Separating cache from persistence allows credentials to be populated during connection and persisted afterward, which is important for the pairing flow where credentials arrive before the user sets a password.AuthStrategyinterface: Defines the contract for authentication methods. Each strategy handles its ownunlock(), credential get/set, and storage checks. Currently onlyPasswordStrategyis implemented;PasskeyStrategyandSessionStrategywill follow in later PRs.StrategyManager: Manages strategy registration and selection. It answers questions like "which strategies are available?" and "which should be preferred?" This becomes important when multiple auth methods are registered.UnifiedCredentialStore: Implements the sameCredentialStoreinterface as the legacy store, so existing code continues to work. Internally delegates to the active strategy for encryption/persistence.Backward compatibility: Setting
useUnifiedStore: truein config opts into the new system. Without this flag,LNCcontinues usingLncCredentialStoreexactly as before. This is a temporary flag that will be removed in a future PR. I just added it here to be able to test the new functionality using the demo app.Steps to Test
useLNChook to usenew LNC{ serverHost: 'localhost:11110' })for regtest testingRelated Issues & Pull Requests
Depends on: