UNPKG

@appwarden/middleware

Version:

Instantly disable all user interaction with your app deployed on Cloudflare or Vercel

198 lines (194 loc) 6.07 kB
import { toNextResponse } from "../chunk-HUWGPM4M.js"; import { getNowMs, logElapsed } from "../chunk-G6BMPIYD.js"; import { checkLockStatus } from "../chunk-SFJU4R6J.js"; import { TEMPORARY_REDIRECT_STATUS, buildLockPageUrl, createHeartbeatConfigError, debug, handleHeartbeatRequest, isHTMLRequest, isHeartbeatRequest, isOnLockPage, makeCSPHeader, parseMergedConfig, printMessage, sanitizeConfigErrors } from "../chunk-Q7ZBW3WZ.js"; import { AppwardenApiHostnameSchema, AppwardenApiTokenSchema, AppwardenConfigErrorMessages, BooleanSchema, HEARTBEAT_SERVICES, UseCSPInputSchema, ValidLockPageSlugSchema } from "../chunk-3OXAKJPA.js"; // src/adapters/nextjs-cloudflare.ts import { getCloudflareContext } from "@opennextjs/cloudflare"; import { NextResponse } from "next/server"; import { ZodError } from "zod"; // src/schemas/nextjs-cloudflare.ts import { z } from "zod"; var NextJsCloudflareCSPInputSchema = UseCSPInputSchema.refine( (values) => { if (!values.directives) return true; const serialized = JSON.stringify(values.directives); return !serialized.includes("{{nonce}}"); }, { path: ["directives"], message: AppwardenConfigErrorMessages["NEXTJS_NONCE_UNSUPPORTED" /* NextJsNonceUnsupported */], params: { appwardenErrorKey: "NEXTJS_NONCE_UNSUPPORTED" /* NextJsNonceUnsupported */ } } ); var NextJsCloudflareConfigSchema = z.object({ /** The slug/path of the lock page to redirect to when the site is locked */ lockPageSlug: ValidLockPageSlugSchema, /** The Appwarden API token for authentication */ appwardenApiToken: AppwardenApiTokenSchema, /** Optional custom API hostname (defaults to https://api.appwarden.io) */ appwardenApiHostname: AppwardenApiHostnameSchema.optional(), /** Enable debug logging */ debug: BooleanSchema.default(false), /** Optional Content Security Policy configuration (headers only, no HTML rewriting; '{{nonce}}' is not supported) */ contentSecurityPolicy: z.lazy(() => NextJsCloudflareCSPInputSchema).optional() }); // src/adapters/nextjs-cloudflare.ts function getAppwardenConfiguration(generatedConfig, config) { return parseMergedConfig( generatedConfig, config, NextJsCloudflareConfigSchema.parse ); } var createNextJsHeartbeatResponse = (request, configFn) => { let runtime; try { runtime = getCloudflareContext(); } catch { return toNextResponse( handleHeartbeatRequest(request, HEARTBEAT_SERVICES.CLOUDFLARE_NEXTJS, [ createHeartbeatConfigError( ["context"], "custom", "Cloudflare context unavailable" ) ]) ); } try { const validationResult = NextJsCloudflareConfigSchema.safeParse( configFn(runtime) ); return toNextResponse( handleHeartbeatRequest( request, HEARTBEAT_SERVICES.CLOUDFLARE_NEXTJS, validationResult.success ? [] : sanitizeConfigErrors(validationResult.error) ) ); } catch (error) { return toNextResponse( handleHeartbeatRequest( request, HEARTBEAT_SERVICES.CLOUDFLARE_NEXTJS, error instanceof ZodError ? sanitizeConfigErrors(error) : [ createHeartbeatConfigError( ["config"], "custom", "Appwarden config evaluation failed" ) ] ) ); } }; function createAppwardenMiddleware(configFn) { return async (request, _event) => { const startTime = getNowMs(); const requestUrl = new URL(request.url); if (isHeartbeatRequest(request, requestUrl)) { return createNextJsHeartbeatResponse(request, configFn); } try { const { env, ctx } = getCloudflareContext(); const rawConfig = configFn({ env, ctx }); const validationResult = NextJsCloudflareConfigSchema.safeParse(rawConfig); if (!validationResult.success) { console.error( printMessage( `Config validation failed: ${validationResult.error.message}` ) ); return NextResponse.next(); } const config = validationResult.data; const debugFn = debug(config.debug); const isHTML = isHTMLRequest(request); debugFn( `Appwarden middleware invoked for ${requestUrl.pathname}`, `isHTML: ${isHTML}` ); if (!isHTML) { return NextResponse.next(); } if (isOnLockPage(config.lockPageSlug, request.url)) { debugFn("Already on lock page - skipping"); return NextResponse.next(); } const result = await checkLockStatus({ request, appwardenApiToken: config.appwardenApiToken, appwardenApiHostname: config.appwardenApiHostname, debug: config.debug, lockPageSlug: config.lockPageSlug, waitUntil: ctx.waitUntil.bind(ctx) }); if (result.isLocked) { const lockPageUrl = buildLockPageUrl(config.lockPageSlug, request.url); debugFn(`Website is locked - redirecting to ${lockPageUrl.pathname}`); return NextResponse.redirect(lockPageUrl, TEMPORARY_REDIRECT_STATUS); } debugFn("Site is unlocked"); if (config.contentSecurityPolicy && config.contentSecurityPolicy.mode !== "disabled") { debugFn( `Applying CSP headers in ${config.contentSecurityPolicy.mode} mode` ); const [headerName, headerValue] = makeCSPHeader( "", config.contentSecurityPolicy.directives, config.contentSecurityPolicy.mode ); const response = NextResponse.next(); response.headers.set(headerName, headerValue); logElapsed(debugFn, startTime); return response; } logElapsed(debugFn, startTime); return NextResponse.next(); } catch (error) { console.error( printMessage( `Unhandled error: ${error instanceof Error ? error.message : String(error)}` ) ); return NextResponse.next(); } }; } export { createAppwardenMiddleware, getAppwardenConfiguration };