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.
8.6 kB ยท 279 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280import { getLogger } from "@logtape/logtape";import { db, eq } from "@openstatus/db";import { type Scope, selectWorkspaceSchema, workspace,} from "@openstatus/db/src/schema";import { apiKey } from "@openstatus/db/src/schema/api-keys";import { shouldUpdateLastUsed, verifyApiKeyHash,} from "@openstatus/db/src/utils/api-key";import { retryRead } from "@openstatus/services";import { isAccessToken, verifyAccessToken } from "@openstatus/services/oauth";import { UnkeyCore } from "@unkey/api/core";import { keysVerifyKey } from "@unkey/api/funcs/keysVerifyKey";import type { Context, Next } from "hono";
import { env } from "@/env";import { OpenStatusApiError } from "@/libs/errors";import type { Variables } from "@/types";
import { MISSING_CREDENTIALS_MESSAGE, extractCredential } from "./credentials";
const logger = getLogger("api-server");
export async function lookupWorkspace(workspaceId: number) { const _workspace = await retryRead(() => db.select().from(workspace).where(eq(workspace.id, workspaceId)).get(), );
if (!_workspace) { throw new OpenStatusApiError({ code: "NOT_FOUND", message: "Workspace not found, please contact support", }); }
const validation = selectWorkspaceSchema.safeParse(_workspace);
if (!validation.success) { throw new OpenStatusApiError({ code: "BAD_REQUEST", message: "Workspace data is invalid", }); }
return validation.data;}
export async function authMiddleware( c: Context<{ Variables: Variables }, "/*">, next: Next,) { const credential = extractCredential(c.req.raw.headers); if (!credential) throw new OpenStatusApiError({ code: "UNAUTHORIZED", message: MISSING_CREDENTIALS_MESSAGE, });
const { error, result } = await validateKey(credential.token);
if (error) { throw new OpenStatusApiError({ code: "UNAUTHORIZED", message: error.message, }); }
if (!result.valid || !result.ownerId) { throw new OpenStatusApiError({ code: "UNAUTHORIZED", message: "Invalid API Key", }); }
const ownerId = Number.parseInt(result.ownerId);
if (Number.isNaN(ownerId)) { throw new OpenStatusApiError({ code: "UNAUTHORIZED", message: "API Key is Not a Number", }); }
const workspaceData = await lookupWorkspace(ownerId);
const event = c.get("event"); event.workspace = { id: workspaceData.id, name: workspaceData.name, plan: workspaceData.plan, stripe_id: workspaceData.stripeId, }; event.auth_method = result.authMethod; c.set("workspace", workspaceData); // Always populate `apiKey` โ falling back to a workspace-scoped // placeholder for auth paths that didn't surface a stable key id // (today: an Unkey response without `data.keyId`). Adapters can rely // on the field being present without optional-chaining. Warn loudly // when the fallback fires โ audit attribution silently degrades to // workspace-level, which is a regression we want to notice. if (!result.keyId) { logger.warn( "authMiddleware: keyId missing, falling back to workspace placeholder {*}", { workspaceId: workspaceData.id, authMethod: result.authMethod, }, ); } c.set("apiKey", { id: result.keyId ?? `ws:${workspaceData.id}`, createdById: result.createdById, scopes: result.scopes ?? ["write"], });
await next();}
export async function validateKey(key: string): Promise<{ result: { valid: boolean; ownerId?: string; authMethod?: string; /** * Stable identifier for the API key itself, not the workspace it * belongs to. Audit logs read this to attribute mutations to the * specific key. Custom keys: the `api_key.id` row id. Unkey: * `data.keyId`. Dev: the input string. Super-admin: a sentinel. */ keyId?: string; /** * The openstatus user who created the API key โ `api_key.created_by_id` * for custom keys. Unkey/dev/super-admin don't expose a user * mapping, so this is undefined for them; audit rows fall back to * `actor_user_id = NULL`. */ createdById?: number; /** * Access-control scopes for the resolved key. Custom keys: row * scopes. Unkey fallback: `['write']` (legacy posture โ keys * minted before scopes existed kept full workspace access). * Super-admin / dev fallback: `['*']` so the matcher treats them * uniformly without a hardcoded bypass. */ scopes?: Scope[]; }; error?: { message: string };}> { // Before the production gate so the OAuth flow works locally. `keyId` // carries the grant id so audit rows attribute to the grant, and // `createdById` is the consenting user. if (isAccessToken(key)) { const grant = await verifyAccessToken(key); if (!grant) { return { result: { valid: false }, error: { message: "Invalid or expired access token" }, }; } return { result: { valid: true, ownerId: String(grant.workspaceId), authMethod: "oauth", keyId: `oat_${grant.grantId}`, createdById: grant.userId, scopes: grant.scopes, }, }; }
if (env.NODE_ENV === "production") { /** * Both custom and Unkey API keys use the `os_` prefix for seamless transition. * Custom keys are checked first in the database, then falls back to Unkey. */ if (key.startsWith("os_")) { // 1. Try custom DB first const prefix = key.slice(0, 11); // "os_" (3 chars) + 8 hex chars = 11 total const customKey = await retryRead(() => db.select().from(apiKey).where(eq(apiKey.prefix, prefix)).get(), );
if (customKey) { if (!(await verifyApiKeyHash(key, customKey.hashedToken))) { return { result: { valid: false }, error: { message: "Invalid API Key" }, }; } if (customKey.expiresAt && customKey.expiresAt < new Date()) { return { result: { valid: false }, error: { message: "API Key expired" }, }; }
// Best-effort: a failed bookkeeping write must never fail auth. if (shouldUpdateLastUsed(customKey.lastUsedAt)) { await db .update(apiKey) .set({ lastUsedAt: new Date() }) .where(eq(apiKey.id, customKey.id)) .catch(() => {}); } return { result: { valid: true, ownerId: String(customKey.workspaceId), authMethod: "custom_key", keyId: String(customKey.id), createdById: customKey.createdById, scopes: customKey.scopes, }, }; }
// 2. Fall back to Unkey (transition period). Unkey-validated // keys predate the scopes column, so they carry the legacy // posture (`write`). When/if Unkey gets RBAC metadata, plumb // it in here. const unkey = new UnkeyCore({ rootKey: env.UNKEY_TOKEN }); const res = await keysVerifyKey(unkey, { key }); if (!res.ok) { logger.error("Unkey Error {*}", { ...res.error }); return { result: { valid: false, ownerId: undefined }, error: { message: "Invalid API verification" }, }; } return { result: { valid: res.value.data.valid, ownerId: res.value.data.identity?.externalId, authMethod: "unkey", keyId: res.value.data.keyId, scopes: ["write"], }, error: undefined, }; } // Special bypass for our workspace. `'*'` is internal-only โ never // settable via any public API. if (key.startsWith("sa_") && key === env.SUPER_ADMIN_TOKEN) { return { result: { valid: true, ownerId: "1", authMethod: "super_admin", keyId: "super_admin", scopes: ["*"], }, }; } // In production, we only accept Unkey keys throw new OpenStatusApiError({ code: "UNAUTHORIZED", message: "Invalid API Key", }); }
// In dev / test mode we can use the key as the ownerId. The same // string also stands in for the keyId โ there is no separate // identity record to reference. Dev keys get `'*'` so local testing // mirrors super-admin and isn't accidentally locked out by scope // checks. return { result: { valid: true, ownerId: key, authMethod: "dev", keyId: key, scopes: ["*"], }, };}