Re-mint user did:webvh onto the current WEBVH_DOMAIN - #215
Merged
Conversation
A did:webvh encodes its domain in the identifier, and these DIDs are minted
`portable: false` (didCreation.ts:57), so per spec the domain cannot move. A DID
minted while the app was served from an old domain names that domain forever.
That is worse than a cosmetic share-link problem: a did:webvh resolves by
fetching did.jsonl from its own domain, so once the old domain stops serving,
the DID is unresolvable and lists published under it cannot be verified.
Re-mint reuses the SAME Turnkey key and the SAME slug (`user-{subOrgId:16}`),
so only the domain and the derived SCID change — the user keeps their keys and
only the identifier moves.
Every row keyed to the old DID has to move with it or it is orphaned (lists
invisible to their owner, assignments pointing at nobody). The rewrite surface
is declared as data — 17 tables, 24 fields — so it can be read against schema.ts
rather than trusting hand-rolled blocks. Three shapes:
- exact match: lists.ownerDid, items.createdByDid, itemAssignees.*, ...
- prefix match: publications.webvhDid is `{userDid}/resources/list-{id}`
- nested: lists.vcProof (incl. the serialized proof JSON), items.vcProofs[],
activities.metadata.assigneeDid
Deliberately NOT rewritten:
- lists.assetDid / listEnvelopes — did:cel asset ids, not user DIDs.
- The did:cel envelope's genesis content records `createdBy: <old DID>` and is
SIGNED. Rewriting it would invalidate the signature, so envelopes keep the
creating DID as a historical record. Provenance will show the old creator DID
against a list whose owner row has moved; reconciling them is re-genesis, not
a field rewrite.
Does not use the legacyDid mechanism. It exists and is already threaded through
~295 call sites from the Turnkey migration, but it is dual-identity legacy
threading, and users.legacyDid already holds the pre-Turnkey DID — writing to it
would clobber that. This rewrites cleanly instead.
The identity row moves LAST, so a mid-run failure leaves `did` on the old value
and the whole run is safe to retry. `preview` counts rows without mutating.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brianorwhatever
added a commit
that referenced
this pull request
Jul 29, 2026
…ves (#216) #215 tried to re-mint server-side via didCreation:createDIDWebVH. That could never work, and running it on prod proved it: Turnkey error 3: organization mismatch: request is targeting organization (20ed9d43-...), but voters are in organization (66cb8d23-...) createDIDWebVH had no callers — it was dead code. The live path is useAuth.tsx -> createUserWebVHDid, which mints in the BROWSER using an Ed25519 key in localStorage (schema.ts:48 says so: "created client-side"). The server has never held that key, so it cannot mint a replacement the user would still control. Removed the unusable action; the tested applyRemint rewrite is kept as-is, since it does not care where the new DID came from. The mint now happens in the browser, reusing the same localStorage key so the new DID has the same controller. Authorization: applyRemint reassigns every row from one DID to another, so exposing it to clients naively would be an account-takeover primitive — and the app's other mutations do trust client-supplied DIDs. remintDid:remintUserDid instead RESOLVES the submitted log (resolveDIDFromLog verifies each entry's Data Integrity proof, so it cannot be forged without the key), requires its update keys to intersect the stored log's keys for the old DID, and requires the new DID to land on this deployment's WEBVH_DOMAIN. Any failure throws before a row is touched. Also fixes the two bugs that produced the broken share link: - domainFromDid returned parts[3] undecoded, so a host with a port yielded `https://localhost%3A5173/...` — an invalid host. Now decoded, and localhost gets http since it has no certificate. - currentWebvhDomain checks isNativePlatform BEFORE VITE_WEBVH_DOMAIN. The env var is baked at build time, so `bun run cap:build` (which reads .env.local) used to stamp a dev host into real native DIDs, overriding the boop.ad branch. useDidDomainRemint is explicitly temporary — a repair pass, not a compatibility layer — and never blocks login: a failure leaves the old DID working as an identifier, just unresolvable, and retries on the next load. 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.
Adds a migration to move a user's
did:webvhoff a stale domain (e.g.trypoo.app) onto the domain the deployment mints on today.Why a re-mint and not an update
did:webvhputs the domain inside the identifier, and these DIDs are createdportable: false(convex/didCreation.ts:57), so per spec the domain cannot move. A DID minted while the app was served from an old domain names that domain forever.That's worse than a cosmetic share-link bug: a
did:webvhresolves by fetchingdid.jsonlfrom its own domain. Once that domain stops serving, the DID is unresolvable and every list published under it fails to verify.Prod
WEBVH_DOMAINis alreadyboop.ad, so new signups are fine — this is only for identities minted earlier.What it does
createDIDWebVHis re-run with the same Turnkey key and the same slug (user-{subOrgId:16}), so only the domain and derived SCID change. The user keeps their keys; only the identifier moves.Then every row keyed to the old DID moves with it — otherwise it's orphaned (lists invisible to their owner, assignments pointing at nobody). The rewrite surface is declared as data at the top of
remintUserDidDb.ts— 17 tables, 24 fields — so it can be read againstschema.tsline by line instead of trusting 17 hand-written blocks. Three shapes:lists.ownerDid,items.createdByDid,itemAssignees.*publications.webvhDid={userDid}/resources/list-{id}lists.vcProof(incl. serialized proof JSON),items.vcProofs[],activities.metadata.assigneeDidDeliberately not rewritten
lists.assetDid/listEnvelopes— those aredid:celasset ids, not user DIDs. A test asserts they're never touched.did:celenvelope's genesis content. It recordscreatedBy: <old DID>and is signed — rewriting it would invalidate the signature added in Finish the Originals 2.x upgrade: did:cel genesis + persisted CEL logs #213. Envelopes keep the creating DID as a historical record, so the provenance panel will show the old creator DID against a list whose owner row has moved. Reconciling those is re-genesis of every list, not a field rewrite.Not using
legacyDidThe mechanism exists and is already threaded through ~295 call sites from the Turnkey migration, so reusing it would have been nearly free. Skipped on purpose: it's dual-identity legacy threading, and
users.legacyDidalready holds the pre-Turnkey DID — writing to it would clobber that. This rewrites cleanly instead.Safety
didon the old value and the whole run is safe to retry.previewcounts affected rows and mutates nothing.8 new tests cover each rewrite shape plus the ways this could silently corrupt data: another user's DID must survive,
did:celids must not move, a re-run must skip rather than half-apply, and domain matching must decode%3A. Suite is 109 pass / 0 fail; both typechecks clean.Running it (after merge + deploy)
Dry run first — counts only:
Then:
https://<old-domain>/...share URLs break permanently — though they're already broken if that domain stopped serving.🤖 Generated with Claude Code
Note
Add internal actions to re-mint user did:webvh DIDs onto the current
WEBVH_DOMAINremintByEmailinternal action that mints a newdid:webvhfor a user via their Turnkey sub-org and rewrites all dependent rows across tables; skips idempotently if the user is already on the target domain.previewinternal action for a dry-run that returns per-table row counts affected without making any mutations.applyRemintrewrites flat fields, prefix-matched fields (e.g.publications.webvhDid), and nested DID references insidevcProof,vcProofs, and activity metadata.scripts/remint-did.test.mjscovering rewrites, idempotency, and domain decoding against an in-memory mock.applyRemintpatches many tables in a single mutation; large datasets per user could hit Convex mutation size or time limits.Macroscope summarized 508fd43.