Files
suaveplan/openspec/archive/2026-04-28-drive-plugin-chunks-port/specs/drive-plugin-chunks/spec.md
2026-05-27 23:29:53 +01:00

82 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Drive Plugin Chunks — Spec Delta (drive-plugin-chunks-port)
## ADDED Requirements
### CHUNKS-FORMAT-V1-01 — Chunk binary layout is byte-exact
A `version 0x0001` chunk MUST be the byte concatenation `metadata(100) ‖ sig(64) ‖ ciphertext(payloadLen)`, total `164 + payloadLen` bytes, big-endian throughout. Field offsets within metadata MUST be exactly: `version` 0–2, `fileId` 2–34, `chunkId` 34–38, `totalChunks` 38–42, `fileSize` 42–50, `chunkSize` 50–54, `payloadLen` 54–58, `flags` 58–60, `nonce` 60–84, `aeadTag` 84–100. `sig` occupies 100–164. Ciphertext starts at 164.
**Verified by:** `src/codec/chunk.spec-conformance.test.ts` (`CHUNKS-FORMAT-V1-01`).
### CHUNKS-FORMAT-V1-02 — `flags` reserved in v1
`flags` MUST be `0x0000` for v1. Receivers MUST reject chunks with non-zero `flags` via `ChunksError(CHUNKS.FLAGS_RESERVED_NONZERO)` before any further parse work.
**Verified by:** `src/codec/chunk.spec-conformance.test.ts` (`CHUNKS-FORMAT-V1-02`).
### CHUNKS-FORMAT-V1-03 — Payload length invariants
For non-final chunks (`chunkId < totalChunks - 1`), `payloadLen` MUST equal `chunkSize - 164`. For the final chunk, `payloadLen` MUST equal `fileSize - chunkId * (chunkSize - 164)`. Mismatches MUST be rejected via `ChunksError(CHUNKS.PAYLOAD_LEN_INVALID)` before sig verification.
**Verified by:** `src/codec/chunk.spec-conformance.test.ts` (`CHUNKS-FORMAT-V1-03`).
### CHUNKS-SIG-01 — Signature covers metadata plus ciphertext
`sig` MUST be `ed25519_sign(serverPrivKey, metadata[0..100] ‖ ciphertext[0..payloadLen])`. Verification MUST be performed before AEAD decryption. Any mutation to any signed byte MUST cause sig verification to fail with `ChunksError(CHUNKS.SIG_INVALID)`.
**Verified by:** `src/pipeline/sign-chunk.spec-conformance.test.ts` (`CHUNKS-SIG-01`).
### CHUNKS-INVARIANT-01 — Store-as-is
Plugin MUST persist received chunks byte-identical to receipt. No re-signing, no re-encryption, no metadata rewrites. The bytes returned by `chunkStore.get(fileId, chunkId)` MUST equal the bytes accepted by `chunks.putChunk(rawBytes)` for that chunk.
**Verified by:** `src/plugin.spec-conformance.test.ts` (`CHUNKS-INVARIANT-01`).
### CHUNKS-VERIFY-01 — Sig-verify gate on receipt
`chunks.putChunk(rawBytes)` MUST run, in order: structural parse → flags check → payload-length invariant → ed25519 sig verification → persist. Any failure aborts persistence and returns the appropriate `ChunksError` code without writing to storage.
**Verified by:** `src/plugin.spec-conformance.test.ts` (`CHUNKS-VERIFY-01`).
### CHUNKS-READ-01 — Read middleware reassembles via streaming
When a `fileId` is registered for `path`, `drive.read(path)` MUST return a `ReadableStream<Uint8Array>` that yields plaintext by iterating chunk 0..N-1, sig-verifying each, AEAD-decrypting, and enqueueing only the first `payloadLen` bytes of each chunk's plaintext. The full file MUST NOT be materialised in memory.
**Verified by:** `src/plugin.spec-conformance.test.ts` (`CHUNKS-READ-01`).
### CHUNKS-LAYOUT-SPARSE-01 — Sparse-file layout
In `sparse-file` mode, the plugin MUST pre-allocate a single storage object of length `(totalChunks - 1) * chunkSize + (164 + lastPayloadLen)` after manifest load. Each chunk MUST be written via `storage.setPartial(fileKey, chunkId * chunkSize, rawBytes)`. A separate `BitSet` at `bitmap:<fileId>` MUST track landed chunks. The plugin MUST require `storage.capabilities.partialWrites === true` and throw `ChunksError(CHUNKS.LAYOUT_INCOMPATIBLE)` otherwise.
**Verified by:** `src/layout/sparse-file.spec-conformance.test.ts` (`CHUNKS-LAYOUT-SPARSE-01`).
### CHUNKS-LAYOUT-KEYS-01 — Chunk-keys layout
In `chunk-keys` mode, each chunk MUST be stored as its own object at key `chunks/<fileId>/<chunkId>`. `hasChunks(fileId)` MUST be derivable from key existence (`storage.exists`). No bitmap object is required.
**Verified by:** `src/layout/chunk-keys.spec-conformance.test.ts` (`CHUNKS-LAYOUT-KEYS-01`).
### CHUNKS-LAYOUT-AUTO-01 — Auto layout selection
In `auto` mode (default), the plugin MUST pick `sparse-file` when `storage.capabilities.partialWrites === true`, otherwise MUST pick `chunk-keys`. The selected layout MUST be observable via `chunks.layout`.
**Verified by:** `src/layout/auto.spec-conformance.test.ts` (`CHUNKS-LAYOUT-AUTO-01`).
### CHUNKS-MANIFEST-01 — Manifest format and signature
The manifest MUST be a Zod-validated object with fields `version`, `fileId`, `fileSize`, `chunkSize`, `totalChunks`, `encryption: { algo, fileKeyWrapped, baseNonce }`, `chunks: [{ chunkId, hash, payloadLen }]`, and `sig`. `sig` MUST be `ed25519_sign(serverPrivKey, canonical_serialize(manifest_without_sig))`. Load failures (parse, schema, sig) MUST throw `ChunksError(CHUNKS.MANIFEST_INVALID)`.
**Verified by:** `src/manifest/load.spec-conformance.test.ts` (`CHUNKS-MANIFEST-01`).
### CHUNKS-MANIFEST-02 — Manifest distribution strategy
Default strategy MUST be `manifest-as-chunk`: the manifest is itself a server-signed object stored at storage key `manifest:<fileId>`. `server-only` strategy MUST be opt-in via `manifestSource: "server-only"`; in that mode `loadManifest(fileId)` fetches via an injected `fetch` against the configured server URL.
**Verified by:** `src/manifest/load.spec-conformance.test.ts` (`CHUNKS-MANIFEST-02`).
### CHUNKS-CODES-01 — Error registry namespaced and frozen
`CHUNKS_CODES` MUST be declared via `defineCodes("CHUNKS", [...])`. `Object.isFrozen(CHUNKS_CODES)` MUST be `true`. Every code value MUST start with `CHUNKS.`. `ChunksError` MUST extend `SuaveplanError<ChunksErrorCode>`.
**Verified by:** `src/error-codes.spec-conformance.test.ts` (`CHUNKS-CODES-01`).