Files

486 lines
15 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { ElyAnalyticsDataPoint, Env } from "../src/bindings.js";
import { authSessionCacheKvKey, authTokenHash } from "../src/auth.js";
import { withAuthenticatedApiControls } from "../src/api_controls.js";
import { handleRequest } from "../src/index.js";
import { jsonResponse } from "../src/responses.js";
import { publicSigningKeysKvKey } from "../src/signing_keys.js";
const PUBLIC_KEY = "a".repeat(64);
const ACCESS_TOKEN = "A".repeat(48);
describe("api controls", () => {
it("rate limits public API routes before reading KV", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const kvReads: string[] = [];
const response = await handleRequest(
new Request("https://elydora.test/api/plugins/signing-keys"),
testEnv({ auditEvents, kvReads, rateLimitSuccess: false }),
);
assert.equal(response.status, 429);
assert.equal(response.headers.get("retry-after"), "60");
assert.deepEqual(await response.json(), { error: "rate_limited" });
assert.deepEqual(kvReads, []);
assert.equal(auditEvents.length, 1);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
"plugins.signing_keys",
"GET",
"/api/plugins/signing-keys",
"rate_limited",
]);
assert.equal(auditEvents[0]?.doubles?.[0], 429);
});
it("records successful public API requests", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const rateLimitKeys: string[] = [];
const response = await handleRequest(
new Request("https://elydora.test/api/plugins/signing-keys", {
headers: { "cf-ray": "ray-1", "user-agent": "ely-test" },
}),
testEnv({ auditEvents, rateLimitKeys }),
);
assert.equal(response.status, 200);
assert.deepEqual(rateLimitKeys, ["local:plugins.signing_keys"]);
assert.equal(auditEvents.length, 1);
assert.deepEqual(auditEvents[0]?.indexes, ["local"]);
assert.deepEqual(auditEvents[0]?.blobs, [
"plugins.signing_keys",
"GET",
"/api/plugins/signing-keys",
"handled",
"ray-1",
"ely-test",
]);
assert.equal(auditEvents[0]?.doubles?.[0], 200);
});
it("records method rejections without consuming rate limit tokens", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const rateLimitKeys: string[] = [];
const response = await handleRequest(
new Request("https://elydora.test/api/plugins/signing-keys", { method: "POST" }),
testEnv({ auditEvents, rateLimitKeys }),
);
assert.equal(response.status, 405);
assert.equal(response.headers.get("allow"), "GET");
assert.deepEqual(rateLimitKeys, []);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
"plugins.signing_keys",
"POST",
"/api/plugins/signing-keys",
"method_not_allowed",
]);
assert.equal(auditEvents[0]?.doubles?.[0], 405);
});
it("rate limits authenticated API routes before reading session state", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const kvReads: string[] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
}),
testEnv({ auditEvents, kvReads, rateLimitSuccess: false }),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 429);
assert.deepEqual(kvReads, []);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
"devices.list",
"GET",
"/api/devices",
"rate_limited",
]);
});
it("rejects missing authenticated API credentials after rate limit", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const rateLimitKeys: string[] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices"),
testEnv({ auditEvents, rateLimitKeys }),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 401);
assert.equal(response.headers.get("www-authenticate"), "Bearer");
assert.deepEqual(await response.json(), { error: "authorization_missing" });
assert.deepEqual(rateLimitKeys, ["local:devices.list:anonymous"]);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
"devices.list",
"GET",
"/api/devices",
"authorization_missing",
]);
});
it("rejects malformed authenticated API credentials with audit coverage", async () => {
const auditEvents: ElyAnalyticsDataPoint[] = [];
const rateLimitKeys: string[] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: "Bearer short" },
}),
testEnv({ auditEvents, rateLimitKeys }),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "authorization_invalid" });
assert.deepEqual(rateLimitKeys, ["local:devices.list:authorization_invalid"]);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
"devices.list",
"GET",
"/api/devices",
"authorization_invalid",
]);
});
it("rejects bearer tokens larger than the native credential contract", async () => {
const d1Queries: string[] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: `Bearer ${"A".repeat(2561)}` },
}),
testEnv({ d1: testD1Database({ queries: d1Queries }) }),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "authorization_invalid" });
assert.deepEqual(d1Queries, []);
});
it("passes authenticated session context and records subject audit fields", async () => {
const tokenHash = await authTokenHash(ACCESS_TOKEN);
const auditEvents: ElyAnalyticsDataPoint[] = [];
const kvReads: string[] = [];
const rateLimitKeys: string[] = [];
let receivedTokenHash = "";
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: {
authorization: `Bearer ${ACCESS_TOKEN}`,
"cf-ray": "ray-auth",
"user-agent": "ely-auth-test",
},
}),
testEnv({
auditEvents,
kvReads,
rateLimitKeys,
}),
"devices.list",
["GET"],
(context) => {
receivedTokenHash = context.tokenHash;
return Promise.resolve(
jsonResponse({ user_id: context.userId, device_id: context.deviceId }, 200),
);
},
);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { user_id: "user-01", device_id: "device-01" });
assert.deepEqual(rateLimitKeys, [`local:devices.list:bearer:${tokenHash}`]);
assert.deepEqual(kvReads, []);
assert.equal(receivedTokenHash, tokenHash);
assert.deepEqual(auditEvents[0]?.blobs, [
"devices.list",
"GET",
"/api/devices",
"handled",
"ray-auth",
"ely-auth-test",
"user-01",
"device-01",
]);
});
it("uses Better Auth D1 sessions as the authoritative source", async () => {
const tokenHash = await authTokenHash(ACCESS_TOKEN);
const auditEvents: ElyAnalyticsDataPoint[] = [];
const kvReads: string[] = [];
const rateLimitKeys: string[] = [];
const d1Queries: string[] = [];
const d1Binds: unknown[][] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
}),
testEnv({
auditEvents,
kvReads,
rateLimitKeys,
d1: testD1Database({
firstRows: [
{
id: "session-01",
userId: "user-01",
expiresAt: "2099-01-01T00:00:00.000Z",
createdAt: new Date().toISOString(),
deviceId: "device-01",
},
],
queries: d1Queries,
binds: d1Binds,
}),
}),
"devices.list",
["GET"],
(context) =>
Promise.resolve(
jsonResponse(
{
user_id: context.userId,
session_id: context.sessionId,
device_id: context.deviceId,
},
200,
),
),
);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
user_id: "user-01",
session_id: "session-01",
device_id: "device-01",
});
assert.deepEqual(rateLimitKeys, [`local:devices.list:bearer:${tokenHash}`]);
assert.deepEqual(kvReads, []);
assert.equal(d1Queries.length, 1);
assert.match(d1Queries[0] ?? "", /FROM better_auth_session/);
assert.deepEqual(d1Binds, [[ACCESS_TOKEN]]);
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 7), [
"devices.list",
"GET",
"/api/devices",
"handled",
"",
"",
"user-01",
]);
assert.equal(auditEvents[0]?.blobs?.[7], "device-01");
});
it("rejects a legacy cached session after D1 revocation", async () => {
const tokenHash = await authTokenHash(ACCESS_TOKEN);
const kvReads: string[] = [];
const d1Queries: string[] = [];
const d1Binds: unknown[][] = [];
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
}),
testEnv({
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument()]],
kvReads,
d1: testD1Database({ firstRows: [], queries: d1Queries, binds: d1Binds }),
}),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "session_not_found" });
assert.deepEqual(kvReads, []);
assert.equal(d1Queries.length, 1);
assert.match(d1Queries[0] ?? "", /FROM better_auth_session/);
assert.deepEqual(d1Binds, [[ACCESS_TOKEN]]);
});
it("rejects expired authenticated sessions", async () => {
const response = await withAuthenticatedApiControls(
new Request("https://elydora.test/api/devices", {
headers: { authorization: `Bearer ${ACCESS_TOKEN}` },
}),
testEnv({
d1: testD1Database({
firstRows: [
{
id: "session-01",
userId: "user-01",
expiresAt: "2026-01-01T00:00:00.000Z",
createdAt: "2025-01-01T00:00:00.000Z",
deviceId: "device-01",
},
],
}),
}),
"devices.list",
["GET"],
() => Promise.resolve(jsonResponse({ ok: true }, 200)),
);
assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "session_expired" });
});
});
interface TestEnvOptions {
auditEvents?: ElyAnalyticsDataPoint[];
d1?: Env["ELY_DB"];
kvEntries?: [string, string][];
kvReads?: string[];
rateLimitKeys?: string[];
rateLimitSuccess?: boolean;
}
function testEnv(options: TestEnvOptions = {}): Env {
const values = new Map<string, string>([
[
publicSigningKeysKvKey("local"),
JSON.stringify({
version: 1,
keys: [{ key_id: "elydora-alpha-plugins", public_key: PUBLIC_KEY }],
}),
],
]);
for (const [key, value] of options.kvEntries ?? []) {
values.set(key, value);
}
return {
ELY_ENVIRONMENT: "local",
ELY_AUTH_BASE_URL: "https://elydora.test",
ELY_AUTH_SECRET: "test-auth-secret-for-api-controls",
ELY_DB: options.d1 ?? testD1Database(),
ELY_KV: {
get(key: string): Promise<string | null> {
options.kvReads?.push(key);
return Promise.resolve(values.get(key) ?? null);
},
put(key: string, value: string): Promise<void> {
values.set(key, value);
return Promise.resolve();
},
delete(key: string): Promise<void> {
values.delete(key);
return Promise.resolve();
},
},
ELY_STORAGE: testR2Bucket(),
ELY_RATE_LIMITER: {
limit(input: { key: string }): Promise<{ success: boolean }> {
options.rateLimitKeys?.push(input.key);
return Promise.resolve({ success: options.rateLimitSuccess ?? true });
},
},
ELY_AUTH_EMAIL_RATE_LIMITER: {
limit(): Promise<{ success: boolean }> {
return Promise.resolve({ success: true });
},
},
ELY_API_AUDIT: {
writeDataPoint(event?: ElyAnalyticsDataPoint): void {
if (event !== undefined) {
options.auditEvents?.push(event);
}
},
},
ELY_DIAGNOSTICS: {
writeDataPoint(): void {},
},
};
}
function sessionDocument(expiresAt = "2099-01-01T00:00:00.000Z"): string {
return JSON.stringify({
version: 1,
user_id: "user-01",
session_id: "session-01",
device_id: "device-01",
expires_at: expiresAt,
});
}
function testR2Bucket(): Env["ELY_STORAGE"] {
return {
get() {
return Promise.resolve(null);
},
put() {
return Promise.resolve({
arrayBuffer() {
return Promise.resolve(new ArrayBuffer(0));
},
});
},
delete() {
return Promise.resolve();
},
};
}
interface TestD1DatabaseOptions {
binds?: unknown[][];
firstRows?: unknown[];
queries?: string[];
}
function testD1Database(options: TestD1DatabaseOptions = {}): Env["ELY_DB"] {
const configuredOptions: TestD1DatabaseOptions = {
...options,
firstRows: options.firstRows ?? [
{
id: "session-01",
userId: "user-01",
expiresAt: "2099-01-01T00:00:00.000Z",
createdAt: new Date().toISOString(),
deviceId: "device-01",
},
],
};
let firstIndex = 0;
return {
prepare(query: string) {
options.queries?.push(query);
return testD1PreparedStatement(configuredOptions, () => firstIndex++);
},
batch() {
return Promise.resolve([]);
},
exec() {
return Promise.resolve({});
},
};
}
function testD1PreparedStatement(
options: TestD1DatabaseOptions,
nextFirstIndex: () => number,
): ReturnType<Env["ELY_DB"]["prepare"]> {
return {
bind(...values: unknown[]) {
options.binds?.push(values);
return this;
},
first<T>() {
return Promise.resolve((options.firstRows?.[nextFirstIndex()] as T | undefined) ?? null);
},
all() {
return Promise.resolve({ results: [] });
},
run() {
return Promise.resolve({});
},
};
}