Skip to content

Account/device ledger schemas (0149/0243 Phase 2 foundation)#328

Merged
crs48 merged 1 commit into
mainfrom
claude/0243-account-ledger
Jun 28, 2026
Merged

Account/device ledger schemas (0149/0243 Phase 2 foundation)#328
crs48 merged 1 commit into
mainfrom
claude/0243-account-ledger

Conversation

@crs48

@crs48 crs48 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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 — stable accountId, controllers, current epoch.
  • DeviceRecord — a device DID admitted to the account, with status + 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 enforceresolveActiveDevices and isDeviceAuthorized ("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, like Grant/SchemaDefinition. 10 new ledger tests; the full @xnetjs/data + devtools-seed suites (1735 tests) stay green.

Remaining Phase 2 (follow-ups)

  • Hub enforcement of controller signatures + epochs on ledger writes.
  • admitDevice / revokeDevice with content-key re-wrap to the new device (computeRecipients / sealed box) — P2.3.
  • Migrate TenantBinding.did → account root with a back-compat read path — P2.2.

Changeset: @xnetjs/data minor. No user-visible behavior yet → skip-changelog.

🤖 Generated with Claude Code

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>
@crs48 crs48 temporarily deployed to pr-328 June 28, 2026 23:45 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jun 28, 2026
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #328.

github-actions Bot added a commit that referenced this pull request Jun 28, 2026
@crs48 crs48 merged commit 9d41a28 into main Jun 28, 2026
16 of 17 checks passed
@crs48 crs48 deleted the claude/0243-account-ledger branch June 28, 2026 23:51
github-actions Bot added a commit that referenced this pull request Jun 28, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant