Files
cfw-autumn/server/src/honoMiddlewares/baseMiddleware.ts

161 lines
4.1 KiB
TypeScript

import {
ApiVersionClass,
AppEnv,
AuthType,
LATEST_VERSION,
type Organization,
tryCatch,
} from "@autumn/shared";
import type { Context, Next } from "hono";
import { createRequestScopedDb } from "@/db/initDrizzle.js";
import { createLogger } from "@/external/logtail/logtailUtils.js";
import { resolveRequestCacheStore } from "@/external/storage/cache/resolveRequestCacheStore.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { generateId } from "@/utils/genUtils.js";
import { addRequestToLogs } from "@/utils/logging/addContextToLogs";
import { resolveCustomerId } from "./utils/resolveCustomerId.js";
import { resolveEntityId } from "./utils/resolveEntityId.js";
const SENSITIVE_REQUEST_BODY_KEYS = new Set(["connectionString"]);
const REDACTED_REQUEST_BODY_VALUE = "[REDACTED]";
const redactSensitiveRequestBody = ({ body }: { body: unknown }): unknown => {
if (!body || typeof body !== "object") return body;
if (Array.isArray(body)) {
return body.map((item) => redactSensitiveRequestBody({ body: item }));
}
return Object.fromEntries(
Object.entries(body).map(([key, value]) => [
key,
SENSITIVE_REQUEST_BODY_KEYS.has(key)
? REDACTED_REQUEST_BODY_VALUE
: redactSensitiveRequestBody({ body: value }),
]),
);
};
const shouldSkipRequestBodyParse = ({
method,
path,
}: {
method: string;
path: string;
}) => {
if (method === "GET" || method === "HEAD") return true;
return path.startsWith("/webhooks/stripe/") || path.startsWith("/webhooks/connect/");
};
/**
* Base middleware that sets up the request context
* Sets up: db, logger, id, timestamp
*/
export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
const env = c.env;
const logger = createLogger(env);
// const env = (c.req.header("app_env") as AppEnv) || AppEnv.Sandbox;
const id =
c.req.header("rndr-id") ||
c.req.header("X-Amzn-Trace-Id") ||
c.req.header("x-amzn-trace-id") ||
generateId("local_req");
const timestamp = Date.now();
const { data: body } = shouldSkipRequestBodyParse({
method: c.req.method,
path: c.req.path,
})
? { data: undefined }
: await tryCatch(c.req.json());
const customerId = resolveCustomerId({
method: c.req.method,
path: c.req.path,
body,
query: c.req.query(),
});
const entityId = resolveEntityId({
method: c.req.method,
path: c.req.path,
body,
query: c.req.query(),
});
const childLogger = addRequestToLogs({
logger,
requestContext: {
id,
method: c.req.method,
url: c.req.url,
timestamp,
customer_id: customerId,
entity_id: entityId,
user_agent: c.req.header("user-agent"),
ip_address: c.req.header("x-forwarded-for"),
region: c.env.AWS_REGION,
query: c.req.query(),
body: redactSensitiveRequestBody({ body }),
name: `${c.req.method} ${c.req.path}`,
},
});
const cacheStore = resolveRequestCacheStore({ env: c.env });
const requestDb = createRequestScopedDb(env);
// Set up the request context
c.set("ctx", {
// Core objects
db: requestDb.db,
logger: childLogger,
workerEnv: c.env,
cacheStore,
// Request info
id,
timestamp,
isPublic: false,
apiVersion: new ApiVersionClass(LATEST_VERSION),
// Auth (will be populated by auth middleware)
org: undefined as unknown as Organization,
features: [],
userId: undefined,
customerId,
entityId,
authType: AuthType.Unknown,
env: AppEnv.Sandbox, // maybe use app_env headers
scopes: [],
// Query params
expand: [],
skipCache:
c.req.header("x-skip-cache") === "true" ||
c.req.query("skip_cache") === "true",
// Test params:
extraLogs: {},
testOptions: {
eventId: c.req.header("x-event-id"),
skipCacheDeletion: c.req.header("x-skip-cache-deletion") === "true",
skipWebhooks: c.req.header("x-skip-webhooks") === "true",
keepInternalFields: c.req.header("x-strip-internal") === "false",
mockVercelApi: c.req.header("x-mock-vercel-api") === "true",
allowVercelTestOidc:
c.env.NODE_ENV !== "production" &&
c.req.header("x-allow-vercel-test-oidc") === "true",
},
});
// childLogger.info(`${method} ${path}`);
try {
await next();
} finally {
c.executionCtx.waitUntil(requestDb.dispose());
}
};