Files
2026-05-27 23:29:53 +01:00

216 lines
16 KiB
Markdown

# 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)