import { readFileSync } from 'node:fs';
import { privateKeyToAccount } from 'viem/accounts';
import { loadChambers } from '../src/load.ts';
import type { AirlockDecision, Chambers, SessionToken, Tier } from '../src/types.ts';
export const fx = JSON.parse(readFileSync(new URL('../fixtures/airlock-cases.json', import.meta.url), 'utf8'));
export const NOW = Date.parse(fx.now);
/** The well known public test accounts of local Ethereum dev chains (anvil accounts 0 to 2): never funded. */
const ANVIL_TEST_ACCOUNTS = [
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',
'0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a',
] as const;
export const owner = privateKeyToAccount(ANVIL_TEST_ACCOUNTS[fx.owner]);
export const other = privateKeyToAccount(ANVIL_TEST_ACCOUNTS[fx.other]);
export interface AirlockCase {
id: string; name: string; record?: string; outside?: number; intent?: string; laws?: string; min_tier?: Tier;
sign?: 'owner' | 'other' | 'garbage'; owner_address?: 'other'; at?: number; replay?: boolean; tamper?: Record<string, string>;
expect: { passed: boolean; refused_at?: number; tier?: Tier; steps?: number; flags?: boolean };
}
export interface SessionCase {
id: string; name: string; tier: Tier; check: number; check_expired?: number; mutate?: Record<string, string>;
job_id?: string; secret?: string; expect: { ok: boolean };
}
const intentText = (k: string): string => {
const v = fx.intents[k];
return typeof v === 'string' ? v : v.repeat.repeat(v.times);
};
let chambers: Chambers | undefined;
export const ch = async () => (chambers ??= await loadChambers());
export async function runCase(c: AirlockCase): Promise<{ d: AirlockDecision; second?: AirlockDecision }> {
const k = await ch();
// a fresh challenge per case, signed over the exact message chamber 1 gives
const challenge = k.createChallenge({ did: fx.did, listingId: fx.listing_id, now: NOW });
const msg = k.challengeMessage(challenge);
const signature = c.sign === 'garbage' ? '0xdeadbeef' : await (c.sign === 'other' ? other : owner).signMessage({ message: msg });
const sent = c.tamper ? { ...challenge, ...c.tamper } : challenge; // message left as signed: a tampered field
const input = {
did: fx.did, listingId: fx.listing_id,
ownerAddress: c.owner_address === 'other' ? other.address : owner.address,
challenge: sent, signature,
record: fx.records[c.record ?? 'proven'], outside: c.outside,
intent: intentText(c.intent ?? 'clean'), laws: fx.laws[c.laws ?? 'all'],
jobId: fx.job_id, minTier: c.min_tier,
};
const opts = { now: NOW + (c.at ?? 1000), secret: fx.secret };
const d = await k.runAirlock(input, opts);
if (!c.replay) return { d };
const second = await k.runAirlock(input, opts);
return { d, second };
}
export async function sessionToken(tier: Tier): Promise<SessionToken> {
return (await ch()).issueSessionToken({ did: fx.did, jobId: fx.job_id, tier, now: NOW, secret: fx.secret });
}
export function mutate(t: SessionToken, m?: Record<string, string>): SessionToken {
if (!m) return t;
const out: any = { ...t };
for (const [key, v] of Object.entries(m)) out[key] = key === 'mac' && v === 'flip' ? (t.mac[0] === 'a' ? 'b' : 'a') + t.mac.slice(1) : v;
return out;
}
agentgate-source: code mirror on Cloudflare Pages. All repos