Files
Sterister e6fdf31b49
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
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
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

105 lines
3.7 KiB
TypeScript

/**
* Test helpers — boot a local prekey server, mint Shade instances, and
* pair them with an in-process delivery transport that calls
* `shade.receive` on the recipient. Mirrors the pattern other Shade
* test suites use, kept private to this package so we don't pull in
* `@shade/server` at runtime.
*/
import { createPrekeyServer, MemoryPrekeyStore, PrekeyServerEvents } from '@shade/server';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { createShade, type Shade } from '@shade/sdk';
import type { ShadeEnvelope } from '@shade/core';
import type { RecoveryDeliver } from '../src/setup.js';
export interface TestEnv {
prekeyUrl: string;
stop: () => void;
}
export async function startTestPrekeyServer(): Promise<TestEnv> {
const crypto = new SubtleCryptoProvider();
const prekey = createPrekeyServer({
crypto,
store: new MemoryPrekeyStore(),
disableRateLimit: true,
events: new PrekeyServerEvents(),
});
const server = Bun.serve({ port: 0, fetch: prekey.fetch });
const port = (server as unknown as { port: number }).port;
return {
prekeyUrl: `http://localhost:${port}`,
stop: () => server.stop(),
};
}
export async function spawnShade(prekeyUrl: string, address: string): Promise<Shade> {
return createShade({ prekeyServer: prekeyUrl, address });
}
/**
* In-process transport that delivers `(to, envelope)` to the named
* Shade by calling `shade.receive(from, envelope)` on it. Matches the
* `RecoveryDeliver` shape so it can be plugged into setup/guardian/
* request flows.
*
* Construction order matters: register every party before delivering
* so the lookup never fails. Use `addr` to keep the from-address
* symmetric with what `Shade.send` uses.
*/
export class MemoryRecoveryTransport {
private readonly directory = new Map<string, Shade>();
/** Per-pair pending-deliveries chain to preserve ordering. */
private readonly chains = new Map<string, Promise<unknown>>();
/** Counters of how many envelopes flowed in each direction (for tests). */
public readonly delivered: Array<{ from: string; to: string }> = [];
/**
* Optional drop-after-N policy used to simulate guardians that go
* unreachable. Keyed on `to` address. Set with `dropAfter(addr, n)`.
*/
private readonly dropPolicies = new Map<string, number>();
add(shade: Shade): void {
this.directory.set(shade.myAddress, shade);
}
/** Drop envelopes addressed to `to` after the first `n` have flowed. */
dropAfter(to: string, n: number): void {
this.dropPolicies.set(to, n);
}
/**
* Build a `RecoveryDeliver` callback bound to `from`. Call this once
* per Shade so its outbound sends route through the transport.
*/
bind(from: Shade): RecoveryDeliver {
return async (to: string, envelope: ShadeEnvelope) => {
const recipient = this.directory.get(to);
if (recipient === undefined) {
throw new Error(`MemoryRecoveryTransport: unknown recipient "${to}"`);
}
// Apply drop policy.
const policy = this.dropPolicies.get(to);
if (policy !== undefined) {
if (policy <= 0) throw new Error(`MemoryRecoveryTransport: dropping for "${to}"`);
this.dropPolicies.set(to, policy - 1);
}
this.delivered.push({ from: from.myAddress, to });
// Serialize per (from→to) pair to preserve ordering.
const key = `${from.myAddress}${to}`;
const prev = this.chains.get(key) ?? Promise.resolve();
const next = prev.then(async () => {
await recipient.receive(from.myAddress, envelope);
});
this.chains.set(
key,
next.catch(() => {
// chain even on failures so subsequent sends don't deadlock
return undefined;
}),
);
await next;
};
}
}