UNPKG

@appwarden/middleware

Version:

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

367 lines (358 loc) 14.4 kB
// src/version.ts var MIDDLEWARE_VERSION = "3.16.6"; // src/constants.ts var LOCKDOWN_TEST_EXPIRY_MS = 5 * 60 * 1e3; var errors = { badCacheConnection: "BAD_CACHE_CONNECTION" }; var globalErrors = [errors.badCacheConnection]; var APPWARDEN_TEST_ROUTE = "/_appwarden/test"; var APPWARDEN_HEARTBEAT_ROUTE = "/_appwarden/heartbeat"; var HEARTBEAT_CONTRACT_VERSION = 1; var HEARTBEAT_VERSION_MAX_LENGTH = 128; var HEARTBEAT_CONFIG_ERROR_MAX_COUNT = 10; var HEARTBEAT_CONFIG_ERROR_MAX_PATH_DEPTH = 10; var HEARTBEAT_CONFIG_ERROR_MAX_CODE_LENGTH = 100; var HEARTBEAT_CONFIG_ERROR_MAX_MESSAGE_LENGTH = 500; var HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES = 12 * 1024; var HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES = 32 * 1024; var HEARTBEAT_CONFIG_ERROR_MAX_PATH_SEGMENT_LENGTH = 100; var APPWARDEN_CACHE_KEY = "appwarden-lock"; var APPWARDEN_MIDDLEWARE_USER_AGENT_PREFIX = "Appwarden/"; var APPWARDEN_MIDDLEWARE_USER_AGENT = `${APPWARDEN_MIDDLEWARE_USER_AGENT_PREFIX}${MIDDLEWARE_VERSION}`; var HEARTBEAT_SERVICE_VALUES = [ "cloudflare", "cloudflare-astro", "cloudflare-react-router", "cloudflare-tanstack-start", "cloudflare-nextjs", "vercel" ]; var [ CLOUDFLARE, CLOUDFLARE_ASTRO, CLOUDFLARE_REACT_ROUTER, CLOUDFLARE_TANSTACK_START, CLOUDFLARE_NEXTJS, VERCEL ] = HEARTBEAT_SERVICE_VALUES; var HEARTBEAT_SERVICES = { CLOUDFLARE, CLOUDFLARE_ASTRO, CLOUDFLARE_REACT_ROUTER, CLOUDFLARE_TANSTACK_START, CLOUDFLARE_NEXTJS, VERCEL }; // src/types/heartbeat.ts import { z } from "zod"; function getSerializedJsonByteLength(value) { return new TextEncoder().encode(JSON.stringify(value)).length; } function getHeartbeatResponseBodyByteBudgetIssue() { return { code: z.ZodIssueCode.custom, message: `Serialized heartbeat response body must be at most ${HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES} bytes`, path: [] }; } function getHeartbeatResponseBodySerializationIssue() { return { code: z.ZodIssueCode.custom, message: "Heartbeat response body must be JSON-serializable", path: [] }; } var HeartbeatConfigErrorPathSegmentSchema = z.union([ z.string().max(HEARTBEAT_CONFIG_ERROR_MAX_PATH_SEGMENT_LENGTH), z.number().int().nonnegative() ]); var HeartbeatConfigErrorSchema = z.object({ path: z.array(HeartbeatConfigErrorPathSegmentSchema).max(HEARTBEAT_CONFIG_ERROR_MAX_PATH_DEPTH), code: z.string().min(1).max(HEARTBEAT_CONFIG_ERROR_MAX_CODE_LENGTH), message: z.string().min(1).max(HEARTBEAT_CONFIG_ERROR_MAX_MESSAGE_LENGTH) }).strict(); var HeartbeatConfigErrorsSchema = z.array(HeartbeatConfigErrorSchema).max(HEARTBEAT_CONFIG_ERROR_MAX_COUNT).superRefine((configErrors, ctx) => { if (getSerializedJsonByteLength(configErrors) > HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Serialized configErrors payload must be at most ${HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES} bytes` }); } }); var HeartbeatResponseBodySchema = z.object({ app: z.literal("appwarden"), kind: z.literal("heartbeat"), status: z.literal("ok"), contractVersion: z.literal(HEARTBEAT_CONTRACT_VERSION), service: z.enum(HEARTBEAT_SERVICE_VALUES), version: z.string().min(1).max(HEARTBEAT_VERSION_MAX_LENGTH), configErrors: HeartbeatConfigErrorsSchema }).strict().superRefine((body, ctx) => { if (getSerializedJsonByteLength(body) > HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Serialized heartbeat response body must be at most ${HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES} bytes` }); } }); function validateHeartbeatResponseBody(value) { let serializedJsonByteLength; try { serializedJsonByteLength = getSerializedJsonByteLength(value); } catch { throw new z.ZodError([getHeartbeatResponseBodySerializationIssue()]); } if (serializedJsonByteLength > HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES) { throw new z.ZodError([getHeartbeatResponseBodyByteBudgetIssue()]); } return HeartbeatResponseBodySchema.parse(value); } // src/schemas/use-content-security-policy.ts import { z as z4 } from "zod"; // src/types/csp.ts import { z as z2 } from "zod"; var stringySchema = z2.union([z2.array(z2.string()), z2.string(), z2.boolean()]); var ContentSecurityPolicySchema = z2.object({ "default-src": stringySchema.optional(), "script-src": stringySchema.optional(), "style-src": stringySchema.optional(), "img-src": stringySchema.optional(), "connect-src": stringySchema.optional(), "font-src": stringySchema.optional(), "object-src": stringySchema.optional(), "media-src": stringySchema.optional(), "frame-src": stringySchema.optional(), sandbox: stringySchema.optional(), "report-uri": stringySchema.optional(), "child-src": stringySchema.optional(), "form-action": stringySchema.optional(), "frame-ancestors": stringySchema.optional(), "plugin-types": stringySchema.optional(), "base-uri": stringySchema.optional(), "report-to": stringySchema.optional(), "worker-src": stringySchema.optional(), "manifest-src": stringySchema.optional(), "prefetch-src": stringySchema.optional(), "navigate-to": stringySchema.optional(), "require-sri-for": stringySchema.optional(), "block-all-mixed-content": stringySchema.optional(), "upgrade-insecure-requests": stringySchema.optional(), "trusted-types": stringySchema.optional(), "require-trusted-types-for": stringySchema.optional() }); // src/utils/errors.ts var AppwardenConfigErrorMessages = { ["APPWARDEN_API_TOKEN_MISSING" /* AppwardenApiTokenMissing */]: "APPWARDEN_API_TOKEN is missing or empty. Learn more at https://appwarden.com/docs/guides/api-token-management.", ["NEXTJS_NONCE_UNSUPPORTED" /* NextJsNonceUnsupported */]: "Nonce-based CSP is not supported in the Next.js Cloudflare adapter. Remove '{{nonce}}' placeholders from your CSP directives, as this adapter does not inject nonces into HTML.", ["VERCEL_NONCE_UNSUPPORTED" /* VercelNonceUnsupported */]: "Nonce-based CSP is not supported in Vercel Edge Middleware. Remove '{{nonce}}' placeholders from your CSP directives, as Vercel does not support nonce injection.", ["APPWARDEN_API_HOSTNAME_INVALID_URL" /* AppwardenApiHostnameInvalidUrl */]: "Invalid `appwardenApiHostname`. Please provide an absolute URL (e.g. https://api.appwarden.io).", ["APPWARDEN_API_HOSTNAME_MUST_USE_HTTPS" /* AppwardenApiHostnameMustUseHttps */]: "`appwardenApiHostname` must use the https:// scheme (e.g. https://api.appwarden.io).", ["APPWARDEN_API_HOSTNAME_MUST_BE_APPWARDEN" /* AppwardenApiHostnameMustBeAppwarden */]: "`appwardenApiHostname` must be https://api.appwarden.io or https://staging-api.appwarden.io.", ["LOCK_PAGE_SLUG_MUST_BE_RELATIVE_PATH" /* LockPageSlugMustBeRelativePath */]: "lockPageSlug must be a relative path", ["LOCK_PAGE_SLUG_REQUIRED" /* LockPageSlugRequired */]: "lockPageSlug must be provided", ["CSP_MODE_INVALID" /* CspModeInvalid */]: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"', ["CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?", ["CSP_DIRECTIVES_REQUIRED" /* CspDirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"', ["CACHE_URL_UNRECOGNIZED" /* CacheUrlUnrecognized */]: "Provided `cacheUrl` is not recognized. Please provide a Vercel Edge Config or Upstash KV url.", ["CACHE_URL_INVALID_EDGE_CONFIG" /* CacheUrlInvalidEdgeConfig */]: "Provided Vercel Edge Config `cacheUrl` is not valid. It should be in the format https://edge-config.vercel.com/ecfg_*", ["CACHE_URL_INVALID_UPSTASH" /* CacheUrlInvalidUpstash */]: "Provided Upstash KV `cacheUrl` is not valid. It should be in the format rediss://:password@hostname.upstash.io:6379", ["VERCEL_API_TOKEN_REQUIRED" /* VercelApiTokenRequired */]: "The `vercelApiToken` option is required when using Vercel Edge Config", ["BOOLEAN_INVALID" /* BooleanInvalid */]: "Invalid value. Must be a boolean or one of 'true', 'false'." }; function getAppwardenErrorKey(issue) { const key = issue.params?.appwardenErrorKey; if (typeof key === "string" && Object.prototype.hasOwnProperty.call(AppwardenConfigErrorMessages, key)) { return key; } return void 0; } var errorsMap = { mode: AppwardenConfigErrorMessages["CSP_MODE_INVALID" /* CspModeInvalid */], directives: { ["DirectivesRequired" /* DirectivesRequired */]: AppwardenConfigErrorMessages["CSP_DIRECTIVES_REQUIRED" /* CspDirectivesRequired */], ["DirectivesBadParse" /* DirectivesBadParse */]: AppwardenConfigErrorMessages["CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */] }, appwardenApiToken: AppwardenConfigErrorMessages["APPWARDEN_API_TOKEN_MISSING" /* AppwardenApiTokenMissing */] }; var getErrors = (error) => { const matches = /* @__PURE__ */ new Set(); const collectKeyedMessages = (issues) => { for (const issue of issues) { const key = getAppwardenErrorKey(issue); if (key) { matches.add(AppwardenConfigErrorMessages[key]); } if ("returnTypeError" in issue && issue.returnTypeError && Array.isArray(issue.returnTypeError.issues)) { collectKeyedMessages(issue.returnTypeError.issues); } } }; collectKeyedMessages(error.issues); const errors2 = [...Object.entries(error.flatten().fieldErrors)]; for (const issue of error.issues) { errors2.push( ...Object.entries( "returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {} ) ); } for (const [field, maybeSchemaErrorKey] of errors2) { let match = errorsMap[field]; if (match) { if (match instanceof Object) { if (maybeSchemaErrorKey) { match = match[maybeSchemaErrorKey[0]]; } else { match = void 0; } } if (typeof match === "string") { matches.add(match); } } } return [...matches]; }; // src/schemas/helpers.ts import { z as z3 } from "zod"; var BoolOrStringSchema = z3.union([z3.string(), z3.boolean()]).optional(); var BooleanSchema = BoolOrStringSchema.refine( (val) => val === "true" || val === true || val === "false" || val === false, { message: AppwardenConfigErrorMessages["BOOLEAN_INVALID" /* BooleanInvalid */], params: { appwardenErrorKey: "BOOLEAN_INVALID" /* BooleanInvalid */ } } ).transform((val) => { if (val === "true" || val === true) { return true; } return false; }); var AppwardenApiTokenSchema = z3.preprocess( (val) => val === void 0 || val === null ? "" : val, z3.string() ).refine((val) => val.trim().length > 0, { message: AppwardenConfigErrorMessages["APPWARDEN_API_TOKEN_MISSING" /* AppwardenApiTokenMissing */], params: { appwardenErrorKey: "APPWARDEN_API_TOKEN_MISSING" /* AppwardenApiTokenMissing */ } }); var AppwardenApiHostnameSchema = z3.string().refine( (value) => { try { new URL(value); return true; } catch { return false; } }, { message: AppwardenConfigErrorMessages["APPWARDEN_API_HOSTNAME_INVALID_URL" /* AppwardenApiHostnameInvalidUrl */], params: { appwardenErrorKey: "APPWARDEN_API_HOSTNAME_INVALID_URL" /* AppwardenApiHostnameInvalidUrl */ } } ).refine((value) => value.startsWith("https://"), { message: AppwardenConfigErrorMessages["APPWARDEN_API_HOSTNAME_MUST_USE_HTTPS" /* AppwardenApiHostnameMustUseHttps */], params: { appwardenErrorKey: "APPWARDEN_API_HOSTNAME_MUST_USE_HTTPS" /* AppwardenApiHostnameMustUseHttps */ } }).refine( (value) => { try { const hostname = new URL(value).hostname; return hostname === "api.appwarden.io" || hostname === "staging-api.appwarden.io"; } catch { return false; } }, { message: AppwardenConfigErrorMessages["APPWARDEN_API_HOSTNAME_MUST_BE_APPWARDEN" /* AppwardenApiHostnameMustBeAppwarden */], params: { appwardenErrorKey: "APPWARDEN_API_HOSTNAME_MUST_BE_APPWARDEN" /* AppwardenApiHostnameMustBeAppwarden */ } } ); var LockValue = z3.object({ isLocked: z3.number(), isLockedTest: z3.number(), lastCheck: z3.number() }); var ValidLockPageSlugSchema = z3.string().refine( (val) => !val.includes("://") && !val.startsWith("//") && !val.includes("\\"), { message: AppwardenConfigErrorMessages["LOCK_PAGE_SLUG_MUST_BE_RELATIVE_PATH" /* LockPageSlugMustBeRelativePath */], params: { appwardenErrorKey: "LOCK_PAGE_SLUG_MUST_BE_RELATIVE_PATH" /* LockPageSlugMustBeRelativePath */ } } ); // src/schemas/use-content-security-policy.ts var CSPDirectivesSchema = z4.union([ z4.string(), ContentSecurityPolicySchema ]); var CSPModeSchema = z4.union([ z4.literal("disabled"), z4.literal("report-only"), z4.literal("enforced") ]); var UseCSPInputSchema = z4.object({ mode: CSPModeSchema, directives: CSPDirectivesSchema.refine( (val) => { try { if (typeof val === "string") { JSON.parse(val); } return true; } catch (error) { return false; } }, { message: AppwardenConfigErrorMessages["CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */], params: { appwardenErrorKey: "CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */ } } ).transform( (val) => typeof val === "string" ? JSON.parse(val) : val ) }); export { MIDDLEWARE_VERSION, LOCKDOWN_TEST_EXPIRY_MS, errors, globalErrors, APPWARDEN_TEST_ROUTE, APPWARDEN_HEARTBEAT_ROUTE, HEARTBEAT_CONTRACT_VERSION, HEARTBEAT_VERSION_MAX_LENGTH, HEARTBEAT_CONFIG_ERROR_MAX_COUNT, HEARTBEAT_CONFIG_ERROR_MAX_PATH_DEPTH, HEARTBEAT_CONFIG_ERROR_MAX_CODE_LENGTH, HEARTBEAT_CONFIG_ERROR_MAX_MESSAGE_LENGTH, HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES, HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES, HEARTBEAT_CONFIG_ERROR_MAX_PATH_SEGMENT_LENGTH, APPWARDEN_CACHE_KEY, APPWARDEN_MIDDLEWARE_USER_AGENT_PREFIX, APPWARDEN_MIDDLEWARE_USER_AGENT, HEARTBEAT_SERVICE_VALUES, HEARTBEAT_SERVICES, AppwardenConfigErrorMessages, getErrors, BooleanSchema, AppwardenApiTokenSchema, AppwardenApiHostnameSchema, LockValue, ValidLockPageSlugSchema, HeartbeatConfigErrorSchema, HeartbeatResponseBodySchema, validateHeartbeatResponseBody, CSPDirectivesSchema, CSPModeSchema, UseCSPInputSchema };