Repository navigation
feat: unsigned action records, verifier and provenance status (#237, part 1) - #294
Conversation
Defines the version-1 action record payload, its RFC 8785 canonical bytes and digest, the 64-byte domain-separated signing input, and a verifier that checks a record only against keys the caller already trusts for the payload's owner DID. Pure, so the server, the browser and outside verifiers share one definition. The payload names the authorizing account and credential but carries no signer or custody field: who holds the signing key is still open (#244). Part of #237. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every recorded transition now goes through recordActions: item create (single, template, recurring), complete and reopen (single and batch, which previously recorded nothing), and list create, copy and rename. Batch and individual calls write records of the same shape, bound to the authenticated session or API-key row separately from the authorizing account. Records are stored unsigned. No signer is wired up until the custody model from #244 is settled, so recording happens entirely inside the mutation, schedules nothing, and cannot fail the write. New unsigned vcProof/vcProofs placeholders are no longer written; existing ones stay readable and untouched. The list placeholder constructor moves into the celAssetDids migration, its only remaining user. Records are deleted with their item, list or account, and are kept out of the DID re-mint rewrite. Adds GET /api/v1/action-records and scripts/verify-action-records.mjs so records can be fetched and checked offline against pinned keys. Part of #237. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lists an item's action records, and a list's list-level records, separately from historical placeholders, with a truthful status: unsigned, pending, failed or signed. A signed record is re-checked in the browser and only called verified when the owner DID itself encodes the signing key. Only unsigned occurs until a signer exists. Part of #237. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part of #237. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The share modal's provenance panel now queries actionRecords:getListActionRecords; without it the fixture threw and the app error boundary replaced the page. The fixture now records list and item creation with real canonical payloads, unsigned by default, and an actionRecordStatus seed option covers all four rendered statuses. Part of #237. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
🚅 Deployed to the boop-pr-294 environment in Friends
|
There was a problem hiding this comment.
Important
One completion/reopen path still bypasses recordActions, so the "every transition is recorded" claim in the docs doesn't hold yet. Details inline, plus two smaller notes.
Reviewed changes
Initial review of the full PR: unsigned action records, the shared canonical payload and verifier, read paths, and the provenance UI.
- Canonical payload and verifier:
shared/actionRecord.tsadds RFC 8785 canonicalization, a digest, the domain-separated 64-byte signing input, andverifyActionRecord, which trusts only keys the caller supplies. - Single writer:
recordActionsinconvex/lib/actionRecords.tswritesunsignedrecords fromctx.actorfor item create/complete/reopen (single, batch, template, recurrence) and list create/copy/rename. - Cleanup: records are deleted in
removeItem,batchDeleteItems,deleteListand the batcheddeleteUserStep.remintUserDidleaves them alone. - Placeholders retired: new
vcProof/vcProofsare no longer written.createListOwnershipVCmoves into its only remaining caller, thecelAssetDidsmigration. - Reads:
getItemActionRecords/getListActionRecords,GET /api/v1/action-records, and the offlinescripts/verify-action-records.mjs. - Provenance UI: shows each record's status and re-checks signed records in the browser. It says "verified" only for
did:keyowners. - E2E fixture: the loopback server records actions and serves the new queries.
claude-opus-5-5 | 𝕏
checkSharedItem / uncheckSharedItem (the DID resource toggle over HTTP) completed and reopened items without writing action records, so provenance could show a reopen with no completion before it. They now record the same item.completed / item.reopened as checkItem / uncheckItem. Every toggle adds a record, so deleting a long-lived list or item could exceed Convex's per-mutation limits. deleteActionRecords now removes up to 500 records in the deleting mutation and schedules deleteRemaining for the rest. Also corrects the E2E fixture comment: the default status is unsigned. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. All three earlier findings are fixed.
Reviewed changes
Reviewed 9309db6, the one commit added since the last review. It fixes all three earlier findings. signed-action-records.test.mjs (13/13) and public-function-boundary.test.mjs both pass locally.
- DID resource toggles recorded:
checkSharedItemanduncheckSharedItemnow writeitemCompleted/itemReopened, matchingcheckItem/uncheckItem. A new parity test checks this, and the docs table lists both writers. - Bounded record deletion:
deleteActionRecordsdeletes at most 500 records per mutation, for both items and lists. The new internalactionRecords.deleteRemainingdeletes the rest in follow-up runs. Tests run it on subjects with more than one batch of records. - Fixture comment corrected:
tests/fixtures/app.tsnow says the default status isunsigned, which matches the fixture.
claude-opus-5-5 | 𝕏

Splits out the parts of #277 that don't depend on how action records get signed, so they can land while the custody model from #244 is settled. #277 is restacked on this branch and now contains only the server-side Turnkey signer.
What this adds
shared/actionRecord.ts): RFC 8785 canonical bytes, digest, the domain-separated signing input, and a verifier that checks a record only against keys the caller already trusts.convex/lib/actionRecords.tsrecordActions): covers item create (single, template, recurring), complete and reopen (single and batch), and list create, copy and rename. Records are bound to the authenticated session or API-key row. Records are deleted along with their item, list or account. The DID re-mint migration doesn't touch them.actionRecords:getListActionRecords/getItemActionRecords, plusGET /api/v1/action-recordsandscripts/verify-action-records.mjsfor offline checks.vcProof/vcProofsplaceholders are no longer written. Existing ones stay readable.Differences from #277
status: "unsigned". Nothing is scheduled, and there's no cron. Recording can't fail or delay a write.signer: { custody: "turnkey", … }into the canonical bytes, which are immutable once stored. That field is gone, so records stored now make no claim about a key model that hasn't been decided.attempts/lastAttemptAtfields and noby_statusindex. Thepending/signed/failedstatuses stay in the schema so a future signer doesn't need to change the schema, verifier or UI.docs/action-records.mddescribes custody as undecided.Testing
bun test: 792 pass.tsc -b, the convex and E2E type checks, and eslint on changed files are clean.bun run test:e2e: 128 pass, including all four provenance statuses.Signed records in tests are signed with a test key to exercise the verifier and read paths. No real signing happens anywhere.
Part of #237. Doesn't close it.
🤖 Generated with Claude Code