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>
This commit is contained in:
2026-05-03 18:35:35 +02:00
parent 8b055912b7
commit e6fdf31b49
298 changed files with 37909 additions and 256 deletions

View File

@@ -0,0 +1,91 @@
/**
* Guardian-side persistence for received shares.
*
* Each guardian holds, per protected user, exactly one record per
* `setupId`:
*
* {
* originalAddress, // the protected user's Shade address
* setupId, // disambiguates if a user re-runs setup
* shareIndex, // their assigned x-coordinate
* shareBytes, // base64-encoded Shamir share
* shareSecretCiphertext, // base64 — encrypted backup payload
* shareSecretNonce, // base64 — AES-GCM nonce
* setupFingerprint, // safety number at deposit time
* guardianCount, threshold, // informational
* receivedAt
* }
*
* The interface is deliberately minimal so consumers can back it with
* whatever they already use for app state — IndexedDB, AsyncStorage,
* SQLite, etc. The package ships an in-memory implementation suitable
* for tests and small one-device demos. Guardian apps that survive
* restart MUST supply a persistent implementation.
*/
export interface GuardianShareEntry {
originalAddress: string;
setupId: string;
shareIndex: number;
shareBytes: string;
shareSecretCiphertext: string;
shareSecretNonce: string;
setupFingerprint: string;
guardianCount: number;
threshold: number;
receivedAt: number;
}
/**
* The minimal contract a guardian-side share store must implement.
*
* Shapes:
* - `save` is upsert by (originalAddress, setupId). If a user re-runs
* setup with a different setupId, both entries persist; the guardian
* can choose which to release based on the recovery-request's
* setupId. If the same (originalAddress, setupId) is saved twice,
* the second write wins (idempotent re-deposit).
* - `get` returns the deposit for the (originalAddress, setupId) pair,
* or `null` when none exists.
* - `list` enumerates everything (used by guardian-UX widgets).
* - `delete` removes a single deposit; the guardian-UX exposes this so
* a user can prune deposits from people they no longer wish to
* protect.
*/
export interface RecoveryStore {
save(entry: GuardianShareEntry): Promise<void>;
get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null>;
list(): Promise<GuardianShareEntry[]>;
delete(originalAddress: string, setupId: string): Promise<void>;
}
/**
* Process-local in-memory store. Suitable for tests and one-shot demos;
* LOSES STATE ON RESTART. Production guardian apps must supply a
* persistent `RecoveryStore` (see `docs/recovery.md` for backing-store
* recommendations).
*/
export class MemoryRecoveryStore implements RecoveryStore {
private readonly entries = new Map<string, GuardianShareEntry>();
async save(entry: GuardianShareEntry): Promise<void> {
this.entries.set(keyOf(entry.originalAddress, entry.setupId), { ...entry });
}
async get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null> {
const found = this.entries.get(keyOf(originalAddress, setupId));
return found === undefined ? null : { ...found };
}
async list(): Promise<GuardianShareEntry[]> {
return Array.from(this.entries.values()).map((e) => ({ ...e }));
}
async delete(originalAddress: string, setupId: string): Promise<void> {
this.entries.delete(keyOf(originalAddress, setupId));
}
}
function keyOf(originalAddress: string, setupId: string): string {
return `${originalAddress} ${setupId}`;
}