Skip to content

feat: unsigned action records, verifier and provenance status (#237, part 1) - #294

Merged
brianorwhatever merged 6 commits into
mainfrom
feat/237-action-records
Oct 16, 2026
Merged

brianorwhatever merged 6 commits into
mainfrom
feat/237-action-records

Conversation

@brianorwhatever

Copy link
Copy Markdown
Contributor

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

  • Canonical action record payload and independent verifier (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.
  • One recording path (convex/lib/actionRecords.ts recordActions): 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.
  • Reads: actionRecords:getListActionRecords / getItemActionRecords, plus GET /api/v1/action-records and scripts/verify-action-records.mjs for offline checks.
  • Provenance UI: shows each record's status (unsigned, pending, failed or signed).
  • E2E fixture: serves the new queries. Without them, 8 share-modal/sharing tests failed on Sign action records with owner Turnkey keys through one shared path #277.
  • New unsigned vcProof/vcProofs placeholders are no longer written. Existing ones stay readable.

Differences from #277

  • No signer. Every record is written with status: "unsigned". Nothing is scheduled, and there's no cron. Recording can't fail or delay a write.
  • No custody claim in the payload. Sign action records with owner Turnkey keys through one shared path #277 wrote 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.
  • The schema has no attempts/lastAttemptAt fields and no by_status index. The pending/signed/failed statuses stay in the schema so a future signer doesn't need to change the schema, verifier or UI.
  • UI and doc text no longer say "Turnkey-held". docs/action-records.md describes 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

brianorwhatever and others added 5 commits October 14, 2026 20:44
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>
@railway-app

railway-app Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the boop-pr-294 environment in Friends

Service Status Web Updated
boop ✅ Success (View Logs) Web Oct 15, 2026 at 3:55 am UTC

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts adds RFC 8785 canonicalization, a digest, the domain-separated 64-byte signing input, and verifyActionRecord, which trusts only keys the caller supplies.
  • Single writer: recordActions in convex/lib/actionRecords.ts writes unsigned records from ctx.actor for item create/complete/reopen (single, batch, template, recurrence) and list create/copy/rename.
  • Cleanup: records are deleted in removeItem, batchDeleteItems, deleteList and the batched deleteUserStep. remintUserDid leaves them alone.
  • Placeholders retired: new vcProof/vcProofs are no longer written. createListOwnershipVC moves into its only remaining caller, the celAssetDids migration.
  • Reads: getItemActionRecords / getListActionRecords, GET /api/v1/action-records, and the offline scripts/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:key owners.
  • E2E fixture: the loopback server records actions and serves the new queries.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread docs/action-records.md Outdated
Comment thread convex/lib/actionRecords.ts Outdated
Comment thread tests/fixtures/app.ts Outdated
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>
@railway-app
railway-app Bot temporarily deployed to Friends / boop-pr-294 October 15, 2026 03:54 Destroyed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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: checkSharedItem and uncheckSharedItem now write itemCompleted/itemReopened, matching checkItem/uncheckItem. A new parity test checks this, and the docs table lists both writers.
  • Bounded record deletion: deleteActionRecords deletes at most 500 records per mutation, for both items and lists. The new internal actionRecords.deleteRemaining deletes the rest in follow-up runs. Tests run it on subjects with more than one batch of records.
  • Fixture comment corrected: tests/fixtures/app.ts now says the default status is unsigned, which matches the fixture.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@brianorwhatever
brianorwhatever merged commit 00969d4 into main Oct 16, 2026
11 checks passed

This branch was successfully deployed

No deployments
Friends / boop-pr-294 — 9309db6f Deployed Oct 15, 2026 by railway-app[bot]
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