Commit Graph

12 Commits

Author SHA1 Message Date
84d3166ca1 feat(vault): server-side kryptert fillager (V4.13)
Shade kunne flytte filer mellom peers (@shade/files) og lagre én liten
profil-blob per konto, men hadde ingen alltid-på lagring av krypterte filer.
Uten den kan ingen Shade-app tilby backup, og ingen klient lese data mens
peeren som eier dem er avslått.

Objekter er innholdsadresserte på hashen av CHIFFERTEKSTEN, så relayen kan
lagre, deduplisere og verifisere uten nøkkel — den regner om hashen ved
opplasting og avviser feilnavngitte objekter. Stier bor inne i det krypterte
manifestet, aldri i objektnavn: relayen skal ikke lære hva filene heter.
Loggen er append-only, så historikk og rollback følger av modellen.

SqliteVaultStore har med vilje INGEN minne-fallback, i motsetning til
blob-storen. Den fallbacken slettet Prisms profil ved en rutine-redeploy
2026-08-12 fordi den fungerte helt til containeren ble recreated, uten en
eneste feilmelding. En backup som glemmer er verre enn ingen backup, så uten
SHADE_VAULT_DB_PATH mountes rutene ikke — med en logglinje som sier hvorfor.

Én feil fanget av testene: pubkeyen ble først lagt på UTENFOR signaturen,
som både brøt verifyPayload og ville latt hvem som helst bytte identitet i
transit på den TOFU-pinnende førsteskrivingen.

16 vault- + 7 store-tester, alle mot de ekte rutehåndtererne gjennom Honos
fetch. Kjeden er dessuten kjørt mot en ekte HTTP-server med et ekte
workspace: 491 filer / 7,7 MB, alle bit-identiske etter gjenoppretting,
og andre push etter én endring sendte 0 KB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 11:58:37 +02:00
306bf08452 chore: adopter Scaffold plan-kontrakt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:04:48 +02:00
037f994572 release(v4.11.0): streaming Double-Ratchet sub-sessions (ShadeStream)
Some checks failed
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Answers Vyvern FR shade-ws-streaming-ratchet.md with a first-class
streaming-session API rather than the documented-contract fallback.
The Double-Ratchet crypto was already safe for high-frequency
one-directional use; the send/receive wrapper was not (per-frame
saveSession keystore write; shared per-peer mutex + single stored
session row coupling reuse to the HTTP path).

