Account/device ledger schemas (0149/0243 Phase 2 foundation)#328
Merged
Conversation
The data-plane foundation for the account/device ledger: AccountRecord, DeviceRecord, RecoveryRecord, RevocationRecord. A stable account subject owns records of which devices may act as it, which recovery methods exist, and which keys are revoked (status + epoch), so the cloud binding can later pin to the account root instead of one device DID (completes P2.1). - Deterministic ids (accountRecordId/deviceRecordId/…) for upsert. - Pure resolution the hub will enforce: resolveActiveDevices + isDeviceAuthorized. - Registered in builtInSchemas, seed-excluded (identity infra), and authorization-exempt (controller-signed + epoch-gated, hub-enforced — not a per-node role cascade). 10 ledger tests; 1735 data+seed tests green. Signing enforcement, grant/recipient migration to account subjects, and the TenantBinding.did→account migration are the remaining Phase 2 work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: xNet Test <test@xnet.dev>
Contributor
|
✓ Changelog fragment found — thanks! |
Contributor
|
Preview removed for PR #328. |
crs48
added a commit
that referenced
this pull request
Jun 29, 2026
Builds on the [#328](#328) ledger schemas — the operation layer of **Phase 2** of [exploration 0243](docs/explorations/0243_[_]_ACCOUNT_VALIDATION_AND_RECOVERY_BINDING_THE_PAYER_TO_THE_PASSKEY.md). ## What this adds Pure builders in `@xnetjs/data` ([account-ledger-ops.ts](packages/data/src/schema/schemas/account-ledger-ops.ts)) that turn a ledger intent into the deterministic node to upsert: - `createAccountRecord` — the account root at epoch 0. - `admitDeviceRecord` — admit a device DID to the account (upsert by account+device). - `revokeDeviceRecord` / `revokeSubjectRecord` — revoke a device/recovery key and **bump the account epoch** so stale authorizations are detectable. - `accountState` — resolve the current epoch and the set of devices that may currently act as the account. Keeping these pure makes the admit/revoke/epoch rules unit-testable in isolation (7 new tests; full `@xnetjs/data` suite green). The store/hub wiring (controller-signature enforcement) and the **content-key re-wrap** that rides on `admitDevice` are the next steps toward **P2.3**; the `TenantBinding.did → account` migration is **P2.2**. Changeset: `@xnetjs/data` minor. No user-visible behavior → `skip-changelog`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 29, 2026
…330) **P2.2** of [exploration 0243](docs/explorations/0243_[_]_ACCOUNT_VALIDATION_AND_RECOVERY_BINDING_THE_PAYER_TO_THE_PASSKEY.md) — migrate the cloud binding from "one device DID" to a stable account root. ## What this changes `TenantBinding` gains a stable **`account`** subject (`xnet:account:…`) that survives a recovery/rebind, so the billing identity pins to the *account root* rather than a single device key — the cloud-side half of the 0149 account model. `did` is retained as "the currently-bound device key". - Set once at first bind (`accountSubjectForDid`), then **preserved** through `recoverPaidAccount` and `completeRebind` — so the account id stays fixed while the device DID changes across a lost-passkey recovery. - **Back-compat:** bindings written before this field resolve via `bindingAccount()` (falls back to deriving from `did`). The field is optional, so stored (Firestore) bindings keep working untouched. A future ledger-backed `AccountRecord` ([#328](#328)) can reconcile to this id. 8 binding tests (incl. account-survives-rebind) + 60 apps/cloud consumer tests stay green; `@xnetjs/cloud` typechecks clean. `@xnetjs/cloud` is changeset-ignored (FSL) and there's no user-visible behavior → `skip-changelog`. ## Remaining Phase 2 - **P2.3 content-key re-wrap** — the one deep, forward-looking piece left: it integrates the account→active-devices resolution into `computeRecipients` so a newly-admitted *distinct-DID* device can decrypt existing data. (Not needed for today's single-DID phrase recovery, where the recovered device keeps the same DID.) Best as a focused follow-up alongside hub controller-signature enforcement. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 29, 2026
Completes **P2.3** of [exploration 0243](docs/explorations/0243_[_]_ACCOUNT_VALIDATION_AND_RECOVERY_BINDING_THE_PAYER_TO_THE_PASSKEY.md) — the content-key re-wrap that lets a user's data follow their devices, on top of the ledger schemas (#328) and operations (#329). ## What this adds `computeRecipients` gains an optional **`expandDeviceRecipients`** dependency: each DID recipient expands to every *currently active* device of the account it belongs to (built from ledger records via the new **`deviceRecipientExpander`**). So: - **Admit** a device (a `DeviceRecord`) → it becomes a recipient on the next recompute → it can decrypt the account's data. - **Revoke** a device (a `RevocationRecord`) → it's dropped from future re-wraps. - An identity that belongs to **no account expands to only itself** → an unrelated DID never gains access to another account's data (privacy guarantee holds). - **Omitting** the dependency leaves recipients exactly as before — fully additive, no behavior change for today's single-DID paths. 8 new tests (4 pure expander + 4 `computeRecipients` integration covering admit/revoke/no-leak/no-op); the full `@xnetjs/data` suite (1729 tests) stays green. ## Status This checks **P2.3** plus the "admit grants / revoke removes" and "unrelated DID can't decrypt" validation items. The doc is now **11/13** implementation, **7/9** validation. Remaining are the two deliberately-deferred items: **P1.4** (synced-passkey surfacing — convenience) and **P3.1** (opt-in WorkOS-gated KMS escrow — optional, privacy tradeoff). Changeset: `@xnetjs/data` minor. No user-visible behavior wired yet → `skip-changelog`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
First step of Phase 2 of exploration 0243 — the data-plane foundation for the 0149 account/device ledger. Completes P2.1.
What this adds
Four schemas in
@xnetjs/data(account-ledger.ts) so a stable account subject can own a signed, syncable record of its devices and recovery methods — the prerequisite for binding the cloud subscription to an account root instead of a single device DID:AccountRecord— stableaccountId, controllers, currentepoch.DeviceRecord— a device DID admitted to the account, withstatus+epoch.RecoveryRecord— a registered recovery method (commitment only, never the secret).RevocationRecord— a signed revocation of a device/recovery key that bumps the epoch.Plus deterministic ids for upsert and the pure authorization resolution the hub will enforce —
resolveActiveDevicesandisDeviceAuthorized("is this device currently authorized for this account?").Registered in
builtInSchemas, seed-excluded (identity infrastructure, not user content), and authorization-exempt at the schema level — access is controller-signed and epoch-gated (hub-enforced), not a per-node role cascade, likeGrant/SchemaDefinition. 10 new ledger tests; the full@xnetjs/data+ devtools-seed suites (1735 tests) stay green.Remaining Phase 2 (follow-ups)
admitDevice/revokeDevicewith content-key re-wrap to the new device (computeRecipients/ sealed box) — P2.3.TenantBinding.did→ account root with a back-compat read path — P2.2.Changeset:
@xnetjs/dataminor. No user-visible behavior yet →skip-changelog.🤖 Generated with Claude Code