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