Authorize DID re-mint with the JWT, not key continuity - #217
Merged
Conversation
#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
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>
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.
Follow-up to #216. Signing in on prod produced the error that invalidates its design:
Why #216 could never work
didwebvh-ts raises that when a log's proof key isn't in its
updateKeys. The browser's currentlocalStoragekey is not the key that minted the existing DID —localStorageis 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/remintDidcallsrequireAuth(), 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/updateDIDalready lets an authenticated user set their DID to anything, and today that silently strands every row on the old value. Pairing the swap withapplyRemintcloses that gap.applyRemint— the 17-table/24-field rewrite with 8 tests — is unchanged. It never cared where the new DID came from.Also
getConvexHttpUrlintolib/convexUrlsso the hook can reach HTTP actions without importing the auth provider, which imports the hook.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.appURLs stay dead either way, anddid:celenvelopes 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: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
remintUserDIDPOST /api/user/remintDidHTTP endpoint (convex/userHttp.ts) that reassigns all rows from a user's current DID to a newly minteddid:webvh, authorizing via JWT rather than trusting any user ID in the request body.turnkeySubOrgIdfrom the JWT using a new internal queryfindUserBySubOrg(convex/migrations/remintUserDidDb.ts), optionally persisting a DID log whendidLogandpathare provided.fetchwithAuthorization: Bearer, replacing the previous Convex action invocation; forces a full page reload on success.getConvexHttpUrlinto a shared src/lib/convexUrls.ts utility to avoid cyclic imports.Macroscope summarized b43dc87.