Skip to content

feat(api,web): access model v2 P2 - sign once, listen freely#78

Merged
knzeng-e merged 2 commits into
feat/access-v2-p1bfrom
feat/access-v2-p2
Jul 9, 2026
Merged

feat(api,web): access model v2 P2 - sign once, listen freely#78
knzeng-e merged 2 commits into
feat/access-v2-p1bfrom
feat/access-v2-p2

Conversation

@knzeng-e

@knzeng-e knzeng-e commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Stacked on #77 (P1b). Ticket 24 P2: session-based key delivery - one wallet signature per ~24h instead of one per listen.

Design (as approved in dotify-v2-access-and-streaming.md)

  1. Sign in once: the wallet signs a canonical SIGN_IN message (nonce, expiry, replay-protected - same fail-closed order as key requests: expiry -> signature -> nonce consumption). POST /api/auth/session exchanges it for a bearer token.
  2. Token rides key requests: POST /:contentHash/key-request accepts {sessionToken, purpose}. The service verifies the token and runs the on-chain access check against the token's address on every request - the token proves identity, never access.
  3. Client: ensureDotifySession reuses the stored per-address token (60s refresh margin); on 401 it re-signs once and retries; backends without session support (404/503) fall back to the per-request signed path unchanged. Wallet disconnect signs out server-side (POST /api/auth/logout, idempotent) and forgets the token.

Token design - no new secret, no new dependency

HMAC-SHA256 over a strict claim shape (address, chainId, jti, iat, exp), key HKDF-derived from CONTENT_KEY_MASTER_SECRET with a dedicated info label - token keys and content keys never share bytes, and operators provision nothing new. Constant-time signature comparison; 24h TTL; jti revocation blocklist.

Boundaries (stated in the threat model, same PR)

  • A stolen token is bounded by TTL + revocation and can only fetch keys its address could already obtain.
  • Revocation is in-memory (process lifetime) - matches the single-instance deployment; scale-out needs a shared store first, flagged.
  • Master secret unconfigured -> session auth 503s and clients keep the per-request signed path: fail closed, never open.

Verification

  • API: 37/37 (5 sessionTokens unit: roundtrip/expiry/tamper/malformed/revoke; 5 auth route; 3 session key path incl. proof the chain check uses the token's address), tsc --noEmit clean.
  • Web: 90/90 unit, lint 0 errors, tsc -b + build clean.
  • Not verified here: a live browser pass of the sign-in prompt flow (first listen asks once, subsequent listens silent). Worth a manual check alongside the P1b e2e run.

Merge order: #76 -> #77 -> this.

knzeng-e and others added 2 commits July 9, 2026 05:35
One SIWE-style signature opens a ~24h session; every later key request rides
a bearer token instead of a fresh wallet signature. The token proves identity
only - the on-chain access check still runs per track on every request.

Key service:
- sessionTokens.ts: HMAC-SHA256 tokens over a strict claim shape, key
  HKDF-derived from CONTENT_KEY_MASTER_SECRET with a dedicated info label (no
  new secret, no shared bytes with content keys), constant-time verify, 24h
  TTL, jti revocation (in-memory, process-lifetime - documented boundary).
- signatures.ts: canonical SIGN_IN message + verifySignInRequest (expiry ->
  signature -> nonce consumption, same fail-closed order).
- POST /api/auth/session (401 on bad signature, 503 when unconfigured so the
  per-request path remains) and idempotent POST /api/auth/logout.
- key-request route accepts {sessionToken, purpose}; the chain check runs
  against the token's address, never client input. 13 new tests (37 total).

Web:
- ensureDotifySession: reuse the stored per-address token (refresh margin),
  else one sign-in signature; requestContentKey rides the token, retries once
  with a fresh sign-in on 401, and falls back to per-request signing when the
  backend lacks session support (404/503).
- Wallet disconnect also signs the session out server-side and forgets the
  stored token.

Threat model gains the session-auth boundary section; ticket 24 delivery
notes updated. API 37/37 + typecheck; web 90 unit, lint 0 errors, build ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@knzeng-e knzeng-e self-assigned this Jul 9, 2026
@knzeng-e
knzeng-e merged commit ef0bab2 into feat/access-v2-p1b Jul 9, 2026
1 check passed
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.

1 participant