Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import { eq } from "@openstatus/db";import { apiKey } from "@openstatus/db/src/schema";import { expect } from "@std/expect";import { beforeAll, describe, test } from "@std/testing/bdd";
import { createWorkspaceFixture, expectAuditRow, makeApiKeyCtx, makeUserCtx, readAuditLog, withTestTransaction,} from "../../../test/helpers";import type { ServiceContext } from "../../context";import { ForbiddenError, NotFoundError } from "../../errors";import { createApiKey, listApiKeys, revokeApiKey, updateApiKeyLastUsed, verifyApiKey,} from "../index.ts";
const TEST_PREFIX = "svc-apikey-test";
let teamCtx: ServiceContext;
beforeAll(async () => { const team = (await createWorkspaceFixture("team")).workspace; teamCtx = makeUserCtx(team, { userId: 1 });});
describe("createApiKey", () => { test("returns plaintext token once and stores a bcrypt hash", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { token, key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-create` }, });
expect(token).toMatch(/^os_[a-f0-9]{32}$/); expect(key.workspaceId).toBe(teamCtx.workspace.id);
const stored = await tx .select({ hashedToken: apiKey.hashedToken }) .from(apiKey) .where(eq(apiKey.id, key.id)) .get(); expect(stored?.hashedToken).toMatch(/^\$2[aby]\$/);
await expectAuditRow({ workspaceId: ctx.workspace.id, action: "api_key.create", entityType: "api_key", entityId: key.id, db: tx, }); }); });
test("defaults scopes to ['write'] when omitted", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-default-scope` }, }); expect(key.scopes).toEqual(["write"]); }); });
test("persists ['read'] when explicitly set", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-read-scope`, scopes: ["read"] }, }); expect(key.scopes).toEqual(["read"]); }); });
test("rejects '*' at the input boundary (privilege-escalation guard)", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; await expect( createApiKey({ ctx, input: { name: `${TEST_PREFIX}-star`, scopes: ["*"] as any, }, }), ).rejects.toThrow(); }); });
test("rejects empty scopes []", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; await expect( createApiKey({ ctx, input: { name: `${TEST_PREFIX}-empty-scope`, scopes: [] }, }), ).rejects.toThrow(); }); });
test("rejects read-only actor", async () => { await withTestTransaction(async (tx) => { const readOnlyCtx = { ...makeApiKeyCtx(teamCtx.workspace, { keyId: "k-read", userId: 1, scopes: ["read"], }), db: tx, }; await expect( createApiKey({ ctx: readOnlyCtx, input: { name: `${TEST_PREFIX}-rejects-read` }, }), ).rejects.toBeInstanceOf(ForbiddenError); }); });});
describe("listApiKeys", () => { test("enriches each key with creator info", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-list` }, });
const rows = await listApiKeys({ ctx }); const found = rows.find((r) => r.id === key.id); expect(found).toBeDefined(); expect(found?.createdBy?.id).toBe(1); }); });});
describe("revokeApiKey", () => { test("deletes the key and throws NotFoundError for unknown ids", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-revoke` }, });
await revokeApiKey({ ctx, input: { id: key.id } });
const row = await tx .select({ id: apiKey.id }) .from(apiKey) .where(eq(apiKey.id, key.id)) .get(); expect(row).toBeUndefined();
await expect( revokeApiKey({ ctx, input: { id: 999_999_999 } }), ).rejects.toBeInstanceOf(NotFoundError); }); });
test("self-revoke carve-out: read-only key can revoke itself", async () => { await withTestTransaction(async (tx) => { // Create the key as a user so we get a real row id back. const { key } = await createApiKey({ ctx: { ...teamCtx, db: tx }, input: { name: `${TEST_PREFIX}-self-revoke`, scopes: ["read"] }, });
// Now revoke as the read-only key itself โ should succeed. const selfCtx = { ...makeApiKeyCtx(teamCtx.workspace, { keyId: String(key.id), userId: 1, scopes: ["read"], }), db: tx, }; await revokeApiKey({ ctx: selfCtx, input: { id: key.id } });
const row = await tx .select({ id: apiKey.id }) .from(apiKey) .where(eq(apiKey.id, key.id)) .get(); expect(row).toBeUndefined(); }); });
test("audit row captures scopes in `before` snapshot", async () => { // Create with a non-default scope, revoke, and confirm the audit // row's `before` snapshot carries it. Asserts the create/delete // pair stays symmetric โ both sides should expose `scopes` so // reviewers can answer "what scope did this key have?" without // querying a deleted row. await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-audit-snap`, scopes: ["read"] }, }); await revokeApiKey({ ctx, input: { id: key.id } });
const rows = await readAuditLog({ workspaceId: ctx.workspace.id, entityType: "api_key", entityId: key.id, db: tx, }); const deleteRow = rows.find((r) => r.action === "api_key.delete"); const createRow = rows.find((r) => r.action === "api_key.create");
expect(deleteRow?.before).toMatchObject({ id: key.id, prefix: key.prefix, scopes: ["read"], }); expect(createRow?.after).toMatchObject({ id: key.id, prefix: key.prefix, scopes: ["read"], }); // Symmetry: snapshots strip the same secret/header columns. const forbidden = ["hashedToken", "workspaceId", "createdById"]; for (const k of forbidden) { expect(deleteRow?.before).not.toHaveProperty(k); expect(createRow?.after).not.toHaveProperty(k); } }); });
test("read-only actor cannot revoke a different key", async () => { await withTestTransaction(async (tx) => { const { key } = await createApiKey({ ctx: { ...teamCtx, db: tx }, input: { name: `${TEST_PREFIX}-other-key` }, });
// Read-only actor with a different keyId tries to revoke `key`. const otherCtx = { ...makeApiKeyCtx(teamCtx.workspace, { keyId: "different-key-id", userId: 1, scopes: ["read"], }), db: tx, }; await expect( revokeApiKey({ ctx: otherCtx, input: { id: key.id } }), ).rejects.toBeInstanceOf(ForbiddenError); }); });});
describe("verifyApiKey", () => { test("resolves the stored row for a valid token", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { token, key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-verify` }, });
const row = await verifyApiKey({ token }, { db: tx }); expect(row?.id).toBe(key.id); }); });
test("returns null for malformed tokens", async () => { await withTestTransaction(async (tx) => { const row = await verifyApiKey({ token: "not-an-os-key" }, { db: tx }); expect(row).toBeNull(); }); });
test("returns null for a prefix-match with wrong body", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { token } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-verify-wrong` }, });
const wrong = `${token.slice(0, 11)}${"f".repeat(24)}`; const row = await verifyApiKey({ token: wrong }, { db: tx }); expect(row).toBeNull(); }); });});
describe("updateApiKeyLastUsed", () => { test("skips the write when lastUsedAt is recent", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-lastused` }, });
const now = new Date(); const wrote = await updateApiKeyLastUsed( { id: key.id, lastUsedAt: now }, { db: tx }, ); expect(wrote).toBe(false); }); });
test("writes when lastUsedAt is null or past the debounce window", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const { key } = await createApiKey({ ctx, input: { name: `${TEST_PREFIX}-lastused-null` }, });
const wrote = await updateApiKeyLastUsed( { id: key.id, lastUsedAt: null }, { db: tx }, ); expect(wrote).toBe(true);
const row = await tx .select({ lastUsedAt: apiKey.lastUsedAt }) .from(apiKey) .where(eq(apiKey.id, key.id)) .get(); expect(row?.lastUsedAt).toBeInstanceOf(Date); }); });});