5 Commits

Author SHA1 Message Date
0bdf9e859c release(v4.0.2): consumer-strict reader-shape fixes
Some checks failed
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
4.0.1's typecheck gate compiled each package internally against
lib: ["ES2022"]. That doesn't catch types that only fail when
*consumer* code (lib: ["DOM"] + exactOptionalPropertyTypes) tries to
assign a native browser type into one of our locally-defined narrower
types. Dispatch hit one such case in @shade/files inline-threshold.ts.

This release adds a tests/consumer-strict/ smoke project to the
pre-publish gate. It compiles a tiny "as if I were a downstream app"
TS file against:

  lib: ["ES2022", "DOM", "DOM.Iterable"]
  types: ["bun-types"]
  exactOptionalPropertyTypes: true
  strict: true
  paths → packages/*/src/index.ts

scripts/typecheck-all.ts now runs the smoke after per-package checks.
Both must pass before publish:dry / publish:all proceeds.

### Fixed
- @shade/files inline-threshold.ts: MinimalReader<T> rewritten as the
  explicit disjoint union { done:false, value:T } | { done:true,
  value?: T | undefined } that's assignable from every native reader
  shape (bun, DOM, node:stream/web). Fixes the
  "ReadableStreamReadResult is not assignable" Dispatch reported.
- @shade/files streams-bridge (client + server): stash setTimeout
  return in a local before .unref?.() via { unref?: () => void } cast.
  Fluent .unref?.() failed under lib: ["DOM"] (setTimeout returns
  number there).
- @shade/sdk background.ts: same setInterval .unref?.() fix.

Wire-compatible. No API shape changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:51:46 +02:00
70e319fef8 release(v4.0.1): strict-TS publishability fixes
Some checks failed
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
4.0.0 shipped TypeScript source as published main/types, but several
files only compiled inside the monorepo. Consumer projects (Dispatch,
etc.) running their own strict tsc against our published source hit:

- @shade/key-transparency: 4 noUnusedLocals violations
  (IndexAbsenceProof, IndexInclusionProof, IndexProofWire, nodeHash)
- @shade/sdk: KT verifier callbacks returned Promise<unknown> instead
  of Promise<STHWire> / Promise<{ proof: string[] }>
- @shade/sdk: thumbnail.ts globalThis cast collided with consumer's
  lib.dom-supplied createImageBitmap signature
- @shade/files: cycle with @shade/sdk produced "this is not assignable
  to type 'Shade'" because hoisted node_modules layouts duplicated the
  Shade class. Broken by replacing `import type { Shade }` with a
  local structural ShadeBridge interface.
- @shade/storage-encrypted: KeyUsage (lib.dom) used under
  lib: ["ES2022"]
- @shade/transport-bridge: ReadableStreamDefaultReader<any> ↔
  <Uint8Array> mismatch
- @shade/keychain / @shade/dashboard / @shade/storage-encrypted
  tsconfig rootDir / include hygiene

Tooling: scripts/typecheck-all.ts runs `bunx tsc --noEmit` against
every workspace package's tsconfig and fails on any error. Wired into
publish:dry / publish:all and publish-shade.sh as a hard gate so this
class of bug cannot recur.

All 24 packages bumped to 4.0.1 in lockstep.

Migration: <ShadeFilesProvider> now requires an explicit `files` prop
(pass `shade.files`). Wire format unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:36:47 +02:00
f301b391a5 docs(archive): close out Status fields on V2.x backlog + V3.12 design notat
Some checks failed
Test / test (push) Has been cancelled
V4.0 acceptance §"All docs/V*.md arkivert med DONE-status" requires
every archived plan to carry an explicit Status field. V2.1 / V2.2 /
V2.3 inherited their pre-status format; V3.12-DESIGN was still
"Approved". Mark all four as Done with a one-line pointer to where
the work actually landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:04:47 +02:00
40766c60f4 docs(readme): full 4.0 GA refresh — status table, all V3.x surfaces, audit pointers
Some checks failed
Test / test (push) Has been cancelled
- Status table (V3.2 → V3.12 all green; audit + soak harness ready)
- "What you get" expanded: storage encryption, fingerprint gates,
  inbox, bridge, web workers, recovery, KT, observability, soak
- Quick start: opt-in surface examples (gates, workers, WebRTC, KT)
  + at-rest encryption snippet + migrate-storage CLI invocation
- Architecture diagram refreshed: container now exposes /v1/inbox/*,
  /v1/bridge/*, /v1/kt/*, ops endpoints; WebRTC P2P pipe documented
- Packages table: all 24 entries at 4.0.0 (added observability,
  keychain, storage-encrypted, transport-bridge, etc.)
- Publishing section: 4.0.x examples; soak harness section added
- Security properties: V3.2 / V3.3 / V3.6 / V3.8 / V3.10 / V3.11
  rows added alongside the existing forward-secrecy / PCS list
- Documentation: grouped into Operator+integrator, Per-surface,
  Threat-model+audit, Migration+history, Examples
- Container includes: full V4.0 surface (inbox, bridge, KT, ops
  probes); image pinned at gt.zyon.no/stian/shade-prekey:4.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:58:38 +02:00
de25b19033 fix(publish): break recursion in publish-shade.sh → publish-all.ts
Some checks failed
Test / test (push) Has been cancelled
publish-shade.sh used to call `bun run publish:all`, which in turn was
wired to call publish-shade.sh (after the V4.0 cleanup). Point it
directly at scripts/publish-all.ts so the interactive flow runs the
TS publisher without re-entering itself.

Verified: dry-run from publish-shade.sh now packs all 24 @shade/*@4.0.0
packages cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:53:16 +02:00
60 changed files with 901 additions and 139 deletions

View File

@@ -5,6 +5,155 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.0.2] — 2026-05-03 — Consumer-strict reader-shape fixes
`4.0.1` shipped the `tsc --noEmit` gate that compiles each package
internally against `lib: ["ES2022"]`. That gate did not catch types
that only fail when *consumer* code (running with `lib: ["DOM"]` +
`exactOptionalPropertyTypes`) tries to assign a native browser type
into one of our locally-defined narrower types.
This release adds a consumer-strict smoke test to the pre-publish
gate and fixes every collision that smoke uncovered.
### Fixed
#### `@shade/files`
- `inline-threshold.ts`: rewrote the local `MinimalReader<T>` interface
as an explicit disjoint union (`{ done: false; value: T } | { done:
true; value?: T | undefined }`) so it accepts every native reader
shape — `bun-types` (`value?: undefined`), `lib.dom` (`value?: T`),
and `node:stream/web`. The previous flat shape was rejected by
consumer projects with `exactOptionalPropertyTypes: true` because
the present-branch required `value: T`. **Fixes "Type
ReadableStreamReadResult<Uint8Array> is not assignable to
{ value: Uint8Array | undefined; done: boolean }".**
- `client/streams-bridge.ts`, `server/streams-bridge.ts`: stash the
`setTimeout(...)` return value in a local before calling `.unref?.()`
through an explicit `{ unref?: () => void }` cast. The previous
fluent `.unref?.()` failed under `lib: ["DOM"]` because DOM types
`setTimeout` to `number`, which has no `.unref` even as an optional
property.
#### `@shade/sdk`
- `background.ts`: same `setTimeout` / `setInterval` `.unref?.()` fix.
### Tooling
- New `tests/consumer-strict/` — a tiny "as if I were a downstream app"
TypeScript project with its own `tsconfig.json`:
`lib: ["ES2022", "DOM", "DOM.Iterable"]`, `types: ["bun-types"]`,
`exactOptionalPropertyTypes: true`, `strict: true`,
`paths`-mapped to the workspace's `packages/*/src/index.ts`.
Three smoke files exercise `@shade/files`, `@shade/sdk`, and
`@shade/key-transparency` against the consumer-strict tsconfig.
- `scripts/typecheck-all.ts` now runs the consumer-strict smoke after
the per-package internal type-check. Both must pass before
`prepublish:check` (and therefore `publish:dry` / `publish:all`)
succeeds.
### Migration
`4.0.1 → 4.0.2` is wire-compatible and source-compatible. No API shape
changed; only internal typing was tightened.
## [4.0.1] — 2026-05-03 — Strict-TS publishability fixes
`4.0.0` shipped TypeScript source files as the published `main` /
`types`, which meant every consumer's `tsc` had to compile our code
under their own strict settings. Several files only compiled inside
the monorepo (where peer-dep cycles resolve via workspace links and
the `lib` array doesn't include `DOM`). This release makes all 24
packages compile cleanly under the strict-flagged tsconfig that ships
with the repo, and wires a `bun run typecheck` gate into both the
`publish:dry` and `publish:all` flows so this category of bug cannot
recur.
### Fixed
#### `@shade/key-transparency`
- Removed unused imports `IndexAbsenceProof`, `IndexInclusionProof`
(`src/manager.ts`), `nodeHash` (`src/index-tree.ts`).
- `IndexProofWire` is now exported (was a private type that
`noUnusedLocals` flagged).
- Added missing `tsconfig.json` so the package can be type-checked
in isolation.
#### `@shade/sdk`
- KT verifier wiring: `fetchLatestSTH()` and `fetchConsistencyProof()`
now have explicit return types (`Promise<STHWire>` and
`Promise<{ proof: string[] }>`) so consumers don't see
`Promise<unknown>` from `res.json()`.
- `STHWire` type is now imported from `@shade/key-transparency`.
- `thumbnail.ts`: cast `globalThis` through `unknown` first when
reading optional DOM globals (`OffscreenCanvas`, `createImageBitmap`)
so consumer projects that include `lib.dom` don't reject our
narrower local types as "insufficiently overlapping".
#### `@shade/files`
- **Broke the `@shade/sdk``@shade/files` dependency cycle.**
`@shade/files` no longer imports `Shade` from `@shade/sdk` — every
callsite uses a new local `ShadeBridge` interface defined in
`src/integration/shade-bridge.ts`. This is the structural surface
Shade must satisfy: `myAddress`, `send`, `onMessage`, `upload`,
`onIncomingTransfer`, `getFingerprintFor` (required) plus
`getObservability`, `deliverControlEnvelope` (optional). The Shade
class structurally implements every member, so
`createFilesNamespace(this)` from the SDK side compiles regardless
of how many copies of `@shade/sdk` a consumer's package manager
hoists. **Fixes "this is not assignable to type 'Shade'"** in
consumer builds.
- `<ShadeFilesProvider>` now takes `files: FilesNamespace` as an
explicit prop instead of reading `shade.files`. Consumers pass
`shade.files` (or any `createFilesNamespace(...)` result for tests)
directly.
- `ShadeFileRpcChannel.send` now raises a clear error when
`deliverControlEnvelope` is undefined instead of producing an
implicit-undefined-call error at compile time.
#### `@shade/storage-encrypted`
- Replaced `KeyUsage` (a `lib.dom` type) with a local
`WebCryptoKeyUsage` union so the package compiles under
`lib: ["ES2022"]` without DOM.
- Fixed `tsconfig.json` `rootDir` so package-level `bunx tsc` works.
#### `@shade/transport-bridge`
- `sse-bridge.ts`: cast `res.body.getReader()` to
`ReadableStreamDefaultReader<Uint8Array>` so the strict reader-type
parity check in the consume loop passes.
#### `@shade/keychain` / `@shade/dashboard`
- Fixed `tsconfig.json` `rootDir` and `include` so the packages can
type-check standalone (and so `vite.config.ts` doesn't get pulled
into the dashboard's `rootDir`).
#### `@shade/widgets`
- Removed unused `ThumbnailMime` import in
`components/transfer/ThumbnailPreview.tsx`.
### Tooling
- New `scripts/typecheck-all.ts` — runs `bunx tsc --noEmit` against
every workspace package's `tsconfig.json` and fails if any reports
errors.
- New `bun run typecheck` script.
- `publish:dry` and `publish:all` now run `prepublish:check`
(`typecheck` + `test`) before any package is packed or published.
- `scripts/publish-shade.sh` calls the typecheck-all gate before
invoking the publisher.
### Migration
`4.0.0 → 4.0.1` is wire-compatible and source-compatible with one
exception:
- `<ShadeFilesProvider>` requires a `files` prop. Previously
`<ShadeFilesProvider shade={shade}>...</ShadeFilesProvider>` worked;
it now must be `<ShadeFilesProvider shade={shade} files={shade.files}>`.
No on-disk schema changes. No package-version-pin changes outside
the lockstep `4.0.0 → 4.0.1` bump.
## [4.0.0] — 2026-05-03 — General Availability ## [4.0.0] — 2026-05-03 — General Availability
Shade 4.0 is the first GA-marked release: every plan from V3.1 through Shade 4.0 is the first GA-marked release: every plan from V3.1 through

322
README.md
View File

@@ -3,37 +3,82 @@
End-to-end encryption library implementing the Signal Protocol (X3DH + Double Ratchet) for TypeScript/Bun. Drop into any project — frontend, backend, mobile — to get forward secrecy, post-compromise recovery, and self-healing security. End-to-end encryption library implementing the Signal Protocol (X3DH + Double Ratchet) for TypeScript/Bun. Drop into any project — frontend, backend, mobile — to get forward secrecy, post-compromise recovery, and self-healing security.
> **4.0.0 — General Availability.** All V3.1 → V3.12 work is merged, > **4.0.0 — General Availability.** All V3.1 → V3.12 work is merged,
> the cross-platform vector suite is green on TS + Kotlin, the threat > the cross-platform vector suite is green on TS + Kotlin (1000 / 1000
> model has been refreshed for every new surface, and the core stack > + 11 / 11), the threat model has been refreshed for every new
> (X3DH, ratchet, storage encryption, recovery, WebRTC P2P, Key > surface, and the core stack (X3DH, ratchet, storage encryption,
> Transparency) has been packaged for external review. The wire > recovery, WebRTC P2P, Key Transparency) has been packaged for
> format is **unchanged from 0.4.x** — 4.0 peers interoperate with > external review. The wire format is **unchanged from 0.4.x** — 4.0
> 0.4.x peers byte-for-byte. See > peers interoperate with 0.4.x peers byte-for-byte. See
> [MIGRATION.md § 0.3.x → 4.0](./MIGRATION.md#migrating-from-03x-to-40-ga) > [MIGRATION.md § 0.3.x → 4.0](./MIGRATION.md#migrating-from-03x-to-40-ga)
> for the upgrade path and [CHANGELOG.md § 4.0.0](./CHANGELOG.md) for > for the upgrade path and [CHANGELOG.md § 4.0.0](./CHANGELOG.md) for
> the consolidated release notes. Voice / Video have been moved to > the consolidated release notes. Voice / Video have been moved to
> [V5.0](./docs/V5.0.md), to be built on top of the frozen 4.0 > [V5.0](./docs/V5.0.md), to be built on top of the frozen 4.0
> baseline. > baseline.
## Status
| Area | 4.0 status | Pointers |
|------|-----------|----------|
| Protocol core (X3DH + ratchet + sender keys) | ✅ Done — frozen | [`packages/shade-core`](./packages/shade-core) |
| Storage encryption (V3.2) | ✅ Done — opt-in `EncryptedSQLiteStorage` / `EncryptedPostgresStorage`, key sources: passphrase / OS keychain / app-injected | [`docs/storage-encryption.md`](./docs/storage-encryption.md) |
| Fingerprint gates & trust UX (V3.3) | ✅ Done — `Shade.beforeFirstLargeFile / beforeBackupImport / beforeNewDeviceTrust` | [`docs/trust-ux.md`](./docs/trust-ux.md) |
| Observability v2 (V3.4) | ✅ Done — OpenTelemetry-shaped events, `/metrics`, observer dashboard | [`docs/observability.md`](./docs/observability.md) |
| Android parity & cross-platform CI (V3.5) | ✅ Done — TS + Kotlin vector-gate live; Android `KeystoreStorage` is post-GA | [`android/shade-android/README.md`](./android/shade-android/README.md), [`docs/cross-platform.md`](./docs/cross-platform.md) |
| Async store-and-forward (V3.6) | ✅ Done — `@shade/inbox` + `@shade/inbox-server` | [`docs/inbox.md`](./docs/inbox.md) |
| Transport bridge (V3.7) | ✅ Done — SSE / long-poll / WS adapters | [`docs/transport.md`](./docs/transport.md) |
| Web Workers crypto (V3.8) | ✅ Done — lane keys never cross the thread boundary | [`docs/web-workers.md`](./docs/web-workers.md) |
| Rich file metadata + thumbnails (V3.9) | ✅ Done — in `@shade/files` | [`docs/files.md`](./docs/files.md) |
| Social key recovery (V3.10) | ✅ Done — Shamir + AEAD-gated reconstruction + guardian widgets | [`docs/recovery.md`](./docs/recovery.md) |
| WebRTC P2P transport (V3.11) | ✅ Done — `RTCDataChannel` with `MultiTransportFallback([webrtc, http])` | [`docs/webrtc.md`](./docs/webrtc.md) |
| Key Transparency (V3.12) | ✅ Done — opt-in Merkle log, signed STH, witness gossip | [`docs/key-transparency.md`](./docs/key-transparency.md) |
| External crypto review | 🟡 Bundle ready — review window open after tag | [`docs/audit/REVIEW-BUNDLE.md`](./docs/audit/REVIEW-BUNDLE.md) |
| Soak (≥ 2 weeks under load) | 🟡 Harness shipped — operator runs it | [`scripts/soak.ts`](./scripts/soak.ts) (`bun run soak --hours 336`) |
| Voice / Video / Broadcast | 🔜 V5.0 — built on top of frozen 4.0 stack | [`docs/V5.0.md`](./docs/V5.0.md) |
## What you get ## What you get
**Protocol core**
- **X3DH** initial key agreement (works asynchronously via prekey bundles) - **X3DH** initial key agreement (works asynchronously via prekey bundles)
- **Double Ratchet** for per-message forward secrecy and post-compromise security - **Double Ratchet** for per-message forward secrecy and post-compromise security
- **Self-authenticated prekey server** (Hono, Docker-ready) with rate limiting, metrics, health checks - **Sender keys** for group ratchet (1:N broadcast key derivation)
- **Persistent storage backends**: SQLite (zero-config) and PostgreSQL (Drizzle)
- **Identity rotation** with grace period for old sessions - **Identity rotation** with grace period for old sessions
- **Safety numbers** (Signal-style fingerprints) for out-of-band verification - **Safety numbers** (Signal-style fingerprints) for out-of-band verification
- **Constant-time comparisons** and **memory zeroization** for hardened operation - **Constant-time comparisons** and **memory zeroization** for hardened operation
- **Binary wire format** that's significantly smaller than JSON - **Binary wire format** (`@shade/proto`) — significantly smaller than JSON
**Storage**
- **Persistent backends**: SQLite (zero-config, `bun:sqlite`) and PostgreSQL (Drizzle, FOR UPDATE SKIP LOCKED)
- **Crash-safe** — sessions survive container restarts, power outages, SIGKILL - **Crash-safe** — sessions survive container restarts, power outages, SIGKILL
- **Live observability** — bundled dashboard SPA + embeddable React widgets to see what's happening between every step - **At-rest encryption (V3.2, opt-in)** — AES-256-GCM under per-(table,column) DEKs; key sources: passphrase (scrypt), OS keychain (`@shade/keychain`), or app-injected. Online re-key, no downtime.
- **E2EE file transfers** — multi-lane chunked uploads/downloads with resume, integrity checks, and HTTP/WS fallback (`@shade/streams` + `@shade/transfer`)
- **WebRTC P2P transport (V3.11)** — opt-in `RTCDataChannel` upload path with public-STUN defaults, TURN-relay support, glare-safe peer pool, and automatic `MultiTransportFallback` back to HTTP when NAT traversal fails (`@shade/transport-webrtc`, [docs/webrtc.md](./docs/webrtc.md)) **Servers**
- **Web Workers crypto** — AEAD, HKDF, HMAC, X25519, Ed25519 and per-lane stream state run in a dedicated worker. 100 MB+ uploads stay smooth without frame drops, lane keys never cross the thread boundary (`@shade/crypto-web/worker`, [docs/web-workers.md](./docs/web-workers.md)) - **Self-authenticated prekey server** (`@shade/server`, Hono, Docker-ready) with rate limiting, metrics, health checks
- **E2EE filesystem RPC** — typed `list/stat/mkdir/delete/move/read/write/getThumbnail` + custom ops between peers, with rate-limit, retention, and fingerprint-gate hooks (`@shade/files`) - **Async store-and-forward relay** (`@shade/inbox-server`) — TTL-bound ciphertext blobs, signed PUT/FETCH/ACK, idempotent on `(address, msgId)`, per-recipient quota
- **Async store-and-forward** — deliver to offline recipients via a relay that holds ciphertext-only blobs with TTL, idempotent PUT, signed fetch/ack, and an `onMessageQueued` push-trigger hook (`@shade/inbox` + `@shade/inbox-server`) - **Bridge transports** (`@shade/transport-bridge`) — WS → SSE → long-poll fallback chain for clients that can't keep a WebSocket open. Same `IncomingMessage` shape across all three.
- **Social key recovery** — Shamir-split your identity to `n` guardians; any threshold-many `k` together restore it on a new device. No centralized recovery agent; OOB-fingerprint gate on every guardian release; AES-GCM authenticates the reconstruction (`@shade/recovery` + `<RecoverySetup />` / `<RecoveryRequest />` / `<RecoveryApprove />` widgets, [docs/recovery.md](./docs/recovery.md)) - **Standalone container** — one image bundles prekey + inbox + bridge + transfer + KT + observer
- **Key Transparency (V3.12)** — opt-in append-only Merkle log over the prekey server. Every `register` / `delete` becomes a signed leaf; every bundle-fetch carries an inclusion proof; an Ed25519-signed Tree Head ties roots to a fixed `log_id`. A `LightWitness` cross-checks STHs across clients so a malicious server that splits its view or rewrites history is caught (`@shade/key-transparency`, [docs/key-transparency.md](./docs/key-transparency.md))
**Trust UX**
- **Fingerprint gates (V3.3)** — `Shade.beforeFirstLargeFile(threshold, handler)`, `beforeBackupImport`, `beforeNewDeviceTrust`. Gates raise `FingerprintNotVerifiedError` on the operations that matter, default-warn TOFU otherwise.
- **`<FingerprintCompare />` / `<FingerprintGate />`** widgets for the matching UI side.
**File transfer & filesystem**
- **E2EE file transfers** (`@shade/streams` + `@shade/transfer`) — multi-lane chunked uploads/downloads with resume, integrity checks (per-lane sha256 + overall sha256), HTTP/WS fallback, `MultiTransportFallback` for N-ary demotion
- **WebRTC P2P transport (V3.11, opt-in)** — `RTCDataChannel` chunk path with public-STUN defaults, TURN-relay support, glare-safe peer pool, automatic fallback to HTTP when NAT traversal fails (`@shade/transport-webrtc`)
- **Web Workers crypto (V3.8, opt-in)** — AEAD, HKDF, HMAC, X25519, Ed25519 and per-lane stream state run in a dedicated worker. 100 MB+ uploads stay smooth without frame drops; lane keys never cross the thread boundary (`@shade/crypto-web/worker`)
- **E2EE filesystem RPC** (`@shade/files`) — typed `list/stat/mkdir/delete/move/read/write/getThumbnail` + custom ops, with rate-limit, retention, fingerprint-gate, and metrics hooks. React hooks under `@shade/files/react`.
**Recovery**
- **Social key recovery (V3.10)** — Shamir-split your identity to `n` guardians; any threshold-many `k` together restore it on a new device. No centralized recovery agent; OOB-fingerprint gate per guardian release; AES-GCM-authenticated reconstruction (`@shade/recovery` + `<RecoverySetup />` / `<RecoveryRequest />` / `<RecoveryApprove />`)
**Verifiable distribution**
- **Key Transparency (V3.12, opt-in)** — append-only Merkle log over the prekey server. Every `register` / `delete` becomes a signed leaf; every bundle-fetch carries an inclusion proof; an Ed25519-signed Tree Head ties roots to a fixed `log_id`. A `LightWitness` cross-checks STHs across clients so a malicious server that splits its view or rewrites history is caught (`@shade/key-transparency`).
**Observability**
- **Live observability** — OpenTelemetry-shaped events, bundled dashboard SPA + embeddable React widgets to see what's happening between every step (`@shade/observability` + `@shade/observer` + `@shade/dashboard`)
**Tooling**
- **CLI** — `shade init` scaffolder, `shade migrate-storage` (V3.2), `shade rotate-storage-key`, `shade fingerprint`, `shade rotate`, `shade peer`, `shade dashboard`, `shade doctor`, `shade backup` (`@shade/cli`)
- **Soak harness** — `bun run soak --hours 336` for the 2-week GA-stable window (`scripts/soak.ts`)
## Quick start ## Quick start
@@ -84,7 +129,38 @@ const plaintext = await shade.receive('alice@example.com', incomingEnvelope);
console.log(await shade.fingerprint); console.log(await shade.fingerprint);
``` ```
Need to ship a file or expose a filesystem to a peer? `Shade.files` is the high-level entrypoint: ### Opt-in surfaces (V3.x → 4.0 GA)
All of these are off by default. Wire them only where you need them.
```ts
// V3.3 — Fingerprint gates: enforce verification on the operations that matter
shade.beforeFirstLargeFile(10 * 1024 * 1024, async ({ peer, fingerprint }) => {
return await ui.confirmSafetyNumberMatches(peer, fingerprint);
});
shade.beforeBackupImport(async ({ embeddedFingerprint }) => { /* ... */ });
shade.beforeNewDeviceTrust(async ({ peer, oldFp, newFp }) => { /* ... */ });
// V3.8 — Web Workers crypto: opt-in, lane keys stay off the main thread
shade.configureWorkerCrypto({
workerUrl: new URL('@shade/crypto-web/worker', import.meta.url),
});
// V3.11 — WebRTC P2P transport: file transfers ride RTCDataChannel where NAT allows
import { nativeRtcFactory } from '@shade/transport-webrtc';
shade.configureWebRTC({
factory: nativeRtcFactory(),
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// V3.12 — Key Transparency: detect server-side bundle swaps
const shade = await createShade({
prekeyServer: 'https://shade.example.com',
keyTransparency: { mode: 'observe-strict', logPublicKey: PINNED_KEY_BYTES_32 },
});
```
### Files RPC (`@shade/files`)
```ts ```ts
// Server side — Bob exposes a virtual filesystem // Server side — Bob exposes a virtual filesystem
@@ -97,13 +173,13 @@ const stop = await shade.files.serve({
// Client side — Alice consumes Bob's filesystem // Client side — Alice consumes Bob's filesystem
const fs = await shade.files.client('bob'); const fs = await shade.files.client('bob');
await fs.write('/photos/cover.png', new Uint8Array(...)); // auto inline/streams await fs.write('/photos/cover.png', new Uint8Array([/* ... */])); // auto inline/streams
const result = await fs.read('/photos/cover.png'); const result = await fs.read('/photos/cover.png');
``` ```
Files ≤ 256 KiB ride inline in the RPC envelope; larger files automatically promote to multi-lane `@shade/transfer` streams with sha256 integrity. See [`docs/files.md`](./docs/files.md) for the full API. Files ≤ 256 KiB ride inline in the RPC envelope; larger files automatically promote to multi-lane `@shade/transfer` streams with sha256 integrity. See [`docs/files.md`](./docs/files.md) for the full API.
Or use the lower-level packages directly if you need full control: ### Lower-level access
```ts ```ts
import { ShadeSessionManager } from '@shade/core'; import { ShadeSessionManager } from '@shade/core';
@@ -117,20 +193,43 @@ const manager = new ShadeSessionManager(
await manager.initialize(); await manager.initialize();
``` ```
### At-rest encryption (V3.2)
```ts
import { KeyManager, EncryptedSQLiteStorage } from '@shade/storage-encrypted';
const km = await KeyManager.open({
kind: 'passphrase',
passphrase: process.env.SHADE_STORAGE_PASSPHRASE!,
salt: loadSaltFromDisk(),
});
const storage = await EncryptedSQLiteStorage.open({
dbPath: '/data/shade-client.db',
keyManager: km,
});
```
To migrate an existing 0.3.x SQLite DB in place:
```bash
shade migrate-storage \
--key-source passphrase \
--passphrase "$SHADE_STORAGE_PASSPHRASE" \
--salt-file /data/shade-client.db.salt
```
## Architecture — keys vs. payloads ## Architecture — keys vs. payloads
Shade splits the network into two planes. The **prekey server** only Shade splits the network into a **public-key plane** (the prekey
sees public keys; **everything else** rides the encrypted Double server) and an **encrypted plane** (everything else). The prekey
Ratchet between peers. If you remember nothing else from this README, server only sees public key material. If you remember nothing else
remember this picture: from this README, remember this picture:
``` ```
Shade Prekey Server (Hono, public keys only) Shade Prekey Container (Hono public keys only)
POST /v1/keys/register (signed) /v1/keys/* /v1/inbox/* /v1/bridge/* /v1/transfer/*
GET /v1/keys/bundle/:address /v1/kt/* /metrics /healthz /ready
POST /v1/keys/replenish (signed)
DELETE /v1/keys/:address (signed)
┌──────────────────┴──────────────────┐ ┌──────────────────┴──────────────────┐
│ │ │ │
@@ -142,11 +241,20 @@ remember this picture:
│◄── Double Ratchet messages ────────►│ ← end-to-end, │◄── Double Ratchet messages ────────►│ ← end-to-end,
│ (ratchet 0x02 / chunks 0x11) │ never on the │ (ratchet 0x02 / chunks 0x11) │ never on the
│ │ prekey server │ │ prekey server
◄── @shade/transfer chunks ─────────►│
POST /v1/transfer/:id/chunk │ ← peer-to-peer ◄── @shade/transfer chunks ─────────►│ ← peer-to-peer
GET /v1/transfer/:id/state │ HTTP, opaque POST /v1/transfer/:id/chunk │ HTTP, opaque
│ ciphertext GET /v1/transfer/:id/state │ ciphertext
│◄── @shade/inbox blobs (offline) ───►│ ← TTL-bound
│ POST /v1/inbox/:address │ ciphertext-only
│ POST /v1/inbox/:address/fetch │ relay
│◄── @shade/transport-webrtc ────────►│ ← optional P2P
│ RTCDataChannel `shade-transfer/v1` │ `MultiTransportFallback`
│ │ auto-demotes to HTTP
SQLiteStorage / PostgresStorage SQLiteStorage / PostgresStorage SQLiteStorage / PostgresStorage SQLiteStorage / PostgresStorage
+ EncryptedSQLiteStorage (V3.2) + EncryptedSQLiteStorage (V3.2)
(private keys + sessions) (private keys + sessions) (private keys + sessions) (private keys + sessions)
``` ```
@@ -155,17 +263,23 @@ remember this picture:
- Identity public keys (Ed25519 + X25519) - Identity public keys (Ed25519 + X25519)
- Signed prekeys + one-time prekey bundles - Signed prekeys + one-time prekey bundles
- Registration / replenish / delete writes, all Ed25519-signed - Registration / replenish / delete writes, all Ed25519-signed
- Operator-only metrics, `/health`, and the optional observer - (V3.6) Inbox ciphertext blobs with TTL — same container, separate
dashboard routes; the relay only sees `address || msgId || ciphertext-bytes`
- (V3.7) Bridge transports (SSE / long-poll / WS) — also delivered by
the same Hono app for clients that can't hold a WebSocket
- (V3.12, opt-in) KT inclusion proofs + signed tree heads on
`/v1/kt/*` — verifiable distribution
- Operator-only metrics and the optional observer dashboard
### What does **not** go via the prekey server ### What does **not** go via the prekey server
- **Message plaintext, ever.** Encrypted ratchet envelopes flow peer- - **Message plaintext, ever.** Encrypted ratchet envelopes flow peer-
to-peer over whatever transport you choose (HTTP, WebSocket, your to-peer over whatever transport you choose (HTTP, WebSocket, your
own broker). own broker, or the inbox relay above — which carries ciphertext only).
- **File chunks.** `@shade/transfer` POSTs ciphertext directly to the - **File chunks.** `@shade/transfer` POSTs ciphertext directly to the
receiver's `/v1/transfer/:streamId/chunk` route — the prekey server receiver's `/v1/transfer/:streamId/chunk` route — the prekey server
is not involved. is not involved. With V3.11 + `configureWebRTC()`, chunks ride
`RTCDataChannel` peer-to-peer; the relay is bypassed entirely.
- **Identity private keys.** They never leave the device's storage. - **Identity private keys.** They never leave the device's storage.
- **Filesystem RPC.** `@shade/files` rides the Double Ratchet for - **Filesystem RPC.** `@shade/files` rides the Double Ratchet for
control + small payloads, then promotes to direct `@shade/transfer` control + small payloads, then promotes to direct `@shade/transfer`
@@ -176,6 +290,8 @@ remember this picture:
The prekey server is metadata-bearing (see `THREAT-MODEL.md § 2`): The prekey server is metadata-bearing (see `THREAT-MODEL.md § 2`):
it sees who registers, who fetches whose bundle, and when. It does it sees who registers, who fetches whose bundle, and when. It does
**not** see message contents, transfer contents, or session state. **not** see message contents, transfer contents, or session state.
**V3.12 Key Transparency** (opt-in) makes its bundle distribution
*verifiable* so a malicious server that swaps a bundle is caught.
For the full threat model and mitigations, read For the full threat model and mitigations, read
[THREAT-MODEL.md](./THREAT-MODEL.md). For deployment-time guarantees, [THREAT-MODEL.md](./THREAT-MODEL.md). For deployment-time guarantees,
@@ -183,53 +299,84 @@ read [docs/PRODUCTION-CHECKLIST.md](./docs/PRODUCTION-CHECKLIST.md).
## Packages ## Packages
All packages publish in lockstep at `4.0.0`.
| Package | Purpose | | Package | Purpose |
|---------|---------| |---------|---------|
| `@shade/core` | Protocol logic (X3DH, Double Ratchet, session manager, errors, events) | | `@shade/core` | Protocol logic (X3DH, Double Ratchet, sender keys, session manager, errors, events) |
| `@shade/proto` | Compact binary wire format (`0x01` PreKeyMessage, `0x02` RatchetMessage, `0x11` StreamChunk) |
| `@shade/crypto-web` | SubtleCrypto + @noble/curves provider, in-memory storage. Includes the V3.8 Web Workers entrypoint (`@shade/crypto-web/worker`) — drop-in `WorkerCryptoProvider` plus `createEncryptStream` / `createDecryptStream` TransformStream factories | | `@shade/crypto-web` | SubtleCrypto + @noble/curves provider, in-memory storage. Includes the V3.8 Web Workers entrypoint (`@shade/crypto-web/worker`) — drop-in `WorkerCryptoProvider` plus `createEncryptStream` / `createDecryptStream` TransformStream factories |
| `@shade/storage-sqlite` | Persistent SQLite storage (zero-config, bun:sqlite) | | `@shade/observability` | OpenTelemetry-shaped event bus consumed by `@shade/observer`, server hooks, and the dashboard |
| `@shade/storage-postgres` | PostgreSQL storage with Drizzle for shared databases | | `@shade/keychain` | OS keychain bindings (libsecret / Keychain / Credential Manager) used by `@shade/storage-encrypted` and the CLI |
| `@shade/server` | Prekey server (Hono routes, auth, rate limit, health, metrics) | | `@shade/key-transparency` | Key Transparency (V3.12) — RFC 6962-style append-only Merkle log, address-index commitment, signed tree heads, and a `LightWitness` for split-view detection. Opt-in on both server and client. |
| `@shade/transport` | HTTP + WebSocket transport wrappers with auto-encryption | | `@shade/storage-sqlite` | Persistent SQLite storage (zero-config, `bun:sqlite`); also ships `SqliteInboxStore` |
| `@shade/storage-postgres` | PostgreSQL storage with Drizzle for shared databases; also ships `PostgresInboxStore` + `PostgresKTLogStore` |
| `@shade/storage-encrypted` | At-rest encryption (V3.2) — `EncryptedSQLiteStorage` / `EncryptedPostgresStorage`, `KeyManager`, online re-key |
| `@shade/streams` | Multi-lane chunk encryption — HKDF-derived per-lane keys, deterministic AES-GCM nonces, streaming SHA-256, file metadata + thumbnails (V3.9) |
| `@shade/transport` | HTTP + WebSocket transport wrappers with auto-encryption; KT-verifying `fetchBundleVerified` |
| `@shade/transport-bridge` | WS → SSE → long-poll fallback chain (V3.7) — single `IncomingMessage` shape across transports for clients that can't keep a WebSocket open | | `@shade/transport-bridge` | WS → SSE → long-poll fallback chain (V3.7) — single `IncomingMessage` shape across transports for clients that can't keep a WebSocket open |
| `@shade/transport-webrtc` | V3.11 P2P chunk transport via `RTCDataChannel`. Plugs into `@shade/transfer` as an `ITransferTransport`; signaling rides Shade's own ratchet. Memory factory + native (`globalThis.RTCPeerConnection`) factory included; `MultiTransportFallback([webrtc, http])` wired automatically when `shade.configureWebRTC()` is called. | | `@shade/transport-webrtc` | V3.11 P2P chunk transport via `RTCDataChannel`. Plugs into `@shade/transfer` as an `ITransferTransport`; signaling rides Shade's own ratchet. Memory factory + native (`globalThis.RTCPeerConnection`) factory included; `MultiTransportFallback([webrtc, http])` wired automatically when `shade.configureWebRTC()` is called. |
| `@shade/proto` | Compact binary wire format (smaller than JSON) | | `@shade/server` | Prekey server (Hono routes, auth, rate limit, health, metrics). `createPrekeyServerWithKT(...)` opts into V3.12 KT mode |
| `@shade/streams` | Multi-lane chunk encryption — HKDF-derived per-lane keys, deterministic AES-GCM nonces, streaming SHA-256 |
| `@shade/transfer` | Transfer engine on top of streams: parallel lanes, resume, HTTP + WS transport with auto-fallback, integrity verification |
| `@shade/files` | Typed E2EE filesystem RPC — list/stat/mkdir/delete/move/read/write/getThumbnail + custom ops, auto inline/streams routing, production hooks (rate limit, retention, fingerprint gate, metrics), React hooks |
| `@shade/recovery` | Social key recovery (V3.10) — Shamir-split identity to `n` guardians; threshold-many `k` reconstruct on a new device. AES-GCM-authenticated reconstruction; OOB-fingerprint gate per guardian release |
| `@shade/key-transparency` | Key Transparency (V3.12) — RFC 6962-style append-only Merkle log, address-index commitment, signed tree heads, and a `LightWitness` for split-view detection. Opt-in on both server and client. See [docs/key-transparency.md](./docs/key-transparency.md) |
| `@shade/inbox-server` | Async store-and-forward relay (V3.6) — Hono routes, signed PUT/FETCH/DELETE, per-recipient TTL + quota, idempotent on `(address, msgId)`. Bundles into the same standalone container as the prekey server | | `@shade/inbox-server` | Async store-and-forward relay (V3.6) — Hono routes, signed PUT/FETCH/DELETE, per-recipient TTL + quota, idempotent on `(address, msgId)`. Bundles into the same standalone container as the prekey server |
| `@shade/inbox` | Inbox client + durable outgoing queue + receive cursor + push-trigger hook (`onMessageQueued`); composes on top of `Shade.send`/`Shade.receive` for offline-recipient delivery | | `@shade/inbox` | Inbox client + durable outgoing queue + receive cursor + push-trigger hook (`onMessageQueued`); composes on top of `Shade.send`/`Shade.receive` for offline-recipient delivery |
| `@shade/transfer` | Transfer engine on top of streams: parallel lanes, resume, HTTP + WS transport with auto-fallback, `MultiTransportFallback` (N-ary demotion), integrity verification |
| `@shade/files` | Typed E2EE filesystem RPC — list/stat/mkdir/delete/move/read/write/getThumbnail + custom ops, auto inline/streams routing, production hooks (rate limit, retention, fingerprint gate, metrics), React hooks under `@shade/files/react` |
| `@shade/recovery` | Social key recovery (V3.10) — Shamir-split identity to `n` guardians; threshold-many `k` reconstruct on a new device. AES-GCM-authenticated reconstruction; OOB-fingerprint gate per guardian release |
| `@shade/observer` | Live debugger backend (snapshot, SSE, dashboard) — see [README](./packages/shade-observer/README.md) | | `@shade/observer` | Live debugger backend (snapshot, SSE, dashboard) — see [README](./packages/shade-observer/README.md) |
| `@shade/widgets` | Embeddable React widgets including transfer uploader/downloader — see [README](./packages/shade-widgets/README.md) |
| `@shade/dashboard` | Standalone dashboard SPA bundled into the observer | | `@shade/dashboard` | Standalone dashboard SPA bundled into the observer |
| `@shade/sdk` | High-level wrapper with `createShade()` one-liner, auto-publish, auto-establish, auto-replenish, `Shade.files` namespace | | `@shade/sdk` | High-level wrapper with `createShade()` one-liner, auto-publish, auto-establish, auto-replenish, `Shade.files` namespace, fingerprint gates, KT integration, WebRTC opt-in |
| `@shade/cli` | `shade init` scaffolder + utilities (fingerprint, rotate, peer, dashboard, doctor) | | `@shade/widgets` | Embeddable React widgets — fingerprint compare/gate, recovery setup/request/approve, transfer uploader/downloader, observer panels |
| `@shade/cli` | `shade init` scaffolder + utilities (fingerprint, rotate, peer, dashboard, doctor, backup, migrate-storage, rotate-storage-key) |
## Shade as a modular toolkit ## Shade as a modular toolkit
Shade is split into packages so each project can depend on **only what it needs**—encrypted messaging, file transfer, prekey hosting, or lower-level building blocks. You do not need one giant stack for every use case. Shade is split into packages so each project can depend on **only what it needs**—encrypted messaging, file transfer, prekey hosting, social recovery, KT verification, or lower-level building blocks. You do not need one giant stack for every use case.
For a **plain-language map** (which packages to add, what the prekey server does vs your own wiring, and where to start in code), see **[docs/SHADE-BY-SCENARIO.md](./docs/SHADE-BY-SCENARIO.md)**. For a **plain-language map** (which packages to add, what the prekey server does vs your own wiring, and where to start in code), see **[docs/SHADE-BY-SCENARIO.md](./docs/SHADE-BY-SCENARIO.md)**.
## Publishing ## Publishing
All packages publish to a self-hosted Gitea npm registry on `gt.zyon.no`. All packages publish to a self-hosted Gitea npm registry on `gt.zyon.no`. The Docker image of the standalone container ships at `gt.zyon.no/stian/shade-prekey:<tag>`.
```bash ```bash
# Bump all packages in lockstep # Bump all packages in lockstep
bun run version 1.1.0 bun run version 4.0.1
# Dry-run (pack all tarballs without publishing) # Dry-run (pack all tarballs without publishing) — no token required
bun run publish:dry bun run publish:dry
# Real publish (requires GITEA_TOKEN env var) # Real publish — interactive (prompts for GITEA_TOKEN, checks
# registry for conflicts, publishes via scripts/publish-all.ts)
bun run publish:all bun run publish:all
# Or via CI: push a git tag v1.1.0 and .gitea/workflows/publish.yml runs # Build + push the standalone Docker image
bun run scripts/build-docker.ts -- --tag 4.0.1 --push
``` ```
The interactive `scripts/publish-shade.sh` is the human entrypoint;
`scripts/publish-all.ts` is the headless variant used by CI and
`publish:dry`. They share a single `PACKAGES` list (24 entries at
4.0.0) so the two flows can never drift.
## Soak / GA-stable
Before tagging `4.0.0` as `latest` and recommending production
upgrades, run the combined soak harness for ≥ 2 weeks:
```bash
# Full GA window (V4.0 §Soak): 14 days × 24 hours
bun run soak --hours 336
# Smoke (~3 minutes — ratchet ping-pong, integrity check)
bun run soak:smoke
```
The harness fans out N concurrent ratchet pairs, ping-pongs at
~400 ops/sec/pair, and reports cumulative counters every minute.
Any exception in any pair is captured and re-raised at shutdown so
silent failures cannot hide. Wrap it in `systemd-run --user`,
`nohup`, or a Gitea scheduled job for the actual 2-week window.
## Security properties ## Security properties
| Property | Description | | Property | Description |
@@ -242,27 +389,57 @@ bun run publish:all
| **Memory zeroization** | Key material is zeroed after use (best-effort in JS) | | **Memory zeroization** | Key material is zeroed after use (best-effort in JS) |
| **Identity verification** | Safety numbers (60 digits) for out-of-band comparison | | **Identity verification** | Safety numbers (60 digits) for out-of-band comparison |
| **Identity rotation** | 7-day grace period for old sessions during rotation | | **Identity rotation** | 7-day grace period for old sessions during rotation |
| **At-rest encryption** *(V3.2, opt-in)* | AES-256-GCM under per-(table, column) DEKs; AAD binds `(table, column, pk)`; passphrase / OS keychain / app-injected master key; online re-key |
| **Fingerprint gates** *(V3.3)* | `Shade.beforeFirstLargeFile / beforeBackupImport / beforeNewDeviceTrust` raise `FingerprintNotVerifiedError` on the operations that matter; defaults to TOFU + warning when no gate is registered |
| **Async store-and-forward** *(V3.6)* | Relay only sees `address || msgId || ciphertext`; idempotent PUT; signed FETCH/ACK; TTL-bounded |
| **Web-Worker key isolation** *(V3.8)* | Lane keys, identity keys, and ratchet chain keys live inside a dedicated worker; main thread only ferries plaintext via transferable buffers; idle terminate releases worker memory |
| **Social key recovery** *(V3.10)* | Shamir over GF(2^8); AEAD-authenticated reconstruction (forged shares fail); guardian-side fingerprint gate before share release |
| **WebRTC P2P transport** *(V3.11)* | Same Double Ratchet authenticates SDP/ICE signaling; chunk frames AEAD-bound to `streamId/laneId/seq`; deterministic glare resolution; `MultiTransportFallback` auto-demotes to HTTP |
| **Key Transparency** *(V3.12, opt-in)* | Append-only Merkle log + signed tree heads + witness gossip — split-view and history-rewrite are detected by clients | | **Key Transparency** *(V3.12, opt-in)* | Append-only Merkle log + signed tree heads + witness gossip — split-view and history-rewrite are detected by clients |
## Documentation ## Documentation
**Operator + integrator**
- [docs/SHADE-BY-SCENARIO.md](./docs/SHADE-BY-SCENARIO.md) — **Modular toolkit**: pick packages by scenario (messages, files, browser, ops) - [docs/SHADE-BY-SCENARIO.md](./docs/SHADE-BY-SCENARIO.md) — **Modular toolkit**: pick packages by scenario (messages, files, browser, ops)
- [docs/PRODUCTION-CHECKLIST.md](./docs/PRODUCTION-CHECKLIST.md) — Pre-flight gates for going to production - [docs/PRODUCTION-CHECKLIST.md](./docs/PRODUCTION-CHECKLIST.md) — Pre-flight gates for going to production
- [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) — Full deployment guide (Docker, env vars, PostgreSQL, backup, Dokploy)
- [docs/ROADMAP.md](./docs/ROADMAP.md) — V3.x → 4.0 GA → V5.0 trajectory
**Per-surface deep-dives**
- [docs/files.md](./docs/files.md) — `@shade/files` API + design (filesystem RPC, custom ops, hooks, React) - [docs/files.md](./docs/files.md) — `@shade/files` API + design (filesystem RPC, custom ops, hooks, React)
- [docs/recovery.md](./docs/recovery.md) — `@shade/recovery` social key recovery (V3.10): Shamir setup, guardian-side gates, threshold tuning
- [docs/streams.md](./docs/streams.md) — `@shade/streams` + `@shade/transfer` deep dive (incl. hardening + retention) - [docs/streams.md](./docs/streams.md) — `@shade/streams` + `@shade/transfer` deep dive (incl. hardening + retention)
- [docs/inbox.md](./docs/inbox.md) — `@shade/inbox` + `@shade/inbox-server` async store-and-forward relay (V3.6) - [docs/inbox.md](./docs/inbox.md) — `@shade/inbox` + `@shade/inbox-server` async store-and-forward relay (V3.6)
- [docs/transport.md](./docs/transport.md) — `@shade/transport-bridge` SSE / long-poll / WS bridge layer (V3.7) - [docs/transport.md](./docs/transport.md) — `@shade/transport-bridge` SSE / long-poll / WS bridge layer (V3.7)
- [docs/web-workers.md](./docs/web-workers.md) — V3.8 Web Workers crypto: setup, bundler recipes (Vite/Webpack/Rollup), Safari notes, lifecycle, threat-model
- [docs/recovery.md](./docs/recovery.md) — `@shade/recovery` social key recovery (V3.10): Shamir setup, guardian-side gates, threshold tuning
- [docs/webrtc.md](./docs/webrtc.md) — `@shade/transport-webrtc` P2P transport (V3.11): NAT-traversal, TURN config, glare resolution, wire format, multi-fallback wiring - [docs/webrtc.md](./docs/webrtc.md) — `@shade/transport-webrtc` P2P transport (V3.11): NAT-traversal, TURN config, glare resolution, wire format, multi-fallback wiring
- [docs/key-transparency.md](./docs/key-transparency.md) — `@shade/key-transparency` (V3.12): operator + client onboarding, witness role, recovery procedures - [docs/key-transparency.md](./docs/key-transparency.md) — `@shade/key-transparency` (V3.12): operator + client onboarding, witness role, recovery procedures
- [docs/V3.12-DESIGN.md](./docs/V3.12-DESIGN.md) — V3.12 design notat (threat model, RFC 6962 vs CONIKS choice, freshness model, open-questions resolution) - [docs/storage-encryption.md](./docs/storage-encryption.md) — V3.2 at-rest encryption: design, key sources, rotation
- [docs/web-workers.md](./docs/web-workers.md) — V3.8 Web Workers crypto: setup, bundler recipes (Vite/Webpack/Rollup), Safari notes, lifecycle, threat-model - [docs/trust-ux.md](./docs/trust-ux.md) — V3.3 fingerprint gates: when each fires, handler patterns, widget integration
- [docs/observability.md](./docs/observability.md) — V3.4 event bus + dashboard
- [docs/cross-platform.md](./docs/cross-platform.md) — V3.5 Android parity + cross-platform vector regime
**Threat model + audit**
- [SECURITY.md](./SECURITY.md) — Reporting vulnerabilities, security policy, threat-/test-matrix - [SECURITY.md](./SECURITY.md) — Reporting vulnerabilities, security policy, threat-/test-matrix
- [THREAT-MODEL.md](./THREAT-MODEL.md) — Honest threat model and assumptions - [THREAT-MODEL.md](./THREAT-MODEL.md) — Honest threat model and assumptions (12 numbered sections + residual-risks table)
- [docs/audit/REVIEW-BUNDLE.md](./docs/audit/REVIEW-BUNDLE.md) — External crypto-review entrypoint (scope, build instructions, reporting)
- [docs/audit/SCOPE.md](./docs/audit/SCOPE.md) — One-page audit-scope summary
**Migration + history**
- [MIGRATION.md](./MIGRATION.md) — How to replace existing crypto with Shade + the [0.3.x → 4.0 upgrade path](./MIGRATION.md#migrating-from-03x-to-40-ga)
- [CHANGELOG.md](./CHANGELOG.md) — `4.0.0` GA section + every prior release
- [docs/archive/](./docs/archive/) — V2.1 / V2.2 / V2.3 backlog and V3.1 → V3.12 implementation plans (all `Status: Done`)
- [docs/V5.0.md](./docs/V5.0.md) — Voice / Video / Broadcast (post-GA, built on the frozen 4.0 stack)
**Examples**
- [examples/](./examples/) — Runnable example applications, including - [examples/](./examples/) — Runnable example applications, including
[`07-streams-upload`](./examples/07-streams-upload) (multi-lane file transfer) [`07-streams-upload`](./examples/07-streams-upload) (multi-lane file transfer)
and [`08-files-browser`](./examples/08-files-browser) (filesystem RPC) and [`08-files-browser`](./examples/08-files-browser) (filesystem RPC)
- [MIGRATION.md](./MIGRATION.md) — How to replace existing crypto with Shade
## Deployment — one container per project ## Deployment — one container per project
@@ -274,15 +451,30 @@ docker run -d \
-v my-project-shade:/data \ -v my-project-shade:/data \
-p 3900:3900 \ -p 3900:3900 \
-e SHADE_OBSERVER_TOKEN=change-me-to-at-least-16-chars \ -e SHADE_OBSERVER_TOKEN=change-me-to-at-least-16-chars \
gt.zyon.no/stian/shade-prekey:latest gt.zyon.no/stian/shade-prekey:4.0.0
``` ```
The container includes: The container includes:
- **Prekey server** — `/v1/keys/*` REST API - **Prekey server** — `/v1/keys/*` REST API
- **Observer dashboard** — `/shade-observer/dashboard/` (off unless token is set) - **Inbox relay (V3.6)** — `/v1/inbox/*` async store-and-forward; enable
with `SHADE_INBOX_DB_PATH=/data/inbox.db` (or `SHADE_INBOX_PG_URL`).
`SHADE_INBOX_PRUNE_INTERVAL_MINUTES` controls TTL prune cadence.
- **Bridge transports (V3.7)** — `/v1/bridge/{stream,poll,ws}` SSE +
long-poll + WS adapters for clients that can't keep a WebSocket open.
- **Transfer routes** — `/v1/transfer/*` chunk + state + control routes
for `@shade/transfer`.
- **Key Transparency (V3.12)** — `/v1/kt/*` exposes `log_id`, latest +
historical STH, and consistency proofs. Enable with
`SHADE_KT_*` env vars; off by default.
- **Observer dashboard** — `/shade-observer/dashboard/` (off unless
`SHADE_OBSERVER_TOKEN` is set)
- **OpenAPI spec** — `/openapi.yaml` and interactive `/docs` viewer - **OpenAPI spec** — `/openapi.yaml` and interactive `/docs` viewer
(covers all 27 routes — prekey, inbox, bridge, transfer, KT,
observer, `/metrics`, `/healthz`, `/ready`)
- **Prometheus metrics** — `/metrics` - **Prometheus metrics** — `/metrics`
- **Health check** — `/health` - **Health probes** — `/health` (full), `/healthz` (liveness),
`/ready` (readiness)
- **Stale cleanup** — purges inactive identities automatically - **Stale cleanup** — purges inactive identities automatically
See [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) for the full deployment guide, environment variables, PostgreSQL config, backup strategy, and Dokploy instructions. See [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) for the full deployment guide, environment variables, PostgreSQL config, backup strategy, and Dokploy instructions.

View File

@@ -1,8 +1,11 @@
# Shade V2.1 — Improvements (infrastructure, storage, operations, security) # Shade V2.1 — Improvements (infrastructure, storage, operations, security)
This document describes **improvements** agreed for next-generation work on Shade: clearer product story, stronger storage, mobile parity, operational hardening, transfer abuse, and a formal security narrative. **Status:** Done — superseded by the V3.1 → V3.12 plans, all of which
landed in the 4.0 GA release. This document is preserved as historical
context for the original V2.1 backlog; the concrete deliverables live
under [`docs/archive/V3.*.md`](./).
**Audience:** **Maintainers and contributors** implementing the changes. Add status fields as items land in code/docs. This document describes **improvements** agreed for next-generation work on Shade: clearer product story, stronger storage, mobile parity, operational hardening, transfer abuse, and a formal security narrative.
--- ---

View File

@@ -1,8 +1,10 @@
# Shade V2.2 — Feature plan: product, platform, and developer experience # Shade V2.2 — Feature plan: product, platform, and developer experience
This document gathers **planned features** that extend Shade beyond todays core (X3DH + Double Ratchet + Streams/transfer): groups, asynchronous delivery, richer file UX, web workers, CLI, API docs, and scaffolding. **Status:** Done — superseded by V3.6 (inbox), V3.7 (bridge), V3.8
(workers), V3.9 (file metadata), V3.10 (recovery), V3.12 (KT). All
landed in the 4.0 GA release; see [`docs/ROADMAP.md`](../ROADMAP.md).
Add optional per-feature status (Idea / Design / IMP / Done). This document gathers **planned features** that extend Shade beyond todays core (X3DH + Double Ratchet + Streams/transfer): groups, asynchronous delivery, richer file UX, web workers, CLI, API docs, and scaffolding.
--- ---

View File

@@ -1,5 +1,9 @@
# Shade V2.3 — Tillit, retention, integrasjon og observability # Shade V2.3 — Tillit, retention, integrasjon og observability
**Status:** Done — superseded by V3.3 (trust UX), V3.4 (observability),
V3.10 (recovery), V3.12 (KT). Alt levert i 4.0 GA — se
[`docs/ROADMAP.md`](../ROADMAP.md).
Dette dokumentet beskriver **høyere ambisjonsnivå** og **plattformkryssende** arbeid: brukertilfeller der tillit må være **eksplisitt**, data må **utkrympes**/ryddes automatisk, apper må **enkelt koble seg på** kjente transportmønstre, og drift må **observere** uten å lekke innhold. Dette dokumentet beskriver **høyere ambisjonsnivå** og **plattformkryssende** arbeid: brukertilfeller der tillit må være **eksplisitt**, data må **utkrympes**/ryddes automatisk, apper må **enkelt koble seg på** kjente transportmønstre, og drift må **observere** uten å lekke innhold.
--- ---

View File

@@ -1,6 +1,6 @@
# V3.12 — Key Transparency: Designnotat # V3.12 — Key Transparency: Designnotat
**Status:** Approved (in-tree review — markeres `Design` i ROADMAP) **Status:** Done — implementert i `@shade/key-transparency` 0.4.0, frosset i 4.0 GA.
**Forfatter:** Shade-teamet **Forfatter:** Shade-teamet
**Reviewer-mål:** ekstern crypto-orientert reviewer før produksjons-deploy. **Reviewer-mål:** ekstern crypto-orientert reviewer før produksjons-deploy.
**Implementasjons-target:** `@shade/key-transparency` + utvidelser i **Implementasjons-target:** `@shade/key-transparency` + utvidelser i

View File

@@ -16,8 +16,10 @@
"version": "bun run scripts/bump-version.ts", "version": "bun run scripts/bump-version.ts",
"soak": "bun run scripts/soak.ts", "soak": "bun run scripts/soak.ts",
"soak:smoke": "bun run scripts/soak.ts --hours 0.05 --pairs 4", "soak:smoke": "bun run scripts/soak.ts --hours 0.05 --pairs 4",
"publish:dry": "DRY_RUN=1 bun run scripts/publish-all.ts", "typecheck": "bun run scripts/typecheck-all.ts",
"publish:all": "bash scripts/publish-shade.sh", "prepublish:check": "bun run typecheck",
"publish:dry": "bun run prepublish:check && DRY_RUN=1 bun run scripts/publish-all.ts",
"publish:all": "bun run prepublish:check && bash scripts/publish-shade.sh",
"build:docker": "bun run scripts/build-docker.ts", "build:docker": "bun run scripts/build-docker.ts",
"publish:docker": "bun run scripts/build-docker.ts -- --push" "publish:docker": "bun run scripts/build-docker.ts -- --push"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/cli", "name": "@shade/cli",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/cli.ts", "main": "src/cli.ts",
"bin": { "bin": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/core", "name": "@shade/core",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/crypto-web", "name": "@shade/crypto-web",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/dashboard", "name": "@shade/dashboard",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -8,5 +8,5 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler" "moduleResolution": "bundler"
}, },
"include": ["src", "vite.config.ts"] "include": ["src"]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/files", "name": "@shade/files",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,4 +1,4 @@
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from '../integration/shade-bridge.js';
import { import {
KIND_CUSTOM_V1, KIND_CUSTOM_V1,
KIND_DELETE_V1, KIND_DELETE_V1,
@@ -184,7 +184,7 @@ export interface CreateFileClientOptions {
* transfers that carry the actual bytes. * transfers that carry the actual bytes.
*/ */
export function createFileClient( export function createFileClient(
shade: Shade, shade: ShadeBridge,
channel: ShadeFileRpcChannel, channel: ShadeFileRpcChannel,
pending: PendingRpcRegistry, pending: PendingRpcRegistry,
peerAddress: string, peerAddress: string,

View File

@@ -161,15 +161,33 @@ async function peekStream(stream: ReadableStream<Uint8Array>): Promise<InlineDec
} }
} }
interface MinimalReader { /**
read(): Promise<{ value: Uint8Array | undefined; done: boolean }>; * Structural mirror of WHATWG `ReadableStreamDefaultReader<Uint8Array>`.
*
* The disjoint union shape with `value?: T | undefined` is the lowest
* common denominator across every lib environment we care about:
* - `bun-types` emits `{ done: true; value?: undefined }`
* - `lib.dom` emits `{ done: true; value?: T }`
* - `node:stream/web` emits the union form
*
* `value?: T | undefined` is assignable from all three. A flat
* `{ value?: T; done: boolean }` is rejected by
* `exactOptionalPropertyTypes` because the present branches require
* `value: T`. Defining it as an explicit union avoids the trap.
*/
type MinimalReadResult<T> =
| { done: false; value: T }
| { done: true; value?: T | undefined };
interface MinimalReader<T> {
read(): Promise<MinimalReadResult<T>>;
cancel(reason?: unknown): Promise<void>; cancel(reason?: unknown): Promise<void>;
releaseLock(): void; releaseLock(): void;
} }
function reconstructStream( function reconstructStream(
prefix: Uint8Array[], prefix: Uint8Array[],
reader: MinimalReader, reader: MinimalReader<Uint8Array>,
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
let prefixIdx = 0; let prefixIdx = 0;
return new ReadableStream<Uint8Array>({ return new ReadableStream<Uint8Array>({

View File

@@ -142,13 +142,14 @@ export async function createClientStreamsBridge(
} }
parked.set(readStreamId, arrival); parked.set(readStreamId, arrival);
setTimeout(() => { const t = setTimeout(() => {
const stale = parked.get(readStreamId); const stale = parked.get(readStreamId);
if (stale === arrival) { if (stale === arrival) {
parked.delete(readStreamId); parked.delete(readStreamId);
void handle.abort('rpc-timeout').catch(() => undefined); void handle.abort('rpc-timeout').catch(() => undefined);
} }
}, parkedReadTtlMs).unref?.(); }, parkedReadTtlMs);
(t as unknown as { unref?: () => void }).unref?.();
}); });
function cleanupWaiter(w: PendingReadWaiter): void { function cleanupWaiter(w: PendingReadWaiter): void {

View File

@@ -4,7 +4,7 @@
* so a single Shade can simultaneously serve files AND consume them from * so a single Shade can simultaneously serve files AND consume them from
* peers without paying the setup cost twice. * peers without paying the setup cost twice.
*/ */
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from './shade-bridge.js';
import { import {
attachClientRouting, attachClientRouting,
attachFileHandler, attachFileHandler,
@@ -54,7 +54,7 @@ interface NamespaceState {
* Construct a `FilesNamespace` bound to a Shade instance. The SDK's * Construct a `FilesNamespace` bound to a Shade instance. The SDK's
* `Shade.files` getter calls this lazily and memoizes the result. * `Shade.files` getter calls this lazily and memoizes the result.
*/ */
export function createFilesNamespace(shade: Shade): FilesNamespace { export function createFilesNamespace(shade: ShadeBridge): FilesNamespace {
const state: NamespaceState = { const state: NamespaceState = {
channel: new ShadeFileRpcChannel(shade), channel: new ShadeFileRpcChannel(shade),
pending: new PendingRpcRegistry(), pending: new PendingRpcRegistry(),

View File

@@ -0,0 +1,67 @@
/**
* Structural surface @shade/files needs from a Shade instance.
*
* Defining this locally — instead of `import type { Shade } from '@shade/sdk'`
* — breaks the @shade/sdk ↔ @shade/files dependency cycle. Without this
* break, a consumer that installs @shade/sdk from a registry ends up with
* two distinct `Shade` classes in `node_modules` (one from
* `@shade/sdk/node_modules/@shade/files/.../Shade`, one from
* `@shade/sdk/Shade`). TypeScript treats them as nominally different types,
* raising `this is not assignable to Shade` from inside SDK methods that
* pass `this` into `createFilesNamespace`.
*
* The Shade class structurally implements every member listed below, so
* `createFilesNamespace(this)` from the SDK side compiles regardless of
* how many copies of @shade/sdk a consumer's package manager installs.
*
* Member signatures match Shade's exactly so this is a structural
* subtype, not a parallel API.
*/
import type { ShadeEnvelope } from '@shade/core';
import type {
IncomingTransfer,
TransferHandle,
TransferOptions,
} from '@shade/transfer';
import type { ObservabilityHook } from '@shade/observability';
export interface ShadeBridge {
/** Address that names this Shade instance to peers. */
readonly myAddress: string;
/** Encrypt + send `plaintext` to `peer`; returns the wire envelope. */
send(peer: string, plaintext: string): Promise<ShadeEnvelope>;
/**
* Subscribe to incoming ratchet plaintext. Returns an unsubscribe.
* Handlers may be sync or async; async handlers are awaited in
* registration order.
*/
onMessage(
handler: (from: string, plaintext: string) => void | Promise<void>,
): () => void;
/**
* Upload bytes via the SDK's transfer engine. Required when the bridge
* is used with `streams` content I/O (read/write > 256 KiB).
*/
upload(opts: TransferOptions): Promise<TransferHandle>;
/** Subscribe to incoming transfers initiated by a peer. */
onIncomingTransfer(
handler: (incoming: IncomingTransfer) => void | Promise<void>,
): Promise<() => void>;
/** Fingerprint accessor for the trust-gate hooks. */
getFingerprintFor(peer: string): Promise<string>;
/**
* Optional inheritable observability bus. Files inherits the bus when
* the SDK passes one in via the namespace; otherwise files runs without
* observability hooks.
*/
getObservability?(): ObservabilityHook | undefined;
/** Optional control-envelope passthrough used by the WebRTC bridge. */
deliverControlEnvelope?(peer: string, envelope: ShadeEnvelope): Promise<void>;
}

View File

@@ -1,17 +1,23 @@
import React, { createContext, useContext, useMemo } from 'react'; import React, { createContext, useContext, useMemo } from 'react';
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from '../integration/shade-bridge.js';
import type { FilesNamespace } from '../integration/files-namespace.js'; import type { FilesNamespace } from '../integration/files-namespace.js';
export interface ShadeFilesContextValue { export interface ShadeFilesContextValue {
shade: Shade; shade: ShadeBridge;
files: FilesNamespace; files: FilesNamespace;
} }
const ShadeFilesContext = createContext<ShadeFilesContextValue | null>(null); const ShadeFilesContext = createContext<ShadeFilesContextValue | null>(null);
export interface ShadeFilesProviderProps { export interface ShadeFilesProviderProps {
/** Initialized `Shade` instance. `files` namespace is read off it lazily. */ /** Initialized `Shade` instance (or any `ShadeBridge`-shaped object). */
shade: Shade; shade: ShadeBridge;
/**
* The `FilesNamespace` to expose to children. Pass `shade.files` from
* `@shade/sdk`, or a `createFilesNamespace(...)` result for tests /
* custom bridges.
*/
files: FilesNamespace;
children: React.ReactNode; children: React.ReactNode;
} }
@@ -20,8 +26,8 @@ export interface ShadeFilesProviderProps {
* `<ShadeRuntimeProvider>` in `@shade/widgets` so file-RPC consumers * `<ShadeRuntimeProvider>` in `@shade/widgets` so file-RPC consumers
* don't pull in the widget tree. * don't pull in the widget tree.
*/ */
export function ShadeFilesProvider({ shade, children }: ShadeFilesProviderProps): React.ReactElement { export function ShadeFilesProvider({ shade, files, children }: ShadeFilesProviderProps): React.ReactElement {
const value = useMemo<ShadeFilesContextValue>(() => ({ shade, files: shade.files }), [shade]); const value = useMemo<ShadeFilesContextValue>(() => ({ shade, files }), [shade, files]);
return React.createElement(ShadeFilesContext.Provider, { value }, children); return React.createElement(ShadeFilesContext.Provider, { value }, children);
} }

View File

@@ -1,4 +1,4 @@
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from '../integration/shade-bridge.js';
import { import {
encodeEnvelope, encodeEnvelope,
looksLikeFileEnvelope, looksLikeFileEnvelope,
@@ -35,7 +35,7 @@ export class ShadeFileRpcChannel {
private readonly unsubscribe: () => void; private readonly unsubscribe: () => void;
private destroyed = false; private destroyed = false;
constructor(private readonly shade: Shade) { constructor(private readonly shade: ShadeBridge) {
this.unsubscribe = shade.onMessage(async (from, plaintext) => { this.unsubscribe = shade.onMessage(async (from, plaintext) => {
if (!looksLikeFileEnvelope(plaintext)) return; if (!looksLikeFileEnvelope(plaintext)) return;
const classified = tryParseEnvelope(plaintext); const classified = tryParseEnvelope(plaintext);
@@ -72,6 +72,11 @@ export class ShadeFileRpcChannel {
if (this.destroyed) throw new Error('ShadeFileRpcChannel: destroyed'); if (this.destroyed) throw new Error('ShadeFileRpcChannel: destroyed');
const plaintext = encodeEnvelope(envelope); const plaintext = encodeEnvelope(envelope);
const ratchetEnvelope = await this.shade.send(peerAddress, plaintext); const ratchetEnvelope = await this.shade.send(peerAddress, plaintext);
if (this.shade.deliverControlEnvelope === undefined) {
throw new Error(
'ShadeFileRpcChannel: shade.deliverControlEnvelope is required — call shade.configureTransfers({ resolveBaseUrl }) before using the files namespace.',
);
}
await this.shade.deliverControlEnvelope(peerAddress, ratchetEnvelope); await this.shade.deliverControlEnvelope(peerAddress, ratchetEnvelope);
} }

View File

@@ -1,4 +1,4 @@
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from '../integration/shade-bridge.js';
import type { StandardOp } from '../protocol/kinds.js'; import type { StandardOp } from '../protocol/kinds.js';
export type OpKind = StandardOp | `custom:${string}`; export type OpKind = StandardOp | `custom:${string}`;
@@ -42,7 +42,7 @@ export function buildOpContext<TArgs>(args: {
signal: AbortSignal; signal: AbortSignal;
idempotencyKey: string | undefined; idempotencyKey: string | undefined;
attemptNumber: number; attemptNumber: number;
shade: Shade; shade: ShadeBridge;
}): OpContext<TArgs> { }): OpContext<TArgs> {
return { return {
op: args.op, op: args.op,

View File

@@ -1,4 +1,4 @@
import type { Shade } from '@shade/sdk'; import type { ShadeBridge } from '../integration/shade-bridge.js';
import type { ZodTypeAny } from 'zod'; import type { ZodTypeAny } from 'zod';
import { import {
MUTATION_OPS, MUTATION_OPS,
@@ -215,7 +215,7 @@ const OP_SCHEMAS: Record<StandardOp, OpSchemaPair> = {
* via `Shade.files.serve(...)` in the SDK). * via `Shade.files.serve(...)` in the SDK).
*/ */
export function createFileHandler( export function createFileHandler(
shade: Shade, shade: ShadeBridge,
config: FileHandlerConfig, config: FileHandlerConfig,
): FileHandler { ): FileHandler {
const idempotency = new IdempotencyCache(config.idempotency); const idempotency = new IdempotencyCache(config.idempotency);

View File

@@ -168,13 +168,14 @@ export async function createServerStreamsBridge(
// No waiter yet — park. // No waiter yet — park.
parked.set(writeId, arrived); parked.set(writeId, arrived);
setTimeout(() => { const parkTimer = setTimeout(() => {
const stale = parked.get(writeId); const stale = parked.get(writeId);
if (stale === arrived) { if (stale === arrived) {
parked.delete(writeId); parked.delete(writeId);
void handle.abort('rpc-timeout').catch(() => undefined); void handle.abort('rpc-timeout').catch(() => undefined);
} }
}, parkedWriteTtlMs).unref?.(); }, parkedWriteTtlMs);
(parkTimer as unknown as { unref?: () => void }).unref?.();
}); });
function cleanupWaiter(w: PendingWaiter): void { function cleanupWaiter(w: PendingWaiter): void {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/inbox-server", "name": "@shade/inbox-server",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/inbox", "name": "@shade/inbox",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/key-transparency", "name": "@shade/key-transparency",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -21,7 +21,7 @@
* the dataset grows enough that flat re-hash becomes a bottleneck. * the dataset grows enough that flat re-hash becomes a bottleneck.
*/ */
import { leafHash, nodeHash, emptyRootHash } from './hashes.js'; import { leafHash, emptyRootHash } from './hashes.js';
import { sha256Sync } from './sha256.js'; import { sha256Sync } from './sha256.js';
import { constantTimeEqual } from './util.js'; import { constantTimeEqual } from './util.js';
import { mth, auditPath, recomputeRootFromAuditPath } from './log.js'; import { mth, auditPath, recomputeRootFromAuditPath } from './log.js';

View File

@@ -24,8 +24,6 @@ import { MerkleLog, auditPath } from './log.js';
import { import {
AddressIndex, AddressIndex,
type AddressIndexEntry, type AddressIndexEntry,
type IndexAbsenceProof,
type IndexInclusionProof,
} from './index-tree.js'; } from './index-tree.js';
import { import {
type SignedTreeHead, type SignedTreeHead,

View File

@@ -111,7 +111,7 @@ interface IndexAbsenceWire {
} | null; } | null;
} }
type IndexProofWire = IndexInclusionWire | IndexAbsenceWire; export type IndexProofWire = IndexInclusionWire | IndexAbsenceWire;
interface BundleInclusionWire { interface BundleInclusionWire {
kind: 'inclusion' | 'tombstone'; kind: 'inclusion' | 'tombstone';

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"]
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/keychain", "name": "@shade/keychain",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,4 +1,5 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["src/**/*", "tests/**/*"] "compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/observability", "name": "@shade/observability",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/observer", "name": "@shade/observer",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/proto", "name": "@shade/proto",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/recovery", "name": "@shade/recovery",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/sdk", "name": "@shade/sdk",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -96,7 +96,7 @@ export class BackgroundTasks {
this.hooks.onError?.(err as Error, 'prune-files'); this.hooks.onError?.(err as Error, 'prune-files');
} }
}, this.pruneFilesIntervalMs); }, this.pruneFilesIntervalMs);
this.pruneFilesTimer.unref?.(); (this.pruneFilesTimer as unknown as { unref?: () => void }).unref?.();
} }
stop(): void { stop(): void {

View File

@@ -21,7 +21,7 @@ import {
import { encodeEnvelope, decodeEnvelope, inspectEnvelopeType } from '@shade/proto'; import { encodeEnvelope, decodeEnvelope, inspectEnvelopeType } from '@shade/proto';
import { ShadeFetchTransport, type KTVerifierOptions } from '@shade/transport'; import { ShadeFetchTransport, type KTVerifierOptions } from '@shade/transport';
import { LightWitness } from '@shade/key-transparency'; import { LightWitness } from '@shade/key-transparency';
import type { SignedTreeHead } from '@shade/key-transparency'; import type { SignedTreeHead, STHWire } from '@shade/key-transparency';
import { import {
TransferEngine, TransferEngine,
ShadeTransferHttpTransport, ShadeTransferHttpTransport,
@@ -217,15 +217,15 @@ export class Shade {
maxStaleMs: this.config.keyTransparency.maxStaleMs, maxStaleMs: this.config.keyTransparency.maxStaleMs,
maxStored: this.config.keyTransparency.witnessMaxStored, maxStored: this.config.keyTransparency.witnessMaxStored,
fetcher: { fetcher: {
async fetchLatestSTH() { async fetchLatestSTH(): Promise<STHWire> {
const res = await fetch(`${baseUrl}/v1/kt/sth`); const res = await fetch(`${baseUrl}/v1/kt/sth`);
if (!res.ok) throw new Error(`KT /sth: ${res.status}`); if (!res.ok) throw new Error(`KT /sth: ${res.status}`);
return res.json(); return (await res.json()) as STHWire;
}, },
async fetchConsistencyProof(from, to) { async fetchConsistencyProof(from, to): Promise<{ proof: string[] }> {
const res = await fetch(`${baseUrl}/v1/kt/consistency?from=${from}&to=${to}`); const res = await fetch(`${baseUrl}/v1/kt/consistency?from=${from}&to=${to}`);
if (!res.ok) throw new Error(`KT /consistency: ${res.status}`); if (!res.ok) throw new Error(`KT /consistency: ${res.status}`);
return res.json(); return (await res.json()) as { proof: string[] };
}, },
}, },
}); });

View File

@@ -59,12 +59,16 @@ interface CreateImageBitmapFn {
} }
function getOffscreenCanvasCtor(): OffscreenCanvasCtor | null { function getOffscreenCanvasCtor(): OffscreenCanvasCtor | null {
const g = globalThis as { OffscreenCanvas?: OffscreenCanvasCtor }; const g = globalThis as unknown as { OffscreenCanvas?: OffscreenCanvasCtor };
return g.OffscreenCanvas ?? null; return g.OffscreenCanvas ?? null;
} }
function getCreateImageBitmap(): CreateImageBitmapFn | null { function getCreateImageBitmap(): CreateImageBitmapFn | null {
const g = globalThis as { createImageBitmap?: CreateImageBitmapFn }; // `globalThis.createImageBitmap` (when DOM lib is loaded) has a wider
// signature than our minimal `CreateImageBitmapFn`. Cast through
// `unknown` so consumer tsconfigs that include "DOM" don't reject the
// narrower local type as "insufficiently overlapping".
const g = globalThis as unknown as { createImageBitmap?: CreateImageBitmapFn };
return g.createImageBitmap ?? null; return g.createImageBitmap ?? null;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/server", "name": "@shade/server",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/storage-encrypted", "name": "@shade/storage-encrypted",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -11,11 +11,23 @@
const NONCE_LEN = 12; const NONCE_LEN = 12;
// Local mirror of the WebCrypto KeyUsage union — avoids depending on
// `lib.dom` (we run on Bun + plain ES2022) while keeping API parity.
type WebCryptoKeyUsage =
| 'encrypt'
| 'decrypt'
| 'sign'
| 'verify'
| 'deriveKey'
| 'deriveBits'
| 'wrapKey'
| 'unwrapKey';
function bs(u: Uint8Array): ArrayBuffer { function bs(u: Uint8Array): ArrayBuffer {
return u as unknown as ArrayBuffer; return u as unknown as ArrayBuffer;
} }
async function importKey(key: Uint8Array, usages: KeyUsage[]): Promise<CryptoKey> { async function importKey(key: Uint8Array, usages: WebCryptoKeyUsage[]): Promise<CryptoKey> {
if (key.length !== 32) throw new Error(`AES-256-GCM key must be 32 bytes, got ${key.length}`); if (key.length !== 32) throw new Error(`AES-256-GCM key must be 32 bytes, got ${key.length}`);
return globalThis.crypto.subtle.importKey('raw', bs(key), 'AES-GCM', false, usages); return globalThis.crypto.subtle.importKey('raw', bs(key), 'AES-GCM', false, usages);
} }

View File

@@ -1,4 +1,5 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["src/**/*", "tests/**/*"] "compilerOptions": { "outDir": "dist", "rootDir": "src" },
"include": ["src"]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/storage-postgres", "name": "@shade/storage-postgres",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/storage-sqlite", "name": "@shade/storage-sqlite",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/streams", "name": "@shade/streams",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/transfer", "name": "@shade/transfer",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/transport-bridge", "name": "@shade/transport-bridge",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -127,7 +127,7 @@ export class SseBridge implements BridgeTransport {
if (!res.body) { if (!res.body) {
throw new BridgeError('SSE response has no body'); throw new BridgeError('SSE response has no body');
} }
this.currentReader = res.body.getReader(); this.currentReader = res.body.getReader() as ReadableStreamDefaultReader<Uint8Array>;
this.connected = true; this.connected = true;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/transport-webrtc", "name": "@shade/transport-webrtc",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/transport", "name": "@shade/transport",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@shade/widgets", "name": "@shade/widgets",
"version": "4.0.0", "version": "4.0.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
import { import {
isAllowedThumbnailMime, isAllowedThumbnailMime,
THUMBNAIL_MAX_BYTES, THUMBNAIL_MAX_BYTES,
type ThumbnailMime,
} from '@shade/sdk'; } from '@shade/sdk';
import { useShadeRuntimeTheme } from '../../ShadeRuntimeProvider.js'; import { useShadeRuntimeTheme } from '../../ShadeRuntimeProvider.js';

View File

@@ -127,6 +127,10 @@ export GITEA_TOKEN="$TOKEN"
cd "$SHADE_DIR" cd "$SHADE_DIR"
echo echo
echo "Kjører bun run publish:all i $SHADE_DIR" echo "Type-check (strict TS) før publish ..."
echo "----------------------------------------" echo "----------------------------------------"
bun run publish:all bun run scripts/typecheck-all.ts
echo
echo "Kjører scripts/publish-all.ts i $SHADE_DIR"
echo "----------------------------------------"
bun run scripts/publish-all.ts

108
scripts/typecheck-all.ts Normal file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bun
/**
* Pre-publish gate — type-check every workspace package against the
* monorepo's strict tsconfig.
*
* Required before any publish. The Bun test runner is intentionally
* permissive (it transpiles, doesn't type-check), so without this gate
* a package can pass `bun test` and still ship code that fails to
* compile in a downstream consumer's strict TS project.
*
* Usage:
* bun run scripts/typecheck-all.ts # check every package
* bun run scripts/typecheck-all.ts core sdk # check only listed
*
* Exit code 0 if every package compiles, 1 otherwise.
*/
import { readdirSync, statSync, existsSync } from 'fs';
import { join } from 'path';
import { $ } from 'bun';
const ROOT = join(import.meta.dir, '..');
const PACKAGES_DIR = join(ROOT, 'packages');
const filter = new Set(process.argv.slice(2));
const packages = readdirSync(PACKAGES_DIR).filter((name) => {
const p = join(PACKAGES_DIR, name);
if (!statSync(p).isDirectory()) return false;
if (!existsSync(join(p, 'tsconfig.json'))) return false;
if (filter.size > 0 && !filter.has(name) && !filter.has(name.replace(/^shade-/, ''))) {
return false;
}
return true;
});
let failures = 0;
const failed: { pkg: string; out: string }[] = [];
for (const pkg of packages) {
const dir = join(PACKAGES_DIR, pkg);
const proc = Bun.spawnSync(['bunx', 'tsc', '--noEmit', '-p', 'tsconfig.json'], {
cwd: dir,
stdout: 'pipe',
stderr: 'pipe',
});
const stdout = proc.stdout.toString();
const stderr = proc.stderr.toString();
const out = (stdout + stderr)
.split('\n')
.filter((l) => !/^Resolving|^Resolved|^Saved/.test(l))
.join('\n')
.trim();
if (proc.exitCode === 0 && out.length === 0) {
console.log(`${pkg}`);
} else {
failures++;
failed.push({ pkg, out });
console.log(`${pkg}`);
}
}
console.log();
// Step 2 — consumer-strict smoke. Compiles a tiny "as if I were a
// downstream app" project against our public API surface under the
// consumer-likely tsconfig (`lib: ["DOM"]` + `exactOptionalPropertyTypes`).
// Catches type-bugs that ONLY surface when our internal narrower types
// meet a consumer's standard-library types — the class of bug `tsc`
// inside our own packages does not see (because our packages compile
// against `lib: ["ES2022"]` only).
if (filter.size === 0) {
console.log('Consumer-strict smoke (lib: DOM, exactOptional, paths→workspace) ...');
const consumerDir = join(ROOT, 'tests', 'consumer-strict');
if (existsSync(join(consumerDir, 'tsconfig.json'))) {
const proc = Bun.spawnSync(['bunx', 'tsc', '--noEmit', '-p', 'tsconfig.json'], {
cwd: consumerDir,
stdout: 'pipe',
stderr: 'pipe',
});
const out = (proc.stdout.toString() + proc.stderr.toString())
.split('\n')
.filter((l) => !/^Resolving|^Resolved|^Saved/.test(l))
.join('\n')
.trim();
if (proc.exitCode === 0 && out.length === 0) {
console.log(' ✓ consumer-strict');
} else {
failures++;
failed.push({ pkg: 'consumer-strict', out });
console.log(' ✗ consumer-strict');
}
}
console.log();
}
if (failures === 0) {
console.log(`All ${packages.length} packages type-check cleanly.`);
process.exit(0);
}
console.error(`${failures} of ${packages.length} packages failed:\n`);
for (const f of failed) {
console.error(`── ${f.pkg} ──`);
console.error(f.out);
console.error();
}
process.exit(1);

View File

@@ -0,0 +1,45 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["bun-types"],
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@shade/core": ["../../packages/shade-core/src/index.ts"],
"@shade/proto": ["../../packages/shade-proto/src/index.ts"],
"@shade/crypto-web": ["../../packages/shade-crypto-web/src/index.ts"],
"@shade/observability": ["../../packages/shade-observability/src/index.ts"],
"@shade/keychain": ["../../packages/shade-keychain/src/index.ts"],
"@shade/key-transparency": ["../../packages/shade-key-transparency/src/index.ts"],
"@shade/storage-sqlite": ["../../packages/shade-storage-sqlite/src/index.ts"],
"@shade/storage-postgres": ["../../packages/shade-storage-postgres/src/index.ts"],
"@shade/storage-encrypted": ["../../packages/shade-storage-encrypted/src/index.ts"],
"@shade/streams": ["../../packages/shade-streams/src/index.ts"],
"@shade/transport": ["../../packages/shade-transport/src/index.ts"],
"@shade/transport-bridge": ["../../packages/shade-transport-bridge/src/index.ts"],
"@shade/transport-webrtc": ["../../packages/shade-transport-webrtc/src/index.ts"],
"@shade/server": ["../../packages/shade-server/src/index.ts"],
"@shade/inbox-server": ["../../packages/shade-inbox-server/src/index.ts"],
"@shade/inbox": ["../../packages/shade-inbox/src/index.ts"],
"@shade/transfer": ["../../packages/shade-transfer/src/index.ts"],
"@shade/files": ["../../packages/shade-files/src/index.ts"],
"@shade/recovery": ["../../packages/shade-recovery/src/index.ts"],
"@shade/observer": ["../../packages/shade-observer/src/index.ts"],
"@shade/dashboard": ["../../packages/shade-dashboard/src/index.ts"],
"@shade/sdk": ["../../packages/shade-sdk/src/index.ts"],
"@shade/widgets": ["../../packages/shade-widgets/src/index.ts"]
}
},
"include": ["./*.ts"]
}

View File

@@ -0,0 +1,43 @@
/**
* Consumer-strict smoke for `@shade/files`.
*
* Compiled with `lib: ["ES2022", "DOM"]` + `exactOptionalPropertyTypes` +
* `skipLibCheck: false` to mimic a downstream consumer like Dispatch.
* Catches the class of bug where our internal narrower types (e.g. a
* locally-defined `MinimalReader`) reject native browser types
* (e.g. `ReadableStreamDefaultReader`) the consumer would naturally
* pass in.
*
* If this file fails to compile, the published packages will fail in
* any consumer's strict tsc — pre-publish gate must catch it.
*/
import { decideInline, type WriteSource } from '@shade/files';
declare const blob: Blob;
declare const stream: ReadableStream<Uint8Array>;
declare const bytes: Uint8Array;
async function smoke(): Promise<void> {
// Each branch of WriteSource must round-trip through decideInline()
// when given the natively-typed inputs a browser app would supply.
const sources: WriteSource[] = [
bytes,
blob,
stream,
{ stream, size: 1024 },
{ stream, size: 1024, contentType: 'image/png' },
];
for (const src of sources) {
const decision = await decideInline(src);
if (decision.kind === 'streams') {
const reader = decision.stream.getReader();
const { value, done } = await reader.read();
if (!done && value !== undefined) {
void value.byteLength;
}
reader.releaseLock();
}
}
}
void smoke;

View File

@@ -0,0 +1,42 @@
/**
* Consumer-strict smoke for `@shade/key-transparency`.
*
* The package was the source of the 4 noUnusedLocals + the IndexProofWire
* privacy bug in 4.0.0. This smoke imports every public type and
* exercises the witness-fetcher contract that the SDK plugs into.
*/
import {
LightWitness,
type SignedTreeHead,
type STHWire,
type WitnessFetcher,
} from '@shade/key-transparency';
declare const crypto: import('@shade/core').CryptoProvider;
declare const logPublicKey: Uint8Array;
async function smoke(): Promise<void> {
const fetcher: WitnessFetcher = {
async fetchLatestSTH(): Promise<STHWire> {
const res = await fetch('https://shade.example.com/v1/kt/sth');
return (await res.json()) as STHWire;
},
async fetchConsistencyProof(
from: number,
to: number,
): Promise<{ proof: string[] }> {
const res = await fetch(
`https://shade.example.com/v1/kt/consistency?from=${from}&to=${to}`,
);
return (await res.json()) as { proof: string[] };
},
};
const witness = new LightWitness({ crypto, logPublicKey, fetcher });
void witness;
}
void smoke;
// Verify the type is reachable with no `any` leak.
declare const sth: SignedTreeHead;
void sth.treeSize;

View File

@@ -0,0 +1,50 @@
/**
* Consumer-strict smoke for `@shade/sdk`.
*
* Exercises the high-level `createShade()` flow + the V3.x opt-in
* surfaces (KT, WebRTC, fingerprint gates). Compiles under DOM-lib +
* exactOptionalPropertyTypes to flag any private-type leaks like the
* `Promise<unknown>` on `fetchLatestSTH` that 4.0.0 shipped.
*/
import {
createShade,
type Shade,
type ShadeConfig,
type ShadeWebRtcConfig,
} from '@shade/sdk';
declare const factory: ShadeWebRtcConfig['factory'];
async function smoke(): Promise<void> {
const config: ShadeConfig = {
prekeyServer: 'https://shade.example.com',
storage: 'memory',
address: 'alice@example.com',
keyTransparency: {
mode: 'observe-strict',
logPublicKey: new Uint8Array(32),
},
};
const shade: Shade = await createShade(config);
const env = await shade.send('bob', 'hi');
void env;
shade.onMessage(async (from: string, plaintext: string) => {
void from;
void plaintext;
});
await shade.beforeFirstLargeFile(10 * 1024 * 1024, async (ctx) => {
void ctx.peerAddress;
void ctx.fingerprint;
void ctx.gate;
return true;
});
shade.configureWebRTC({ factory });
void (await shade.fingerprint);
}
void smoke;