Skip to content

chore(originals): upgrade to 3.0.0-next.0 - #223

Merged
brianorwhatever merged 5 commits into
mainfrom
chore/originals-3
Aug 17, 2026
Merged

chore(originals): upgrade to 3.0.0-next.0#223
brianorwhatever merged 5 commits into
mainfrom
chore/originals-3

Conversation

@brianorwhatever

Copy link
Copy Markdown
Contributor

@originals/sdk and @originals/auth to 3.0.0-next.0, which pulls in the newly split-out @originals/cel@0.2.0-next.0.

Pinned exact rather than caret. npm writes ^3.0.0-next.0 for a prerelease, which also matches 3.0.0 final and every later 3.x; the repo pins these exactly already.

Nothing in our code had to change

tsc -b --force, eslint, the full bun suite (151 pass / 0 fail) and npm run build are all clean. Our import surface is three files — src/lib/originals.ts, src/lib/celKeyStore.ts, convex/migrations/celAssetDids.ts — and none of the breaking changes land on it:

Breaking change Why it misses us
createAsset throws NO_CUSTODY without custody config.keyStore is already localCelKeyStore
BufferUint8Array on the public surface No Buffer in our SDK paths
Deep imports (@originals/sdk/crypto/*) removed We had none
@originals/auth root no longer re-exports ./server We never imported it

3.0 also fixes something in our favour: OriginalsSDK.create used to destructure keyStore out of the config before it reached the LifecycleManager, so our custody is now actually honoured downstream.

The part that could have bitten

3.0 renames the CEL cryptosuite to originals-cel-ed25519-jcs-v1 and starts signing the proof configuration. Logs sealed before it cannot be re-signed, so eddsa-jcs-2022 stays accepted on read — and every list envelope already in the database depends on that promise holding.

Verified rather than assumed. scripts/fixtures/cel-envelope-sdk-2.1.0.json is a genuine envelope minted by SDK 2.1.0 in a throwaway install, not hand-written. It verifies clean under 3.0 through our own verifyListEnvelope, no warnings. The new test also asserts the fixture still carries the pre-3.0 suite, so it cannot quietly decay into proving nothing.

If a later bump drops the legacy suite, this fails here rather than in production — where the symptom would be every existing list going unverifiable at once.

Note for the DID path

3.0 adds normalizeUpdateKey, which strips did:key: down to bare multibase and fixes exactly the didwebvh-ts 2.8 breakage diagnosed in #218. It does not cover us: src/lib/webvh.ts calls didwebvh-ts directly rather than through the SDK's WebVHManager, so #218's own fix is still what carries that path.

Follow-up, not in this PR

convex/lib/turnkeyClient.ts and convex/lib/turnkeySigner.ts are copied out of @originals/auth/server "to avoid extensionless re-exports that break Convex deploy analysis." auth 3.0 ships a curated exports map and every relative import now carries .js, so that workaround should be removable — and turnkeySignBytes would then let a Turnkey key satisfy OriginalsSigner and author CEL events, not just did:webvh logs. Left out here to keep the upgrade minimal ahead of the prerelease, since it touches the Convex module analyzer.

🤖 Generated with Claude Code

brianorwhatever and others added 5 commits August 16, 2026 00:42
@originals/sdk and @originals/auth 3.0.0-next.0, which pulls in the newly
split-out @originals/cel 0.2.0-next.0. Pinned exact rather than caret: npm
writes ^3.0.0-next.0 for a prerelease, which would also match 3.0.0 final and
every later 3.x, and the repo pins these exactly already.

Nothing in our code had to change. `tsc -b --force`, `eslint`, the full bun
suite (151 tests) and `npm run build` are all clean on the new version, and the
import surface is small — src/lib/originals.ts, src/lib/celKeyStore.ts and
convex/migrations/celAssetDids.ts. None of the breaking changes land on it:

  - `createAsset` now throws NO_CUSTODY without custody; config.keyStore is
    already set (localCelKeyStore), so it has some. 3.0 also fixes
    OriginalsSDK.create destructuring keyStore away before it reached the
    LifecycleManager, so our custody is now actually honoured downstream.
  - Buffer -> Uint8Array on the public surface: no Buffer in our SDK paths.
  - Deep imports like @originals/sdk/crypto/* no longer resolve: we had none.
  - @originals/auth's root no longer re-exports ./server: we never imported it.

Adds a back-compat regression test, which is the part that could actually have
bitten. 3.0 renames the CEL cryptosuite to originals-cel-ed25519-jcs-v1 and
starts signing the proof configuration; logs sealed before it cannot be
re-signed, so eddsa-jcs-2022 stays accepted on read. Every list envelope already
in the database depends on that promise holding.

The fixture is a genuine envelope minted by SDK 2.1.0 in a throwaway install,
not a hand-written one, and it verifies clean under 3.0 with no warnings. The
test asserts the fixture still carries the pre-3.0 suite, so it cannot quietly
decay into proving nothing. If a later bump drops the legacy suite this fails
here rather than in production, where the symptom would be every existing list
going unverifiable at once.

Note our did:webvh mint path is unaffected by 3.0's normalizeUpdateKey fix for
the didwebvh-ts 2.8 bare-multibase requirement: src/lib/webvh.ts calls
didwebvh-ts directly rather than through the SDK's WebVHManager, so #218's fix
is still what is carrying us there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SDK 3.0 throws NO_CUSTODY when createAsset is called without a signer, a
keyStore, or an explicit `{ controller: 'ephemeral' }`. celAssetDids.ts mints
with none of them — deliberately, so the server never holds a list's controller
key — so under 3.0 every call throws and the migration cannot run at all.

`ephemeral` is the explicit spelling of exactly what it already did. The header
has always said migrated lists are verifiable but not authorable; 3.0 simply
gives that intent a name and refuses to infer it from an omission.

I claimed in the upgrade commit that NO_CUSTODY missed us because config.keyStore
is set. That holds for src/lib/originals.ts and not for this file, which is the
one place we mint deliberately without custody.

Nothing caught it. Typecheck cannot see a runtime throw, and cel-migration.test
covered only celAssetDidsDb.ts — the database half — so no test had ever called
createAsset on this path. The migration is an internalAction run by hand against
prod, so the first signal would have been a failed run on real rows.

Adds coverage for the minting half: a real mint, a real verification of the
envelope it produces, and an assertion that the genesis resource still commits
to the list's original createdAt rather than the migration's clock. Confirmed
non-vacuous — reverting the fix fails it with NO_CUSTODY.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every list in prod verifies — 28 of 28 have an envelope, checked by diffing
list ids against listEnvelopes.listId, no gaps either way. The problem is not
verifiability, it is custody: 22 of those 28 were given their genesis by the
celAssetDids migration, server-side with an ephemeral controller. Nobody holds
their key, so they can never record another CEL event. Only 6 are authorable.

copyList mints a new list in the owner's browser, so the key lands in their
keyStore, and moves the items across. The copy is honestly new — today's
genesis, its own DID, no claim on the original's history — and the source list
is left untouched.

Details that matter:

- Owner-only. A copy mints an identity naming the owner, so a viewer of a
  shared list must not be able to make one. Enforced server-side; the button
  only decides whether to offer it.
- The free-plan cap is now a shared assertListQuota rather than inline in
  createList, because a copy is a new list and copying must not be a way
  around the limit. There is a test for that specifically.
- parentId is remapped in a second pass. Copying it verbatim would leave
  sub-items in the new list pointing at rows in the old one.
- vcProofs are dropped. They attest actions taken against the SOURCE asset's
  DID; carrying them would attribute one asset's provenance to another, which
  is the exact claim this feature exists to avoid.
- Item fields are rest-spread with a four-field drop list, so a column added to
  items later is carried automatically instead of silently going missing.

Also fixes the reason none of this was visible. EnvelopeVerificationRow
returned null for the absent case, so a list with no log rendered nothing —
indistinguishable from a list whose log just was not shown. Every state now
says something, and a retroactive genesis says so rather than claiming a
history it does not have.

isRetroactiveGenesis compares the genesis proof's `created` against the list's
createdAt. That gap is the only signal distinguishing a migrated list from a
client-minted one; both commit to the list's real creation date. Validated
against prod before relying on it: the split came out 22/6, matching the
migration's own run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both of next.1's changes are in surfaces we do not call:

  - getAnchoringsForDidCel, so production ord providers can verify
    btco-anchored did:cel assets. We do no Bitcoin anchoring — the SDK
    references in convex/bitcoinAnchors.ts are commented-out scaffolding.
  - addResourceVersion now hosts the new bytes for a published did:webvh
    asset. We never call addResourceVersion or publishToWeb.

Verified rather than assumed, since "we don't use it" is exactly the reasoning
that missed NO_CUSTODY in the migration: typecheck, 158 tests, a production
build, and a headless run of the built bundle. Asset creation, fresh and
2.1.0-sealed verification, and tamper-rejection all behave identically to
next.0, with no console errors. The bundle moves +0.8 kB.

@originals/auth has no next.1 and stays on 3.0.0-next.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Publishing wrote a publications row with a hand-built did:webvh and never
touched the list's CEL log. The chain that exists to record an asset's history
was silent about the most significant thing that happens to one: 10 lists are
published in prod and not one of them says so.

recordPublishedVersion appends a signed `update` event carrying a content hash
of what went public — name and items, via buildListSnapshot. The log then
carries a verifiable version history: genesis is v1, the first publish v2, and
a later publish of changed content v3.

Deliberately NOT publishToWeb, despite that being the SDK's publish surface.
publishToWeb migrates the asset did:cel -> did:webvh, which would change the
assetDid every row is keyed by and move the public URL to a content-addressed
key. boop serves published lists live at a stable path, with POST endpoints for
checking items off — an immutable snapshot at a hash URL is a different product.
addResourceVersion gets the provenance without touching identity or URLs.

Two behaviours worth knowing:

Re-publishing unchanged content is a no-op rather than an error. The SDK
refuses a version identical to the current one, and a user re-publishing an
unchanged list has not done anything wrong; the log simply must not grow.

A list whose key was lost to the celAssetDids migration still publishes. It
cannot sign the event, so it gets ListNotAuthorableError, and the modal says
publishing succeeded but the history could not be recorded and points at the
copy action. Failing the publish outright would regress the feature for the 5
published lists in that state.

Known gap, not addressed here: checking an item off a published list goes
through server-side POST endpoints, and the server holds no key, so live edits
after publish are not recorded. Only publishes are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brianorwhatever
brianorwhatever merged commit 60aa025 into main Aug 17, 2026
7 checks passed
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