Skip to content

feat(app)!: LIB-5 wire rename — roster v2 signer + dual-read consumer (workspaceId→libraryId)#138

Merged
eliotlim merged 1 commit into
mainfrom
lib-5-app-roster-v2
Jul 13, 2026
Merged

feat(app)!: LIB-5 wire rename — roster v2 signer + dual-read consumer (workspaceId→libraryId)#138
eliotlim merged 1 commit into
mainfrom
lib-5-app-roster-v2

Conversation

@eliotlim

Copy link
Copy Markdown
Owner

LIB-5 — the wire rename (app side)

The final, breaking phase of workspace → library: rename the wire field workspaceIdlibraryId, 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 emits openbook.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/verifierpackages/sdk/src/forwarding/rosterAssertion.ts

  • Signer emits openbook.roster.v2 carrying libraryId (SAME id value, renamed key).
  • Added ROSTER_ASSERTION_V2, ACCEPTED_ROSTER_VERSIONS, isAcceptedRosterVersion, and a normalized VerifiedRosterAssertion return.
  • Mirror verifier dual-accepts v1 (workspaceId) + v2 (libraryId).

SDK types/routes

  • provenance.ts: WorkspaceBindingLibraryBinding, WorkspaceRoster(Entry)LibraryRoster(Entry), InstanceConfig.workspaceBindinglibraryBinding (all workspaceIdlibraryId).
  • routes.ts: add librarySync: '/api/library/sync'; keep workspaceSync: '/api/workspace/sync' as an alias.
  • Barrel exports (index.ts, forwarding/index.ts) updated.

Server consumer

  • rosterSync.ts: parseWorkspaceRosterparseLibraryRoster (accepts response key libraryId ?? workspaceId); fetch /api/libraries/:id/roster with /api/workspaces/:id/roster fallback (same bearer, one freshness window); ResolvedBinding/RosterSyncStatus workspaceIdlibraryId.
  • app.ts: register both /api/library/sync and legacy /api/workspace/sync (shared handlers).
  • server.ts: workspaceSyncTokenlibrarySyncToken (env OPENBOOK_WORKSPACE_SYNC_TOKEN kept as fallback).

Invariant compliance (1–11)

# Invariant Status
1 Version pinned from the authenticated decoded body before crypto isAcceptedRosterVersion(payload.v) gates before verifyWithSiteKey
2 Signed prefix reconstructed from payload.v, never hardcoded rosterAssertionMessage(version, …) uses the authenticated version
3 Audience field via explicit per-version map (v1→workspaceId, v2→libraryId), not a default-to-v1 ternary AUDIENCE_KEY_BY_VERSION[version]
4 Non-empty audience required ✅ `if (typeof aud !== 'string'
5 Audience bind — id VALUE unchanged (cuid), only the key renamed aud !== input.libraryId → null; values untouched
6 Symmetric ±5min freshness (rejects stale and future) Math.abs(now - ts) > skew
7 Possession — Ed25519 verify against payload.pub
8 Fail-closed on any bad framing/missing field/bad sig ✅ every branch returns null; try/catch → null
9 Dual-accept matrix + canonical v2 vectors (test) ✅ new rosterAssertion.test.ts
10 Double-audience bind not broken; authz never defaults open ✅ route :id-bind preserved on the account side; SDK verify requires explicit libraryId match
11 Legacy path/alias kept so no in-flight caller is stranded /api/workspace/sync + /api/workspaces/:id/roster fallback + workspaceId response fallback

v2 test vectors (exported for cross-repo byte-check)

Fixed keypair + payload → the exact bearer (signRosterAssertion reproduces it byte-for-byte; Ed25519 is deterministic). Exported as V2_VECTOR in packages/sdk/src/forwarding/rosterAssertion.test.ts:

publicKey : V1bSKWux63CaNSd4aS0MzZ_VFwVB05B6YOwn-4BWV7Q
privateKey: MC4CAQAwBQYDK2VwBCIEIO3ux9SxxRdqR6MruNpKlIxvnKj5kVhAT7ar4kSSXWTE
libraryId : clib_5xample0000000000000
ts        : 1720000000000
payloadB64: eyJ2Ijoib3BlbmJvb2sucm9zdGVyLnYyIiwicHViIjoiVjFiU0tXdXg2M0NhTlNkNGFTME16Wl9WRndWQjA1QjZZT3duLTRCV1Y3USIsImxpYnJhcnlJZCI6ImNsaWJfNXhhbXBsZTAwMDAwMDAwMDAwMDAiLCJ0cyI6MTcyMDAwMDAwMDAwMH0
signature : eC_dv4DnLZtHBt3A5uLff0lJJy8u7i-BZGd2DLPKVZh8Aw_5urHwbc5XfHo68OaZauLvhm_Li0nWynWya01zCg
bearer    : eyJ2Ijoib3BlbmJvb2sucm9zdGVyLnYyIiwicHViIjoiVjFiU0tXdXg2M0NhTlNkNGFTME16Wl9WRndWQjA1QjZZT3duLTRCV1Y3USIsImxpYnJhcnlJZCI6ImNsaWJfNXhhbXBsZTAwMDAwMDAwMDAwMDAiLCJ0cyI6MTcyMDAwMDAwMDAwMH0.eC_dv4DnLZtHBt3A5uLff0lJJy8u7i-BZGd2DLPKVZh8Aw_5urHwbc5XfHo68OaZauLvhm_Li0nWynWya01zCg

Signed message = openbook.roster.v2.<payloadB64>. The account verifier, given privateKey + the same payload, MUST produce this exact bearer.

The invariant-9 matrix (SDK rosterAssertion.test.ts)

v1-sign→verify ✓ · v2-sign→verify ✓ · v1 body carrying libraryId→reject · v2 body carrying only workspaceId→reject · v2 wrong-audience→reject · unknown/missing v→reject · version-flip-can't-break-sig→reject · stale and future ts→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 0

build:libs + typecheck + lint + test + test:e2e, all green:

  • unit: sdk 188, ui 981, server 652
  • e2e: server 251/251 checks, mcp 40/40 checks

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 rosterAssertionProvider is 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

…(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
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app.book.pub Ready Ready Preview, Comment Jul 12, 2026 11:20pm

Request Review

@eliotlim eliotlim changed the title feat(app): LIB-5 wire rename — roster v2 signer + dual-read consumer (workspaceId→libraryId) feat(app)!: LIB-5 wire rename — roster v2 signer + dual-read consumer (workspaceId→libraryId) Jul 13, 2026
@eliotlim eliotlim merged commit 3ef6c91 into main Jul 13, 2026
10 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