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>
275 lines
13 KiB
Markdown
275 lines
13 KiB
Markdown
# Changelog
|
|
|
|
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
|
|
|
|
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-safe
|
|
- `PostgresStorage` (`@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`)
|
|
- `ShadeSessionManager` high-level API (`encrypt`, `decrypt`, `initSessionFromBundle`)
|
|
- `getIdentityFingerprint()` — Signal-style 60-digit safety numbers
|
|
- `ensurePreKeyStock()` — auto-replenish when below threshold
|
|
- `resetSession()` and `acceptIdentityChange()` for recovery scenarios
|
|
- `rotateIdentity()` with archived previous identities
|
|
|
|
#### Transport (`@shade/transport`)
|
|
- `ShadeFetchTransport` — HTTP client for the prekey server with auto-signing
|
|
- `ShadeWebSocket` — 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)
|
|
- `randomUint32` via crypto.getRandomValues (no Math.random)
|
|
- Timing-attack regression test
|
|
- Constant-time trust verification in all storage backends
|
|
|
|
#### Errors
|
|
- Stable `SHADE_*` error codes
|
|
- `errorToHttpStatus` for consistent HTTP mapping
|
|
- `toJSON()` 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)
|