82 lines
5.6 KiB
Markdown
82 lines
5.6 KiB
Markdown
# 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`).
|