- @shade/core: stream.ts — identity-bound 3-DH seeding (X3DH-minus-
  prekeys, no prekey-server round trip, mutually authenticated against
  the parent session's pinned identities), bootstrapStreamSession
  reusing init{Sender,Receiver}Session verbatim, in-memory-only
  StreamRatchet (own op-mutex, never persisted, zeroized on close).
  beginStream/acceptStream on ShadeSessionManager; Stream{Closed,
  Handshake}Error; stream.opened/closed events.
- @shade/proto: STREAM_OPEN/OPEN_ACK/FRAME wire (0x31/0x32/0x33),
  additive; inspectEnvelopeType extended.
- @shade/sdk: Shade.openStream/acceptStream → ShadeStream
  (handshakeFrame/handleHandshake/seal/open/close), transport-
  agnostic, independent of encrypt/decrypt queues + parent session,
  identical server (sqlite:) and browser (IndexedDB) — touches no
  storage.
- Tests: 5000-frame one-directional burst (bounded skipped keys + FS
  zeroize), parent-session independence, replay/rewind rejection,
  mutual-auth, proto wire round-trips. Full suite green (1159 pass).
- docs/streaming-sessions.md (R1–R7 contract); SECURITY.md matrix rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:29:09 +02:00
680d6386f3 release(v4.8.1): SHADE_DISABLE_RATE_LIMIT env var for single-tenant deploys
Plumbing fix only — both createPrekeyRoutes and createInboxRoutes
already accepted disableRateLimit; standalone.ts just didn't read
the env. Now SHADE_DISABLE_RATE_LIMIT=1 turns off IP rate-limits on
every prekey + inbox route, with a WARN log on startup so operators
see it.

Single-tenant deployments only — multi-tenant relays must leave it
unset. Documented in docs/DEPLOYMENT.md.

Reported by Prism: ~6 pair attempts/hour from a single dev IP +
the sidecar's register call tripped the 5/hour REGISTER_LIMIT every
dev iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:55:57 +02:00
dbb3a090d8 release(v4.4.0): public accessor for device identity public key
Expose the local device's 32-byte Ed25519 identity public key on Shade
so apps can hand it to their own backend at enrollment time for
signature verification, key pinning or per-device safety-number
computation. Closes the gap that forced consumers to ship placeholder
random bytes their backend could store but never verify against.

- @shade/sdk Shade.identityPublicKey: Promise<Uint8Array> — getter
  mirrors the existing fingerprint accessor. Throws pre-init,
  reflects the current key after rotate(), retired key preserved in
  retired-identities storage per existing grace-period contract.
  Private key remains unreachable.
- Test in shade-sdk/tests/sdk.test.ts: round-trip match against the
  underlying storage's signingPublicKey, plus value updates after
  rotate().
- Lockstep version bump 4.3.0 → 4.4.0 across all 25 packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:58:45 +02:00
f5f42fe557 release(v4.3.0): browser persistence via @shade/storage-indexeddb
Ship an official IndexedDB-backed StorageProvider so browser-based Shade
consumers persist identity, prekeys, sessions, retired identities,
peer-verification state and stream-resume rows across tab refresh and
browser restart. Closes the gap that forced browser apps onto
storage:"memory" (regenerated identity each load, orphaned device
records server-side).

- New package @shade/storage-indexeddb (4.3.0): full StorageProvider
  conformance, schema v1, idb-backed; bumpPeerIdentityVersion is wrapped
  in a single readwrite IDB transaction (atomic, vs SQLite's
  read-then-upsert race).
- @shade/sdk resolveStorage() accepts { type: 'indexeddb', dbName? } via
  dynamic import (lazy, optional dep — same pattern as
  @shade/storage-postgres). Named StorageSpec type now reused by
  ResolvedConfig.
- Tests: 16 new tests in shade-storage-indexeddb (StorageProvider
  surface + peer-verifications + full E2EE conversation surviving a
  simulated tab reload). Run on fake-indexeddb.
- Lockstep version bump 4.2.1 → 4.3.0 across all 25 packages.
- Publish scripts updated to include the new package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:35:02 +02:00
da93b97cce release(v4.1.0): browser-friendly HTTP RPC for @shade/files
Some checks failed
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Default shade.files.client(peer) requires both peers to be mutually
addressable over HTTP — the response round-trips through
Shade.deliverControlEnvelope (POST to peer's /v1/transfer/control).
Browser tabs can't host an HTTP server, so they couldn't consume
@shade/files at all. Dispatch's filutforsker (admin-panel browser UI)
is the canonical use-case.

This release adds a parallel request-response transport: one POST per
RPC, encrypted envelope in the body, encrypted response in the same
HTTP response. No inbound channel needed on the client.

### New API

- shade.files.rpcRoute(opts?) — Hono app exposing POST /rpc.
- shade.files.httpClient(peer, opts) — request-response FileClient.
- FilesNamespace.serve(handler, { inlineOnly: true }) — skip streams-
  bridge (and its configureTransfers pre-condition); also skip
  channel-based dispatch so requests aren't double-dispatched.

### Limitations (v1)

Inline only (≤ 256 KiB). Streamed reads/writes throw clear errors
directing to shade.files.client(peer) on a server-to-server deploy.

### Tests

7 integration tests in tests/integration/http-rpc.test.ts covering
round-trip + negative cases (sender header, empty/garbage body,
maxBodyBytes, rpcRoute-without-serve).

### Symmetry

Mirrors @shade/server's shade-auth-middleware: encrypted envelope in
request body, decrypted via existing ratchet, response in same HTTP
roundtrip. No WebSocket, no SSE, no outbound from server.

Wire-compatible. Source-compatible. Lockstep bump to 4.1.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:08:14 +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
e6fdf31b49 release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.

Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
  Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
  bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
  shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
  non-realtime stack.

Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
  version 4.0.0 on /health.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
fa770d3063 feat(files): @shade/files 0.3.0 — E2EE filesystem RPC primitive
Some checks failed
Test / test (push) Has been cancelled
M-Files-1..6 land the full files-RPC layer + everything 0.3.0 needs to
ship. Apps keep their own UI; this layer ships the typed RPC, the
streams bridge for content I/O, and production hooks (rate limit,
retention, fingerprint gate, metrics).

@shade/files (NEW)
- Standard ops: list/stat/mkdir/delete/move/read/write/getThumbnail with
  Zod-validated wire schemas + clean user-handler types.
- Custom ops: typed via TypeScript declaration merging on CustomOpsMap
  + per-op Zod schemas; client.custom('app.foo', {...}) is fully typed.
- Content I/O: inline (≤ 256 KiB plaintext) base64-in-RPC; streams
  (> 256 KiB) ride @shade/transfer via userMetadata.shadeFilesWriteId
  / shadeFilesReadStreamId correlation. Server-side TransformStream
  bridges accept inbound transfers immediately (engine rejects chunks
  that arrive before accept) and park the readable for the matching
  RPC.
- Directory ops: walk(path, opts) async-iterable depth-first walker;
  uploadDirectory()/downloadDirectory() with bounded concurrency pool
  (default 4, cap 16), aggregated progress, abort.
- Production hooks (callback-based, vendor-neutral): rate-limit (op +
  byte), idempotency cache (LRU + TTL + in-flight de-dupe), path
  policy (traversal + percent-decode hardening), fingerprint gate
  (required/optional/reject), pluggable Ed25519 sig verification with
  ±5 min replay window, onMetric sink (standard names).
- React hooks (subpath @shade/files/react): ShadeFilesProvider,
  useShadeFiles, useFileList, useFileTransfer/Upload/Download.
- Shade.files.serve(handler) + Shade.files.client(peer) high-level
  entrypoint in @shade/sdk; lazy + memoized; one handler per Shade.

Wire format bump
- @shade/proto wire VERSION 0x01 → 0x02. Length prefixes changed from
  u16 to u32. The previous u16 silently truncated payloads above
  64 KiB — a hard correctness ceiling that blocked inline file ops
  up to 256 KiB. Wire-incompatible with 0.2.x peers; new sessions
  only. Cross-platform Kotlin port (android/shade-android) updated to
  match; test-vectors/wire-format.json regenerated.

Concurrency safety
- ShadeSessionManager.encrypt/.decrypt now run under per-peer mutex.
  Concurrent decryptions of the same peer raced ratchet state
  (manifested as sporadic "Failed to decrypt — wrong key or tampered
  data" under load — surfaced once concurrent uploadDirectory pumped
  many writes in flight). Encrypt was already serialized via
  Shade.send's encryptChains; decrypt is now serialized at the
  manager layer too.

@shade/streams extension
- StreamMetadata.userMetadata?: Record<string, string> for
  application-level key/value pairs that round-trip verbatim through
  stream-init plaintext. Used by @shade/files for write/read
  correlation; available to any consumer.

@shade/sdk extension
- Shade.files getter (lazy + memoized).
- BackgroundHooks.onPruneFiles + periodic timer (default 5 min) +
  BackgroundTasks.setHook(name, fn) for runtime hook registration.

Bundles in-flight 0.2.0 work
- packages/shade-streams/, packages/shade-transfer/, related
  shade-sdk streams-bridge + shade-widgets transfer hooks were
  uncommitted prior to this session. Including them keeps the
  workspace consistent at 0.3.0 since @shade/files depends on them.

Tests
- 74 new tests in @shade/files (572 → 646 workspace pass; 0 fail;
  3× stable). Coverage spans unit (inline-threshold + concurrency),
  integration (read-write inline + streams up to 1 MiB, walk +
  upload/download directory, custom-op, metrics, SDK namespace
  end-to-end), and security (tampered-envelope sig verification,
  replay window, fingerprint gate, rate-limit + quota).

Release artifacts
- All packages bumped to 0.3.0 via scripts/bump-version.ts.
- scripts/publish-all.ts PACKAGES updated with shade-files in
  topological order (after shade-transfer, before shade-sdk).
- bun run publish:dry clean (14 packed, 0 failed).
- examples/08-files-browser/ — three-process CLI demo (prekey + Bob
  server + Alice CLI) covering list/stat/mkdir/delete/upload/download.
- docs/files.md — full API + design doc.
- CHANGELOG.md 0.3.0 entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:00:01 +02:00
7e0f7320a9 feat(container): M-Box 1-8 — stack-agnostic standalone Docker container
Some checks failed
Test / test (push) Has been cancelled
Shade now ships as a self-contained Docker image. Deploy one container
per project, any stack (Bun, Python, Go, Rust, Kotlin) can talk to it via
plain HTTP. Zero coupling to consumer codebases.

M-Box 1: Stale identity cleanup API
- touchIdentity + purgeStaleIdentities on PrekeyStore interface
- Implemented for Memory, SQLite, and Postgres backends
- SQLite adds last_activity_at column with migration ALTER for existing DBs
- Postgres adds the same via raw SQL with IF NOT EXISTS guards
- Routes call touchIdentity on register, bundle fetch, replenish
- 4 new tests for the cleanup API

M-Box 2: Stale cleanup background task
- StaleCleanupTask runs purge on startup + every 24h (configurable)
- Reads SHADE_STALE_DAYS (default 30) and SHADE_CLEANUP_INTERVAL_HOURS
- Wired into standalone.ts, stopped on graceful shutdown
- 5 new tests for the task

M-Box 3: Observer baked into the container
- standalone.ts conditionally mounts @shade/observer at /shade-observer
  when SHADE_OBSERVER_TOKEN is set (and >= 16 chars)
- Shared PrekeyServerEvents emitter feeds both routes and observer
- @shade/observer added as optional dependency of @shade/server

M-Box 4: Dockerfile with dashboard build
- Multi-stage build: oven/bun:1 builder → oven/bun:1-alpine runtime
- COPY packages/ wholesale so workspace lockfile resolves cleanly
- RUN bun run build inside shade-dashboard → dist/ → observer/dist/
- Non-root shade user, /data volume, healthcheck, env defaults
- Final image: 260 MB

M-Box 5: OpenAPI spec for stack-agnostic clients
- packages/shade-server/openapi.yaml documents all 9 endpoints with
  request/response schemas, security (Ed25519 signatures + bearer token)
- createOpenApiRoutes serves /openapi.yaml and /docs (Redoc viewer)
- Any language can generate a client with openapi-generator

M-Box 6: Docker CI pipeline
- .gitea/workflows/docker.yml builds + pushes on git tag v*
- scripts/build-docker.ts for local builds, supports --push with GITEA_TOKEN
- Root package.json: build:docker, publish:docker scripts

M-Box 7: Deployment documentation
- packages/shade-server/README rewritten: 5-line quickstart with the image
- docs/DEPLOYMENT.md: full reference, env vars, backup, Dokploy, PG setup
- examples/05-dokploy-deployment/docker-compose.yml updated to pull
  published image (gt.zyon.no/stian/shade-prekey:latest)
- Root README deployment section rewritten

M-Box 8: End-to-end verification
- Image builds locally (bun run build:docker)
- /health, /openapi.yaml, /docs, /metrics, /shade-observer all respond
- 401 without observer token, 200 with
- Real SDK client round-trip: Alice → container → Bob → reply → Alice
- Persistence: identity + prekeys survive container restart (count 20→18
  as expected from two bundle fetches)

285 tests passing, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:29:00 +02:00
7d214dc614 feat: persistent storage — SQLite backends for crash resilience
Shade sessions and keys now survive server crashes, container restarts,
and power outages via SQLite with WAL mode.

New packages:
- @shade/storage-sqlite: SQLiteStorage (StorageProvider) + SqlitePrekeyStore
  (PrekeyStore), both using bun:sqlite with auto-created tables and WAL mode
- Serialization layer in shade-core for SessionState/keys ↔ JSON/base64

Docker usage: mount volume at /data, set SHADE_DB_PATH=/data/shade-client.db
Prekey server auto-detects SHADE_PREKEY_DB_PATH for SQLite persistence

Includes crash recovery integration test: encrypt → close DB → reopen →
conversation continues seamlessly.

129 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:19:54 +02:00