feat(app)!: LIB-5 wire rename — roster v2 signer + dual-read consumer (workspaceId→libraryId)#138
Merged
Merged
Conversation
…(workspaceId→libraryId) The final, breaking phase of "workspace → library": rename the wire field `workspaceId` → `libraryId`, including the copy that lives INSIDE the signed roster assertion. Because the key name is part of the Ed25519-signed bytes the rename is versioned, not a flag-day. SDK signer/verifier (packages/sdk/src/forwarding/rosterAssertion.ts): - signer emits `openbook.roster.v2` carrying `libraryId` (SAME id value); add `ROSTER_ASSERTION_V2` + `ACCEPTED_ROSTER_VERSIONS`. - mirror verifier DUAL-ACCEPTS v1 (`workspaceId`) + v2 (`libraryId`), fail-closed: version pinned from the authenticated body BEFORE crypto; signed prefix reconstructed from `payload.v` (never hardcoded); audience field chosen by an explicit per-version map (v1→workspaceId, v2→libraryId), never a default-to-v1 ternary; non-empty audience required; symmetric ±5min freshness. SDK types/routes: - provenance: WorkspaceBinding→LibraryBinding, WorkspaceRoster(Entry)→LibraryRoster(Entry), InstanceConfig.workspaceBinding→libraryBinding (all workspaceId→libraryId). - routes: add `librarySync: '/api/library/sync'`; keep `workspaceSync` as an alias. - barrel exports updated. Server consumer: - rosterSync: parseWorkspaceRoster→parseLibraryRoster (reads `libraryId ?? workspaceId`); fetch `/api/libraries/:id/roster` with `/api/workspaces/:id/roster` fallback; ResolvedBinding/RosterSyncStatus `workspaceId`→`libraryId`. - app: register BOTH `/api/library/sync` and legacy `/api/workspace/sync` (shared handlers). - server: workspaceSyncToken→librarySyncToken (OPENBOOK_WORKSPACE_SYNC_TOKEN env kept). Tests: new SDK invariant matrix + exported canonical v2 vectors for cross-repo byte-check; rosterSync.test.ts updated (v2 signer, dual-read, alias routing). Signing is inert in prod today; per the runbook deploy AFTER the account verifier dual-accepts. Does NOT change id VALUES (cuids) or the double-audience bind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
LIB-5 — the wire rename (app side)
The final, breaking phase of workspace → library: rename the wire field
workspaceId→libraryId, including the copy inside the signed roster assertion. Because the key name is part of the Ed25519-signed bytes, this is versioned, not a flag-day — the signer emitsopenbook.roster.v2, the verifier dual-accepts v1 + v2 (fail-closed). Ships the v2 signer + dual-read consumer only; a parallel PR does the account/edge verifier + registry.Changes
SDK signer/verifier —
packages/sdk/src/forwarding/rosterAssertion.tsopenbook.roster.v2carryinglibraryId(SAME id value, renamed key).ROSTER_ASSERTION_V2,ACCEPTED_ROSTER_VERSIONS,isAcceptedRosterVersion, and a normalizedVerifiedRosterAssertionreturn.workspaceId) + v2 (libraryId).SDK types/routes
provenance.ts:WorkspaceBinding→LibraryBinding,WorkspaceRoster(Entry)→LibraryRoster(Entry),InstanceConfig.workspaceBinding→libraryBinding(allworkspaceId→libraryId).routes.ts: addlibrarySync: '/api/library/sync'; keepworkspaceSync: '/api/workspace/sync'as an alias.index.ts,forwarding/index.ts) updated.Server consumer
rosterSync.ts:parseWorkspaceRoster→parseLibraryRoster(accepts response keylibraryId ?? workspaceId); fetch/api/libraries/:id/rosterwith/api/workspaces/:id/rosterfallback (same bearer, one freshness window);ResolvedBinding/RosterSyncStatusworkspaceId→libraryId.app.ts: register both/api/library/syncand legacy/api/workspace/sync(shared handlers).server.ts:workspaceSyncToken→librarySyncToken(envOPENBOOK_WORKSPACE_SYNC_TOKENkept as fallback).Invariant compliance (1–11)
isAcceptedRosterVersion(payload.v)gates beforeverifyWithSiteKeypayload.v, never hardcodedrosterAssertionMessage(version, …)uses the authenticatedversionworkspaceId, v2→libraryId), not a default-to-v1 ternaryAUDIENCE_KEY_BY_VERSION[version]aud !== input.libraryId → null; values untouchedMath.abs(now - ts) > skewpayload.pubnull;try/catch → nullrosterAssertion.test.ts:id-bind preserved on the account side; SDK verify requires explicitlibraryIdmatch/api/workspace/sync+/api/workspaces/:id/rosterfallback +workspaceIdresponse fallbackv2 test vectors (exported for cross-repo byte-check)
Fixed keypair + payload → the exact bearer (
signRosterAssertionreproduces it byte-for-byte; Ed25519 is deterministic). Exported asV2_VECTORinpackages/sdk/src/forwarding/rosterAssertion.test.ts:Signed message =
openbook.roster.v2.<payloadB64>. The account verifier, givenprivateKey+ the same payload, MUST produce this exactbearer.The invariant-9 matrix (SDK
rosterAssertion.test.ts)v1-sign→verify ✓ · v2-sign→verify ✓ · v1 body carrying
libraryId→reject · v2 body carrying onlyworkspaceId→reject · v2 wrong-audience→reject · unknown/missingv→reject · version-flip-can't-break-sig→reject · stale and futurets→reject · stripped/empty/3-part/wrong sig→reject · wrong pubkey→reject · tampered payload→reject. Plus the canonical-vector reproduce + verify.Verify —
pnpm run verify= exit 0build:libs + typecheck + lint + test + test:e2e, all green:
DEPLOY NOTE
Ships the v2 signer + dual-read consumer. Per the LIB-5 runbook, deploy AFTER the account verifier dual-accepts (v1+v2) is confirmed live — flipping the signer is the one-way gate. Signing is inert in prod today (the desktop
rosterAssertionProvideris unwired), so live risk is low. Does not change id VALUES (cuids) or the double-audience bind. OWNER-MERGED + owner-deployed — do not merge from here.🤖 Generated with Claude Code
https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w