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>
105 lines
3.7 KiB
TypeScript
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;
|
|
};
|
|
}
|
|
}
|