mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 15:01:16 +00:00
* feat(channels): bundle Reef guarded claw-to-claw channel Moves the Reef channel extension from openclaw/reef into the bundled extensions tree with the wire protocol vendored under extensions/reef/protocol. Includes channelConfigs manifest metadata, runtime status reporting via setStatus/buildAccountSnapshot, abort-aware inbox shutdown, monotonic device-auth timestamps, and immutable-model guard admission (dated snapshots plus documented gpt-5.6 ids). * fix(reef): pin zod exactly per dependency pin guard * style(reef): satisfy extension lint gates Curly braces in vendored protocol, explicit type re-exports, typed catch callbacks, Object.assign over map-spread, abort-aware loop restructure. * fix(reef): CI gates — knip workspace, boundary tsconfig, facade imports, cycle break, contract baselines - knip: reef workspace project includes vendored protocol/ (owns noble deps) - tsconfig: conform to canonical extension package-boundary include/exclude; add @openclaw/plugin-sdk devDependency - imports: channel-inbound/channel-outbound facades instead of deprecated subpaths - protocol: home ReplayStore contract in envelope.ts to break the envelope<->replay type cycle - baselines: reef in unguarded runtime-api list; regenerate bundled channel config metadata * feat(reef): plugin catalog cover art, channel label, changelog revert * fix(reef): drop unused error-class exports, register lint suppression * fix(reef): knip entry surface for vendored protocol, explicit WebSocketLike export
661 lines
21 KiB
TypeScript
661 lines
21 KiB
TypeScript
import { gcm } from "@noble/ciphers/aes.js";
|
|
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
import { describe, expect, it } from "vitest";
|
|
import { MemoryAuditStore, type AuditEntry, type AuditStore } from "./audit.js";
|
|
import { canonicalBytes } from "./canonical.js";
|
|
import { base64, fromBase64url, utf8 } from "./encoding.js";
|
|
import { seal, type Envelope } from "./envelope.js";
|
|
import type { GuardAdapter, Verdict } from "./guard.js";
|
|
import { generateIdentity } from "./identity.js";
|
|
import { composeInbound, composeOutbound, PipelineError } from "./pipeline.js";
|
|
import { MemoryReplayStore } from "./replay.js";
|
|
|
|
const now = 1_752_300_000;
|
|
const auditKey = Uint8Array.from({ length: 32 }, (_, index) => index + 1);
|
|
const allow: Verdict = {
|
|
decision: "allow",
|
|
category: "safe",
|
|
reason: "Safe.",
|
|
model: "mock-2026-07-12",
|
|
policyVersion: "v1",
|
|
};
|
|
|
|
function audit(): MemoryAuditStore {
|
|
return new MemoryAuditStore(auditKey);
|
|
}
|
|
|
|
function mockGuard(...verdicts: Verdict[]): GuardAdapter & { calls: number } {
|
|
return {
|
|
providerId: "mock",
|
|
pinnedModel: "mock-2026-07-12",
|
|
calls: 0,
|
|
async classify() {
|
|
return verdicts[this.calls++] ?? verdicts.at(-1)!;
|
|
},
|
|
};
|
|
}
|
|
|
|
function structuralGuard(
|
|
pinnedModel: string,
|
|
...results: unknown[]
|
|
): GuardAdapter & { calls: number } {
|
|
return {
|
|
providerId: "structural",
|
|
pinnedModel,
|
|
calls: 0,
|
|
async classify() {
|
|
return (results[this.calls++] ?? results.at(-1)) as Verdict;
|
|
},
|
|
};
|
|
}
|
|
|
|
function identities() {
|
|
return { alice: generateIdentity(), bob: generateIdentity() };
|
|
}
|
|
|
|
class FailOnceAuditStore implements AuditStore {
|
|
readonly inner = audit();
|
|
#fail = true;
|
|
|
|
async appendEvent(type: string, payload: unknown, ts?: number): Promise<AuditEntry> {
|
|
if (this.#fail) {
|
|
this.#fail = false;
|
|
throw new Error("transient audit failure");
|
|
}
|
|
return this.inner.appendEvent(type, payload, ts);
|
|
}
|
|
|
|
entries(): Promise<AuditEntry[]> {
|
|
return this.inner.entries();
|
|
}
|
|
}
|
|
|
|
describe("pipeline", () => {
|
|
it("runs an allowed outbound and inbound exchange end to end", async () => {
|
|
const { alice, bob } = identities();
|
|
const outboundAudit = audit();
|
|
const outbound = await composeOutbound({
|
|
id: "01JZ0000000000000000000000",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "hello" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
guard: mockGuard(allow),
|
|
audit: outboundAudit,
|
|
policyVersion: "v1",
|
|
});
|
|
const inboundAudit = audit();
|
|
const inboundGuard = mockGuard(allow);
|
|
const replayStore = new MemoryReplayStore();
|
|
const inboundOptions = {
|
|
envelope: outbound.envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore,
|
|
now,
|
|
guard: inboundGuard,
|
|
audit: inboundAudit,
|
|
policyVersion: "v1",
|
|
};
|
|
const inbound = await composeInbound(inboundOptions);
|
|
expect(inbound.disposition).toBe("accepted");
|
|
if (inbound.disposition !== "accepted") {
|
|
throw new Error("expected accepted result");
|
|
}
|
|
expect(inbound.body.text).toBe("hello");
|
|
expect(inbound.receipt).toMatchObject({ id: outbound.envelope.id, status: "accepted" });
|
|
const outboundEntries = await outboundAudit.entries();
|
|
const inboundEntries = await inboundAudit.entries();
|
|
expect(outboundEntries.map((entry) => entry.event.type)).toEqual([
|
|
"proposal",
|
|
"guard_verdict",
|
|
"envelope",
|
|
]);
|
|
expect(inboundEntries.map((entry) => entry.event.type)).toEqual([
|
|
"guard_verdict",
|
|
"inbox",
|
|
"receipt",
|
|
]);
|
|
expect(
|
|
new Set(
|
|
outboundEntries.map(
|
|
(entry) => (entry.event.payload as { approvalDigest: string }).approvalDigest,
|
|
),
|
|
).size,
|
|
).toBe(1);
|
|
expect(
|
|
new Set(
|
|
inboundEntries.map(
|
|
(entry) => (entry.event.payload as { approvalDigest: string }).approvalDigest,
|
|
),
|
|
).size,
|
|
).toBe(1);
|
|
const duplicate = await composeInbound({ ...inboundOptions, now: now + 10 * 60 });
|
|
expect(duplicate).toEqual({
|
|
disposition: "duplicate",
|
|
body: inbound.body,
|
|
receipt: inbound.receipt,
|
|
});
|
|
expect(inboundGuard.calls).toBe(1);
|
|
expect(await inboundAudit.entries()).toHaveLength(inboundEntries.length);
|
|
});
|
|
|
|
it("rejects a decrypted free-form thread before guard admission", async () => {
|
|
const { alice, bob } = identities();
|
|
const guard = mockGuard(allow);
|
|
const envelope = craftEnvelope({ text: "hello", thread: "free-form thread" }, alice, bob);
|
|
await expect(
|
|
composeInbound({
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore: new MemoryReplayStore(),
|
|
now,
|
|
guard,
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
}),
|
|
).rejects.toMatchObject({ code: "malformed" });
|
|
expect(guard.calls).toBe(0);
|
|
});
|
|
|
|
it("stops a deterministic outbound denial before guard and sealing", async () => {
|
|
const { alice, bob } = identities();
|
|
const guard = mockGuard(allow);
|
|
let rngCalls = 0;
|
|
await expect(
|
|
composeOutbound({
|
|
id: "01JZ0000000000000000000000",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: ["sk-", "abcdefghijklmnopqrstuvwxyz123456"].join("") },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
guard,
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
rng(length) {
|
|
rngCalls++;
|
|
return new Uint8Array(length);
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({ stage: "deterministic" });
|
|
expect(guard.calls).toBe(0);
|
|
expect(rngCalls).toBe(0);
|
|
});
|
|
|
|
it("stops an outbound guard denial before sealing", async () => {
|
|
const { alice, bob } = identities();
|
|
const deny: Verdict = {
|
|
...allow,
|
|
decision: "deny",
|
|
category: "confidential",
|
|
reason: "Denied.",
|
|
};
|
|
let rngCalls = 0;
|
|
await expect(
|
|
composeOutbound({
|
|
id: "01JZ0000000000000000000000",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "ordinary text" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
guard: mockGuard(deny),
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
rng(length) {
|
|
rngCalls++;
|
|
return new Uint8Array(length);
|
|
},
|
|
}),
|
|
).rejects.toBeInstanceOf(PipelineError);
|
|
expect(rngCalls).toBe(0);
|
|
});
|
|
|
|
it("centrally rejects invalid verdicts from structural guard adapters", async () => {
|
|
const { alice, bob } = identities();
|
|
const cases: Array<[string, string, unknown]> = [
|
|
["wrong model", "mock-2026-07-12", { ...allow, model: "other-2026-07-12" }],
|
|
["floating model", "mock-latest", { ...allow, model: "mock-latest" }],
|
|
["wrong policy", "mock-2026-07-12", { ...allow, policyVersion: "v2" }],
|
|
["unknown decision", "mock-2026-07-12", { ...allow, decision: "maybe" }],
|
|
];
|
|
for (const [name, pinnedModel, rawVerdict] of cases) {
|
|
let rngCalls = 0;
|
|
await expect(
|
|
composeOutbound({
|
|
id: "01JZ0000000000000000000008",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: `case ${name}` },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
guard: structuralGuard(pinnedModel, rawVerdict),
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
rng(length) {
|
|
rngCalls++;
|
|
return new Uint8Array(length);
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({
|
|
stage: "guard",
|
|
verdict: { decision: "deny", category: "guard_failure" },
|
|
});
|
|
expect(rngCalls).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("accepts a valid structural adapter and admits the post-review verdict again", async () => {
|
|
const { alice, bob } = identities();
|
|
const common = {
|
|
id: "01JZ0000000000000000000009",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "structural adapter" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
};
|
|
await expect(
|
|
composeOutbound({ ...common, guard: structuralGuard(allow.model, allow) }),
|
|
).resolves.toMatchObject({ verdict: allow });
|
|
const review: Verdict = {
|
|
...allow,
|
|
decision: "review",
|
|
category: "ambiguous",
|
|
reason: "Review.",
|
|
};
|
|
const invalidAfterApproval = { ...allow, model: "wrong-2026-07-12" };
|
|
await expect(
|
|
composeOutbound({
|
|
...common,
|
|
audit: audit(),
|
|
guard: structuralGuard(allow.model, review, invalidAfterApproval),
|
|
reviewGate: async ({ approvalDigest }) => ({ approved: true, approvalDigest }),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
stage: "guard",
|
|
verdict: { decision: "deny", category: "guard_failure" },
|
|
});
|
|
});
|
|
|
|
it("rejects an invalid structural inbound verdict instead of accepting it", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000010",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "inbound structural adapter" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
await expect(
|
|
composeInbound({
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore: new MemoryReplayStore(),
|
|
now,
|
|
guard: structuralGuard(allow.model, { ...allow, policyVersion: "wrong" }),
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
stage: "guard",
|
|
verdict: { decision: "deny", category: "guard_failure" },
|
|
receipt: { status: "rejected", category: "guard_deny" },
|
|
});
|
|
});
|
|
|
|
it("requires exact full-proposal approval and fresh classification for review", async () => {
|
|
const { alice, bob } = identities();
|
|
const review: Verdict = {
|
|
...allow,
|
|
decision: "review",
|
|
category: "ambiguous",
|
|
reason: "Review.",
|
|
};
|
|
const common = {
|
|
id: "01JZ0000000000000000000000",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "ordinary text" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
policyVersion: "v1",
|
|
};
|
|
await expect(
|
|
composeOutbound({ ...common, audit: audit(), guard: mockGuard(review) }),
|
|
).rejects.toMatchObject({ stage: "review" });
|
|
await expect(
|
|
composeOutbound({
|
|
...common,
|
|
audit: audit(),
|
|
guard: mockGuard(review),
|
|
reviewGate: async ({ approvalDigest }) => ({ approved: false, approvalDigest }),
|
|
}),
|
|
).rejects.toMatchObject({ stage: "review", reviewOutcome: "denied", receipt: undefined });
|
|
await expect(
|
|
composeOutbound({
|
|
...common,
|
|
audit: audit(),
|
|
guard: mockGuard(review),
|
|
reviewGate: async () => ({ approved: true, approvalDigest: "wrong" }),
|
|
}),
|
|
).rejects.toMatchObject({ stage: "review" });
|
|
const guard = mockGuard(review, allow);
|
|
const result = await composeOutbound({
|
|
...common,
|
|
audit: audit(),
|
|
guard,
|
|
reviewGate: async ({ approvalDigest }) => ({ approved: true, approvalDigest }),
|
|
});
|
|
expect(result.verdict.decision).toBe("allow");
|
|
expect(guard.calls).toBe(2);
|
|
});
|
|
|
|
it("does not reuse an approval across recipients", async () => {
|
|
const { alice, bob } = identities();
|
|
const review: Verdict = {
|
|
...allow,
|
|
decision: "review",
|
|
category: "ambiguous",
|
|
reason: "Review.",
|
|
};
|
|
const common = {
|
|
id: "01JZ0000000000000000000007",
|
|
from: "sender#1",
|
|
body: { text: "identical body" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
policyVersion: "v1",
|
|
};
|
|
let vincentDigest = "";
|
|
await expect(
|
|
composeOutbound({
|
|
...common,
|
|
to: "vincent#1",
|
|
audit: audit(),
|
|
guard: mockGuard(review),
|
|
reviewGate: async (request) => {
|
|
vincentDigest = request.approvalDigest;
|
|
expect(request).toMatchObject({
|
|
id: common.id,
|
|
from: common.from,
|
|
to: "vincent#1",
|
|
direction: "outbound",
|
|
});
|
|
expect(request.bodyHash).toMatch(/^[0-9a-f]{64}$/);
|
|
return undefined;
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({ stage: "review", reviewOutcome: "pending" });
|
|
await expect(
|
|
composeOutbound({
|
|
...common,
|
|
to: "alice#1",
|
|
audit: audit(),
|
|
guard: mockGuard(review),
|
|
reviewGate: async () => ({ approved: true, approvalDigest: vincentDigest }),
|
|
}),
|
|
).rejects.toMatchObject({ stage: "review", message: "approval digest mismatch" });
|
|
});
|
|
|
|
it("releases a replay claim after transient audit failure and retries successfully", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000001",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "retry me" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
const replayStore = new MemoryReplayStore();
|
|
const inboundAudit = new FailOnceAuditStore();
|
|
const options = {
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore,
|
|
now,
|
|
guard: mockGuard(allow),
|
|
audit: inboundAudit,
|
|
policyVersion: "v1",
|
|
};
|
|
await expect(composeInbound(options)).rejects.toThrow("transient audit failure");
|
|
const retried = await composeInbound(options);
|
|
expect(retried.disposition).toBe("accepted");
|
|
expect(retried.receipt.status).toBe("accepted");
|
|
});
|
|
|
|
it("returns an identical cached rejection receipt on guard-deny redelivery", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000002",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "classify me" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
const replayStore = new MemoryReplayStore();
|
|
const inboundAudit = audit();
|
|
const deny: Verdict = { ...allow, decision: "deny", category: "injection", reason: "Denied." };
|
|
const guard = mockGuard(deny);
|
|
const options = {
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore,
|
|
now,
|
|
guard,
|
|
audit: inboundAudit,
|
|
policyVersion: "v1",
|
|
};
|
|
let rejection: PipelineError | undefined;
|
|
try {
|
|
await composeInbound(options);
|
|
} catch (error) {
|
|
if (error instanceof PipelineError) {
|
|
rejection = error;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
expect(rejection?.receipt).toMatchObject({ status: "rejected", category: "guard_deny" });
|
|
const entryCount = (await inboundAudit.entries()).length;
|
|
const duplicate = await composeInbound({ ...options, now: now + 1_000 });
|
|
expect(duplicate).toEqual({ disposition: "duplicate", receipt: rejection!.receipt });
|
|
expect(duplicate).not.toHaveProperty("body");
|
|
expect((await inboundAudit.entries()).length).toBe(entryCount);
|
|
expect(guard.calls).toBe(1);
|
|
});
|
|
|
|
it("completes explicit inbound review denial and caches its receipt", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000005",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "review me" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
const review: Verdict = {
|
|
...allow,
|
|
decision: "review",
|
|
category: "ambiguous",
|
|
reason: "Review.",
|
|
};
|
|
const guard = mockGuard(review);
|
|
const replayStore = new MemoryReplayStore();
|
|
const inboundAudit = audit();
|
|
const options = {
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore,
|
|
now,
|
|
guard,
|
|
audit: inboundAudit,
|
|
policyVersion: "v1",
|
|
reviewGate: async ({ approvalDigest }: { approvalDigest: string }) => ({
|
|
approved: false,
|
|
approvalDigest,
|
|
}),
|
|
};
|
|
let rejection: PipelineError | undefined;
|
|
try {
|
|
await composeInbound(options);
|
|
} catch (error) {
|
|
if (error instanceof PipelineError) {
|
|
rejection = error;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
expect(rejection).toMatchObject({
|
|
stage: "review",
|
|
reviewOutcome: "denied",
|
|
receipt: { status: "rejected", category: "review_denied" },
|
|
});
|
|
const entryCount = (await inboundAudit.entries()).length;
|
|
await expect(composeInbound(options)).resolves.toEqual({
|
|
disposition: "duplicate",
|
|
receipt: rejection!.receipt,
|
|
});
|
|
expect((await inboundAudit.entries()).length).toBe(entryCount);
|
|
expect(guard.calls).toBe(1);
|
|
});
|
|
|
|
it("releases pending inbound review and accepts a later approved retry", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000006",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: "decide later" },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
const review: Verdict = {
|
|
...allow,
|
|
decision: "review",
|
|
category: "ambiguous",
|
|
reason: "Review.",
|
|
};
|
|
const guard = mockGuard(review, review, allow);
|
|
let decided = false;
|
|
const options = {
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore: new MemoryReplayStore(),
|
|
now,
|
|
guard,
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
reviewGate: async ({ approvalDigest }: { approvalDigest: string }) =>
|
|
decided ? { approved: true, approvalDigest } : undefined,
|
|
};
|
|
await expect(composeInbound(options)).rejects.toMatchObject({
|
|
stage: "review",
|
|
reviewOutcome: "pending",
|
|
receipt: undefined,
|
|
});
|
|
decided = true;
|
|
await expect(composeInbound({ ...options, now: now + 10 * 60 })).resolves.toMatchObject({
|
|
disposition: "accepted",
|
|
body: { text: "decide later" },
|
|
});
|
|
expect(guard.calls).toBe(3);
|
|
});
|
|
|
|
it("completes deterministic inbound denial with a signed rejection", async () => {
|
|
const { alice, bob } = identities();
|
|
const envelope = seal({
|
|
id: "01JZ0000000000000000000003",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
body: { text: ["sk-", "abcdefghijklmnopqrstuvwxyz123456"].join("") },
|
|
senderSigningSecretKey: alice.signing.secretKey,
|
|
recipientEncryptionPublicKey: bob.encryption.publicKey,
|
|
ts: now,
|
|
});
|
|
const guard = mockGuard(allow);
|
|
await expect(
|
|
composeInbound({
|
|
envelope,
|
|
self: "bob#1",
|
|
recipientEncryptionSecretKey: bob.encryption.secretKey,
|
|
recipientSigningSecretKey: bob.signing.secretKey,
|
|
senderSigningPublicKey: alice.signing.publicKey,
|
|
replayStore: new MemoryReplayStore(),
|
|
now,
|
|
guard,
|
|
audit: audit(),
|
|
policyVersion: "v1",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
stage: "deterministic",
|
|
receipt: { status: "rejected", category: "deterministic_deny" },
|
|
});
|
|
expect(guard.calls).toBe(0);
|
|
});
|
|
});
|
|
|
|
function craftEnvelope(
|
|
body: unknown,
|
|
alice: ReturnType<typeof generateIdentity>,
|
|
bob: ReturnType<typeof generateIdentity>,
|
|
): Envelope {
|
|
const ephemeral = x25519.keygen(new Uint8Array(32).fill(7));
|
|
const shared = x25519.getSharedSecret(
|
|
ephemeral.secretKey,
|
|
fromBase64url(bob.encryption.publicKey),
|
|
);
|
|
const key = hkdf(sha256, shared, undefined, utf8("reef-v1"), 32);
|
|
const nonce = new Uint8Array(12).fill(9);
|
|
const unsigned = {
|
|
v: 1 as const,
|
|
id: "01JZ0000000000000000000004",
|
|
from: "alice#1",
|
|
to: "bob#1",
|
|
ts: now,
|
|
epk: base64(ephemeral.publicKey),
|
|
n: base64(nonce),
|
|
ct: base64(gcm(key, nonce).encrypt(canonicalBytes(body))),
|
|
};
|
|
return {
|
|
...unsigned,
|
|
sig: base64(ed25519.sign(canonicalBytes(unsigned), fromBase64url(alice.signing.secretKey))),
|
|
};
|
|
}
|