feat(adapter-local): add openOrInitialize() for honest first-run UX - #165
Merged
Conversation
LocalAdapter.openOrInitialize() does the existence branch (open if the db is there, otherwise generate/initialize) that every adopter was previously left to write by hand. entityId accepts a lazy `() => string | Promise<string>` so keypair generation only runs on the actually-new path, or a plain string, which is asserted to match the existing owner on the open path to catch silent config divergence. Rewrite both READMEs' quick starts to use it and actually persist the generated keypair (via exportDidPrivateKeyJwk), and add a Key custody section covering where to put the JWK on Node/desktop/browser and the consequence of losing it. Closes #143 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSzQUk58M7KYnmuZXKxpsS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The README quick start's happy path called
generateDidKeypair()thenLocalAdapter.initialize()— which throws on a second run (file exists), and as written mints a new identity every run while droppingprivateKeyon the floor. The real first-run choreography (if db exists, open; else generate keypair, persist it, initialize) was left to every adopter to write by hand.LocalAdapter.openOrInitialize(opts)— branches onexistsSync(path).initialize()/open()are unchanged, for callers who want the explicit distinction.entityIdaccepts either a plain DID string or a lazy() => string | Promise<string>, so keypair generation only runs on the actually-new path — the provider is never invoked when the db already exists. A plain-stringentityIdis asserted against the existing stack's owner on the open path (catches silent config divergence); a lazy provider is not checked there, since evaluating it just to compare would defeat the point of laziness.README.mdandpackages/core/README.md) — usesopenOrInitialize()and actually persists the generated keypair viaexportDidPrivateKeyJwk().CryptoKeyitself in IndexedDB instead of exporting), and the asymmetry: losing the key never breaks anything local, but permanently forecloses authenticating as that identity to a server.Closes #143
Spec
No
docs/spec/changes.openOrInitialize()is an additiveLocalAdapterconvenience static, like the existinginitialize()/open()— it isn't part of theStackAdapterinterface or any wire/permission contract.Verification
All green. Added test coverage in
packages/adapter-local/tests/local.test.tsfor: initializing when the db is absent, opening (not re-initializing) when present, the lazy provider never being invoked on the open path, the lazy provider being invoked (sync or async) on the initialize path, the mismatch error when a plain-stringentityIddisagrees with the existing owner, andtimezoneonly applying on the initialize path.Notes for reviewers
The issue's open question — should
entityIdbe lazy — is resolved here: it'sstring | (() => string | Promise<string>), with the lazy form intentionally exempt from the open-path match check (documented in the option's JSDoc rather than silently ignored).Generated by Claude Code