135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
createEncryptedRequest,
|
|
createKeyring,
|
|
createMemoryReplayStore,
|
|
decryptEncryptedResponse,
|
|
decryptHttpBody,
|
|
decryptRequest,
|
|
encryptHttpBody,
|
|
encryptResponse,
|
|
encryptedExchange,
|
|
} from "../src/index.ts";
|
|
|
|
const keyring = createKeyring([
|
|
{
|
|
id: "primary",
|
|
secret: "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=",
|
|
active: true,
|
|
},
|
|
]);
|
|
|
|
describe("encrypted HTTP envelopes", () => {
|
|
test("binds ciphertext to method, URL, request id, age, and replay state", async () => {
|
|
const now = 10_000;
|
|
const replay = createMemoryReplayStore(() => now);
|
|
const envelope = await encryptHttpBody(
|
|
{ message: "secret" },
|
|
{
|
|
keyring,
|
|
method: "POST",
|
|
url: "https://api.example.test/private?view=full",
|
|
requestId: "request-1",
|
|
timestamp: now,
|
|
},
|
|
);
|
|
|
|
const result = await decryptHttpBody<{ message: string }>(envelope, {
|
|
keyring,
|
|
method: "POST",
|
|
url: "https://api.example.test/private?view=full",
|
|
replayStore: replay,
|
|
now: () => now,
|
|
expectedRequestId: "request-1",
|
|
});
|
|
expect(result.body).toEqual({ message: "secret" });
|
|
|
|
await expect(
|
|
decryptHttpBody(envelope, {
|
|
keyring,
|
|
method: "POST",
|
|
url: "https://api.example.test/private?view=full",
|
|
replayStore: replay,
|
|
now: () => now,
|
|
}),
|
|
).rejects.toThrow("REPLAY");
|
|
|
|
await expect(
|
|
decryptHttpBody(envelope, {
|
|
keyring,
|
|
method: "GET",
|
|
url: "https://api.example.test/private?view=full",
|
|
now: () => now,
|
|
}),
|
|
).rejects.toThrow("CONTEXT");
|
|
});
|
|
|
|
test("checks the clear request-id header against the encrypted envelope", async () => {
|
|
const request = await createEncryptedRequest(
|
|
"https://api.example.test/private",
|
|
{ value: 1 },
|
|
{ keyring, requestId: "request-2", method: "POST" },
|
|
);
|
|
request.headers.set("x-wrn-request-id", "tampered-id");
|
|
await expect(decryptRequest(request, { keyring })).rejects.toThrow("REQUEST-ID");
|
|
});
|
|
|
|
test("encrypts a response using and verifying the original request context", async () => {
|
|
const request = await createEncryptedRequest(
|
|
"https://api.example.test/private",
|
|
{ value: 1 },
|
|
{ keyring, requestId: "request-3", method: "POST" },
|
|
);
|
|
const response = await encryptResponse({ ok: true }, request, { keyring });
|
|
const result = await decryptEncryptedResponse<{ ok: boolean }>(response, request, { keyring });
|
|
expect(result.body.ok).toBe(true);
|
|
expect(response.headers.get("x-wrn-request-id")).toBe("request-3");
|
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
|
|
const other = await createEncryptedRequest(
|
|
"https://api.example.test/private",
|
|
{ value: 2 },
|
|
{ keyring, requestId: "request-other", method: "POST" },
|
|
);
|
|
await expect(decryptEncryptedResponse(response, other, { keyring })).rejects.toThrow(
|
|
"REQUEST-ID",
|
|
);
|
|
});
|
|
|
|
test("provides transparent encrypted request and response middleware", async () => {
|
|
const request = await createEncryptedRequest(
|
|
"https://api.example.test/private",
|
|
{ value: 7 },
|
|
{ keyring, requestId: "request-4", method: "POST" },
|
|
);
|
|
const context = {
|
|
req: request,
|
|
locals: {},
|
|
} as Parameters<ReturnType<typeof encryptedExchange>>[0];
|
|
const response = await encryptedExchange({ keyring })(context, () =>
|
|
Response.json({ received: context.locals.encryptedBody }),
|
|
);
|
|
const result = await decryptEncryptedResponse<{
|
|
received: { value: number };
|
|
}>(response, request, { keyring });
|
|
expect(result.body.received.value).toBe(7);
|
|
});
|
|
|
|
test("does not convert application exceptions into invalid-body responses", async () => {
|
|
const request = await createEncryptedRequest(
|
|
"https://api.example.test/private",
|
|
{ value: 7 },
|
|
{ keyring, requestId: "request-5", method: "POST" },
|
|
);
|
|
const context = {
|
|
req: request,
|
|
locals: {},
|
|
} as Parameters<ReturnType<typeof encryptedExchange>>[0];
|
|
await expect(
|
|
encryptedExchange({ keyring })(context, () => {
|
|
throw new Error("application failed");
|
|
}),
|
|
).rejects.toThrow("application failed");
|
|
});
|
|
});
|