DID identity in config, bootstrap, and the token admin surface - #63
Merged
Conversation
Bootstrap: - ENTITY_ID must be a well-formed DID; loadConfig() refuses a malformed one at startup instead of letting it silently mint an unmatchable owner. - initStack() adopts LocalAdapter.openOrInitialize(), collapsing the existsSync-then-branch choreography (a TOCTOU) into one race-free call. entityId is passed as a lazy provider rather than a plain string so the helper's own owner-mismatch check (which throws) never fires on the open path — ENTITY_ID divergence from an existing stack logs a warning instead, matching the documented "ignored once the database exists" behavior instead of silently doing nothing. - Stack.create() now takes ownerProfile when OWNER_NAME is configured, so the owner's own _entity card exists — idempotent, safe on every open. Fixed along the way: ensureOwnerEntity() (core's ownerProfile bootstrap) mints the owner's _entity@1 record with an auto-generated id, keyed by content.did rather than record id. GET/PATCH /entity assumed record id == ownerEntityId, which only worked because the test fixture forced that id by hand — the moment ownerProfile creates a real card, the route would 404 forever. Both handlers now resolve the record by querying content.did. Token admin (POST /tokens): - entityId and onBehalfOf are validated as DIDs (422 otherwise). - createdAt is read back from the store instead of fabricated at the route, so it can't diverge from what GET /tokens reports later. Logging: - errorMiddleware logs the requester's principalId/subjectId when a verified session (not anonymous) is denied by StackPermissionError — actionable signal that plain anonymous noise isn't. OWNER_TOKEN stays required (no DID-based handshake exists yet to make it optional) but is now documented as a break-glass credential. Refs #54. Defers discovery's entityId-is-a-DID field to #55, and the DID challenge-response handshake itself to #53.
Its id never changes once minted, so the content.did lookup this route added doesn't need to run on every request — cache it after the first resolution instead. A null result is never cached (so a card created later, e.g. ownerProfile wasn't configured at boot, is picked up on the next request rather than staying 404 forever), and the cache is cleared whenever a cached id stops resolving (the record was deleted, or in principle recreated under a new id), so a stale id is never trusted past the request that discovers it's stale.
12 tasks
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
Implements #54 —
entityIdis a DID everywhere the server touches config, bootstrap, and token admin.Bootstrap (
src/config.ts,src/stack.ts)ENTITY_IDis validated as a DID at load time; a malformed value fails fast instead of silently minting an owner that can never match a signature or grant.initStack()adoptsLocalAdapter.openOrInitialize(), collapsing theexistsSync-then-branch choreography (a TOCTOU) into one race-free call.entityIdis passed as a lazy provider rather than a plain string so the helper's own owner-mismatch check (which throws) never fires on the open path — anENTITY_IDthat no longer matches an existing stack's owner now logs a warning instead of failing, matching the documented "ignored once the database exists" behavior instead of doing so silently.Stack.create()takesownerProfilewhenOWNER_NAMEis configured, so the owner's own_entitycard exists on every open.Bug found along the way: core's
ownerProfilebootstrap (ensureOwnerEntity()) mints the owner's_entity@1record with an auto-generated id, keyed bycontent.didrather than the record id.GET/PATCH /entityassumedrecord.id === ownerEntityId— which only ever worked because the test fixture forced that id by hand. OnceownerProfileactually creates a card, the route would 404 forever. Both handlers now resolve the owner's record by queryingcontent.did, with the actual read/write still going through the caller's scoped session so the 403-vs-404 distinction (forbidden vs. missing) is unchanged.Token admin (
src/routes/tokens.ts)entityId/onBehalfOfonPOST /tokensare validated as DIDs (422 otherwise).createdAtis read back from the token store instead of fabricated at the route, so it can't diverge from whatGET /tokensreports later.Logging (
src/middleware/errors.ts) — a denied request from a verified (non-anonymous) session now logs the requester'sprincipalId/subjectId— actionable signal that plain anonymous noise isn't.OWNER_TOKENstays required (no DID-based handshake exists yet to make it optional — that's #53) but is now documented as a break-glass credential in the README/.env.example.Out of scope, left to their own issues: discovery's
entityId-is-a-DID field (#55) and the DID challenge–response handshake itself (#53).Test plan
pnpm typecheckpnpm lintpnpm format:checkpnpm test— 133/133 passing, including 16 new tests covering DID validation (config load, token admin), theopenOrInitializebootstrap paths (new-db-without-ENTITY_IDfailure, opening an existing db withoutENTITY_ID, the mismatch warning,ownerProfilecreation), theGET/PATCH /entitylookup-by-content.didfix, and the denied-but-verified logging behavior.Generated by Claude Code