next: cloud-copy collections — service-created, owner-device, and device join (cloud-copy bootstrap C2) - #616
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
|
Merged C1's review fixes (now 4537913). Changes:
Results: server 786 passed; next/* 164 passed against PG16. |
…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.
|
Control touched-path/source owner ACK at exact C2 head 2e1a32b, stacked on C1 f3076cd. Independently reviewed production bootstrap/app/config and the currentness/nil/lock-bound deltas plus regression source. Owner findings are closed in source: after remote generation and log read-back, current connector/account/device and immutable tuple are rechecked and locked; final current collection/enrolment/record lookup and token mint occur under transaction locks; nil IDs fail; all bootstrap transactions set 5s lock_timeout, with no network inside them. Exact persisted genesis/log read-back equality and first-record retention remain; escrow has a genuine unused Noise key, no validator change or endpoint. This is identity/genesis provisioning only, not legal epoch keying or Ready; rekey_recipients is advisory historical genesis identity data, and the consumer derives legal/current recipients and owner authority from the verified log. Source review only: no independent author-test rerun, IAM/private-material verification, actual policy-validator/LAB/epoch/Ready acceptance. Initial lock failure returns busy without registration; later post-commit uncertainty remains not_ready with retained state, not a no-side-effects claim. Scoped security and actual CI qualification remain required. |
…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.
|
Reworked at 336b474 for Callum's 2026-10-06 decision, which replaces §7.1 owner-only keying for cloud copy only. Private collections are unchanged. New routes
Unchanged
Tests
Results: next/* 175 passed against PG16; server suite 786 passed; architecture and changelog checks pass. |
… 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.
|
Control touched-path/source owner ACK at exact new-rule head 0e523eb. Reviewed 2e→336 service/account-join production changes and 336→0e fixes, app/config/docs and new regression source; also checked existing session/authentication helper semantics. Source findings are closed: join locks current connector/account/device/full tuple before enqueue or idempotent acceptance; cookie sessions are rechecked with revocation/expiry/account-session epoch under lock after remote work; whole enrollment tuple is compared; recorded device revocation denies retry/final mint. Tailscale-auth mode uses its existing actual Tailscale identity path (not a cookie-session fallback). Existing bounded transactions, no network within them, strict cloud-copy/private rejection, exact batch read-back, nil checks and retained unknown outcomes carry. This is component/source review ONLY: author 178/786 are not my test executions; no actual policy-validator/service keying or wrapping, IAM, LAB/app-serving or Ready qualification. Response metadata/first-member labels are not authority; legal current mode, membership, signer/recipient and epoch at delivery remain verified-log/consumer obligations. Private→cloud conversion is NOT implemented. Scoped security and actual CI remain required; ordinary device log-token refresh is a separate follow-up, not private-mode expansion here. |
|
security: no blocking findings at 0e523eb, base f3076cd (scoped CloudCopy creation/device-join source review under the revised cloud-copy rule). Reviewed account-session revalidation after awaits and locked current connector/account/full-device identity before enrollment publication, exact genesis/enrollment readback, canonical namespace and domain-separated single-use proof, full-tuple idempotence and later-revoke refusal. Earlier scoped source concerns are corrected here. This is source review; author PG/barrier/suite results, owner ACK and CI metadata are separate—not my independently executed PG or actual genesis-to-Replica interoperability. No private-create/private-join or private-to-cloud conversion authorization, mutable epoch-key delivery, KMS custody, Ready, live acceptance or deployment qualification is conveyed. |
Cloud-copy bootstrap, PR C2. It is stacked on #615 (C1), so review only the last commit until #615 merges. It follows the coordinator's sealed-envelope §7.1 decision: only the owner's device keys the hosted replica. On create, the owner's desktop performs the initial rekey with wraps for desktop + hosted + escrow, and there is no automatic escrow→hosted wrap. Control owns these paths and ACKed the direction; security-2 review requested.
Route
POST /v1/next/collections/cloud-copy {collection_id, device_id, challenge, sig}H("mdbase/v1/cloud-copy-create", cbor[challenge, connector, device, collection]), using a challenge from/v1/next/devices/challenge, consumed once.MDBASE_NEXT_CLOUD_COPY_BOOTSTRAP=1. That setting requires:MDBASE_NEXT_{HOSTED,ESCROW}_SERVICE_URL(https) andMDBASE_NEXT_{HOSTED,ESCROW}_SERVICE_TOKEN;cache-control: no-store.Flow
genesis(cloud-copy),member-set(owner),device-enrol(owner device), thendevice-enrol(hosted, zero account)anddevice-enrol(escrow, zero account). Store both records in the same transaction.The response carries:
rekey_recipients= [owner device, hosted, escrow];A retry must come from the enrolled device: its device-enrol, with the same sign key, must be in the genesis outbox row. It reuses the stored records and does not call the deployments. Other devices, other owners and existing private collections get 409. A failed or mismatched generation registers nothing and returns 503, so the client retries with a fresh proof.
Not here
standardhere is only bootstrap eligibility.Tests
cloud-copy-bootstrap.postgres.test.tsruns against a real Postgres, a fake log and fake deployments:Results: server
pnpm test786 passed;src/features/nextwith PG16, serial: 160 passed. Typecheck, architecture and changelog checks pass.