Skip to content

DID identity in config, bootstrap, and the token admin surface - #63

Merged
cuibonobo merged 2 commits into
mainfrom
claude/issue-54-did-identity-config-bootstrap
Aug 17, 2026
Merged

DID identity in config, bootstrap, and the token admin surface#63
cuibonobo merged 2 commits into
mainfrom
claude/issue-54-did-identity-config-bootstrap

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Implements #54entityId is a DID everywhere the server touches config, bootstrap, and token admin.

Bootstrap (src/config.ts, src/stack.ts)

  • ENTITY_ID is 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() 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 — an ENTITY_ID that 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() takes ownerProfile when OWNER_NAME is configured, so the owner's own _entity card exists on every open.

Bug found along the way: core's ownerProfile bootstrap (ensureOwnerEntity()) mints the owner's _entity@1 record with an auto-generated id, keyed by content.did rather than the record id. GET/PATCH /entity assumed record.id === ownerEntityId — which only ever worked because the test fixture forced that id by hand. Once ownerProfile actually creates a card, the route would 404 forever. Both handlers now resolve the owner's record by querying content.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/onBehalfOf on POST /tokens are validated as DIDs (422 otherwise).
  • createdAt is read back from the token store instead of fabricated at the route, so it can't diverge from what GET /tokens reports later.

Logging (src/middleware/errors.ts) — a denied request from a verified (non-anonymous) session now logs the requester's principalId/subjectId — actionable signal that plain anonymous noise isn't.

OWNER_TOKEN stays 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 typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm test — 133/133 passing, including 16 new tests covering DID validation (config load, token admin), the openOrInitialize bootstrap paths (new-db-without-ENTITY_ID failure, opening an existing db without ENTITY_ID, the mismatch warning, ownerProfile creation), the GET/PATCH /entity lookup-by-content.did fix, and the denied-but-verified logging behavior.

Generated by Claude Code

claude added 2 commits August 16, 2026 22:49
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.
@cuibonobo
cuibonobo merged commit 4809091 into main Aug 17, 2026
4 checks passed
@cuibonobo
cuibonobo deleted the claude/issue-54-did-identity-config-bootstrap branch August 17, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants