Skip to content

Authorize DID re-mint with the JWT, not key continuity - #217

Merged
brianorwhatever merged 1 commit into
mainfrom
fix/remint-jwt-auth
Jul 29, 2026
Merged

Authorize DID re-mint with the JWT, not key continuity#217
brianorwhatever merged 1 commit into
mainfrom
fix/remint-jwt-auth

Conversation

@brianorwhatever

@brianorwhatever brianorwhatever commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #216. Signing in on prod produced the error that invalidates its design:

Key did:key:z6MktvNdMfyjcW6eFDgALRK2cYbdEU9uxqRxLxBy65mRuXah
is not authorized to update.

Why #216 could never work

didwebvh-ts raises that when a log's proof key isn't in its updateKeys. The browser's current localStorage key is not the key that minted the existing DID — localStorage is per-origin, and the original controller key went with the origin the DID was minted on.

#216 required the new log's update keys to intersect the old log's. That's unsatisfiable for exactly the identities this is meant to repair. The hook failed client-side before ever reaching Convex, which is why prod logs showed no invocation at all.

There is no key continuity left to preserve. So the re-mint stops pretending to be a continuation and issues a genuinely new identity.

The new authorization

POST /api/user/remintDid calls requireAuth(), takes the account from the verified token, and rewrites only that account's rows. The request body cannot name a victim — it supplies the new DID, never the identity.

This is stricter than the status quo, not a relaxation: /api/user/updateDID already lets an authenticated user set their DID to anything, and today that silently strands every row on the old value. Pairing the swap with applyRemint closes that gap.

applyRemint — the 17-table/24-field rewrite with 8 tests — is unchanged. It never cared where the new DID came from.

Also

  • Extracts getConvexHttpUrl into lib/convexUrls so the hook can reach HTTP actions without importing the auth provider, which imports the hook.
  • The client reloads after a successful re-mint. The app is keyed by DID throughout, so live subscriptions would otherwise keep reading rows under the old identity.

What this costs

The new DID has no cryptographic link to the old one — it's a fresh identity that inherits the account's data. Given the old key is unrecoverable, the alternative isn't "preserve continuity", it's "stay broken". Old trypoo.app URLs stay dead either way, and did:cel envelopes still record the original creator DID in signed content.

Suite 114 pass / 0 fail; both typechecks clean; lint unchanged from baseline.

After merge

Sign in on web. Expect a [remint] line in the console, a reload, then:

npx convex run --prod migrations/remintUserDidDb:listUsersOnDomain '{"domain":"trypoo.app"}'

should drop by one per account signed in, draining to [].

🤖 Generated with Claude Code

Note

Authorize DID re-mint via JWT instead of key continuity in remintUserDID

  • Adds a new authenticated POST /api/user/remintDid HTTP endpoint (convex/userHttp.ts) that reassigns all rows from a user's current DID to a newly minted did:webvh, authorizing via JWT rather than trusting any user ID in the request body.
  • The endpoint resolves the caller's account by turnkeySubOrgId from the JWT using a new internal query findUserBySubOrg (convex/migrations/remintUserDidDb.ts), optionally persisting a DID log when didLog and path are provided.
  • Refactors src/hooks/useDidDomainRemint.ts to accept a JWT token and call the new HTTP endpoint directly via fetch with Authorization: Bearer, replacing the previous Convex action invocation; forces a full page reload on success.
  • Extracts getConvexHttpUrl into a shared src/lib/convexUrls.ts utility to avoid cyclic imports.
  • Behavioral Change: the repair pass now requires a valid JWT token to run; remint attempts will short-circuit until the token is available.

Macroscope summarized b43dc87.

#216 required the new DID log to be signed by the key controlling the old DID.
Signing in on prod showed that is unsatisfiable:

  Key did:key:z6MktvNdMfyjcW6eFDgALRK2cYbdEU9uxqRxLxBy65mRuXah
  is not authorized to update.

didwebvh-ts raises that when a log's proof key is absent from its updateKeys.
The browser's current localStorage key is not the key that minted the existing
DID — localStorage is per-origin, and the original key went with the origin the
DID was minted on. So the key-intersection check could never pass, and the hook
failed client-side before reaching Convex (which is why prod logs showed no
invocation).

There is no continuity left to preserve, so the re-mint now issues a genuinely
NEW identity and is authorized differently: POST /api/user/remintDid runs
requireAuth(), takes the account from the verified token, and rewrites only that
account's rows. The request body cannot name a victim.

This is strictly safer than the status quo rather than a relaxation:
/api/user/updateDID already lets an authenticated user set their DID to anything
and silently strands every row on the old value. Pairing the swap with
applyRemint closes that.

Also extracts getConvexHttpUrl into lib/convexUrls so the hook can reach HTTP
actions without importing the auth provider (which imports the hook).

The client reloads after a successful re-mint: the app is keyed by DID
throughout, so live subscriptions would otherwise hold rows under the old one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brianorwhatever
brianorwhatever merged commit 8bce662 into main Jul 29, 2026
4 checks passed
brianorwhatever added a commit that referenced this pull request Jul 29, 2026
…roken (#218)

No did:webvh could be created at all since the didwebvh-ts 2.8 upgrade. Every
mint threw:

  Key did:key:z6Mk… is not authorized to update.

2.8's isKeyAuthorized parses the proof's verificationMethod down to its
keyMultibase and compares by exact equality:

  updateKeys.some((k) => k === parsed.keyMultibase)

Every call site passed `signer.getVerificationMethodId()` — the full
`did:key:z6Mk…` URI — so the comparison could never match. didwebvh-ts verifies
the log it has just created, so creation failed closed.

Reproduced with a brand-new key and empty localStorage, which ruled out the
earlier theory that this was about a lost controller key: it failed identically
for a fresh identity. That also explains why the re-mint hook never reached
Convex — it died in the browser at the mint step, before any request.

Fixed at all four sites: user DIDs (lib/webvh.ts), server-side DID creation
(didCreation.ts), and both site paths (siteActions.ts createDID + updateDID) —
site publishing carried the same defect.

Adds scripts/webvh-mint.test.mjs, which performs a real mint. didwebvh-ts
verifies its own output, so a malformed updateKeys fails the test exactly as it
failed in the browser. The harness installs browser globals via defineProperty
(bun's localStorage is readonly) and restores them, since bun shares globals
across test files.

Also removes convex/remintDid.ts. It was meant to go in #217 — the `rm` was in a
command the sandbox blocked, so it merged as dead code; the client now uses the
JWT-authorized /api/user/remintDid endpoint.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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