@appwarden/middleware
Version:
Instantly disable all user interaction with your app deployed on Cloudflare or Vercel
419 lines (411 loc) • 12.8 kB
JavaScript
import {
isCacheUrl,
isValidCacheUrl
} from "./chunk-QEFORWCW.js";
import {
toNextResponse
} from "./chunk-HUWGPM4M.js";
import {
MemoryCache,
TEMPORARY_REDIRECT_STATUS,
buildLockPageUrl,
debug,
handleHeartbeatRequest,
isHTMLRequest,
isHeartbeatRequest,
isOnLockPage,
makeCSPHeader,
parseMergedConfig,
printMessage,
sanitizeConfigErrors
} from "./chunk-Q7ZBW3WZ.js";
import {
APPWARDEN_CACHE_KEY,
APPWARDEN_MIDDLEWARE_USER_AGENT,
AppwardenApiHostnameSchema,
AppwardenApiTokenSchema,
AppwardenConfigErrorMessages,
CSPDirectivesSchema,
CSPModeSchema,
HEARTBEAT_SERVICES,
LockValue,
ValidLockPageSlugSchema,
errors,
getErrors,
globalErrors
} from "./chunk-3OXAKJPA.js";
// src/runners/appwarden-on-vercel.ts
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";
// src/schemas/vercel.ts
import { z } from "zod";
// src/utils/vercel/get-lock-value.ts
var getLockValue = async (context) => {
try {
let shouldDeleteEdgeValue = false;
let serializedValue, lockValue = {
isLocked: 0,
isLockedTest: 0,
lastCheck: 0
};
switch (context.provider) {
case "edge-config": {
const { createClient } = await import("@vercel/edge-config");
const edgeConfig = createClient(context.cacheUrl);
serializedValue = await edgeConfig.get(
context.keyName
);
break;
}
case "upstash": {
const { hostname, password } = new URL(context.cacheUrl);
const { Redis } = await import("@upstash/redis");
const redis = new Redis({
url: `https://${hostname}`,
token: password
});
const redisValue = await redis.get(
context.keyName
);
serializedValue = redisValue === null ? void 0 : redisValue;
break;
}
default:
throw new Error(`Unsupported provider: ${context.provider}`);
}
if (!serializedValue) {
return { lockValue: void 0 };
}
try {
lockValue = LockValue.parse(
typeof serializedValue === "string" ? JSON.parse(serializedValue) : serializedValue
);
} catch (error) {
console.error(
printMessage(
`Failed to parse ${context.keyName} from edge cache - ${error}`
)
);
shouldDeleteEdgeValue = true;
}
return { lockValue, shouldDeleteEdgeValue };
} catch (e) {
const message = "Failed to retrieve edge value";
console.error(
printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
);
if (e instanceof Error) {
if (e.message.includes("Invalid connection string provided")) {
throw new Error(errors.badCacheConnection);
}
}
return { lockValue: void 0 };
}
};
// src/utils/vercel/sync-edge-value.ts
var APIError = class extends Error {
constructor(message) {
super(message);
this.name = "APIError";
}
};
var API_TIMEOUT_MS = 1e4;
function resolveApiHostname(hostname) {
if (!hostname) {
return "https://api.appwarden.io";
}
const parsed = AppwardenApiHostnameSchema.safeParse(hostname);
if (!parsed.success) {
return "https://api.appwarden.io";
}
return parsed.data;
}
var syncEdgeValue = async (context) => {
context.debug("syncing with api");
const apiHostname = resolveApiHostname(context.appwardenApiHostname);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
try {
const response = await fetch(new URL("/v1/appwarden/status", apiHostname), {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": APPWARDEN_MIDDLEWARE_USER_AGENT
},
body: JSON.stringify({
service: "vercel",
cacheUrl: context.cacheUrl,
fqdn: context.requestUrl.hostname,
vercelApiToken: context.vercelApiToken ?? "",
appwardenApiToken: context.appwardenApiToken
}),
signal: controller.signal
});
if (response.status === 403) {
console.log(
printMessage(
"Verifying domain ownership... this will only take a few minutes."
)
);
}
if (response.status !== 200) {
throw new Error(`${response.status} ${response.statusText}`);
}
if (response.headers.get("content-type")?.includes("application/json")) {
const result = await response.json();
if (result.error) {
throw new APIError(result.error.message);
}
}
} catch (e) {
const message = "Failed to fetch from check endpoint";
console.error(
printMessage(
e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
)
);
} finally {
clearTimeout(timeout);
}
};
// src/utils/validate-config.ts
function validateConfig(config, schema) {
const result = schema.safeParse(config);
const hasErrors = !result.success;
if (hasErrors) {
const mappedErrors = getErrors(result.error);
if (mappedErrors.length > 0) {
for (const error of mappedErrors) {
console.error(printMessage(error));
}
} else {
console.error(printMessage(result.error.message));
}
}
return hasErrors;
}
// src/schemas/vercel.ts
var VercelCSPSchema = z.object({
mode: CSPModeSchema,
directives: z.lazy(() => CSPDirectivesSchema).refine(
(val) => {
try {
if (typeof val === "string") {
JSON.parse(val);
}
return true;
} catch {
return false;
}
},
{
message: AppwardenConfigErrorMessages["CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */],
params: {
appwardenErrorKey: "CSP_DIRECTIVES_BAD_PARSE" /* CspDirectivesBadParse */
}
}
).refine(
(val) => {
const serialized = typeof val === "string" ? val : JSON.stringify(val);
return !serialized.includes("{{nonce}}");
},
{
message: AppwardenConfigErrorMessages["VERCEL_NONCE_UNSUPPORTED" /* VercelNonceUnsupported */],
params: {
appwardenErrorKey: "VERCEL_NONCE_UNSUPPORTED" /* VercelNonceUnsupported */
}
}
).transform(
(val) => typeof val === "string" ? JSON.parse(val) : val
)
});
var BaseNextJsConfigSchema = z.object({
cacheUrl: z.string(),
appwardenApiToken: AppwardenApiTokenSchema,
appwardenApiHostname: AppwardenApiHostnameSchema.optional(),
vercelApiToken: z.string().optional(),
debug: z.boolean().optional(),
lockPageSlug: ValidLockPageSlugSchema.default("").transform(
(val) => val.replace(/^\/?/, "/")
),
contentSecurityPolicy: VercelCSPSchema.optional()
});
var AppwardenConfigSchema = BaseNextJsConfigSchema.refine(
(data) => {
return isCacheUrl.edgeConfig(data.cacheUrl) || isCacheUrl.upstash(data.cacheUrl);
},
{
message: AppwardenConfigErrorMessages["CACHE_URL_UNRECOGNIZED" /* CacheUrlUnrecognized */],
params: {
appwardenErrorKey: "CACHE_URL_UNRECOGNIZED" /* CacheUrlUnrecognized */
},
path: ["cacheUrl"]
}
).superRefine((data, ctx) => {
if (isCacheUrl.edgeConfig(data.cacheUrl) && !isValidCacheUrl.edgeConfig(data.cacheUrl)) {
ctx.addIssue({
code: "custom",
message: AppwardenConfigErrorMessages["CACHE_URL_INVALID_EDGE_CONFIG" /* CacheUrlInvalidEdgeConfig */],
params: {
appwardenErrorKey: "CACHE_URL_INVALID_EDGE_CONFIG" /* CacheUrlInvalidEdgeConfig */
},
path: ["cacheUrl"]
});
return false;
}
if (isCacheUrl.upstash(data.cacheUrl) && !isValidCacheUrl.upstash(data.cacheUrl)) {
ctx.addIssue({
code: "custom",
message: AppwardenConfigErrorMessages["CACHE_URL_INVALID_UPSTASH" /* CacheUrlInvalidUpstash */],
params: {
appwardenErrorKey: "CACHE_URL_INVALID_UPSTASH" /* CacheUrlInvalidUpstash */
},
path: ["cacheUrl"]
});
return false;
}
return true;
}).refine(
(data) => isCacheUrl.edgeConfig(data.cacheUrl) ? !!data.vercelApiToken : true,
{
message: AppwardenConfigErrorMessages["VERCEL_API_TOKEN_REQUIRED" /* VercelApiTokenRequired */],
params: {
appwardenErrorKey: "VERCEL_API_TOKEN_REQUIRED" /* VercelApiTokenRequired */
},
path: ["vercelApiToken"]
}
);
// src/runners/appwarden-on-vercel.ts
function getAppwardenConfiguration(generatedConfig, config) {
return parseMergedConfig(
generatedConfig,
config,
AppwardenConfigSchema.parse
);
}
var memoryCache = new MemoryCache({ maxSize: 1 });
function safeWaitUntil(promise) {
try {
waitUntil(promise);
} catch {
promise.catch(console.error);
}
}
function createAppwardenMiddleware(config) {
return async (request) => {
const requestUrl = new URL(request.url);
if (isHeartbeatRequest(request, requestUrl)) {
const validationResult = AppwardenConfigSchema.safeParse(config);
const configErrors = validationResult.success ? [] : sanitizeConfigErrors(validationResult.error);
const response = handleHeartbeatRequest(
request,
HEARTBEAT_SERVICES.VERCEL,
configErrors
);
return toNextResponse(response);
}
if (validateConfig(config, AppwardenConfigSchema)) {
return NextResponse.next();
}
const parsedConfig = AppwardenConfigSchema.parse(config);
const debugFn = debug(parsedConfig.debug ?? false);
const applyCspHeaders = (response) => {
const cspConfig = parsedConfig.contentSecurityPolicy;
if (cspConfig && ["enforced", "report-only"].includes(cspConfig.mode)) {
const [headerName, headerValue] = makeCSPHeader(
"",
cspConfig.directives,
cspConfig.mode
);
response.headers.set(headerName, headerValue);
}
return response;
};
const createMutableRedirectResponse = (location) => {
return new Response(null, {
status: TEMPORARY_REDIRECT_STATUS,
headers: {
Location: location
}
});
};
try {
const isHTML = isHTMLRequest(request);
debugFn(
`Appwarden middleware invoked for ${requestUrl.pathname}`,
`isHTML: ${isHTML}`
);
if (!isHTML) {
debugFn("Non-HTML request detected - passing through");
return NextResponse.next();
}
if (!parsedConfig.lockPageSlug) {
debugFn("No lockPageSlug configured - passing through");
return NextResponse.next();
}
if (isOnLockPage(parsedConfig.lockPageSlug, request.url)) {
debugFn("Already on lock page - passing through");
return NextResponse.next();
}
const provider = isCacheUrl.edgeConfig(parsedConfig.cacheUrl) ? "edge-config" : "upstash";
debugFn(`Using provider: ${provider}`);
const cacheValue = memoryCache.get(APPWARDEN_CACHE_KEY);
const shouldRecheck = MemoryCache.isExpired(cacheValue);
if (!cacheValue || shouldRecheck) {
debugFn(
"Memory cache miss or expired - syncing edge value in background",
`shouldRecheck=${shouldRecheck}`
);
safeWaitUntil(
syncEdgeValue({
requestUrl,
cacheUrl: parsedConfig.cacheUrl,
appwardenApiToken: parsedConfig.appwardenApiToken,
appwardenApiHostname: parsedConfig.appwardenApiHostname,
vercelApiToken: parsedConfig.vercelApiToken,
debug: debugFn
})
);
}
const lockValue = cacheValue ?? (await getLockValue({
cacheUrl: parsedConfig.cacheUrl,
keyName: APPWARDEN_CACHE_KEY,
provider
})).lockValue;
if (lockValue?.isLocked) {
debugFn(
`Website is locked - redirecting to ${parsedConfig.lockPageSlug}`
);
const lockPageUrl = buildLockPageUrl(
parsedConfig.lockPageSlug,
request.url
);
const redirectResponse = createMutableRedirectResponse(
lockPageUrl.toString()
);
return applyCspHeaders(redirectResponse);
}
const response = NextResponse.next();
debugFn("Site is not locked - passing through");
return applyCspHeaders(response);
} catch (e) {
debugFn(
"Error in Appwarden Vercel middleware",
e instanceof Error ? e.message : String(e)
);
const message = "Appwarden encountered an unknown error. Please contact Appwarden support at https://appwarden.io/join-community.";
if (e instanceof Error) {
if (!globalErrors.includes(e.message)) {
console.error(printMessage(`${message} - ${e.message}`));
}
} else {
console.error(printMessage(message));
}
return NextResponse.next();
}
};
}
export {
createAppwardenMiddleware,
getAppwardenConfiguration
};