# OpenSpec Change: drive-template-v1 - **Change ID**: drive-template-v1 - **Package**: `@suaveplan/drive-template` (new — types + codecs + validators), with surface additions to `@suaveplan/drive` (`hydrateFrom`, `getKeyStatus`, `getPresence`) - **Capability**: `drive-template` (new) - **Status**: PROPOSED - **Date**: 2026-05-02 --- ## Summary Define `DriveTemplate` — a server-signed configuration that pre-establishes a drive's folder structure, file metadata, encryption keys, and policies before any bytes are present locally. The drive lazily hydrates from peers (via `drive-mesh-protocol`) and falls back to a CDN with a reversed-ID URL convention. Adds per-file policy flags (`autoDownload`, `meshAllowed`, `cdnAllowed`, `retention`), inline and retroactive key distribution paths, lock-icon UX (`getKeyStatus`), progress UX (`getPresence`), and replay-protected template signature verification. This is the "ship a 2 GB game asset bundle to N players, see lock icons that flip when DLC unlocks, watch progress bars fill as peers seed" feature — the production scenario the user described. --- ## Why The current drive design assumes drives are populated by writes. It has no concept of "the structure is known up front; the bytes arrive later." Concrete gaps that this proposal closes: 1. **No dehydrated drive concept.** A drive issuer cannot publish a description of what *will* exist in a drive without already having the bytes locally and writing them through. (See [`docs/system-review.md`](../../../docs/system-review.md) §3 — Blocker.) 2. **No per-file policy flags.** Users today cannot say "this file is mesh-only" or "this file should auto-download but only from the CDN, not from peers." Policy is global per drive. (See §5 — Blocker.) 3. **No CDN URL convention.** `drive-transport-https` is described as "server fallback" but the URL layout is unspecified. Without a convention, every deployment hand-rolls one. (See §4 — Major.) 4. **No retroactive key distribution.** The user's brief calls for "decryption keys could be included as part of the signed config, or would have to be retro-actively added to the drive for files to be unlocked." Today there's only the share-grant flow inside `drive-sharing`. (See §8 — Major.) 5. **No lock-icon UX contract.** No drive method tells the UI whether a file is encrypted-locked, encrypted-unlocked, or unencrypted. (See §6 — Major.) 6. **No progress UX contract.** `Drive.list()` omits chunk presence/total, so a UI cannot render `███░░░░░░░ 33%` next to a file. (See §7 — Major.) 7. **No retention strategy.** A drive with `autoDownload` enabled fills disk forever. No GC policy is specified. (See §9 — Major.) 8. **No CDN write-side authentication.** Without server-issued upload tokens, anyone with the URL could poison the CDN. (See §12 — Major.) 9. **No template replay protection.** A captured template within its expiry could be replayed against a fresh client. (See §13 — Major.) The architecture this proposal codifies is documented in [`docs/drive/dehydrated-drives.md`](../../../docs/drive/dehydrated-drives.md). This change folder is the formal OpenSpec mandate. --- ## What changes ### ADDED — `drive-template` capability spec 26 ADDED requirements under `specs/drive-template/spec.md`: - **`DTP-01..04`** — `DriveTemplate` shape, schema versioning, canonical serialization, signature. - **`DTP-05..09`** — 8-step validation order, replay protection (nonce + issuance window), folder depth cap, file consistency. - **`DTP-10..13`** — `PerFilePolicy` (autoDownload/meshAllowed/cdnAllowed/retention) and global defaults. - **`DTP-14..16`** — Retention modes ('permanent' / 'lru' / `{ maxAgeMs }`) and chunk-granularity eviction. - **`DTP-17..19`** — CDN reversed-ID URL convention, parameterized levels and padding, write-back via `CdnUploadToken`. - **`DTP-20..23`** — Inline and retroactive key distribution, `KeyGrantEnvelope`, lock-icon state transitions. - **`DTP-24`** — Inline files (`kind: 'inline'`) carry their own Ed25519 sig. - **`DTP-25..26`** — `Drive.hydrateFrom()`, `Drive.getKeyStatus()`, `Drive.getPresence()` API contracts; hydration error codes. ### ADDED — new package `@suaveplan/drive-template` Types-and-codecs package mirroring `@suaveplan/drive-mesh-protocol`'s structure: `DriveTemplate` schema, canonical serialization, signature verification, `KeyGrantEnvelope`, CDN URL builder. Zero runtime side effects. ### MODIFIED — `@suaveplan/drive` capability spec Adds three drive surface methods documented in this proposal: - `drive.hydrateFrom(template, options)` — instantiates DirIndex + FileLocationIndex from a verified template; schedules autoDownload fetches. - `drive.getKeyStatus(path): 'unencrypted' | 'encrypted-locked' | 'encrypted-unlocked'` — for lock-icon UX. - `drive.getPresence(path): { chunksPresent, chunksTotal, sourceHints, stuckChunks }` — for progress UX. `DirEntry` returned by `Drive.list()` gains optional `keyStatus` and `presence` fields. ### MODIFIED — `@suaveplan/drive-plugin-chunks` capability spec `FileLocation.storageHints` `kind: 'inline'` variant gains a required `sig: Uint8Array` field (Ed25519 over `fileId ‖ bytes`). Backward-incompatible at the wire format; bumps inline-format minor version. ### REMOVED — none. Strictly additive at the surface level; the inline-sig change is the only on-wire format bump and only affects inline files (a small minority). --- ## Source mapping > Genesis-original. The dehydrated-drive concept is novel to genesis; antecedents are NPM lockfiles (deterministic dependency descriptions), Apple App Thinning (server-issued resource manifests), and IPFS pinning services (declarative content lists). | Requirement | Source / inspiration | Notes | |---|---|---| | `DriveTemplate` shape | Genesis-original | Composes `@suaveplan/bitpacker-schema` for canonical serialization. | | Reversed-ID URL convention | Custom | Distributes leaves regardless of how IDs were generated. Levels parameterized. | | `PerFilePolicy` flags | User's design brief | Three booleans + retention enum. | | Inline vs retroactive key distribution | User's design brief; partial parallel to JWE direct vs key-agreement | `crypto_box_seal` for envelope. | | `KeyGrantEnvelope` | Genesis-original | Standalone bus event; not coupled to share-grant flow. | | `CdnUploadToken` | Standard pattern (signed S3-style URL with payload binding) | Server-attested upload intent. | | Lock-icon UX states | User's design brief | Three-state union maps directly to UI. | | Replay protection (nonce + 24h window) | Standard cryptographic pattern | LRU-cached for last 1024 nonces. | ### Requirements with no source line | Requirement | Justification | |---|---| | 32-level folder depth cap | Real filesystems rarely exceed 16; cap prevents DoS via maliciously deep templates. | | 24-hour issuance window | Long enough for templates to propagate via API/launcher, short enough that a captured template doesn't outlive its intended distribution channel. | | 1024-entry nonce LRU | Bounded memory (~16 KiB) supporting reasonable issuance rates per drive. | | 60-second `CdnUploadToken` expiry | Stolen tokens expire before useful re-use; clock skew tolerable. | --- ## Parity Checklist > Genesis-original. No upstream `suaveplan/main` analogue exists. | Capability | Upstream | Migrated | Notes | |---|---|---|---| | Server-signed drive config | n/a | ✅ | Genesis-original. | | Per-file policy flags | n/a | ✅ | Genesis-original. | | CDN reversed-ID layout | n/a | ✅ | Genesis-original. | | Retroactive key grant | partial (suaveplan had share-grant) | ⬆️ | Enhanced: per-file granularity, separate from share partitions. | | `Drive.hydrateFrom()` | n/a | ✅ | Genesis-original. | | `Drive.getKeyStatus()` / `getPresence()` | n/a | ✅ | Genesis-original; surface additions to drive core. | | Replay protection | n/a | ✅ | Genesis-original. | | Chunk-granularity LRU | n/a | ✅ | Genesis-original. | --- ## Out of scope - **Server-side template authoring tools.** This proposal defines what a template *is* and how it's verified. The CLI / web tool that *produces* templates is a separate concern and is out of scope. - **Subscription / payment flows.** Retroactive key grants are the mechanism; the policy that decides *when* to grant (paid the user, joined the group, won the tournament) is application logic above the drive. - **Server-side CDN implementation.** This proposal defines the URL convention readers and writers MUST follow. Building a CDN that hosts the URLs is application infrastructure. - **Cross-template diffs.** Issuing a new template that adds files to an existing drive is straightforward (just a fresh template with the same `driveId`); explicit "diff" or "patch" templates are out of scope. - **Compression of file plaintext.** The chunks plugin handles per-chunk AEAD; whole-file compression (zstd, brotli) before chunking is a future change. --- ## Verification approach ### `@suaveplan/drive-template` package tests - `tests/template-roundtrip.test.ts` — `canonicalSerialize` / sign / verify cycle. - `tests/template-replay.test.ts` — second hydrate with same nonce rejected. - `tests/template-expired.test.ts` — expired template rejected with `DRIVE.TEMPLATE_EXPIRED`. - `tests/template-stale.test.ts` — `issuedAt` more than 24h ago rejected with `DRIVE.TEMPLATE_STALE`. - `tests/template-tampered.test.ts` — fast-check property: any single bit flip breaks the sig. - `tests/template-untrusted-issuer.test.ts` — issuer not in `trustedKeyAuthorities` rejected. - `tests/template-too-deep.test.ts` — folder recursion > 32 rejected. - `tests/template-malformed.test.ts` — `chunkSize <= 164`, `totalChunks * chunkSize < size`, etc. rejected. - `tests/cdn-reverse-id-url.test.ts` — URL builder produces correct path for various `levels`/`padding`. - `tests/cdn-bad-sig.test.ts` — corrupted CDN response is discarded; mesh fallback succeeds. - `tests/cdn-upload-token.test.ts` — CDN write requires valid signed token; expired/tampered tokens rejected. ### `@suaveplan/drive` integration tests - `tests/hydrate-from.test.ts` — successful hydration creates DirIndex + FileLocationIndex matching the template. - `tests/policy-meshallowed-false.test.ts` — bus interceptor verifies no `cd:request` emitted for `meshAllowed: false` files. - `tests/policy-cdnallowed-false.test.ts` — `drive-transport-https` not invoked for `cdnAllowed: false` files. - `tests/policy-autodownload.test.ts` — fetches scheduled on hydrate for `autoDownload: true` files only. - `tests/retention-lru.test.ts` — eviction respects `maxLocalCacheBytes`; `'permanent'` files immune. - `tests/retention-max-age.test.ts` — `{ maxAgeMs }` chunks evicted at GC pass. - `tests/inline-key-unlock.test.ts` — hydrate flips lock icon to 🔓 for files with inline keys. - `tests/retroactive-key-grant.test.ts` — incoming `key:grant` flips lock icon and unlocks reads. - `tests/key-grant-untrusted-issuer.test.ts` — grant from non-`keyAuthority` rejected. - `tests/key-grant-replay.test.ts` — second grant with same payload rejected (nonce-equivalent). - `tests/get-key-status.test.ts` — returns `'unencrypted' | 'encrypted-locked' | 'encrypted-unlocked'` correctly per file state. - `tests/get-presence.test.ts` — returns `{ chunksPresent, chunksTotal, sourceHints, stuckChunks }` matching internal state. - `tests/list-with-presence.test.ts` — `Drive.list()` DirEntries carry `keyStatus` and `presence` fields. - `tests/inline-file-sig.test.ts` — inline files (kind:'inline') carry valid Ed25519 sig; bad sig rejected. --- ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/drive-template` and `bunx turbo ls --filter=...@suaveplan/drive` once the packages exist. - `@suaveplan/drive` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: gains `hydrateFrom`, `getKeyStatus`, `getPresence` surface methods) - `@suaveplan/drive-plugin-chunks` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: `kind: 'inline'` storage hint gains required `sig` field — minor format bump) - `@suaveplan/drive-mesh` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: hydration triggers chunk fetches via mesh-protocol) - `@suaveplan/drive-transport-https` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: implements the CDN reversed-ID URL builder and upload token flow) - `@suaveplan/drive-sharing` — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: retroactive key grants are independent of partition KEKs but coexist; `KeyRing` interface stays the same) - `@suaveplan/crypto-vault` — [ ] tests run / [x] docs reviewed / [ ] no action needed (reason: provides X25519 sealed-box for wrappedKey unwrap; no API changes) - `@suaveplan/network` (via `NetworkBus`) — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: bus is generic; new `key:grant` topic is just another `MessageEnvelope.type`) - `@suaveplan/api-plugin-file-upload` (pending — main) — [ ] tests run / [ ] docs reviewed / [ ] no action needed (reason: server-side counterpart for `CdnUploadToken` minting; will be addressed when ported) Cumulative blast radius: 8 packages, 3 of which gain new surface (drive, drive-plugin-chunks, drive-transport-https). `drive-plugin-chunks` is the only package with a wire-format change (inline-file `sig` field). --- ## Migration ### From the previous design This is largely additive: drives that don't call `hydrateFrom` are unaffected. Three migrations are required: 1. **Inline storage hint format.** `FileLocation.storageHints` entries with `kind: 'inline'` MUST add a `sig: Uint8Array` field. Drives reading old-format inline entries SHALL accept them with a warning and rewrite them on next write. Search command: ```bash grep -rln "kind: ['\"]inline['\"]" packages/ --include="*.ts" ``` 2. **`Drive.list()` consumers.** UIs that consume `DirEntry` SHOULD start reading `entry.keyStatus` and `entry.presence` to render lock icons and progress bars. The fields are optional; consumers that ignore them work as before. 3. **Server template signing keys.** Servers issuing templates MUST publish their issuer pubkey (out of band) so clients can populate `globalPolicy.trustedKeyAuthorities`. Without this, `hydrateFrom` rejects every template with `DRIVE.TEMPLATE_UNTRUSTED_ISSUER`. ### Behavior changes The five behavior changes consumers need to know about (per AGENTS §13a): - **New error codes**: `DRIVE.TEMPLATE_*` family (8 codes), `DRIVE.UNAVAILABLE` (when read fails because no source has the chunks), `DMP.*` family from the mesh-protocol package. - **`Drive.read` may now block longer** when chunks need fetching — `AbortSignal` honored throughout but expect higher p99 latency on dehydrated drives. - **Disk usage may grow asynchronously** post-hydrate. UIs displaying free space should subscribe to `drive:cache:evicted` events. - **Lock icon state** transitions emit `drive:key:granted` / `drive:key:revoked` events — UIs should rebind their indicators on these. - **Bus topic added**: `key:grant` (per `DTP-21..23`). Plugins that snoop the bus for security audit purposes should add this to their watchlist. --- ## References - Architectural reference: [`docs/drive/dehydrated-drives.md`](../../../docs/drive/dehydrated-drives.md) - System review findings: [`docs/system-review.md`](../../../docs/system-review.md) §3, §4, §5, §6, §7, §8, §9, §12, §13, §14, §15, §16 - Companion change: [`drive-mesh-protocol-v1`](../drive-mesh-protocol-v1/proposal.md) — the chunk-request protocol used during hydration - Composition map: [`docs/drive-storage-network.md`](../../../docs/drive-storage-network.md)