Files
suaveplan/openspec/archive/2026-05-04-drive-template-v1/proposal.md
2026-05-27 23:29:53 +01:00

16 KiB

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 §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. 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:

    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