feat(files): @shade/files 0.3.0 — E2EE filesystem RPC primitive
Some checks failed
Test / test (push) Has been cancelled
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:
184
CHANGELOG.md
184
CHANGELOG.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user