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>
13 KiB
Changelog
All notable changes to Shade are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[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 onCustomOpsMap+ per-op Zod schemas registered server-side. - Content I/O: inline (≤ 256 KiB plaintext) base64-in-RPC; streams (> 256 KiB)
ride
@shade/transferwith automatic correlation viauserMetadata.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/QuotaExceededErrorwithretryAfterMs. - 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,
extrapredicate. - Fingerprint gate:
requireFingerprintVerifiedFor(ctx)→'required' | 'optional' | 'reject'+isFingerprintVerified(sender). - Signature verification: pluggable
verifySender(sender, canonical, sig)with replay-window enforcement (±5 minsignedAtskew 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).
- Rate limit: token-bucket per sender, op-cost + byte-quota,
- 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)andShade.files.client(peer)in@shade/sdk. Lazy + memoized; one handler per Shade instance. - Drop-in adapter:
createMemoryDirectory()for tests; structurally compatible with browserFileSystemDirectoryHandle.
Wire format bump
@shade/protowire VERSION bumped from0x01to0x02. 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/.decryptnow run under per-peer mutex. Previously, concurrent decryptions of the same peer raced ratchet state (manifested as sporadicFailed to decrypt — wrong key or tampered dataunder load). Encrypt was already serialized viaShade.send'sencryptChains; decrypt is now serialized at the manager layer too.
@shade/streams extension
StreamMetadatagets optionaluserMetadata?: Record<string, string>— application-level key/value pairs that round-trip verbatim throughstream-initplaintext. Used by@shade/filesfor write/read correlation but available to any consumer.
@shade/sdk extension
Shade.filesgetter (lazy + memoized).BackgroundHooks.onPruneFiles?: () => void+ periodic timer (default 5 min) for@shade/filesretention.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.jsfor memory-bounded integrity StreamSender/StreamReceiverper-lane state machines with strict in-order seq + replay detection (StreamReplayError,StreamOutOfOrderError,StreamDecryptionError,StreamProtocolError)MultiLaneSender/MultiLaneReceivercoordinators 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 existing0x02ratchet messages with JSONkinddiscriminator
Transfer orchestration (@shade/transfer)
TransferEngine— single class wrapping outgoing + incoming lifecycle- Default
ShadeTransferHttpTransportfor chunk POSTs, opt-inShadeTransferWsTransportwithFallbackTransferTransportfor auto-fallback createTransferRoutes()Hono factory mounts/v1/transfer/*routes (chunk,state,health)IControlChannel+MemoryControlChannelfor in-process testing; the SDK providesShadeControlChanneloverShade.send/receive- Resume protocol:
MemoryResumeStore,StorageBackedResumeStore,deriveDeviceKey()for at-rest streamSecret encryption,engine.resumeUpload(streamId, freshInput)for kill-restart-verify flows ProgressTrackerwith 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 + shipsShade.onIncomingTransfer(handler)— receiver-side subscriptionShade.transferRoute()— Hono router to mount on the consumer's HTTP serverShade.acceptTransferEnvelope(from, env)— low-level entry for custom transportsShade.resumeUpload(streamId, freshInput)— pick up an interrupted transferShade.listTransfers(filter?)— list resumable / active transfers from storageShadeTransferAuthenticator— Ed25519-signing authenticator for HTTP/WS transportsShade.onMessage(handler)now acceptsPromise<void>-returning handlers (awaited in sequence) — supports flow-control over the control plane
Storage (all backends)
- New optional
StorageProvidermethods:saveStreamState,getStreamState,removeStreamState,listActiveStreamStates,pruneStreamStates. Existing v0.1.x providers compile cleanly (optional methods) - SQLite (
stream_statetable) and Postgres (shade_stream_statetable) schemas with at-rest encrypted streamSecret MemoryStorageextended 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 || isLastso 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
deviceKeyderived 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/rotateunchanged (onMessagewidened to support async handlers — sync handlers still work)- Existing wire types
0x01(PreKeyMessage) /0x02(RatchetMessage) unchanged StorageProviderinterface extension uses optional methods@shade/streamsand@shade/transferare new packages; no migration
[1.0.0] — 2026-04-10
First production release
Shade implements the Signal Protocol (X3DH + Double Ratchet) as a standalone, audit-friendly E2EE library for TypeScript/Bun.
Added
Core protocol
- X3DH key agreement (X25519 + Ed25519, supports asynchronous bundles)
- Double Ratchet with forward secrecy and post-compromise recovery
- Skipped message key cache for out-of-order delivery (max 1000 per chain)
- Header-bound AAD on AES-256-GCM encrypts (tampered headers fail decryption)
- Memory zeroization of message keys, chain keys, root keys, and DH private keys after use
Storage
MemoryStorage(in-memory, for tests/embedded)SQLiteStorage(@shade/storage-sqlite) — bun:sqlite, WAL mode, crash-safePostgresStorage(@shade/storage-postgres) — Drizzle, FOR UPDATE SKIP LOCKED- All backends survive container restarts and SIGKILL
- Identity history with 7-day grace period for rotation
Prekey server (@shade/server)
- Hono-based REST API with self-authenticated registration (Ed25519 signatures)
- Anonymous bundle fetches (read-only)
- Per-IP and per-identity rate limiting (token bucket)
- Address validation (NFKC normalization, alphanumeric +
:_-.) - ±5 minute replay window on signed requests
- Health endpoints (
/health,/healthz,/ready) - Prometheus metrics (
/metrics) - Structured JSON logging
- Graceful shutdown on SIGTERM/SIGINT
- Production Dockerfile with non-root user, healthcheck, multi-stage build
- docker-compose.yml example for Dokploy
Session manager (@shade/core)
ShadeSessionManagerhigh-level API (encrypt,decrypt,initSessionFromBundle)getIdentityFingerprint()— Signal-style 60-digit safety numbersensurePreKeyStock()— auto-replenish when below thresholdresetSession()andacceptIdentityChange()for recovery scenariosrotateIdentity()with archived previous identities
Transport (@shade/transport)
ShadeFetchTransport— HTTP client for the prekey server with auto-signingShadeWebSocket— WebSocket wrapper with transparent encrypt/decrypt
Wire format (@shade/proto)
- Compact binary encoding (significantly smaller than JSON)
- Length-prefixed byte arrays, big-endian integers
- Version-tagged envelopes for forward compatibility
Cryptographic hardening
constantTimeEqual(XOR-accumulator, no early exit)randomUint32via crypto.getRandomValues (no Math.random)- Timing-attack regression test
- Constant-time trust verification in all storage backends
Errors
- Stable
SHADE_*error codes errorToHttpStatusfor consistent HTTP mappingtoJSON()for network serialization- 14 specific error types (Validation, Network, Storage, RateLimit, etc.)
Documentation
- README, SECURITY.md, THREAT-MODEL.md
- 5 runnable examples (basic conversation, prekey server, WebSocket tunnel, identity verification, Dokploy deployment)
- Per-package READMEs
- Inline TSDoc throughout
Testing
- 195+ tests across all packages
- Crash recovery integration test
- Cross-platform PostgreSQL tests (skip without
SHADE_TEST_PG_URL) - CI workflow with PostgreSQL service
- Benchmark suite
Security properties
- Forward secrecy
- Post-compromise security
- Authenticated identity verification
- Replay protection
- Constant-time secret comparisons
- Memory zeroization (best-effort)