first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# @suaveplan/drive-plugin-chunks - Chunk-addressing plugin for @suaveplan/drive. Defines the version 0x0001 binary chunk format, server-signed manifests, storage layouts, and streaming encrypt-then-chunk pipeline.
## Summary
Chunk-addressing plugin for `@suaveplan/drive`. Splits files into signed, encrypted chunks using the locked `version 0x0001` binary format, with server-signed manifests and two storage layouts (`sparse-file` and `chunk-keys`).
<!-- begin:auto-docs -->
## Documentation tree
- [chunk](./src/codec/chunk.md) — Wraps the chunk wire format above the bare header: takes raw chunk bytes (header + signature + ciphertext) and parses t…
- [header](./src/codec/header.md) — Encodes and parses the fixed-width 100-byte metadata header that prefixes every chunk in the `@suaveplan/drive-plugin-c…
- [inline](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [inline-hint](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [auto](./src/layout/auto.md) — Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capab…
- [chunk-keys](./src/layout/chunk-keys.md) — Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<c…
- [sparse-file](./src/layout/sparse-file.md) — Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single…
- [build](./src/manifest/build.md) — Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize…
- [load](./src/manifest/load.md) — Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`mani…
- [source](./src/manifest/source.md) — Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small…
- [decrypt-chunk](./src/pipeline/decrypt-chunk.md) — The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via…
- [encrypt-stream](./src/pipeline/encrypt-stream.md) — The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence…
- [sign-chunk](./src/pipeline/sign-chunk.md) — Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes pl…
- [verify-chunk](./src/pipeline/verify-chunk.md) — Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk —…
- [schemas](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [manifest.schema](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin…
- [plugin](./src/plugin.md) — Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into…
<!-- end:auto-docs --> - [inline-hint](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_- [auto](./src/layout/auto.md) — Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capab…
- [chunk-keys](./src/layout/chunk-keys.md) — Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<c…
- [sparse-file](./src/layout/sparse-file.md) — Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single…
- [build](./src/manifest/build.md) — Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize…
- [load](./src/manifest/load.md) — Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`mani…
- [source](./src/manifest/source.md) — Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small…
- [decrypt-chunk](./src/pipeline/decrypt-chunk.md) — The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via…
- [encrypt-stream](./src/pipeline/encrypt-stream.md) — The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence…
- [sign-chunk](./src/pipeline/sign-chunk.md) — Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes pl…
- [verify-chunk](./src/pipeline/verify-chunk.md) — Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk —…
- [schemas](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [manifest.schema](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin…
- [plugin](./src/plugin.md) — Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into…
- [inline-hint](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_- [auto](./src/layout/auto.md) — Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capab…
- [chunk-keys](./src/layout/chunk-keys.md) — Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<c…
- [sparse-file](./src/layout/sparse-file.md) — Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single…
- [build](./src/manifest/build.md) — Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize…
- [load](./src/manifest/load.md) — Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`mani…
- [source](./src/manifest/source.md) — Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small…
- [decrypt-chunk](./src/pipeline/decrypt-chunk.md) — The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via…
- [encrypt-stream](./src/pipeline/encrypt-stream.md) — The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence…
- [sign-chunk](./src/pipeline/sign-chunk.md) — Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes pl…
- [verify-chunk](./src/pipeline/verify-chunk.md) — Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk —…
- [schemas](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [manifest.schema](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin…
- [plugin](./src/plugin.md) — Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into…
- [inline-hint](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_- [auto](./src/layout/auto.md) — Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capab…
- [chunk-keys](./src/layout/chunk-keys.md) — Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<c…
- [sparse-file](./src/layout/sparse-file.md) — Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single…
- [build](./src/manifest/build.md) — Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize…
- [load](./src/manifest/load.md) — Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`mani…
- [source](./src/manifest/source.md) — Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small…
- [decrypt-chunk](./src/pipeline/decrypt-chunk.md) — The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via…
- [encrypt-stream](./src/pipeline/encrypt-stream.md) — The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence…
- [sign-chunk](./src/pipeline/sign-chunk.md) — Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes pl…
- [verify-chunk](./src/pipeline/verify-chunk.md) — Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk —…
- [schemas](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [manifest.schema](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin…
- [plugin](./src/plugin.md) — Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into…
- [inline-hint](./src/inline/inline-hint.md) — <!-- begin:auto-docs --> _(no submodules)_- [auto](./src/layout/auto.md) — Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capab…
- [chunk-keys](./src/layout/chunk-keys.md) — Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<c…
- [sparse-file](./src/layout/sparse-file.md) — Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single…
- [build](./src/manifest/build.md) — Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize…
- [load](./src/manifest/load.md) — Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`mani…
- [source](./src/manifest/source.md) — Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small…
- [decrypt-chunk](./src/pipeline/decrypt-chunk.md) — The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via…
- [encrypt-stream](./src/pipeline/encrypt-stream.md) — The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence…
- [sign-chunk](./src/pipeline/sign-chunk.md) — Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes pl…
- [verify-chunk](./src/pipeline/verify-chunk.md) — Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk —…
- [schemas](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ - [manifest.schema](./src/schemas/manifest.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/error-codes.md) — Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin…
- [plugin](./src/plugin.md) — Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into…
## Install
```bash
bun add @suaveplan/drive-plugin-chunks
```
## Quick start
```ts
import { Drive } from "@suaveplan/drive";
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
import { MemoryStorage } from "@suaveplan/storage/memory";
const storage = Object.assign(new MemoryStorage(), { id: "local" });
const drive = new Drive(storage);
await drive.initialize();
const chunks = await drive.usePlugin(
chunksPlugin({
serverPrivKey: ed25519PrivKey,
serverPubKey: ed25519PubKey,
chunkSize: 65536,
})
);
await drive.write("hello.txt", new TextEncoder().encode("world"));
await chunks.ingest("hello.txt");
const manifest = await chunks.loadManifest(fileId);
console.log(manifest.totalChunks);
await drive.dispose();
```
## Modules
- [src/codec/header.md](src/codec/header.md) — binary header encoding / parsing
- [src/codec/chunk.md](src/codec/chunk.md) — full chunk encode / parse
- [src/pipeline/encrypt-stream.md](src/pipeline/encrypt-stream.md) — XChaCha20-Poly1305 streaming encryption
- [src/pipeline/chop-stream.md](src/pipeline/chop-stream.md) — ciphertext slicing into chunk payloads
- [src/pipeline/sign-chunk.md](src/pipeline/sign-chunk.md) — ed25519 server signing
- [src/pipeline/verify-chunk.md](src/pipeline/verify-chunk.md) — signature verification
- [src/layout/sparse-file.md](src/layout/sparse-file.md) — single-object sparse layout
- [src/layout/chunk-keys.md](src/layout/chunk-keys.md) — one-key-per-chunk layout
- [src/layout/auto.md](src/layout/auto.md) — automatic layout selection
- [src/manifest/build.md](src/manifest/build.md) — manifest assembly and signing
- [src/manifest/load.md](src/manifest/load.md) — manifest fetching and validation
- [src/manifest/source.md](src/manifest/source.md) — distribution strategy switch
- [src/plugin.md](src/plugin.md) — plugin handle and Drive middleware
- [src/types/chunks.types.md](src/types/chunks.types.md) — public TypeScript interfaces
- [src/schemas/manifest.schema.md](src/schemas/manifest.schema.md) — Zod boundary schemas
- [src/error-codes.md](src/error-codes.md) — `CHUNKS_CODES` and `ChunksError`

View File

@@ -0,0 +1,51 @@
# chunk
## Purpose
Wraps the chunk wire format above the bare header: takes raw chunk bytes (header + signature + ciphertext) and parses them into the structured `Chunk` and `EncodedChunk` shapes that the rest of the plugin consumes. This is the inverse of `encrypt-stream`'s output and the input to `verify-chunk` / `decrypt-chunk`.
## Features
- `parseChunk(raw): Chunk` — splits raw bytes into `metadata`, `sig`, and `ciphertext` slices using `HEADER_SIZE` as the boundary.
- Length validation — rejects anything shorter than `HEADER_SIZE + payloadLen`.
- Allocation-light — uses `Uint8Array.subarray` views where safe and `slice` only where ownership matters.
- Round-trip stable when paired with `encodeHeader` and the encrypt-stream output.
## Usage: Basic Example
```ts
import { parseChunk } from "@suaveplan/drive-plugin-chunks";
const raw: Uint8Array = await chunks.getChunk(fileId, 0).then((b) => b!);
const chunk = parseChunk(raw);
console.log(chunk.metadata.chunkId, chunk.ciphertext.length);
```
## Usage: Advanced Example
```ts
import {
decryptChunk,
parseChunk,
verifyChunkSig,
} from "@suaveplan/drive-plugin-chunks";
async function readVerifiedChunk(fileId: Uint8Array, idx: number) {
const raw = await chunks.getChunk(fileId, idx);
if (!raw) throw new Error("missing chunk");
const chunk = parseChunk(raw);
verifyChunkSig(raw, serverPubKey);
const plaintext = decryptChunk(chunk.metadata, chunk.ciphertext, fileKey);
return plaintext.slice(0, chunk.metadata.payloadLen);
}
```
## API Reference
- `parseChunk(raw: Uint8Array): Chunk` — returns `{ metadata, sig, ciphertext }`. `metadata` flows through `parseHeader`; `sig` is the 64-byte Ed25519 signature slice; `ciphertext` is the trailing `payloadLen` bytes.
- Throws `ChunksError(CHUNKS.METADATA_INVALID)` for under-length buffers (delegated to `parseHeader`).
- Throws `ChunksError(CHUNKS.PAYLOAD_LEN_INVALID)` when the trailing payload is shorter than `metadata.payloadLen`.
## Implementation Notes
`parseChunk` does not verify the signature or decrypt — those are independent stages so peers can short-circuit on metadata-only inspection (manifest checks, availability gossip). The ciphertext is sliced rather than viewed when its length differs from the underlying buffer to avoid pinning the larger raw allocation in caches.

View File

@@ -0,0 +1,67 @@
# header
## Purpose
Encodes and parses the fixed-width 100-byte metadata header that prefixes every chunk in the `@suaveplan/drive-plugin-chunks` wire format. Big-endian throughout, no trailing version bytes, layout pinned by constants so receivers can locate each field without consulting the manifest.
## Features
- `encodeHeader(metadata): Uint8Array` — packs `ChunkMetadata` into a 100-byte buffer using big-endian byte order.
- `parseHeader(buf): ChunkMetadata` — reverses the encoding and rejects under-length buffers with `CHUNKS.METADATA_INVALID`.
- `METADATA_SIZE` (100) — header length excluding signature.
- `SIG_SIZE` (64) — signature length.
- `HEADER_SIZE` (164) — `METADATA_SIZE + SIG_SIZE`, used by `encrypt-stream` and the layout helpers.
## Usage: Basic Example
```ts
import { encodeHeader, parseHeader } from "@suaveplan/drive-plugin-chunks";
const metadata = {
version: 1, fileId: new Uint8Array(32), chunkId: 0, totalChunks: 1,
fileSize: 8n, chunkSize: 1024, payloadLen: 8, flags: 0,
nonce: new Uint8Array(24), aeadTag: new Uint8Array(16),
};
const header = encodeHeader(metadata);
const back = parseHeader(header);
```
## Usage: Advanced Example
```ts
import { HEADER_SIZE, METADATA_SIZE, SIG_SIZE, parseHeader } from "@suaveplan/drive-plugin-chunks";
function decodeChunk(raw: Uint8Array) {
const metadata = parseHeader(raw.subarray(0, METADATA_SIZE));
const sig = raw.subarray(METADATA_SIZE, HEADER_SIZE);
const ciphertext = raw.subarray(HEADER_SIZE, HEADER_SIZE + metadata.payloadLen);
return { metadata, sig, ciphertext };
}
```
## API Reference
- `METADATA_SIZE: 100` — bytes reserved for metadata.
- `SIG_SIZE: 64` — bytes reserved for the Ed25519 signature.
- `HEADER_SIZE: 164` — `METADATA_SIZE + SIG_SIZE`.
- `encodeHeader(metadata: ChunkMetadata): Uint8Array` — returns a freshly-allocated 100-byte buffer.
- `parseHeader(buf: Uint8Array): ChunkMetadata` — throws `ChunksError(CHUNKS.METADATA_INVALID)` if `buf.length < METADATA_SIZE`. Slices `fileId`, `nonce`, and `aeadTag` so callers can mutate the input safely.
## Implementation Notes
Layout (big-endian):
| Offset | Length | Field |
|-------:|-------:|--------------|
| 0 | 2 | version |
| 2 | 32 | fileId |
| 34 | 4 | chunkId |
| 38 | 4 | totalChunks |
| 42 | 8 | fileSize (u64) |
| 50 | 4 | chunkSize |
| 54 | 4 | payloadLen |
| 58 | 2 | flags |
| 60 | 24 | nonce |
| 84 | 16 | aeadTag |
The header pads to exactly 100 bytes so `encrypt-stream` can compute payload offsets without recomputing field sums.

View File

@@ -0,0 +1,59 @@
# error-codes
## Purpose
Defines the structured error registry for `@suaveplan/drive-plugin-chunks`. Every chunk-format failure mode the plugin can produce is registered through `defineCodes("CHUNKS", [...])` so that downstream consumers can branch on stable string codes (`"CHUNKS.AEAD_TAG_INVALID"`, etc.) without parsing English-language error messages. The companion `ChunksError` class extends the project-wide `SuaveplanError` base so chunked-file errors compose with cause-chain traversal, redaction, and OTel span-event adapters defined in `@suaveplan/error`.
## Features
- Frozen `CHUNKS_CODES` enum-shaped constant covering the twelve failure classes that can occur during chunk encoding, signing, manifest exchange, layout selection, ingest, and inline-hint verification.
- `ChunksErrorCode` literal union derived from `CHUNKS_CODES` so call sites stay type-safe.
- `ChunksError` class with the `SuaveplanError` constructor signature widened to public, accepting `(code, message, options?)` plus the standard `cause` field.
- Side-effect-safe registration via `defineCodes` so subpath consumers automatically see codes registered too.
## Usage: Basic Example
```ts
import { CHUNKS_CODES, ChunksError } from "@suaveplan/drive-plugin-chunks";
throw new ChunksError(
CHUNKS_CODES.AEAD_TAG_INVALID,
"chunk AEAD tag verification failed"
);
```
## Usage: Advanced Example
```ts
import { CHUNKS_CODES, ChunksError } from "@suaveplan/drive-plugin-chunks";
try {
await chunks.ingest("photo.jpg");
} catch (err) {
if (err instanceof ChunksError) {
switch (err.code) {
case CHUNKS_CODES.SIG_INVALID:
return alertOps("server signing key drift");
case CHUNKS_CODES.MANIFEST_NOT_FOUND:
return rebuildManifest(err);
default:
throw new ChunksError(
CHUNKS_CODES.INGEST_FAILED,
"wrapped ingest failure",
{ cause: err }
);
}
}
throw err;
}
```
## API Reference
- `CHUNKS_CODES` — `Readonly<Record<string, string>>` of `"CHUNKS.<NAME>"` strings.
- `ChunksErrorCode` — string-literal union of every value in `CHUNKS_CODES`.
- `ChunksError` — `class ChunksError extends SuaveplanError<ChunksErrorCode>` with constructor `(code: ChunksErrorCode, message: string, options?: SuaveplanErrorOptions)`. Inherits `name`, `code`, `message`, `cause`, and the structured-redaction surface from `SuaveplanError`.
## Implementation Notes
The twelve codes are deliberately narrow: each maps to one failure semantic (tag verification, signature verification, payload length mismatch, inline-hint signature failure, etc.) so monitoring dashboards can attach SLOs per code without redacting cardinality. `INLINE_SIG_INVALID` was added in DTP-25 to signal that an `InlineHint` failed Ed25519 verification against the drive's signing key over `fileId ‖ bytes`; legacy `LegacyInlineHint` shapes (no `sig` field) do not throw — they emit a `drive:inline:legacy` warning and trigger a rewrite-with-sig on next write.

View File

@@ -0,0 +1,81 @@
# inline-hint
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements DTP-25 (drive-plugin-chunks-inline-sig). Provides the signed `InlineHint` shape used by `FileLocation.storageHints` entries with `kind: 'inline'`, the Ed25519 signing helper that produces them, the verifier that the drive runs before returning inline bytes, and the legacy-detection / upgrade helpers that let pre-DTP-25 hints (no `sig` field) round-trip safely. The signing input is the canonical concatenation `fileId ‖ bytes`, which binds the signature to one file so a replay onto a different fileId would fail verification.
## Features
- `InlineHint` — `{ kind: 'inline'; bytes: Uint8Array; sig: Uint8Array }` for the DTP-25 wire format.
- `LegacyInlineHint` — `{ kind: 'inline'; bytes: Uint8Array }` for backward-compat reads.
- `signInlineHint` — Ed25519-sign over `fileId ‖ bytes` under the drive's secret key.
- `verifyInlineHint` — verify and return bytes; throws `CHUNKS.INLINE_SIG_INVALID` on failure (catch arm covers verifier exceptions too).
- `isLegacyInlineHint` — type-guard distinguishing legacy from signed shapes.
- `upgradeLegacyInlineHint` — sign a legacy hint with the drive's key so the next write rewrites it as `InlineHint`.
- `inlineSigningInput` — exposes the canonical concatenation so other layers can re-derive the bytes that are signed.
- Cause-chain preservation: verifier exceptions are wrapped as `ChunksError` with the original error attached as `cause`.
## Usage: Basic Example
```ts
import {
signInlineHint,
verifyInlineHint,
} from "@suaveplan/drive-plugin-chunks";
import { sha256 } from "@suaveplan/crypto-hash";
const fileId = sha256(plaintext);
const hint = signInlineHint(fileId, plaintext, driveSecretKey);
// later, on read:
const bytes = verifyInlineHint(hint, fileId, drivePublicKey);
console.log(bytes.byteLength);
```
## Usage: Advanced Example
```ts
import {
isLegacyInlineHint,
upgradeLegacyInlineHint,
verifyInlineHint,
type AnyInlineHint,
} from "@suaveplan/drive-plugin-chunks";
function readInline(
hint: AnyInlineHint,
fileId: Uint8Array,
driveSecretKey: Uint8Array,
drivePublicKey: Uint8Array,
onLegacyWarning: () => void
): Uint8Array {
if (isLegacyInlineHint(hint)) {
onLegacyWarning(); // emit drive:inline:legacy
const upgraded = upgradeLegacyInlineHint(
hint,
fileId,
driveSecretKey
);
// Persist `upgraded` on the next write; for the current read we
// can return the bytes directly because legacy hints predate the
// sig requirement.
return upgraded.bytes;
}
return verifyInlineHint(hint, fileId, drivePublicKey);
}
```
## API Reference
- `signInlineHint(fileId: Uint8Array, bytes: Uint8Array, driveSecretKey: Uint8Array): InlineHint` — Ed25519-sign `fileId ‖ bytes` and return the signed hint.
- `verifyInlineHint(hint: InlineHint, fileId: Uint8Array, drivePublicKey: Uint8Array): Uint8Array` — verify and return `hint.bytes`. Throws `ChunksError(CHUNKS.INLINE_SIG_INVALID, ...)` if the signature does not verify or the verifier raises.
- `isLegacyInlineHint(hint: AnyInlineHint): hint is LegacyInlineHint` — true when the hint omits `sig`.
- `upgradeLegacyInlineHint(legacy: LegacyInlineHint, fileId: Uint8Array, driveSecretKey: Uint8Array): InlineHint` — sign the legacy bytes under the drive's key.
- `inlineSigningInput(fileId: Uint8Array, bytes: Uint8Array): Uint8Array` — canonical signing input.
- `InlineHint`, `LegacyInlineHint`, `AnyInlineHint`, `StorageHint`, `FileLocation` — wire types.
## Implementation Notes
The verifier uses `@noble/curves` Ed25519 via `@suaveplan/crypto-curve/ed25519`. `verify()` is implemented to never throw on malformed inputs in the noble path, but the catch arm exists for engine-substitution scenarios where a custom verifier may throw — the `injectable verifyFn` pattern is intentionally not surfaced here because the chunks plugin always uses noble. The `CHUNKS.INLINE_SIG_INVALID` code is registered through `defineCodes("CHUNKS", ...)` in `error-codes.ts`. Domain separation is provided by including `fileId` in the signing input; this is sufficient because every inline-hint context already has a unique fileId, so the signature cannot be replayed across files even if an attacker controls one inline body.

View File

@@ -0,0 +1,46 @@
# auto
## Purpose
Resolves the best chunk layout for a given storage at runtime: prefers `sparse-file` when the storage advertises `capabilities.partialWrites`, falls back to `chunk-keys` otherwise. This is the default `layout` value used by `chunksPlugin` when no explicit strategy is passed in.
## Features
- `createAutoLayout(storage): ChunkLayout` — single-call dispatcher.
- Capability-driven: reads `storage.capabilities.partialWrites` exactly once.
- Stable per storage: the returned `ChunkLayout` keeps the same `name` for the lifetime of the storage instance.
- No additional dependencies — composes the two concrete layouts.
## Usage: Basic Example
```ts
import { createAutoLayout } from "@suaveplan/drive-plugin-chunks";
const layout = createAutoLayout(storage);
console.log(layout.name); // "sparse-file" or "chunk-keys"
```
## Usage: Advanced Example
```ts
import { Drive } from "@suaveplan/drive";
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
const drive = new Drive(storage);
await drive.initialize();
// `auto` is the default; equivalent to omitting `layout`
const chunks = await drive.usePlugin(
chunksPlugin({ serverPrivKey, serverPubKey, layout: "auto" })
);
console.log(chunks.layout); // resolved by createAutoLayout(storage)
```
## API Reference
- `createAutoLayout(storage: DriveStorage): ChunkLayout` — returns either a `chunk-keys` or `sparse-file` `ChunkLayout` based on `storage.capabilities`.
## Implementation Notes
Capability inspection is intentionally narrow — only `partialWrites` matters here because the sparse-file layout is the only one that needs offset-addressable writes. Storages that advertise other capabilities (`atomicWrites`, `contentAddressable`) but not `partialWrites` will receive the `chunk-keys` layout. There is no probe for write feasibility — the auto layer trusts the capabilities object; storages with mis-declared capabilities will fail at the first `setPartial` call rather than at layout-creation time.
The dispatcher returns a fully-constructed `ChunkLayout` rather than a thin proxy so subsequent calls into `putChunk` / `getChunk` go straight to the concrete implementation with no per-call branch. This matters when a single ingest produces tens of thousands of chunks: the resolved layout is captured once in the plugin's closure and referenced by stable identity for the lifetime of the plugin instance. Switching layouts mid-flight is intentionally unsupported — the auto resolver runs only at plugin construction.

View File

@@ -0,0 +1,54 @@
# chunk-keys
## Purpose
Implements the `chunk-keys` layout strategy that stores each chunk under a discrete storage key (`chunks/<fileIdHex>/<chunkId>`). This is the simplest layout — every chunk is a separate `set/get` operation, no preallocation, no offset arithmetic — and it works on any `DriveStorage` whose `set` and `list` methods are implemented.
## Features
- `createChunkKeysLayout(storage): ChunkLayout` — factory returning a `ChunkLayout` with `name: "chunk-keys"`.
- `putChunk(fileId, chunkId, rawBytes)` — single `storage.set` call.
- `getChunk(fileId, chunkId)` — single `storage.get` call returning `undefined` for missing chunks (no error).
- `hasChunks(fileId)` — returns a sparse `boolean[]` indexed by chunkId, derived from `storage.list({ prefix })`.
- `availableChunks(fileId)` — flattens `hasChunks` into the list of present indices.
## Usage: Basic Example
```ts
import { Drive } from "@suaveplan/drive";
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
import { MemoryStorage } from "@suaveplan/storage/memory";
const storage = Object.assign(new MemoryStorage(), { id: "default" });
const drive = new Drive(storage);
await drive.initialize();
const chunks = await drive.usePlugin(
chunksPlugin({ serverPrivKey, serverPubKey, layout: "chunk-keys" })
);
```
## Usage: Advanced Example
```ts
import { createChunkKeysLayout } from "@suaveplan/drive-plugin-chunks";
const layout = createChunkKeysLayout(storage);
await layout.putChunk(fileId, 0, raw);
const present = await layout.hasChunks(fileId);
const available = await layout.availableChunks(fileId);
console.log(`have ${available.length}/${present.length}`);
```
## API Reference
- `createChunkKeysLayout(storage: DriveStorage): ChunkLayout`.
- `ChunkLayout.name === "chunk-keys"`.
- `putChunk(fileId, chunkId, rawBytes): Promise<void>`.
- `getChunk(fileId, chunkId): Promise<Uint8Array | undefined>`.
- `hasChunks(fileId): Promise<boolean[]>` — `bits.length === maxChunkId + 1`; uses `-1` sentinel when no chunks exist (yields empty array).
- `availableChunks(fileId): Promise<number[]>`.
## Implementation Notes
The `hex` helper formats `fileId` to lowercase hex with leading zeros, identical to the formatting in `plugin.ts` so stored chunks remain addressable across plugin and layout boundaries. `hasChunks` uses `storage.list({ prefix })` once and then computes the bit array client-side, which is O(n) over present chunks and avoids `n` storage round trips. Missing chunks correspond to `false` slots in the bit array; gaps are explicit, not implicit.

View File

@@ -0,0 +1,51 @@
# sparse-file
## Purpose
Implements the `sparse-file` chunk layout that stores all chunks of a file as offset-addressable bytes inside a single sparse storage object. Chunks are written via `storage.setPartial(key, offset, bytes)` and read back via `storage.getPartial`. A side-band bitmap key records which chunks have been written so callers can answer `hasChunks` / `availableChunks` without paging the full file.
## Features
- `createSparseFileLayout(storage): ChunkLayout` — factory that throws `CHUNKS.LAYOUT_INCOMPATIBLE` if `storage.capabilities.partialWrites !== true`.
- `preallocate(storage, fileId, totalChunks, chunkSize, lastPayloadLen)` — sets the file size up-front via a single `storage.set` call so subsequent `setPartial` writes never extend the underlying object.
- Bitmap state tracked in `bitmap:<fileIdHex>` keys, packed eight bits per byte.
- Per-fileId `WeakMap` caches keep `chunkSize` and `totalChunks` reachable inside the layout's read path without forcing every call to consult the manifest.
## Usage: Basic Example
```ts
import { Drive } from "@suaveplan/drive";
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
const drive = new Drive(sparseStorage);
await drive.initialize();
const chunks = await drive.usePlugin(
chunksPlugin({ serverPrivKey, serverPubKey, layout: "sparse-file" })
);
```
## Usage: Advanced Example
```ts
import {
createSparseFileLayout,
preallocate,
} from "@suaveplan/drive-plugin-chunks";
const layout = createSparseFileLayout(storage);
await preallocate(storage, fileId, totalChunks, chunkSize, lastPayloadLen);
for (const c of encryptedChunks) {
await layout.putChunk(fileId, c.metadata.chunkId, c.rawBytes);
}
const present = await layout.hasChunks(fileId);
```
## API Reference
- `createSparseFileLayout(storage: DriveStorage): ChunkLayout` — name `"sparse-file"`.
- `preallocate(storage, fileId, totalChunks, chunkSize, lastPayloadLen): Promise<void>` — populates the WeakMap caches and zero-fills the file/bitmap keys.
- Layout methods mirror `chunk-keys`: `putChunk`, `getChunk`, `hasChunks`, `availableChunks`.
## Implementation Notes
The total file length is computed as `(totalChunks - 1) * chunkSize + HEADER_SIZE + lastPayloadLen`, matching the encrypt-stream output shape. The 164 in the inline arithmetic is `HEADER_SIZE` from `header.ts`. The bitmap is grown lazily inside `setBit` so the layout can survive a missing or truncated bitmap key (e.g. partial restore from backup) without losing chunks. `getChunk` returns `undefined` for missing files (`getItemSize === null`) and trusts `getPartial` for present-but-not-yet-populated regions.

View File

@@ -0,0 +1,48 @@
# build
## Purpose
Constructs a server-signed `Manifest` for an ingested file: bundles the file metadata (`fileId`, `fileSize`, `chunkSize`, `totalChunks`), encryption envelope (`algo`, `fileKeyWrapped`, `baseNonce`), and per-chunk integrity hashes into a single record, then signs the canonical encoding under the server private key. The manifest is the consensus artefact: peers that trust the server public key can verify it without holding any chunk bytes.
## Features
- `buildManifest(options): Manifest` — pure function, no I/O, no global state.
- `canonicalSerialize(manifest): Uint8Array` — deterministic big-endian length-prefixed encoding suitable for cross-platform Ed25519 signing.
- `algo` is encoded as a length-prefixed UTF-8 string so future encryption algorithms can be added without breaking the wire format.
- Per-chunk entries pack `(chunkId: u32, hash: 32 bytes, payloadLen: u32)` into 40 bytes — bounded growth, predictable memory.
## Usage: Basic Example
```ts
import { buildManifest } from "@suaveplan/drive-plugin-chunks";
const manifest = buildManifest({
fileId: sha256Bytes,
fileSize: 12n,
chunkSize: 1024,
totalChunks: 1,
encryption: { algo: "xchacha20poly1305", fileKeyWrapped: fileKey, baseNonce },
chunks: [{ chunkId: 0, hash: chunkHash, payloadLen: 12 }],
serverPrivKey,
});
```
## Usage: Advanced Example
```ts
import { buildManifest, canonicalSerialize } from "@suaveplan/drive-plugin-chunks";
import { verify } from "@suaveplan/crypto-curve/ed25519";
const manifest = buildManifest({ /* ... */ });
const { sig, ...base } = manifest;
const ok = verify(sig, canonicalSerialize(base), serverPubKey);
```
## API Reference
- `buildManifest(options): Manifest` — returns `{ version, fileId, fileSize, chunkSize, totalChunks, encryption, chunks, sig }`.
- `canonicalSerialize(manifest: Omit<Manifest, "sig">): Uint8Array` — used both for signing and for verification.
## Implementation Notes
Field order in `canonicalSerialize` is fixed and big-endian; changing it would break every previously-stored manifest. The `algo` length prefix makes the encoded form self-delimiting so receivers don't need an explicit version bump to add new algorithms — they only need to add a parsing branch keyed on the algo string. The final `buf.slice(0, off)` trims any over-allocated tail, but `totalLen` is computed exactly so this is normally a no-op.

View File

@@ -0,0 +1,54 @@
# load
## Purpose
Loads server-signed manifests from the two supported sources: a sentinel chunk inside the file's own chunk space (`manifestSource: "manifest-as-chunk"`) and a separate manifest server keyed by file id (`manifestSource: "server-only"`). Every code path runs the same `parseAndVerifyManifest` so signature verification can never be skipped.
## Features
- `loadManifestAsChunk(fileId, getChunk, serverPubKey)` — reads chunkId `-1` from the chunk source.
- `loadManifestFromServer(fileId, url, fetchFn, serverPubKey)` — issues a `fetch(url + "/manifest/<idHex>")` GET; injectable `fetchFn` for testing.
- `parseAndVerifyManifest(raw, serverPubKey)` — JSON-decodes (with `base64:` Uint8Array unwrapping), validates against `ManifestSchema`, and runs Ed25519 verification with the canonical encoder.
- `MANIFEST_NOT_FOUND` and `MANIFEST_INVALID` are the only errors that can come out of this module — every internal failure is mapped onto one of those two codes.
## Usage: Basic Example
```ts
import { loadManifestAsChunk } from "@suaveplan/drive-plugin-chunks";
const manifest = await loadManifestAsChunk(
fileId,
(id, idx) => chunks.getChunk(id, idx),
serverPubKey
);
```
## Usage: Advanced Example
```ts
import {
loadManifestFromServer,
parseAndVerifyManifest,
} from "@suaveplan/drive-plugin-chunks";
const manifest = await loadManifestFromServer(
fileId,
"https://manifests.example.com",
fetch,
serverPubKey
);
// Verifying a manifest received over a side-channel
const verified = parseAndVerifyManifest(receivedBytes, serverPubKey);
```
## API Reference
- `loadManifestAsChunk(fileId, getChunk, serverPubKey): Promise<Manifest>`.
- `loadManifestFromServer(fileId, url, fetchFn, serverPubKey): Promise<Manifest>`.
- `parseAndVerifyManifest(raw: Uint8Array, serverPubKey: Uint8Array): Manifest`.
- All three throw `ChunksError(CHUNKS.MANIFEST_NOT_FOUND)` (missing input) or `ChunksError(CHUNKS.MANIFEST_INVALID)` (schema or signature failure).
## Implementation Notes
`parseAndVerifyManifest` first tries to parse as UTF-8 JSON because manifests are persisted as JSON inside `plugin.ingest`; the catch falls back to treating the raw bytes as the parsed value, which lets future binary manifest formats slot in without changing the call site. Signature verification rebuilds `withoutSig` with a 64-byte zero placeholder and asks the canonical encoder to lay out the bytes; this matches the layout used at sign time so the verification is over identical bytes.

View File

@@ -0,0 +1,44 @@
# source
## Purpose
Validates the `manifestSource` configuration before the plugin starts wiring its read middleware. The function is small but load-bearing: it short-circuits the `"server-only"` branch when no `manifestServerUrl` is supplied so the rest of the plugin can assume the source is well-formed by the time it runs.
## Features
- `resolveManifestSource(source, manifestServerUrl?): { source, url }` — returns the normalised configuration tuple.
- Eager validation — throws synchronously instead of failing inside the read pipeline.
- Pure function — no I/O, no global state, deterministic for fixed inputs.
- Unaware of `getChunk` / `fetch` — those concerns live in `load.ts`.
## Usage: Basic Example
```ts
import { resolveManifestSource } from "@suaveplan/drive-plugin-chunks";
const cfg = resolveManifestSource("manifest-as-chunk");
```
## Usage: Advanced Example
```ts
import { resolveManifestSource } from "@suaveplan/drive-plugin-chunks";
const cfg = resolveManifestSource("server-only", "https://manifests.example.com");
console.log(cfg.url); // "https://manifests.example.com"
try {
resolveManifestSource("server-only");
} catch (err) {
console.error("missing manifestServerUrl");
}
```
## API Reference
- `resolveManifestSource(source: ManifestSource, manifestServerUrl?: string): { source: ManifestSource; url: string | undefined }`.
- Throws a plain `Error` (not `ChunksError`) when `source === "server-only"` and `manifestServerUrl` is missing, because this is a programmer error in plugin construction, not a runtime fault that consumers should branch on.
## Implementation Notes
The two valid `ManifestSource` values are intentionally narrow: `"manifest-as-chunk"` reuses the chunk-storage substrate as the manifest store, while `"server-only"` keeps manifests out-of-band on a separate HTTPS endpoint. Future sources (e.g. a libp2p PubSub channel) would slot in by extending the union type and the dispatcher in `load.ts`. The plain `Error` is surfaced as soon as the plugin object is constructed so misconfigurations show up at boot, not during the first `read`.

View File

@@ -0,0 +1,57 @@
# decrypt-chunk
## Purpose
The single-chunk decryption helper that runs on the read side of the pipeline. Given chunk metadata (already parsed via `header.parseHeader`), the chunk ciphertext, and the file key, it concatenates the ciphertext with the embedded AEAD tag and asks `@suaveplan/crypto-cipher/xchacha20-poly1305` to verify-and-decrypt in one call.
## Features
- `decryptChunk(metadata, ciphertext, fileKey): Uint8Array` — returns plaintext bytes; the caller is expected to truncate to `metadata.payloadLen`.
- Tag-failure path is structured: a `decrypt` failure is wrapped as `ChunksError(CHUNKS.AEAD_TAG_INVALID)` regardless of whether the underlying error was a tag mismatch, an invalid nonce length, or an internal `@noble/ciphers` rejection.
- Allocation-light — only one fresh buffer (`fullCiphertext`) is allocated per call.
## Usage: Basic Example
```ts
import { decryptChunk, parseChunk } from "@suaveplan/drive-plugin-chunks";
const chunk = parseChunk(rawChunk);
const plain = decryptChunk(chunk.metadata, chunk.ciphertext, fileKey);
const trimmed = plain.slice(0, chunk.metadata.payloadLen);
```
## Usage: Advanced Example
```ts
import {
CHUNKS_CODES,
ChunksError,
decryptChunk,
parseChunk,
} from "@suaveplan/drive-plugin-chunks";
async function reassembleSafely(rawChunks: Uint8Array[], fileKey: Uint8Array) {
const out: Uint8Array[] = [];
for (const raw of rawChunks) {
const c = parseChunk(raw);
try {
const plain = decryptChunk(c.metadata, c.ciphertext, fileKey);
out.push(plain.slice(0, c.metadata.payloadLen));
} catch (err) {
if (err instanceof ChunksError && err.code === CHUNKS_CODES.AEAD_TAG_INVALID) {
return null; // request a re-fetch from a different peer
}
throw err;
}
}
return out;
}
```
## API Reference
- `decryptChunk(metadata: ChunkMetadata, ciphertext: Uint8Array, fileKey: Uint8Array): Uint8Array` — concatenates `ciphertext || metadata.aeadTag` and runs XChaCha20-Poly1305 decrypt under `fileKey` and `metadata.nonce`. Throws `ChunksError(CHUNKS.AEAD_TAG_INVALID)` on any verification failure.
## Implementation Notes
The function does NOT trim the output to `payloadLen` — that's the caller's job. This avoids a redundant `.slice` allocation in transport code that already writes plaintext into a preallocated reassembly buffer. The error message is intentionally generic (`"chunk AEAD tag verification failed"`) so consumers cannot use the message text to distinguish between attacker-induced tag mismatches and accidental ciphertext corruption — both are fatal.

View File

@@ -0,0 +1,54 @@
# encrypt-stream
## Purpose
The encryption pipeline that transforms a plaintext file into a stream of signed encrypted chunks ready for persistence. It owns the chunk-level nonce derivation, the AEAD ciphertext + tag split, the per-chunk metadata population, and the immediate signing pass. Together these steps form the deterministic transform from `{ plaintext, fileId, fileKey, baseNonce, chunkSize, serverPrivKey }` to a stream of `EncodedChunk` objects.
## Features
- `encryptChunks(options): Generator<EncodedChunk>` — yields one chunk per `chunkSize - HEADER_SIZE` payload window, signed and ready to persist.
- `deriveNonce(baseNonce, chunkId): Uint8Array` — XORs the chunk index into the high-order bytes of the 24-byte XChaCha20 nonce; rejects non-24-byte inputs with `CHUNKS.METADATA_INVALID`.
- `TAG_LENGTH` constant (16) for the AEAD tag size.
- Synchronous generator — no async overhead, allocations bounded per chunk.
## Usage: Basic Example
```ts
import { randomPrivateKey } from "@suaveplan/crypto-curve/ed25519";
import { encryptChunks } from "@suaveplan/drive-plugin-chunks";
const chunks = Array.from(
encryptChunks({
plaintext: new TextEncoder().encode("hello"),
fileId: new Uint8Array(32),
fileKey: new Uint8Array(32).fill(7),
baseNonce: new Uint8Array(24).fill(11),
chunkSize: 1024,
serverPrivKey: randomPrivateKey(),
})
);
```
## Usage: Advanced Example
```ts
import { deriveNonce, encryptChunks, TAG_LENGTH } from "@suaveplan/drive-plugin-chunks";
for (const chunk of encryptChunks({ plaintext, fileId, fileKey, baseNonce, chunkSize: 4096, serverPrivKey })) {
await storage.set(`chunks:${idHex}:${chunk.metadata.chunkId}`, chunk.rawBytes);
}
// Reconstruct the per-chunk nonce on the receiver
const nonce = deriveNonce(baseNonce, chunkIdReceived);
console.log(TAG_LENGTH); // 16
```
## API Reference
- `encryptChunks(options: EncryptOptions): Generator<EncodedChunk>` — `EncryptOptions` carries `plaintext`, `fileId`, `fileKey`, `baseNonce` (24 bytes), `chunkSize`, `serverPrivKey`. Yields `{ rawBytes, metadata }` per chunk.
- `deriveNonce(baseNonce: Uint8Array, chunkId: number): Uint8Array` — XORs `chunkId` into bytes 0–3.
- `TAG_LENGTH: 16` — AEAD tag size.
## Implementation Notes
`encryptChunks` writes the signature into the raw buffer at `[METADATA_SIZE, HEADER_SIZE)` immediately after computing it, so consumers can stream chunks through transports without ever holding a full file plaintext in memory. The XOR-based nonce derivation is sound for chunk indices ≤ 2³² because XChaCha20-Poly1305 takes a 24-byte nonce: under fixed `baseNonce` no two chunks of the same file ever share a nonce.

View File

@@ -0,0 +1,50 @@
# sign-chunk
## Purpose
Signs a chunk under the server's Ed25519 private key. Signatures cover the full encoded chunk (metadata header bytes plus the encrypted payload) so receivers can verify integrity without first decrypting. The output is fixed at 64 bytes, which is the slot reserved by `header.ts` (`SIG_SIZE = 64`).
## Features
- `signChunk(chunk, privKey): Uint8Array` — produces a deterministic Ed25519 signature over `metadata || ciphertext`.
- `buildSignedChunk(chunk, privKey): Chunk` — convenience wrapper that returns a chunk with the freshly-computed signature inlined.
- Pure function — no global state, no side effects, deterministic for fixed inputs (Ed25519 with seeded keys).
## Usage: Basic Example
```ts
import { randomPrivateKey } from "@suaveplan/crypto-curve/ed25519";
import { signChunk } from "@suaveplan/drive-plugin-chunks";
const privKey = randomPrivateKey();
const chunk = {
metadata: { /* ChunkMetadata */ } as never,
sig: new Uint8Array(64),
ciphertext: new Uint8Array([1, 2, 3]),
};
const sig = signChunk(chunk, privKey);
```
## Usage: Advanced Example
```ts
import {
buildSignedChunk,
parseChunk,
verifyChunkSig,
} from "@suaveplan/drive-plugin-chunks";
const signed = buildSignedChunk(parseChunk(rawChunk), serverPriv);
verifyChunkSig(/* raw bytes containing signed.sig */, serverPub);
```
## API Reference
- `signChunk(chunk: Chunk, privKey: Uint8Array): Uint8Array` — returns 64-byte signature. Concatenates `encodeHeader(metadata)` and `chunk.ciphertext` then signs the concatenation.
- `buildSignedChunk(chunk: Chunk, privKey: Uint8Array): Chunk` — same chunk with a populated `sig` field.
## Implementation Notes
The signature input deliberately excludes the existing `chunk.sig` field — re-signing a chunk produces the same signature for fixed inputs even though Ed25519 is not strictly deterministic across implementations (suaveplan pins to `@noble/curves` which is RFC-8032 deterministic). Verification is implemented in `verify-chunk.ts` using the matching `getPublicKey` derivation.
The signed payload is `encodeHeader(metadata) || ciphertext` rather than the full raw chunk because the signature occupies a fixed slot inside the raw bytes; including it in the signing input would be circular. This split also lets transports verify a chunk in two passes: parse the metadata to learn `payloadLen`, then sign-verify a contiguous range of the buffer without copying. Signing happens at ingest time and is the only place the `serverPrivKey` ever leaves the plugin's option object — verification only needs the public key.

View File

@@ -0,0 +1,52 @@
# verify-chunk
## Purpose
Verifies a chunk's Ed25519 signature against the server's public key as the gate that runs on receipt of every chunk — before persistence, before decryption, before reassembly. A mismatched or corrupted signature surfaces as `ChunksError(CHUNKS.SIG_INVALID)` so callers can drop bad chunks without ever exposing them to the rest of the pipeline.
## Features
- `verifyChunkSig(raw, pubKey): void` — reads the embedded signature out of the raw bytes, recomputes the signed payload, and verifies in constant-time via `@noble/curves/ed25519`.
- Single-pass — runs over the original raw buffer without re-encoding.
- Throws on failure rather than returning `false`, matching the rest of the pipeline's error contract.
## Usage: Basic Example
```ts
import { verifyChunkSig } from "@suaveplan/drive-plugin-chunks";
verifyChunkSig(rawChunk, serverPubKey);
// throws ChunksError(CHUNKS.SIG_INVALID) on mismatch
```
## Usage: Advanced Example
```ts
import {
CHUNKS_CODES,
ChunksError,
parseChunk,
verifyChunkSig,
} from "@suaveplan/drive-plugin-chunks";
async function ingestPeerChunk(raw: Uint8Array) {
try {
verifyChunkSig(raw, expectedServerPub);
} catch (err) {
if (err instanceof ChunksError && err.code === CHUNKS_CODES.SIG_INVALID) {
return reportPeerCorruption(raw);
}
throw err;
}
const parsed = parseChunk(raw);
return persist(parsed);
}
```
## API Reference
- `verifyChunkSig(raw: Uint8Array, pubKey: Uint8Array): void` — validates that `raw` carries a correct signature for its embedded metadata + ciphertext under `pubKey`. Throws `ChunksError(CHUNKS.SIG_INVALID)` on any failure (including under-length buffers, since `parseHeader` runs first).
## Implementation Notes
The function rebuilds the signed payload as `metadata || ciphertext` rather than treating the raw bytes as already-signed because the signature occupies a fixed offset between metadata and ciphertext — re-encoding metadata after parsing keeps verification independent of any future header padding changes. suaveplan pins to `@noble/curves` for both signing and verification, which is constant-time and audited.

View File

@@ -0,0 +1,63 @@
# plugin
## Purpose
Implements the `@suaveplan/drive-plugin-chunks` plugin object that consumes the `Drive` plugin slot, splits files into signed encrypted chunks, persists them through a `DriveStorage`, and reassembles them on read. The plugin is the orchestration layer that wires together encryption, signing, manifest construction, layout selection, and the storage middleware that intercepts `drive.read` calls so consumers see whole files even though the bytes live in chunks.
## Features
- `chunksPlugin(options)` factory returning a `DrivePlugin<ChunksApi>` ready to be passed to `drive.usePlugin(...)`.
- `ingest(path)` reads a plaintext file from the drive, deterministically derives the file id (`sha256(plaintext)`), encrypts with XChaCha20-Poly1305 under a random per-file key and base nonce, signs each chunk with `serverPrivKey`, stores chunks via the chosen layout, and persists a server-signed manifest.
- Read middleware that, for any path with a known file id, reassembles plaintext on demand and short-circuits the storage read with the original bytes.
- Layout-aware behaviour: `sparse-file` preallocates a single offset-addressable file via `setPartial`; `chunk-keys` writes per-chunk keys; `auto` selects per storage capabilities.
- `getChunk(fileId, chunkId)`, `loadManifest(fileId)`, and `availableChunks(fileId)` for transport peers.
- Structured error codes for every failure path (signature, AEAD tag, manifest, layout, ingest).
## Usage: Basic Example
```ts
import { Drive } from "@suaveplan/drive";
import { chunksPlugin } from "@suaveplan/drive-plugin-chunks";
import { MemoryStorage } from "@suaveplan/storage/memory";
import { getPublicKey, randomPrivateKey } from "@suaveplan/crypto-curve/ed25519";
const privKey = randomPrivateKey();
const drive = new Drive(Object.assign(new MemoryStorage(), { id: "default" }));
await drive.initialize();
const chunks = await drive.usePlugin(
chunksPlugin({ serverPrivKey: privKey, serverPubKey: getPublicKey(privKey) })
);
await drive.write("hello.txt", new TextEncoder().encode("world"));
const fileId = await chunks.ingest("hello.txt");
const back = await drive.read("hello.txt"); // reassembled plaintext
```
## Usage: Advanced Example
```ts
const chunks = await drive.usePlugin(
chunksPlugin({
serverPrivKey,
serverPubKey,
chunkSize: 64 * 1024,
layout: "sparse-file",
manifestSource: "manifest-as-chunk",
})
);
const fileId = await chunks.ingest("big.bin");
const raw = await chunks.getChunk(fileId, 0);
const manifest = await chunks.loadManifest(fileId);
```
## API Reference
- `chunksPlugin(options: ChunksPluginOptions): DrivePlugin<ChunksApi>` — factory.
- `ChunksApi.ingest(path: string): Promise<Uint8Array>` — returns `fileId`.
- `ChunksApi.getChunk(fileId: Uint8Array, chunkId: number): Promise<Uint8Array | undefined>`.
- `ChunksApi.loadManifest(fileId: Uint8Array): Promise<Manifest>`.
- `ChunksApi.availableChunks(fileId: Uint8Array): Promise<number[]>`.
- `ChunksApi.layout: ChunkLayoutStrategy`.
## Implementation Notes
`ingest` collects all encoded chunks into an array before persistence so the sparse-file layout can preallocate the underlying file before the first `setPartial` call — without preallocation, sparse-file storages refuse out-of-order writes. The catch block in `ingest` re-throws `ChunksError` instances unchanged and wraps everything else (`Error` or arbitrary throwable) as `CHUNKS.INGEST_FAILED` with a synthesised message; this keeps the public failure surface narrow without losing the underlying cause.

View File

@@ -0,0 +1,61 @@
# manifest.schema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod schemas that gate every manifest the plugin parses from untrusted bytes. Schema validation is the line between "raw bytes that may or may not be a manifest" and "a typed `Manifest` ready to flow through the rest of the plugin"; it runs before signature verification so structural errors surface as `MANIFEST_INVALID` without paying the cost of an Ed25519 verify.
## Features
- `ManifestSchema` — the top-level schema for a complete signed manifest, including a 64-byte signature, version literal `0x0001`, 32-byte `fileId`, positive `chunkSize` / `totalChunks`, and an array of per-chunk entries.
- `ManifestEncryptionSchema` — the embedded encryption envelope: `algo` is locked to `"xchacha20poly1305"`, `baseNonce` is exactly 24 bytes, and `fileKeyWrapped` is left length-flexible to leave room for future KEMs.
- `ManifestChunkEntrySchema` — per-chunk entries with non-negative `chunkId` / `payloadLen` and a 32-byte SHA-256 `hash`.
- `fileSize` accepts both `bigint` and decimal-string inputs and normalises to `bigint`, which makes JSON-serialised manifests round-trip cleanly even though native JSON has no `bigint` type.
## Usage: Basic Example
```ts
import { ManifestSchema } from "@suaveplan/drive-plugin-chunks";
const result = ManifestSchema.safeParse(decodedFromJson);
if (!result.success) throw new Error(result.error.message);
const manifest = result.data;
```
## Usage: Advanced Example
```ts
import {
ManifestChunkEntrySchema,
ManifestEncryptionSchema,
ManifestSchema,
} from "@suaveplan/drive-plugin-chunks";
function isLooselyValid(maybe: unknown): boolean {
return ManifestSchema.safeParse(maybe).success;
}
const enc = ManifestEncryptionSchema.parse({
algo: "xchacha20poly1305",
fileKeyWrapped: new Uint8Array(32),
baseNonce: new Uint8Array(24),
});
const entry = ManifestChunkEntrySchema.parse({
chunkId: 0,
hash: new Uint8Array(32),
payloadLen: 50,
});
```
## API Reference
- `ManifestSchema: ZodTypeAny` — top-level boundary schema.
- `ManifestEncryptionSchema: ZodTypeAny` — encryption sub-schema.
- `ManifestChunkEntrySchema: ZodTypeAny` — chunk-entry sub-schema.
- `type ManifestInput = z.input<typeof ManifestSchema>` — the pre-transform JSON shape.
## Implementation Notes
The `// schema: boundary-only` annotation tells the conformance script that these schemas are deliberately part of the public surface even when this file is the only place inside `src/` that calls `.parse` / `.safeParse` on them. Removing the annotation (or moving the schemas) would be an enterprise-grade signal change so consumers should not depend on internal validation paths.

View File

@@ -0,0 +1,60 @@
# chunks.types
## Purpose
Holds the cross-module type vocabulary for the chunks plugin: chunk metadata and chunk shapes, manifest entries and the manifest itself, the `ChunkLayout` interface that pluggable storage layouts implement, the `ChunkSource` interface that mesh / transport peers expose, and the plugin options object. Centralising the types here avoids `any` leaks across module boundaries and lets the rest of the codebase share a single language for chunk-shaped data.
## Features
- `ChunkMetadata` — every field of the on-wire metadata header in a single TypeScript shape.
- `Chunk` / `EncodedChunk` — parsed chunk with separate `sig` and `ciphertext` fields, plus the encoded bytes ready for storage.
- `Manifest` / `ManifestChunkEntry` / `ManifestEncryption` — manifest tree mirroring `ManifestSchema`.
- `ChunkLayout` — minimal interface: `name`, `putChunk`, `getChunk`, `hasChunks`, `availableChunks`.
- `ChunkSource` — peer-facing interface (`hasChunks`, `availableChunks`).
- `ChunksPluginOptions` — frozen options object with required `serverPrivKey` / `serverPubKey` and optional `chunkSize` / `layout` / `manifestSource` / `manifestServerUrl`.
- `ChunkLayoutStrategy = "sparse-file" | "chunk-keys" | "auto"`.
- `ManifestSource = "manifest-as-chunk" | "server-only"`.
## Usage: Basic Example
```ts
import type {
ChunkLayout,
ChunkMetadata,
Manifest,
} from "@suaveplan/drive-plugin-chunks";
declare const layout: ChunkLayout;
declare const metadata: ChunkMetadata;
declare const manifest: Manifest;
```
## Usage: Advanced Example
```ts
import type {
ChunksPluginOptions,
ChunkSource,
} from "@suaveplan/drive-plugin-chunks";
function buildOptions(serverPrivKey: Uint8Array, serverPubKey: Uint8Array): ChunksPluginOptions {
return { serverPrivKey, serverPubKey, chunkSize: 65536, layout: "auto" };
}
function describeAvailability(src: ChunkSource, fileId: Uint8Array) {
return src.availableChunks(fileId);
}
```
## API Reference
- `ChunkMetadata` — `{ version, fileId, chunkId, totalChunks, fileSize, chunkSize, payloadLen, flags, nonce, aeadTag }`.
- `Chunk` — `{ metadata, sig, ciphertext }`.
- `EncodedChunk` — `{ rawBytes, metadata }`.
- `Manifest`, `ManifestChunkEntry`, `ManifestEncryption` — see schemas.
- `ChunkLayout`, `ChunkSource`, `ChunksPluginOptions` — interfaces consumed by the plugin and its layouts.
- `ChunkLayoutStrategy`, `ManifestSource` — string-literal unions.
## Implementation Notes
`ChunkMetadata.fileSize` is `bigint` because chunk arithmetic on files larger than 2³² bytes requires 64-bit math; consumers serialising to JSON must convert with `String(value)` and re-parse via `BigInt` (the `ManifestSchema` accepts both forms). Every `Uint8Array` field is treated as opaque bytes — none of the types pin a specific endianness because that responsibility belongs to `header.encodeHeader` / `parseHeader`.