UNPKG

@appwarden/middleware

Version:

Instantly shut off access your app deployed on Cloudflare or Vercel

342 lines (332 loc) 10.3 kB
import { getEdgeConfigId, isCacheUrl, isValidCacheUrl } from "./chunk-QEFORWCW.js"; import { LockValue, MemoryCache, getErrors } from "./chunk-47MLTBFC.js"; import { APPWARDEN_CACHE_KEY, APPWARDEN_TEST_ROUTE, APPWARDEN_USER_AGENT, debug, errors, globalErrors, printMessage } from "./chunk-JWUAFJ2E.js"; // src/runners/appwarden-on-vercel.ts import { NextResponse } from "next/server"; // src/schemas/vercel.ts import { z } from "zod"; // src/utils/vercel/delete-edge-value.ts var deleteEdgeValue = async ({ keyName, provider, cacheUrl, vercelApiToken }) => { try { switch (provider) { case "edge-config": { const edgeConfigId = getEdgeConfigId(cacheUrl); if (!edgeConfigId) { throw new Error("Failed to parse `edgeConfigId`"); } const res = await fetch( `https://api.vercel.com/v1/edge-config/${edgeConfigId}/items`, { method: "PATCH", headers: { Authorization: `Bearer ${vercelApiToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { key: keyName, operation: "delete" } ] }) } ); if (res.status !== 200) { let response = void 0; try { response = await res.json(); } catch (error) { } throw new Error( `api.vercel.com/v1/edge-config responded with ${res.status} - ${res.statusText}${response?.error?.message ? ` - ${response?.error?.message}` : ""}` ); } break; } case "upstash": { const { hostname, password } = new URL(cacheUrl); const { Redis } = await import("@upstash/redis"); const redis = new Redis({ url: `https://${hostname}`, token: password }); await redis.del(keyName); break; } default: throw new Error(`Unsupported provider: ${provider}`); } } catch (e) { const message = "Failed to delete edge value"; console.error( printMessage(e instanceof Error ? `${message} - ${e.message}` : message) ); } }; // src/utils/vercel/get-lock-value.ts var getLockValue = async (context) => { try { let shouldDeleteEdgeValue = false; let serializedValue, lockValue = { isLocked: 0, isLockedTest: 0, lastCheck: 0, code: "" }; 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 syncEdgeValue = async (context) => { debug(`syncing with api`); try { const response = await fetch(new URL("/v1/status/check", "https://bot-gateway.appwarden.io"), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ service: "vercel", cacheUrl: context.cacheUrl, fqdn: context.requestUrl.hostname, vercelApiToken: context.vercelApiToken ?? "", appwardenApiToken: context.appwardenApiToken }) }); 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 ) ); } }; // src/utils/handle-vercel-request.ts var handleVercelRequest = async (context, options) => { let cachedLockValue = context.memoryCache.get(context.keyName); const shouldRecheck = MemoryCache.isExpired(cachedLockValue); if (shouldRecheck) { const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context); if (lockValue) { context.memoryCache.put(context.keyName, lockValue); } if (shouldDeleteEdgeValue) { await deleteEdgeValue(context); } cachedLockValue = lockValue; } if (cachedLockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(cachedLockValue)) { options.onLocked(); } return cachedLockValue; }; // src/schemas/vercel.ts var BaseNextJsConfigSchema = z.object({ cacheUrl: z.string(), appwardenApiToken: z.string(), vercelApiToken: z.string().optional(), lockPageSlug: z.string().default("").transform((val) => val.replace(/^\/?/, "/")) }); var AppwardenConfigSchema = BaseNextJsConfigSchema.refine( (data) => { return isCacheUrl.edgeConfig(data.cacheUrl) || isCacheUrl.upstash(data.cacheUrl); }, { message: printMessage( "Provided `cacheUrl` is not recognized. Please provide a Vercel Edge Config or Upstash KV url." ), path: ["cacheUrl"] } ).superRefine((data, ctx) => { if (!isValidCacheUrl.edgeConfig(data.cacheUrl) && isValidCacheUrl.upstash(data.cacheUrl)) { ctx.addIssue({ code: "custom", message: printMessage( "Provided Vercel Edge Config `cacheUrl` is not valid. It should be in the format https://edge-config.vercel.com/ecfg_*" ), path: ["cacheUrl"] }); return false; } if (!isValidCacheUrl.upstash(data.cacheUrl) && isValidCacheUrl.edgeConfig(data.cacheUrl)) { ctx.addIssue({ code: "custom", message: printMessage( "Provided Upstash KV `cacheUrl` is not valid. It should be in the format rediss://:password@hostname.upstash.io:6379" ), path: ["cacheUrl"] }); return false; } return true; }).refine( (data) => isCacheUrl.edgeConfig(data.cacheUrl) ? !!data.vercelApiToken : true, { message: printMessage( "The `vercelApiToken` option is required when using Vercel Edge Config" ), path: ["vercelApiToken"] } ).refine((data) => !!data.appwardenApiToken, { message: printMessage( "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management." ), path: ["appwardenApiToken"] }); // src/runners/appwarden-on-vercel.ts debug("Instantiating isolate"); var renderLockPage = (context) => { context.req.nextUrl.pathname = context.lockPageSlug; return NextResponse.rewrite(context.req.nextUrl, { headers: { // no browser caching, otherwise we need to hard refresh to disable lock screen "Cache-Control": "no-store" } }); }; var memoryCache = new MemoryCache({ maxSize: 1 }); var appwardenOnVercel = (input) => async (req, event) => { event.passThroughOnException(); const parsedConfig = AppwardenConfigSchema.safeParse(input); if (!parsedConfig.success) { for (const error of getErrors(parsedConfig.error)) { console.error(printMessage(error)); } return NextResponse.next(); } try { const requestUrl = new URL(req.url); const isHTMLRequest = req.headers.get("accept")?.includes("text/html"); const isMonitoringRequest = req.headers.get("User-Agent") === APPWARDEN_USER_AGENT; debug({ isHTMLRequest, url: requestUrl.pathname }); if (isHTMLRequest && !isMonitoringRequest) { let appwardenResponse = void 0; const context = { req, event, requestUrl, memoryCache, waitUntil: (fn) => event.waitUntil(fn), keyName: APPWARDEN_CACHE_KEY, provider: isCacheUrl.edgeConfig(parsedConfig.data.cacheUrl) ? "edge-config" : "upstash", ...parsedConfig.data }; const cacheValue = await handleVercelRequest(context, { onLocked: () => { appwardenResponse = renderLockPage(context); } }); const shouldRecheck = MemoryCache.isExpired(cacheValue); if (!cacheValue || shouldRecheck) { event.waitUntil(syncEdgeValue(context)); } if (appwardenResponse) { return appwardenResponse; } } } catch (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)); } throw e; } return NextResponse.next(); }; // src/bundles/vercel.ts var withAppwarden = appwardenOnVercel; export { BaseNextJsConfigSchema, withAppwarden };