feat(files): @shade/files 0.3.0 — E2EE filesystem RPC primitive
Some checks failed
Test / test (push) Has been cancelled

M-Files-1..6 land the full files-RPC layer + everything 0.3.0 needs to
ship. Apps keep their own UI; this layer ships the typed RPC, the
streams bridge for content I/O, and production hooks (rate limit,
retention, fingerprint gate, metrics).

@shade/files (NEW)
- Standard ops: list/stat/mkdir/delete/move/read/write/getThumbnail with
  Zod-validated wire schemas + clean user-handler types.
- Custom ops: typed via TypeScript declaration merging on CustomOpsMap
  + per-op Zod schemas; client.custom('app.foo', {...}) is fully typed.
- Content I/O: inline (≤ 256 KiB plaintext) base64-in-RPC; streams
  (> 256 KiB) ride @shade/transfer via userMetadata.shadeFilesWriteId
  / shadeFilesReadStreamId correlation. Server-side TransformStream
  bridges accept inbound transfers immediately (engine rejects chunks
  that arrive before accept) and park the readable for the matching
  RPC.
- Directory ops: walk(path, opts) async-iterable depth-first walker;
  uploadDirectory()/downloadDirectory() with bounded concurrency pool
  (default 4, cap 16), aggregated progress, abort.
- Production hooks (callback-based, vendor-neutral): rate-limit (op +
  byte), idempotency cache (LRU + TTL + in-flight de-dupe), path
  policy (traversal + percent-decode hardening), fingerprint gate
  (required/optional/reject), pluggable Ed25519 sig verification with
  ±5 min replay window, onMetric sink (standard names).
- React hooks (subpath @shade/files/react): ShadeFilesProvider,
  useShadeFiles, useFileList, useFileTransfer/Upload/Download.
- Shade.files.serve(handler) + Shade.files.client(peer) high-level
  entrypoint in @shade/sdk; lazy + memoized; one handler per Shade.

Wire format bump
- @shade/proto wire VERSION 0x01 → 0x02. Length prefixes changed from
  u16 to u32. The previous u16 silently truncated payloads above
  64 KiB — a hard correctness ceiling that blocked inline file ops
  up to 256 KiB. Wire-incompatible with 0.2.x peers; new sessions
  only. Cross-platform Kotlin port (android/shade-android) updated to
  match; test-vectors/wire-format.json regenerated.

Concurrency safety
- ShadeSessionManager.encrypt/.decrypt now run under per-peer mutex.
  Concurrent decryptions of the same peer raced ratchet state
  (manifested as sporadic "Failed to decrypt — wrong key or tampered
  data" under load — surfaced once concurrent uploadDirectory pumped
  many writes in flight). Encrypt was already serialized via
  Shade.send's encryptChains; decrypt is now serialized at the
  manager layer too.

@shade/streams extension
- StreamMetadata.userMetadata?: Record<string, string> for
  application-level key/value pairs that round-trip verbatim through
  stream-init plaintext. Used by @shade/files for write/read
  correlation; available to any consumer.

@shade/sdk extension
- Shade.files getter (lazy + memoized).
- BackgroundHooks.onPruneFiles + periodic timer (default 5 min) +
  BackgroundTasks.setHook(name, fn) for runtime hook registration.

Bundles in-flight 0.2.0 work
- packages/shade-streams/, packages/shade-transfer/, related
  shade-sdk streams-bridge + shade-widgets transfer hooks were
  uncommitted prior to this session. Including them keeps the
  workspace consistent at 0.3.0 since @shade/files depends on them.

Tests
- 74 new tests in @shade/files (572 → 646 workspace pass; 0 fail;
  3× stable). Coverage spans unit (inline-threshold + concurrency),
  integration (read-write inline + streams up to 1 MiB, walk +
  upload/download directory, custom-op, metrics, SDK namespace
  end-to-end), and security (tampered-envelope sig verification,
  replay window, fingerprint gate, rate-limit + quota).

