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;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|