Repository navigation
next: private collection create and device enrol - #621
Conversation
…lections Add next_service_devices (0048): hosted/escrow members of cloud_copy collections only, via the (collection_id, sync) composite key. The control plane stores public keys and the deployment's KMS-wrapped keys and never unwraps them. - service-devices.ts: strict record parsing (canonical 32-byte hex keys, canonical base64 wrapped keys <= 64 KiB, arn), idempotent put-first store that refuses a different device, lookup that requires a current cloud copy under a share lock, and a bounded HTTPS-only generate client (no redirects, timeout, 128 KiB response cap) for the C2 bootstrap. - hosted-routes.ts: GET collections/:id/service-devices/:kind and POST service-devices/:device/log-token, kind-matched to the caller's internal token, 409 unless standard. Tokens are role 0, name the device, its sign key and one collection, and last 15 minutes.
…OOTSTRAP) Per the coordinator's sealed-envelope 7.1 decision: the owner's device signs a fresh challenge; the control plane asks the hosted and escrow deployments to generate their service devices (outbound tokens distinct from inbound), then registers a cloud-copy genesis that enrols the owner's device and both service devices (zero account) and stores the records. The desktop performs the initial rekey with wraps for desktop, hosted and escrow; the CP never holds a collection key and makes no escrow-to-hosted wrap. Retries must come from the enrolled device and reuse stored records; other owners, other devices and existing private collections get 409. A failed generation registers nothing. Success is reported only after the log returns the exact genesis bytes.
…mint; CHECK key and wrapped sizes; validate every writer Review (control): fetch and mint now run in one transaction under FOR SHARE of the collection row, so a concurrent leave waits for the mint and a later fetch/mint sees it. 0048 CHECKs 32-byte keys and 1..64 KiB wrapped keys (pg-mem gains octet_length); storeServiceDevice validates records from any writer. Deployments need not be idempotent: the CP's first stored record wins.
…try after leaving sync
…await; refuse nil IDs Review (control): after generation and after the log read-back, the owner's connector (not revoked), account (not suspended) and device (same keys) are re-read under FOR SHARE, and the final transaction checks the collection is still this owner's current cloud copy and the device the genesis enrolled before minting. PG tests for revoke, suspend and device removal during generation, and leave/revoke during the read-back. Nil collection/device IDs are refused.
…-zero noise_pk except for recovery)
…on answers busy Review (control): every transaction sets lock_timeout 5s like C1; a timeout answers 503 busy with no driver detail, and registers nothing. PG regression holds the collection lock from another session.
…oin, per Callum's 2026-10-06 decision - POST /v1/next/collections/cloud-copy/service: account session; genesis [genesis cloud-copy, member-set owner, enrol hosted, enrol escrow]; hosted is the first member and keys the collection. Same phases as the owner path (generate with no lock held, recheck account and collection under lock, exact genesis read-back), no token. - POST /v1/next/collections/:id/devices: a device registered under the owner's connector, with a join-domain proof, is enrolled via queueNextPolicy only into a current cloud copy (private, foreign or left collections refuse before any op); idempotent for the same keys; the token is minted after the enrol batch reads back exactly. - The owner-device create path stays. Tests cover genesis ops, retries, session/suspension/foreign refusals, private exclusion (no outbox row), join proof binding and revocation.
… rechecked after awaits; full enrolment tuple; revoked devices never revive Review (control, security-2) on 336b474: - join: currentIdentity (connector/account/device, exact keys) under FOR SHARE before the enrolment is queued and through its commit; PG barrier: a revocation racing the request wins and nothing is queued. - service-created: the session itself (not revoked/expired, same session epoch, account active) is rechecked under lock after every await (Tailscale identities: the account). - idempotent re-join and the owner-path check compare the whole immutable tuple (device, account, kind, sign/KEM/Noise keys). - a device with a later device-revoke is never accepted again.
…hind MDBASE_NEXT_PRIVATE_BOOTSTRAP=1 Create: owner's registered device; e2e genesis enrolling only that device. Enrol: a current member's registered device with its sas_commit (key 7); no key until an existing keyed device approves and grants. Shared proof/identity/ membership helpers move from cloud-copy-bootstrap.ts to bootstrap-common.ts.
…; bootstrap transactions bound every statement Per control/security-2 on #621: P1's final locked transaction adds currentMember (a pending member-remove refuses); inTransaction sets statement_timeout 5s and 57014 answers 503 busy like lock_timeout (shared by the cloud-copy routes).
|
Control SOURCE OWNER ACK exact 0940c0c (base #616 0e523eb). Read private route/common helpers, app/config/extraction delta and PostgreSQL test source; final locked P1 membership check after log awaits and 5s transaction statement/lock bounds with timeout refusal correct the source findings. Current whole identity, private/NEXT/not-left mode, exact enrollment/SAS commitment, effective membership and revoke guards remain required before queue/mint. Private genesis/enrollment remain service-free and credential-only; existing keyed user-device SAS approval/key_grant delivers the key. This is independent source review, not independent PG execution or a PG after-await barrier result. Author tests, S2 model execution/security and actual keying/Ready/runtime acceptance are separate. Coordinator main-merge hold until beta129 tag remains in force. |
|
security: no blocking findings at 0940c0c, base 0e523eb (scoped private-create/SAS-enrollment CP delta). Reviewed private-only owner/self genesis, registered full device identity, SAS-commitment-bound single-use proof, locked current membership before enrollment and after awaits, exact batch readback, idempotence/revoke refusal, and shared-helper extraction. The final create membership recheck and 5s statement/lock bounds with retryable timeout refusal correct the scoped source concerns. Independently executed 15 hermetic actual-digest/HTTP/helper and SQL-query-model checks, including removal during the head await: PASS. These are not independent PostgreSQL, actual log/genesis-to-Replica, SAS approval/key-grant or native execution; author PG and S1 structural/user-keying review are separate evidence. CP issues credentials/enrollment only, not epoch keys or approval. No service actor/recipient, conversion, Ready, custody/live/whole-consumer or approval-UI ship clearance is conveyed. |
Stacked on #616 (
next/cloud-copy-create). Coordinator 2026-10-06: build the private create and private device-enrol routes; control reviews.Behind
MDBASE_NEXT_PRIVATE_BOOTSTRAP=1(0/1; anything else is refused at startup).POST /v1/next/collections/privateH("mdbase/v1/private-create", cbor[challenge, connector, device, collection])over a fresh single-use challenge.genesis(e2e),member-set owner, anddevice-enrolof that device only. No network call runs inside it, and no hosted or escrow device is ever enrolled (assertPrivateOpsalso guards this).sync='private' AND runtime='next' AND left_sync_at IS NULL, no revoke and the exact enrolment in the first outbox row are then rechecked under locks before the token is minted.rekey_recipients: [device](advisory; that device signs the initial rekey wrapped to itself) and a log token.POST /v1/next/collections/:id/private/devicessas_commit(32 bytes), with its connector.H("mdbase/v1/private-device-enrol", cbor[challenge, connector, device, collection, sas_commit]), so the commitment is bound to the proof.member-removecounts even while pending,member-setonly once appended; LIMIT 1, the same projection as next: device log-token refresh for synced collections #618);device-enrolwith key 7 =sas_commit, drains, and verifies the exact appended bytes.{enrolled_at, approval: "pending", device token}. The device holds no key until an existing keyed device approves it by SAS (chore: move public packages to mdbase-dev scope #144 in mdbase-next) and appends akey_grant.device_enrolled_differently; re-committing is the device's ownapproval-request, never this route.Refactor. The shared helpers (
authenticate,currentIdentity,currentSession,currentAccount,inTransaction,lock, the enrolment lookups,refuseRevokedandrefuse) move unchanged fromcloud-copy-bootstrap.tstobootstrap-common.ts, pluscurrentMember. The cloud-copy routes are behaviourally unchanged; its 23 PG tests still pass.Tests (
private-collections.postgres.test.ts, 9):sas_commitis refused);sas_commiton the log, idempotent, and a different commitment refused;not_memberwhile themember-setis pending, then enrolled), and a removed member refused;Six mutations were checked (membership,
sas_commitin the digest, the runtime check, the sync check, the commitment-exact idempotency and the revoke check), and each fails a test.Validation:
src/features/next+ app: 236 passed, 1 skipped.architecture.d/next-private-collections.json) and the changelog check passes.Review: control (owner), security.