release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers byte-for-byte. The version bump is semantic: audit-cycle complete, opt-in surface fully exposed, threat model refreshed for every new surface. Highlights: - All 24 @shade/* packages bumped to 4.0.0 in lockstep. - CHANGELOG 4.0.0 section is the canonical manifest of what landed. - THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12 Web-Worker boundary) + residual-risks table refreshed. - OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox, bridge, observer, /metrics, /healthz, /ready. - MIGRATION 0.3.x → 4.0 documented + smoke-tested against shade migrate-storage on a real SQLite DB. - docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer. - scripts/soak.ts harness for the GA-stable 2-week soak window. - All V*.md plans archived under docs/archive/ with Status: Done. - Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen non-realtime stack. Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green. Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports version 4.0.0 on /health. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
640
CHANGELOG.md
640
CHANGELOG.md
@@ -5,6 +5,646 @@ 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).
|
||||
|
||||
## [4.0.0] — 2026-05-03 — General Availability
|
||||
|
||||
Shade 4.0 is the first GA-marked release: every plan from V3.1 through
|
||||
V3.12 is merged, the cross-platform vector suite is green on TS + Kotlin,
|
||||
the threat model has been updated to reflect every new surface, and the
|
||||
core stack (X3DH, Double Ratchet, storage encryption, recovery, WebRTC
|
||||
P2P, Key Transparency) has been packaged for external review. Voice and
|
||||
video — the only big-ticket V2.x ask — have been moved to V5.0 so the
|
||||
4.0 audit can focus on a frozen non-realtime core.
|
||||
|
||||
The wire format is **unchanged from 0.4.x**: 4.0 peers interoperate with
|
||||
0.4.x peers byte-for-byte. The version bump is semantic (audit-cycle
|
||||
complete, opt-in surface fully exposed), not breaking. Apps that have
|
||||
been running 0.4.x in production move forward by `bun add @shade/sdk@^4.0.0`
|
||||
and (optionally) wiring any of the new opt-in surfaces.
|
||||
|
||||
### Highlights
|
||||
|
||||
- **External crypto-review-ready.** A "review-bundle" (`docs/audit/`)
|
||||
ships with this release: links to every protocol spec, the threat
|
||||
model, the cross-platform test corpus, the build instructions, and
|
||||
scope guidance for the auditor.
|
||||
- **Migration guide locked in.** `MIGRATION.md` documents the exact
|
||||
0.3.x → 4.0 path, including the optional opt-ins, the schema
|
||||
superset, and the `shade migrate-storage` workflow.
|
||||
- **Cross-platform parity gated in CI.** `.gitea/workflows/cross-vectors.yml`
|
||||
runs the same vector corpus on TS (bun) and Kotlin (gradle). A
|
||||
divergent KDF label, AAD layout, or wire byte fails the build.
|
||||
- **All V*.md plans archived.** `docs/V3.1.md` through `docs/V3.12.md`
|
||||
and the original V2.1/V2.2/V2.3 backlog now live under
|
||||
`docs/archive/` with `Status: Done`. Active planning continues in
|
||||
`docs/V5.0.md` (Voice & Video).
|
||||
- **Operator-facing OpenAPI is complete.** `packages/shade-server/openapi.yaml`
|
||||
now covers prekey, transfer, KT, inbox, bridge (SSE / long-poll / WS),
|
||||
observer, and the `/metrics`, `/healthz`, `/ready` operations
|
||||
endpoints — every HTTP surface a 4.0 client can talk to.
|
||||
- **Threat-model refresh.** Sections 10 (V3.3 fingerprint gates), 11
|
||||
(V3.11 WebRTC), 12 (V3.8 Web-Worker boundary) are new; the residual-
|
||||
risk table updates the §1 / §2 / §6 entries with the
|
||||
4.0 mitigations now landed.
|
||||
|
||||
### What's already in 4.0 (consolidated from 0.4.x)
|
||||
|
||||
The detailed CHANGELOG entries below list everything that landed in
|
||||
the 0.4.x series and is now part of the GA baseline:
|
||||
|
||||
- V3.2 — At-Rest Storage Encryption (`@shade/storage-encrypted`,
|
||||
`@shade/keychain`, `shade migrate-storage`).
|
||||
- V3.3 — Fingerprint Gates & Trust UX (`Shade.beforeFirstLargeFile` /
|
||||
`beforeBackupImport` / `beforeNewDeviceTrust`,
|
||||
`<FingerprintCompare />`, `<FingerprintGate />`).
|
||||
- V3.4 — Observability v2 (OpenTelemetry-shaped events,
|
||||
`@shade/observability`).
|
||||
- V3.5 — Android parity + cross-platform CI gate.
|
||||
- V3.6 — Async Store-and-Forward (`@shade/inbox`,
|
||||
`@shade/inbox-server`, `InboxPruneTask`).
|
||||
- V3.7 — Transport Bridge (`@shade/transport-bridge`, SSE +
|
||||
long-poll + WS adapters).
|
||||
- V3.8 — Web Workers Crypto (`@shade/crypto-web/worker`).
|
||||
- V3.9 — Rich File Metadata + thumbnails (in `@shade/files`).
|
||||
- V3.10 — Social Key Recovery (`@shade/recovery`,
|
||||
`<RecoverySetup />`, `<RecoveryRequest />`,
|
||||
`<RecoveryApprove />`).
|
||||
- V3.11 — WebRTC P2P Transport (`@shade/transport-webrtc`,
|
||||
`MultiTransportFallback`).
|
||||
- V3.12 — Key Transparency (`@shade/key-transparency`,
|
||||
`createPrekeyServerWithKT(...)`, `LightWitness`).
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- [x] V3.1 → V3.12 merged into `main`.
|
||||
- [x] No open critical / high-severity security issues at the time of
|
||||
tagging.
|
||||
- [x] Cross-platform test vectors green: TS (1000 / 1000) and
|
||||
Kotlin (11 / 11).
|
||||
- [x] Production-checklist (`docs/PRODUCTION-CHECKLIST.md`) is the
|
||||
canonical operator gate.
|
||||
- [x] OpenAPI covers every HTTP surface (`/v1/keys/*`,
|
||||
`/v1/transfer/*`, `/v1/kt/*`, `/v1/inbox/*`, `/v1/bridge/*`,
|
||||
`/metrics`, `/healthz`, `/ready`).
|
||||
- [x] Threat model reflects every new V3.x surface.
|
||||
- [x] `0.3.x → 4.0` migration documented in `MIGRATION.md` and
|
||||
validated against the `shade migrate-storage` CLI on a real
|
||||
SQLite DB.
|
||||
- [ ] **Pending external review.** A `docs/audit/REVIEW-BUNDLE.md`
|
||||
pointer is shipped; the actual external review window opens
|
||||
after tag.
|
||||
|
||||
### Migration
|
||||
|
||||
See [MIGRATION.md § Migrating from 0.3.x to 4.0 (GA)](./MIGRATION.md#migrating-from-03x-to-40-ga).
|
||||
The short version: bump every `@shade/*` to `^4.0.0`, run
|
||||
`bun install`, restart, opt in to the V3.x surfaces you actually need.
|
||||
No on-disk schema is destructive; no peer wire format changes.
|
||||
|
||||
## [Unreleased] — Key Transparency (V3.12) + WebRTC (V3.11)
|
||||
|
||||
### V3.12 — Key Transparency
|
||||
|
||||
Verifiable prekey distribution. The prekey server can now run in
|
||||
**Key-Transparency mode**: every register / delete event is committed
|
||||
to an append-only Merkle log (RFC 6962-style), every bundle-fetch
|
||||
includes an inclusion proof, and every Signed Tree Head (STH) is
|
||||
signed with an operator-controlled Ed25519 key that clients pin
|
||||
out-of-band.
|
||||
|
||||
A malicious server that swaps a bundle, splits its view between two
|
||||
clients, or rewrites history is detected by the client's KT verifier
|
||||
or by an independent witness. KT is **opt-in** on both server and
|
||||
client — existing deployments work unchanged until upgraded.
|
||||
|
||||
See `docs/V3.12-DESIGN.md` for the design notat (threat model,
|
||||
data-structure choices, freshness model, recovery procedures) and
|
||||
`docs/key-transparency.md` for operator + client onboarding.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/key-transparency` (new package)
|
||||
- `MerkleLog` — RFC 6962 append-only hash tree over pre-hashed leaves.
|
||||
In-memory mirror with O(N) leaf storage and O(log N) audit-path /
|
||||
consistency-proof generation.
|
||||
- `auditPath`, `recomputeRootFromAuditPath`, `consistencyProof`,
|
||||
`verifyConsistencyProof` — standalone primitives matching RFC 6962
|
||||
§2.1.1 and §2.1.2.
|
||||
- `AddressIndex` + `verifyInclusionProof` / `verifyAbsenceProof` —
|
||||
lexicographically sorted address commitment with both inclusion and
|
||||
neighbor-pair absence proofs. The index commitment becomes part of
|
||||
every STH so `address → bundle_hash` is auditable, not just the
|
||||
raw event log.
|
||||
- `SignedTreeHead` + `signSth` / `verifySthSignature` /
|
||||
`canonicalSthBytes` / `computeLogId` — Ed25519-signed commitment to
|
||||
the tree state. `log_id = SHA-256(public_key)` so a forged STH that
|
||||
claims a different log key is rejected.
|
||||
- `KTLogManager` — server-side orchestration that wires `MerkleLog`,
|
||||
`AddressIndex`, persistent `KTLogStore`, and STH signing under one
|
||||
serial-mutation API (`recordRegister`, `recordReplenish`,
|
||||
`recordDelete`, `publishSTH`, `buildBundleInclusionProof`,
|
||||
`buildBundleAbsenceProof`, `buildConsistencyProof`).
|
||||
- `KTLogStore` interface + `MemoryKTLogStore` reference impl. The
|
||||
interface is append-only by contract (no `update()` or `delete()`
|
||||
on historical leaves).
|
||||
- `LightWitness` — passive observer that polls a server's `/v1/kt/sth`
|
||||
endpoint, verifies signature + freshness + consistency, stores
|
||||
observed STHs, and exposes `compare(otherSth)` for split-view
|
||||
detection. Used by both witness CLIs and (transparently) by the SDK.
|
||||
- Bundle-proof verifiers: `verifyBundleInclusion`,
|
||||
`verifyBundleAbsence`, `verifyBundleTombstone`. Each re-derives the
|
||||
bundle hash, checks the audit path against the STH root, verifies
|
||||
the index commitment, and confirms freshness.
|
||||
- Errors: `KTError`, `KTVerificationError`, `KTSplitViewError`,
|
||||
`KTStaleSTHError`, `KTLogIdMismatchError`. Mapped to
|
||||
`SHADE_KT_*` codes.
|
||||
- Wire-format helpers: `ktProofToWire` / `ktProofFromWire` /
|
||||
`sthToWire` / `sthFromWire` for JSON-safe transport.
|
||||
|
||||
#### `@shade/server`
|
||||
- `createPrekeyServerWithKT(...)` — convenience that builds the KT
|
||||
service and wires it into the prekey routes in one call.
|
||||
- `KeyTransparencyService` — single-writer wrapper around
|
||||
`KTLogManager` with mutex-serialized mutations, cached latest STH,
|
||||
and configurable heartbeat interval (default 10 min).
|
||||
- New routes mounted under `/v1/kt/`:
|
||||
- `GET /v1/kt/log_id` — operator's signing public key + log_id.
|
||||
- `GET /v1/kt/sth` — latest signed tree head.
|
||||
- `GET /v1/kt/sth/:treeSize` — historical STH lookup.
|
||||
- `GET /v1/kt/consistency?from=N1&to=N2` — RFC 6962 consistency proof.
|
||||
- `POST /v1/keys/register` and `DELETE /v1/keys/:address` now commit
|
||||
to the KT log (when enabled). `GET /v1/keys/bundle/:address`
|
||||
returns a `ktProof` field on success and on 404 (absence/tombstone).
|
||||
- KT is fully opt-in. Existing deployments are byte-compatible until
|
||||
`keyTransparency` is configured.
|
||||
|
||||
#### `@shade/storage-postgres`
|
||||
- `PostgresKTLogStore` — durable KTLogStore on Postgres. Uses three
|
||||
tables (`shade_kt_leaves`, `shade_kt_index`, `shade_kt_sths`) with
|
||||
an `BEFORE UPDATE/DELETE/TRUNCATE` trigger on `shade_kt_leaves`
|
||||
that blocks any mutation — defense-in-depth against operator error.
|
||||
- `ensureKTLogTables(sql)` exported for embedding.
|
||||
|
||||
#### `@shade/transport`
|
||||
- `ShadeFetchTransport` accepts `keyTransparency: KTVerifierOptions`.
|
||||
Modes: `'observe'` verifies when proof present, `'observe-strict'`
|
||||
requires proof on every response.
|
||||
- `fetchBundleVerified(address)` returns `{ bundle, ktSth? }` so
|
||||
callers can route the verified STH into a `LightWitness`.
|
||||
- 404 responses are also verified (absence or tombstone proof) under
|
||||
strict mode.
|
||||
|
||||
#### `@shade/sdk`
|
||||
- `ShadeConfig.keyTransparency` — opt-in client config:
|
||||
```ts
|
||||
createShade({
|
||||
prekeyServer: 'https://shade.example.com',
|
||||
keyTransparency: { mode: 'observe-strict', logPublicKey: KEY_BYTES_32 },
|
||||
});
|
||||
```
|
||||
- `Shade.getKTWitness()` returns the auto-wired `LightWitness` so app
|
||||
code can introspect observed STHs or run manual gossip checks.
|
||||
- The SDK transparently feeds every fetched STH into the witness so
|
||||
split-view detection runs by default whenever KT is on.
|
||||
|
||||
### Tests
|
||||
|
||||
- 76 new tests across the KT stack: hash primitives, Merkle audit
|
||||
paths, consistency proofs, address-index inclusion/absence proofs,
|
||||
STH signing, manager orchestration, witness ingest, server-side
|
||||
HTTP routes, transport-side verification, and an end-to-end
|
||||
acceptance test that simulates two divergent server views and
|
||||
asserts a `KTSplitViewError` is raised.
|
||||
|
||||
### V3.11 — WebRTC P2P Transport
|
||||
|
||||
Direct peer-to-peer chunk delivery for `@shade/transfer` (and therefore
|
||||
`@shade/files`) via `RTCDataChannel`. Signaling — SDP offer / answer +
|
||||
trickle ICE — rides on top of `Shade.send` / `Shade.onMessage` so the
|
||||
same Double Ratchet that authenticates regular messages authenticates
|
||||
WebRTC negotiation. Throughput-heavy uploads (multi-MB / multi-GB) skip
|
||||
the HTTP relay entirely when NAT allows; when traversal fails, the new
|
||||
`MultiTransportFallback([webrtc, http])` demotes back to HTTP within
|
||||
the configured connect-timeout window without losing any chunks already
|
||||
in flight. See `docs/webrtc.md` and `docs/V3.11.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/transport-webrtc` (new package)
|
||||
- `WebRtcConnection` — per-peer wrapper around an `IPeerConnection`
|
||||
plus the single bidirectional `RTCDataChannel` (label
|
||||
`shade-transfer/v1`). Drives offer/answer/ICE through a
|
||||
`WebRtcSignalingChannel`; handles the receiver-side dispatch loop
|
||||
for chunk-ack / resume-state / ping-pong / error frames; exposes
|
||||
per-request reqId-correlated `request()` for the transport layer.
|
||||
- `WebRtcConnectionManager` — per-peer pool with deterministic glare
|
||||
resolution (lexicographic address compare). `getOrCreate(peer)`
|
||||
returns the live connection or initiates a fresh one; following
|
||||
through a glare-yield is automatic so the user-facing promise
|
||||
resolves to whichever role survives.
|
||||
- `WebRtcSignalingChannel` — multiplexes the four signaling kinds
|
||||
(`shade.webrtc-offer/v1`, `shade.webrtc-answer/v1`,
|
||||
`shade.webrtc-ice/v1`, `shade.webrtc-bye/v1`) over any `ShadeBridge`
|
||||
(real `Shade.send`/`onMessage`, or `MemoryShadeBridge` for tests).
|
||||
Non-signaling plaintext is forwarded to a configurable `passthrough`
|
||||
hook so consumer `onMessage` handlers stay untouched.
|
||||
- `WebRtcTransferTransport` — implements
|
||||
`@shade/transfer`'s `ITransferTransport` over the managed
|
||||
DataChannel. Encodes chunks into the package's binary wire format,
|
||||
awaits chunk-ack frames matched by 16-byte requestId tokens, and
|
||||
enforces SCTP-friendly backpressure by polling `bufferedAmount`
|
||||
(default threshold 4 MiB).
|
||||
- `IRtcFactory` interface + `nativeRtcFactory()` adapter wrapping
|
||||
`globalThis.RTCPeerConnection` for browsers / Deno / Cloudflare
|
||||
Workers. `MemoryRtcFactory` ships an in-process WebRTC simulator
|
||||
used by the package's own tests and by `@shade/sdk` integration
|
||||
tests.
|
||||
- `createShadeBridgeFromShade(shade)` — turns any `Shade`-shaped
|
||||
object into a `ShadeBridge`. Calls `shade.send(plaintext)` to
|
||||
ratchet-encrypt the JSON, then `shade.deliverControlEnvelope(...)`
|
||||
(when present) to ship the envelope over HTTP — same path the
|
||||
existing control-plane already uses.
|
||||
- Wire-format constants (`WIRE_CHUNK`, `WIRE_CHUNK_ACK`, etc.) +
|
||||
`encode*Frame` / `decodeFrame` helpers exported for adapters that
|
||||
want to interoperate with `ShadeTransferWsTransport` (the wire
|
||||
matches frame-for-frame).
|
||||
- Errors: `WebRtcConnectError`, `WebRtcDataChannelError`,
|
||||
`WebRtcSignalingError`, `WebRtcTimeoutError` — all extend
|
||||
`TransferTransportError` so `MultiTransportFallback` automatically
|
||||
demotes on failure.
|
||||
|
||||
#### `@shade/transfer`
|
||||
- `MultiTransportFallback` — N-ary generalisation of the existing
|
||||
two-arg `FallbackTransferTransport`. Constructor takes
|
||||
`[{ name: 'webrtc', transport }, { name: 'ws', transport }, ...]`;
|
||||
layers are tried in order and demote sticky on
|
||||
`TransferTransportError`. Exposes `activeName`, `hasFallenBack`,
|
||||
`failures` (diagnostic log), and `onSwitch((from, to) => ...)` for
|
||||
observability hooks.
|
||||
|
||||
#### `@shade/sdk`
|
||||
- `Shade.configureWebRTC({ factory, iceServers?, iceTransportPolicy?,
|
||||
bundlePolicy?, connectTimeoutMs?, requestTimeoutMs?,
|
||||
backpressureThresholdBytes? })` — opt-in entrypoint. MUST be called
|
||||
before the engine is built (i.e. before the first `upload()`,
|
||||
`onIncomingTransfer()`, or `transferRoute()` call). When
|
||||
configured, the engine is wired with
|
||||
`MultiTransportFallback([webrtc, http])` and the WebRTC manager
|
||||
receives receiver-hooks pointing at `engine.receiveChunk` /
|
||||
`engine.getResumeState`.
|
||||
- `Shade.getWebRtcRuntime(): ShadeWebRtcRuntime | null` — diagnostic
|
||||
accessor returning the live signaling channel, manager, transport,
|
||||
and `MultiTransportFallback` after `engine()` builds.
|
||||
- `@shade/transport-webrtc` is a (optional) peer-dep — projects that
|
||||
don't call `configureWebRTC()` don't pay the install or runtime
|
||||
cost.
|
||||
|
||||
### Tests
|
||||
- `packages/shade-transport-webrtc/tests/` — wire-format roundtrips,
|
||||
signaling routing, full memory-factory caller/callee handshake,
|
||||
receiver-hook dispatch (chunk + resume-query), glare convergence,
|
||||
TURN-only configuration plumbing, native-adapter availability
|
||||
smoke test.
|
||||
- `packages/shade-transfer/tests/multi-fallback.test.ts` — N-ary
|
||||
demotion, sticky-after-failure, non-transport-error preservation,
|
||||
empty-list rejection.
|
||||
- `packages/shade-sdk/tests/webrtc-integration.test.ts` — two real
|
||||
Shade instances upload via WebRTC primary; verifies the engine
|
||||
picks `webrtc` and never demotes during the run.
|
||||
- `packages/shade-sdk/tests/webrtc-failover.test.ts` — broken-RTC
|
||||
factory provokes connect timeout; SDK demotes to HTTP within the
|
||||
V3.11 5-second SLO without losing chunks.
|
||||
- `packages/shade-sdk/tests/webrtc-throughput.test.ts` — 4 MiB / 4
|
||||
lanes loopback over WebRTC vs HTTP; integrity match across both
|
||||
transports + diagnostic speedup ratio.
|
||||
|
||||
### Documentation
|
||||
- `docs/webrtc.md` — full V3.11 guide (NAT-traversal table, TURN
|
||||
config matrix, connection flow, glare resolution, backpressure,
|
||||
multi-fallback wiring, diagnostics, wire format, limits, migration).
|
||||
- `packages/shade-transport-webrtc/README.md` — package quickstart.
|
||||
- README + CHANGELOG + ROADMAP marked V3.11 as Done.
|
||||
|
||||
## [Earlier Unreleased] — Social Key Recovery (V3.10)
|
||||
|
||||
The biggest UX hole in any E2EE system — "what happens if I lose my
|
||||
phone?" — closed without a centralized recovery agent. Pick `n`
|
||||
guardians from your peers, set a threshold `k`; any `k` of them
|
||||
together can rebuild your identity onto a new device, but `k-1` or
|
||||
fewer cannot. Shamir Secret Sharing over GF(2^8) gates the recovery
|
||||
key; AES-GCM authentication on the backup blob detects forged
|
||||
shares; an OOB-confirmed fingerprint gate on the guardian side
|
||||
blocks social-engineering. See `docs/recovery.md` and
|
||||
`docs/V3.10.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/recovery` (new package)
|
||||
- `setupRecovery({ shade, guardians, threshold, deliver })` —
|
||||
primary-device flow. Generates a 32-byte `recoveryKey`,
|
||||
encrypts an identity backup under the recoveryKey-derived
|
||||
passphrase via `Shade.exportBackup`, Shamir-splits the key into
|
||||
`n` shares, and ships one `share-deposit` envelope per guardian
|
||||
over the existing 1:1 Shade session. Returns a per-guardian
|
||||
delivery report so partial-distribution is recoverable.
|
||||
- `attachGuardian({ shade, store, approve, deliver })` —
|
||||
guardian-side receiver. Wires a `Shade.onMessage` handler that
|
||||
persists incoming deposits in a caller-supplied `RecoveryStore`
|
||||
and gates `recovery-request` envelopes behind a user-driven
|
||||
`approve` callback. Auto-declines requests for unknown
|
||||
`(originalAddress, setupId)` pairs.
|
||||
- `requestRecovery({ shade, originalAddress, setupId, threshold,
|
||||
guardians, deliver })` — new-device flow. Sends one
|
||||
`recovery-request` per guardian, collects `share-grant` /
|
||||
`share-decline` replies, Shamir-combines the threshold-many
|
||||
grants, and atomically swaps in the restored identity via
|
||||
`Shade.importBackup`. Forged shares are detected by the
|
||||
AES-GCM tag on the backup blob; the loop tries every
|
||||
threshold-sized subset of grants before giving up.
|
||||
- Pure-TS Shamir Secret Sharing primitives (`splitSecret`,
|
||||
`combineShares`, `encodeShare`, `decodeShare`) over GF(2^8)
|
||||
with constant-time table lookups. Exported for advanced
|
||||
callers and hardware-token integrations.
|
||||
- `MemoryRecoveryStore` for tests + a `RecoveryStore` interface
|
||||
apps implement against IndexedDB / SQLite / AsyncStorage / etc.
|
||||
- Errors: `RecoveryError`, `RecoveryDeclinedError`,
|
||||
`RecoveryTimeoutError`, `RecoveryReconstructionError`,
|
||||
`RecoveryProtocolError`, `RecoveryGuardianRejectedError`.
|
||||
- Wire protocol: `share-deposit`, `recovery-request`,
|
||||
`share-grant`, `share-decline` JSON envelopes carried over
|
||||
Double-Ratchet plaintext.
|
||||
|
||||
#### `@shade/widgets`
|
||||
- `<RecoverySetup />` — primary-device guardian-picker + threshold
|
||||
slider, drives `setupRecovery` and exposes `formatRecoveryCard`
|
||||
for the user's offline copy.
|
||||
- `<RecoveryRequest />` — new-device widget that displays the
|
||||
temporary fingerprint prominently, drives `requestRecovery`,
|
||||
and reports per-guardian progress live.
|
||||
- `<RecoveryApprove />` — guardian-side widget. Renders the
|
||||
pending request with original-vs-new fingerprint side-by-side
|
||||
and enforces a two-checkbox gate ("matches" + "OOB-verified")
|
||||
before the release button is clickable.
|
||||
- `createApprovalQueue()` — turns the `attachGuardian.approve`
|
||||
callback into a deferred queue the widget can consume.
|
||||
|
||||
#### `@shade/core`
|
||||
- **Bug fix.** `initReceiverSession` now copies the
|
||||
`localDHKeyPair` into the session so the eventual zeroize on
|
||||
DH ratchet step touches a scratch buffer, not the persisted
|
||||
signed prekey. Pre-V3.10 this corrupted the receiver's signed
|
||||
prekey after the first incoming X3DH from any sender — a bug
|
||||
surfaced by V3.10's multi-sender recovery flow but harmful to
|
||||
any user receiving messages from more than one peer.
|
||||
Regression test in `packages/shade-core/tests/ratchet.test.ts`.
|
||||
|
||||
### Acceptance criteria (V3.10)
|
||||
- [x] 3-of-5 recovery works end-to-end on two separate Shade
|
||||
instances. (`packages/shade-recovery/tests/integration.test.ts`)
|
||||
- [x] No coalition of `(k-1)` guardians can reconstruct the
|
||||
`recoveryKey` (verified with `fast-check` property tests).
|
||||
(`packages/shade-recovery/tests/shamir.test.ts`,
|
||||
`tests/adversarial.test.ts`)
|
||||
- [x] Guardian-side widget requires fingerprint-confirmation
|
||||
before sending a share. Two-checkbox enforcement +
|
||||
symmetric tests of both honest-OOB-confirm and
|
||||
hostile-fingerprint-mismatch paths.
|
||||
|
||||
## [Unreleased] — Web Workers Crypto (V3.8)
|
||||
|
||||
Big in-browser uploads stay smooth: AES-GCM, HKDF, HMAC, X25519, Ed25519
|
||||
and full per-lane stream state now run in a dedicated Web Worker. The
|
||||
main thread only buffers and forwards plaintext slices over zero-copy
|
||||
`postMessage`; lane keys never cross the thread boundary. Opt-in via
|
||||
`shade.configureWorkerCrypto({ workerUrl })`. See `docs/web-workers.md`
|
||||
and `docs/archive/V3.8.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/crypto-web`
|
||||
- `WorkerCryptoProvider` — drop-in `CryptoProvider` proxy that forwards
|
||||
every async op to a dedicated Web Worker via the `worker-protocol`.
|
||||
Sync helpers (`randomBytes`, `randomUint32`, `constantTimeEqual`,
|
||||
`zeroize`) execute on the calling thread — no useless round-trips.
|
||||
- `createWorkerCryptoProvider({ workerUrl, idleTimeoutMs?, spawn? })`
|
||||
factory. Spawns lazily, completes a protocol-version handshake, and
|
||||
self-terminates after 30 s (configurable) of inactivity. Idempotent
|
||||
re-spawn on next call.
|
||||
- `WorkerStreamSender` / `WorkerStreamReceiver` — main-thread handles on
|
||||
`StreamSender` / `StreamReceiver` instances that live entirely inside
|
||||
the worker. Plaintext is shipped via transferable `ArrayBuffer`s; lane
|
||||
keys + running sha256 stay worker-side.
|
||||
- `createEncryptStream` / `createDecryptStream` — TransformStream
|
||||
factories. `pipeThrough(encryptStream)` consumes plaintext and emits
|
||||
one wire-encoded `stream-chunk` envelope per write. Both expose a
|
||||
`laneSha256` promise that resolves once the stream finishes.
|
||||
- New subpath export: `@shade/crypto-web/worker` is the dedicated
|
||||
module-worker entrypoint. Bundle with the standard
|
||||
`new URL('@shade/crypto-web/worker', import.meta.url)` idiom.
|
||||
- `rotate()` and `destroy()` lifecycle controls — call after identity
|
||||
rotation to bound the worst-case duration any lane key sits in worker
|
||||
memory.
|
||||
|
||||
#### `@shade/sdk`
|
||||
- `shade.configureWorkerCrypto({ workerUrl, idleTimeoutMs? })` —
|
||||
opt-in setup. Without it, `encryptStream` / `decryptStream` throw a
|
||||
clear error pointing to the docs.
|
||||
- `shade.encryptStream({ streamId, streamSecret, laneId?, chunkSize? })`
|
||||
→ `{ stream, laneSha256 }` — TransformStream with an end-of-stream
|
||||
sha256 promise for end-to-end integrity proofs.
|
||||
- `shade.decryptStream(...)` — inverse. Strict in-order seq, AAD-bound
|
||||
AEAD, replay-rejecting.
|
||||
- `shade.getWorkerCrypto()` — direct access to the worker-backed
|
||||
`CryptoProvider` for one-off heavy ops.
|
||||
- `shade.shutdown()` now also `destroy()`s the worker provider.
|
||||
|
||||
### Acceptance criteria (V3.8)
|
||||
- [x] 100 MB upload in Chrome without blocking the main thread
|
||||
> 16 ms in P99 (verification recipe in
|
||||
`docs/web-workers.md#verifying-main-thread-budget`).
|
||||
- [x] Safari works at default chunk-size — every `postMessage` carries
|
||||
≤ 256 KiB + AEAD overhead, far below Safari's transferable cap.
|
||||
- [x] Worker terminates within 30 s of last use (default
|
||||
`idleTimeoutMs`), and re-spawns transparently on the next call.
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — Transport Bridge (V3.7)
|
||||
|
||||
A canonical fallback chain for clients that cannot or will not run a
|
||||
WebSocket: SSE primary, long-poll secondary, plus a thin WS adapter for
|
||||
the happy path. All three transports surface the same `IncomingMessage`
|
||||
shape so application code stays portable across browser-extension,
|
||||
edge-runtime, and proxy-locked environments. See `docs/transport.md`
|
||||
and `docs/archive/V3.7.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/transport-bridge` (new)
|
||||
- `IncomingMessage` — `{ from, bytes, receivedAt, msgId? }` — single
|
||||
shape across every transport.
|
||||
- `BridgeTransport` — `connect({ onMessage }) → disconnect()` contract.
|
||||
- `WsBridge`, `SseBridge`, `LongPollBridge` — three concrete transports
|
||||
consuming the matching `/v1/bridge/{ws,stream,poll}` endpoints.
|
||||
- `FallbackBridgeTransport` — sticky-after-first-success priority chain.
|
||||
Exposes `activeKind` and `attempts` for observability.
|
||||
- `signBridgeQuery` — Ed25519-signed query-string builder (the only
|
||||
carrier that survives `EventSource`'s no-headers restriction).
|
||||
- Auto-reconnect with exponential backoff for WS + SSE; `Last-Event-ID`
|
||||
cursor resume for SSE; bounded one-outstanding-request loop for
|
||||
long-poll.
|
||||
|
||||
#### `@shade/inbox-server`
|
||||
- `createBridgeRoutes({ store, crypto, events, … })` returns
|
||||
`{ app, websocket }`.
|
||||
- `GET /v1/bridge/stream` — SSE feed, one envelope per `event:
|
||||
envelope`. Heartbeats every 15 s as `: ping` comments.
|
||||
- `GET /v1/bridge/poll?timeoutMs=…` — long-poll, default 25 s server
|
||||
hold under typical proxy idle cutoffs, hard cap 55 s.
|
||||
- `GET /v1/bridge/ws` — Bun-WebSocket upgrade, JSON frame per
|
||||
envelope.
|
||||
- Push-style delivery via `InboxServerEvents`
|
||||
(`inbox.blob_stored`); falls back to a 1 s polling timer when no
|
||||
events emitter is wired.
|
||||
- Cross-endpoint replay-protected: `kind` is bound into the canonical
|
||||
signed payload so a `/poll` signature cannot reach `/stream`.
|
||||
|
||||
#### `@shade/server` standalone container
|
||||
- Bridge routes mount on the same Hono app + Bun.serve as the prekey
|
||||
and inbox routes — no extra port, no extra env vars.
|
||||
|
||||
### Acceptance criteria (V3.7)
|
||||
- [x] Same "send 100 small messages" suite passes on WS, SSE, and
|
||||
long-poll.
|
||||
- [x] Client that starts with WS and is blocked by proxy continues
|
||||
automatically via SSE — and on through to long-poll if SSE is
|
||||
also blocked — without message loss.
|
||||
- [x] Long-poll fallback uses no more than one outstanding request per
|
||||
client.
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — Async Store-and-Forward (V3.6)
|
||||
|
||||
A dedicated relay (`@shade/inbox-server`) holds ciphertext blobs with TTL
|
||||
+ auth so a sender can deliver to an offline recipient. Server stores
|
||||
only `address || msgId || ciphertext-bytes || expires_at`; the prekey
|
||||
server stays public-keys-only, and the relay never holds plaintext or
|
||||
private keys. See `docs/inbox.md` and `docs/archive/V3.6.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/inbox` (new)
|
||||
- `Inbox` — high-level orchestrator. Buffers outgoing PUTs in a durable
|
||||
queue, polls + acks incoming blobs, and exposes
|
||||
`onMessageQueued(handler)` (the vendor-neutral push-trigger hook
|
||||
mandated by V3.6) and `onIncoming(handler)`.
|
||||
- `InboxClient` — low-level HTTP client (`register`, `put`, `fetch`,
|
||||
`ack`, `unregister`).
|
||||
- `OutgoingQueueStore` interface + `MemoryOutgoingQueueStore` default —
|
||||
swap in a SQLite/IDB backend so queue survives a process restart.
|
||||
- `CursorStore` interface + `MemoryCursorStore` default for the receive
|
||||
cursor.
|
||||
- `computeMsgId(ciphertext)` helper — `lowercase-hex(sha256(ciphertext))`.
|
||||
|
||||
#### `@shade/inbox-server` (new)
|
||||
- `createInboxServer({ crypto, store, ... })` Hono app exposing:
|
||||
- `POST /v1/inbox/register` — TOFU bind address ↔ signing key.
|
||||
- `DELETE /v1/inbox/register/:address` — signed unregister.
|
||||
- `POST /v1/inbox/:address` — signed PUT, idempotent on `(address, msgId)`,
|
||||
rejects mismatched `msgId !== sha256(ciphertext)` and bodies past
|
||||
`maxBlobBytes` (default 1 MiB) or per-recipient quota (default 1000).
|
||||
- `POST /v1/inbox/:address/fetch` — signed challenge, cursor-paginated.
|
||||
- `DELETE /v1/inbox/:address/:msgId` — signed ack.
|
||||
- `InboxStore` interface + `MemoryInboxStore` default.
|
||||
- `InboxPruneTask` — periodic prune of expired blobs (cron, default 5 min).
|
||||
- `InboxServerEvents` — structural-only event emitter for observability.
|
||||
|
||||
#### `@shade/storage-sqlite`
|
||||
- `SqliteInboxStore` — `(address, expires_at)` + `(address, received_at)` +
|
||||
`(expires_at)` indexes. `SHADE_INBOX_DB_PATH` env var for the file path.
|
||||
|
||||
#### `@shade/storage-postgres`
|
||||
- `PostgresInboxStore` — concurrent-safe via `INSERT … ON CONFLICT` and a
|
||||
per-row `nextval('shade_inbox_seq')`. `ensureInboxServerTables(sql)` is
|
||||
exported for embedded deployments.
|
||||
|
||||
#### `@shade/server` standalone container
|
||||
- Inbox routes mount alongside prekey routes on the same Hono app.
|
||||
- New env vars: `SHADE_INBOX_DB_PATH`, `SHADE_INBOX_PG_URL`,
|
||||
`SHADE_INBOX_PRUNE_INTERVAL_MINUTES`. If `SHADE_INBOX_PG_URL` is unset
|
||||
the inbox falls back to `SHADE_PREKEY_PG_URL` (single Postgres deploy).
|
||||
|
||||
### Acceptance criteria (V3.6)
|
||||
- [x] Sender → recipient with no online overlap; payload < 1 MiB; first
|
||||
poll after recipient startup pulls the queued message.
|
||||
- [x] Server-DB dump exposes no plaintext and no sender-recipient graph
|
||||
beyond byte-pair sizes (sender pubkey is per-PUT TOFU; only the
|
||||
recipient address is persisted).
|
||||
- [x] Replay of PUT with the same `msgId` returns 200 with
|
||||
`idempotent: true` instead of 409, and no second row is written.
|
||||
|
||||
## [0.4.0] — 2026-05-02 — Fingerprint Gates & Trust UX (V3.3)
|
||||
|
||||
Blocking verification gates for the handful of operations where MITM risk
|
||||
is real. Apps stay alert-fatigue-free for ordinary chat, but `upload()`
|
||||
of a large file, `importBackup()`, and `acceptIdentityChange()` now run
|
||||
through user-registered handlers before they touch anything sensitive.
|
||||
See `docs/trust-ux.md` and `docs/archive/V3.3.md`.
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/sdk`
|
||||
- `Shade.beforeFirstLargeFile(threshold, handler)` — gate runs in
|
||||
`upload()` when the file size meets the threshold (default 10 MiB) and
|
||||
the peer is unverified.
|
||||
- `Shade.beforeBackupImport(handler)` — gate receives the fingerprint of
|
||||
the identity *embedded in the backup blob*, before any state is written.
|
||||
- `Shade.beforeNewDeviceTrust(handler)` — gate runs from
|
||||
`Shade.acceptIdentityChange()`. The peer's identity-version is bumped
|
||||
first, so any prior verification automatically goes stale.
|
||||
- `Shade.beforeInboxFanout(handler)` — reserved hook for V3.6 fan-out;
|
||||
apps can register today.
|
||||
- `Shade.markPeerVerified(address)` / `isPeerVerified(address)` /
|
||||
`unmarkPeerVerified(address)` — manual control over persisted
|
||||
verification state.
|
||||
- `decryptBackup` / `applyBackupPayload` — split of the backup pipeline
|
||||
so callers can inspect a backup's identity fingerprint before writing.
|
||||
- New `FingerprintGateRegistry` exported for advanced integrations.
|
||||
|
||||
#### `@shade/core`
|
||||
- `FingerprintNotVerifiedError` (HTTP 403) — raised when a gate handler
|
||||
returns `false`, throws, or is missing in environments that policy-
|
||||
forbid TOFU.
|
||||
- `PeerVerification` + `PeerVerificationSource` types and storage
|
||||
methods on `StorageProvider`: `savePeerVerification`,
|
||||
`getPeerVerification`, `removePeerVerification`,
|
||||
`getPeerIdentityVersion`, `bumpPeerIdentityVersion`.
|
||||
|
||||
#### Storage backends
|
||||
- `MemoryStorage`, `SQLiteStorage`, `PostgresStorage`,
|
||||
`EncryptedSQLiteStorage`, `EncryptedPostgresStorage` all carry the new
|
||||
`peer_verifications` + `peer_identity_versions` tables.
|
||||
|
||||
#### `@shade/widgets`
|
||||
- `<FingerprintGate peerAddress=... />` — render-prop wrapper that blocks
|
||||
children until the peer's safety number is verified at the current
|
||||
identity-version. SSR-safe; ships a default fallback with "Copy OOB
|
||||
text" + "I have verified" actions.
|
||||
- `<FingerprintCompare onVerified=... />` — existing widget extended with
|
||||
the same two actions when wired to a callback.
|
||||
- `formatOobText(peerAddress, fingerprint)` helper exported.
|
||||
|
||||
### Changed
|
||||
- `@shade/sdk` version bumped to 0.4.0 alongside all packages (lockstep
|
||||
per ROADMAP convention).
|
||||
|
||||
### Migration
|
||||
- No breaking changes. Apps that don't register gate handlers get
|
||||
warning-mode TOFU automatically (`'tofu-after-warning'` source on the
|
||||
persisted verification). To upgrade to hard gates, register handlers
|
||||
for the operations you use. Existing `<FingerprintCompare />` calls
|
||||
keep working.
|
||||
|
||||
## [0.3.0] — 2026-05-02 — Shade Files
|
||||
|
||||
E2EE filesystem RPC primitive — drop-in entrypoints for any consumer that
|
||||
|
||||
Reference in New Issue
Block a user