Files
cfw-autumn/server/src/honoMiddlewares/secretKeyMiddleware.ts
2026-06-08 10:10:05 +01:00

97 lines
2.7 KiB
TypeScript

import { getBearerToken } from "@autumn/auth";
import { AuthType, ErrCode, type Feature, RecaseError } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
import { betterAuthMiddleware } from "./betterAuthMiddleware.js";
import { publicKeyMiddleware } from "./publicKeyMiddleware.js";
const maskApiKey = (apiKey: string) => {
return apiKey.slice(0, 15) + apiKey.slice(15).replace(/./g, "*");
};
/**
* Middleware to verify secret key and populate auth context
* Falls back to Better Auth (dashboard session) if request is from dashboard
* Delegates to publicKeyMiddleware if key is a publishable key (am_pk)
*
* Steps:
* 1. Check if Authorization header is present
* 2. If from dashboard and no auth header, use Better Auth
* 3. Check if it has correct Bearer format
* 4. Handle publishable key verification if key starts with am_pk
* 5. Verify the secret API key
* 6. Store org, features, env, userId, authType in context
*/
export const secretKeyMiddleware = async (c: Context<HonoEnv>, next: Next) => {
const ctx = c.get("ctx");
if (c.req.header("x-client-type") === "dashboard") {
return betterAuthMiddleware(c, next);
}
const apiKey = getBearerToken({ headers: c.req.raw.headers });
// Step 1 & 2: Check if Authorization header exists
// If from dashboard and no Bearer token, use Better Auth session instead
if (!apiKey) {
throw new RecaseError({
message: "Secret key not found in Authorization header",
code: ErrCode.NoSecretKey,
statusCode: 401,
});
}
if (!apiKey.startsWith("am_")) {
throw new RecaseError({
message: `Invalid secret key: ${maskApiKey(apiKey)}`,
code: ErrCode.InvalidSecretKey,
statusCode: 401,
});
}
// Step 3: Handle publishable key verification
if (apiKey.startsWith("am_pk")) {
return publicKeyMiddleware(c, apiKey, next);
}
// Step 4: Verify the API key
const { valid, data } = await verifyKey({
db: ctx.db,
key: apiKey,
});
if (!valid || !data) {
const maskedKey = maskApiKey(apiKey);
throw new RecaseError({
message: `Invalid secret key: ${maskedKey}`,
code: ErrCode.InvalidSecretKey,
statusCode: 401,
});
}
// Step 5: Store auth data in context
const { org, features, env, userId } = data;
const scopes = (data as { scopes?: string[] | null }).scopes ?? [];
if (features) {
features.sort((a: Feature, b: Feature) => {
if (a.archived && !b.archived) return 1;
if (!a.archived && b.archived) return -1;
return 0;
});
}
ctx.org = org;
ctx.features = features;
ctx.env = env;
ctx.userId = userId ?? undefined;
ctx.authType = AuthType.SecretKey;
ctx.scopes = scopes;
if (data?.user) {
ctx.user = data.user;
}
await next();
};