@appwarden/middleware
Version:
Instantly disable all user interaction with your app deployed on Cloudflare or Vercel
198 lines (194 loc) • 6.07 kB
JavaScript
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
};