Release artifacts
- All packages bumped to 0.3.0 via scripts/bump-version.ts.
- scripts/publish-all.ts PACKAGES updated with shade-files in
  topological order (after shade-transfer, before shade-sdk).
- bun run publish:dry clean (14 packed, 0 failed).
- examples/08-files-browser/ — three-process CLI demo (prekey + Bob
  server + Alice CLI) covering list/stat/mkdir/delete/upload/download.
- docs/files.md — full API + design doc.
- CHANGELOG.md 0.3.0 entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 14:00:01 +02:00
parent 7e0f7320a9
commit fa770d3063
198 changed files with 20412 additions and 256 deletions

View File

@@ -5,6 +5,190 @@ All notable changes to Shade are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.0] — 2026-05-02 — Shade Files
E2EE filesystem RPC primitive — drop-in entrypoints for any consumer that
wants to expose a filesystem (or filesystem-like surface) over Shade. Apps
keep their own UI; this layer ships the typed RPC, the streams bridge for
content I/O over 256 KiB, and production hooks (rate limit, retention,
fingerprint gate, metrics).
### Added
#### `@shade/files` (NEW)
- Standard ops: `list`, `stat`, `mkdir`, `delete`, `move`, `read`, `write`,
`getThumbnail` — Zod-validated wire schemas + clean user-handler types.
- Custom ops: `client.custom('app.foo', {...})` with full type-safety via
TypeScript declaration merging on `CustomOpsMap` + per-op Zod schemas
registered server-side.
- Content I/O: inline (≤ 256 KiB plaintext) base64-in-RPC; streams (> 256 KiB)
ride `@shade/transfer` with automatic correlation via
`userMetadata.shadeFilesWriteId` / `shadeFilesReadStreamId`.
- Directory ops: `walk(path, opts)` async-iterable depth-first walker;
`uploadDirectory()` / `downloadDirectory()` with bounded concurrency
pool (default 4, cap 16), aggregated progress events, abort support.
- Production hooks (all callback-based, vendor-neutral):
- **Rate limit**: token-bucket per sender, op-cost + byte-quota,
`FsRateLimitError` / `QuotaExceededError` with `retryAfterMs`.
- **Idempotency cache**: per-sender LRU + TTL, in-flight de-dupe,
periodic prune via `BackgroundHooks.onPruneFiles`.
- **Path policy**: built-in traversal hardening, percent-decode,
forbidden-bytes check, root-scope, symlink toggle, `extra` predicate.
- **Fingerprint gate**: `requireFingerprintVerifiedFor(ctx)`
`'required' | 'optional' | 'reject'` + `isFingerprintVerified(sender)`.
- **Signature verification**: pluggable `verifySender(sender, canonical, sig)`
with replay-window enforcement (±5 min `signedAt` skew rejected).
- **Metrics**: `onMetric(name, value, tags)` with standard names
(`shade_files_op_duration_ms`, `_op_total`, `_bytes_in/out`,
`_idempotency_hit/conflict_total`, `_rate_limit_reject_total`,
`_fingerprint_reject_total`, `_signature_reject_total`).
- React hooks (subpath import `@shade/files/react`):
`<ShadeFilesProvider>`, `useShadeFiles`, `useFileList`,
`useFileTransfer` / `useFileUpload` / `useFileDownload`. SSR-safe; no UI
components — apps bring their own.
- High-level entry: `Shade.files.serve(handler)` and `Shade.files.client(peer)`
in `@shade/sdk`. Lazy + memoized; one handler per Shade instance.
- Drop-in adapter: `createMemoryDirectory()` for tests; structurally
compatible with browser `FileSystemDirectoryHandle`.
#### Wire format bump
- `@shade/proto` wire VERSION bumped from `0x01` to `0x02`. Length prefixes
changed from u16 to u32 — previous limit was 64 KiB ratchet payloads,
which blocked inline file ops up to 256 KiB.
**Wire-incompatible with 0.2.x peers.** New sessions only.
- Cross-platform Kotlin port (`android/shade-android`) updated to match.
#### Concurrency safety
- `ShadeSessionManager.encrypt` / `.decrypt` now run under per-peer mutex.
Previously, concurrent decryptions of the same peer raced ratchet state
(manifested as sporadic `Failed to decrypt — wrong key or tampered data`
under load). Encrypt was already serialized via `Shade.send`'s
`encryptChains`; decrypt is now serialized at the manager layer too.
#### `@shade/streams` extension
- `StreamMetadata` gets optional `userMetadata?: Record<string, string>`
application-level key/value pairs that round-trip verbatim through
`stream-init` plaintext. Used by `@shade/files` for write/read correlation
but available to any consumer.
#### `@shade/sdk` extension
- `Shade.files` getter (lazy + memoized).
- `BackgroundHooks.onPruneFiles?: () => void` + periodic timer (default 5 min)
for `@shade/files` retention.
- `BackgroundTasks.setHook(name, fn)` for runtime hook registration.
### Examples
- `examples/08-files-browser/` — three-process demo (prekey + Bob server +
Alice CLI) covering list/stat/mkdir/delete/upload/download with both
inline and streamed paths.
### Tests
- 100+ new tests across `tests/{unit,integration,security}/` in
`@shade/files`. End-to-end coverage for streams I/O up to 1 MiB, custom-op
registration + Zod validation, fingerprint-gate rejection, replay-window
enforcement, idempotent retries, rate-limit + quota enforcement, walk
+ bulk transfer aggregated progress.
## [0.2.0] — 2026-05-01 — Shade Streams
E2EE chunked upload/download with parallel lanes, resumable transfers, and a
"magic drop-in" UX for any Shade-using app. Adds two new packages
(`@shade/streams`, `@shade/transfer`) and extends `@shade/sdk` and
`@shade/widgets` with high-level transfer APIs.
### Added
#### Streams crypto layer (`@shade/streams`)
- HKDF stream/lane key derivation (`deriveStreamKey`, `deriveLaneKey`)
- Deterministic AES-GCM nonce construction `nonce = laneId(4) || seq(8)`
- Streaming SHA-256 via `@noble/hashes/sha2.js` for memory-bounded integrity
- `StreamSender` / `StreamReceiver` per-lane state machines with strict
in-order seq + replay detection (`StreamReplayError`,
`StreamOutOfOrderError`, `StreamDecryptionError`, `StreamProtocolError`)
- `MultiLaneSender` / `MultiLaneReceiver` coordinators for parallel transfers
- Range and round-robin partitioning helpers (`planRangePartition`,
`planRoundRobinPartition`, `chunkRange`)
- Wire format: new envelope type `0x11` (stream-chunk) in `@shade/proto`,
control envelopes (`stream-init` / `-finish` / `-abort` / `-resume-*`)
ride existing `0x02` ratchet messages with JSON `kind` discriminator
#### Transfer orchestration (`@shade/transfer`)
- `TransferEngine` — single class wrapping outgoing + incoming lifecycle
- Default `ShadeTransferHttpTransport` for chunk POSTs, opt-in
`ShadeTransferWsTransport` with `FallbackTransferTransport` for auto-fallback
- `createTransferRoutes()` Hono factory mounts `/v1/transfer/*` routes
(`chunk`, `state`, `health`)
- `IControlChannel` + `MemoryControlChannel` for in-process testing;
the SDK provides `ShadeControlChannel` over `Shade.send`/`receive`
- Resume protocol: `MemoryResumeStore`, `StorageBackedResumeStore`,
`deriveDeviceKey()` for at-rest streamSecret encryption,
`engine.resumeUpload(streamId, freshInput)` for kill-restart-verify flows
- `ProgressTracker` with EMA-smoothed throughput + ETA
- Retry/backoff (`withRetry`) with exponential delay + jitter
- Error hierarchy: `TransferError`, `TransferAbortError`,
`TransferIntegrityError`, `TransferProtocolError`, `TransferOfflineError`,
`TransferResumeError`, `TransferTransportError`
#### SDK (`@shade/sdk`)
- `Shade.upload(opts)` — high-level entry; encrypts + chunks + ships
- `Shade.onIncomingTransfer(handler)` — receiver-side subscription
- `Shade.transferRoute()` — Hono router to mount on the consumer's HTTP server
- `Shade.acceptTransferEnvelope(from, env)` — low-level entry for custom transports
- `Shade.resumeUpload(streamId, freshInput)` — pick up an interrupted transfer
- `Shade.listTransfers(filter?)` — list resumable / active transfers from storage
- `ShadeTransferAuthenticator` — Ed25519-signing authenticator for HTTP/WS transports
- `Shade.onMessage(handler)` now accepts `Promise<void>`-returning handlers
(awaited in sequence) — supports flow-control over the control plane
#### Storage (all backends)
- New optional `StorageProvider` methods: `saveStreamState`,
`getStreamState`, `removeStreamState`, `listActiveStreamStates`,
`pruneStreamStates`. Existing v0.1.x providers compile cleanly (optional methods)
- SQLite (`stream_state` table) and Postgres (`shade_stream_state` table)
schemas with at-rest encrypted streamSecret
- `MemoryStorage` extended with in-memory stream-state map
#### Widgets (`@shade/widgets`)
- `<ShadeRuntimeProvider runtime={shade}>` — separate React context for
upload/download widgets (distinct from the observer-dashboard `<ShadeProvider>`)
- `useShadeUpload()` / `useShadeDownload()` headless hooks
- `<ShadeUploader />` / `<ShadeDownloader />` composite components with
render-prop pattern for full UI replacement
- Sub-components: `<DropZone />`, `<TransferRow />`, `<ProgressBar />`,
`<SpeedReadout />`, `<ETAReadout />`, `<LaneIndicator />`
- Theme-token additions for progress, drop zone, and lane indicator colors
### Security properties
- Per-chunk AES-256-GCM with deterministic nonce; AAD binds
`streamId || laneId || seq || isLast` so any header tamper invalidates AEAD
- streamSecret never on the wire in plaintext — shipped via Double Ratchet
control envelope; lane keys derived locally and never transmitted
- Resume state encrypted at rest with `deviceKey` derived from identity's
signing private key (rotation invalidates in-flight resume — by design)
- Receiver enforces strict in-order seq per lane (`StreamOutOfOrderError`,
`StreamReplayError`); finish-time integrity check verifies per-lane sha256
+ overall sha256 over original byte order
### Tests added (118 new across 47 files; 444 total)
- Unit: KDF, nonce, AEAD, streaming SHA, sender/receiver, partition
- Integration: 1/4/16-lane parity, range vs round-robin parity,
Bun.serve loopback at 100 KiB / 1 MiB / 8 MiB, two real Shade instances
end-to-end at 64 KiB / 512 KiB / 4 MiB
- Resume: kill-restart-verify on 256 KiB with 4 lanes
- WS fallback: WS connect failure → transparent HTTP completion
- Tamper: bit-flip ciphertext / tag / header field; replay; out-of-order
- Wire: 0x11 envelope encode/decode roundtrip + edge cases
### Backward compatibility
- `Shade.send`/`receive`/`onMessage`/`fingerprint`/`rotate` unchanged
(`onMessage` widened to support async handlers — sync handlers still work)
- Existing wire types `0x01` (PreKeyMessage) / `0x02` (RatchetMessage) unchanged
- `StorageProvider` interface extension uses optional methods
- `@shade/streams` and `@shade/transfer` are new packages; no migration
## [1.0.0] — 2026-04-10
### First production release