UNPKG

@appwarden/middleware

Version:

Instantly shut off access your app deployed on Cloudflare or Vercel

390 lines (372 loc) 11.2 kB
import { useContentSecurityPolicy } from "./chunk-NZNMFDZ7.js"; import { BooleanSchema, LockValue, MemoryCache, getErrors } from "./chunk-47MLTBFC.js"; import { APPWARDEN_CACHE_KEY, APPWARDEN_TEST_ROUTE, APPWARDEN_USER_AGENT, debug, printMessage } from "./chunk-JWUAFJ2E.js"; // src/runners/appwarden-on-cloudflare.ts import { ZodError } from "zod"; // src/schemas/cloudflare.ts import { z as z2 } from "zod"; // src/schemas/use-appwarden.ts import { z } from "zod"; var UseAppwardenInputSchema = z.object({ debug: BooleanSchema.default(false), lockPageSlug: z.string(), appwardenApiToken: z.string().refine((val) => !!val, { path: ["appwardenApiToken"] }) }); // src/schemas/cloudflare.ts var ConfigFnInputSchema = z2.function().args(z2.custom()).returns( UseAppwardenInputSchema.extend({ middleware: z2.object({ before: z2.custom().array().default([]) }).default({}) }) ); // src/utils/middleware.ts var usePipeline = (...initMiddlewares) => { const stack = [...initMiddlewares]; const execute = async (context) => { const runner = async (prevIndex, index) => { if (index === prevIndex) { throw new Error("next() called multiple times"); } if (index >= stack.length) { return; } const middleware = stack[index]; const next = async () => runner(index, index + 1); await middleware(context, next); }; await runner(-1, 0); }; return { execute }; }; // src/utils/render-lock-page.ts var renderLockPage = (context) => fetch(new URL(context.lockPageSlug, context.requestUrl.origin), { headers: { // no browser caching, otherwise we need to hard refresh to disable lock screen "Cache-Control": "no-store" } }); // src/utils/cloudflare/cloudflare-cache.ts var store = { json: (context, cacheKey, options) => { const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin); return { getValue: () => getCacheValue(context, cacheKeyUrl), updateValue: (json) => updateCacheValue(context, cacheKeyUrl, json, options?.ttl), deleteValue: () => clearCache(context, cacheKeyUrl) }; } }; var getCacheValue = async (context, cacheKey) => { const match = await context.cache.match(cacheKey); if (!match) { debug(`[${cacheKey.pathname}] Cache MISS!`); return void 0; } debug(`[${cacheKey.pathname}] Cache MATCH!`); return match; }; var updateCacheValue = async (context, cacheKey, value, ttl) => { debug( "updating cache...", cacheKey.href, value, ttl ? `expires in ${ttl}s` : "" ); await context.cache.put( cacheKey, new Response(JSON.stringify(value), { headers: { "content-type": "application/json", ...ttl && { "cache-control": `max-age=${ttl}` } } }) ); }; var clearCache = (context, cacheKey) => context.cache.delete(cacheKey); // src/utils/cloudflare/delete-edge-value.ts var deleteEdgeValue = async (context) => { try { switch (context.provider) { case "cloudflare-cache": { const success = await context.edgeCache.deleteValue(); if (!success) { throw new Error(); } break; } default: throw new Error(`Unsupported provider: ${context.provider}`); } } catch (e) { const message = "Failed to delete edge value"; console.error( printMessage(e instanceof Error ? `${message} - ${e.message}` : message) ); } }; // src/utils/cloudflare/get-lock-value.ts var getLockValue = async (context) => { try { let shouldDeleteEdgeValue = false; let cacheResponse, lockValue = { isLocked: 0, isLockedTest: 0, lastCheck: Date.now(), code: "" }; switch (context.provider) { case "cloudflare-cache": { cacheResponse = await context.edgeCache.getValue(); break; } default: throw new Error(`Unsupported provider: ${context.provider}`); } if (!cacheResponse) { return { lockValue: void 0 }; } try { const clonedResponse = cacheResponse?.clone(); lockValue = LockValue.parse( clonedResponse ? await clonedResponse.json() : void 0 ); } 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) ); return { lockValue: void 0 }; } }; // src/utils/cloudflare/insert-errors-logs.ts var insertErrorLogs = async (context, error) => { const errors = getErrors(error); for (const err of errors) { console.log(printMessage(err)); } return new HTMLRewriter().on("body", { element: (elem) => { elem.append( `<script> ${errors.map((err) => `console.error(\`${printMessage(err)}\`)`).join("\n")} </script>`, { html: true } ); } }).transform(await fetch(context.request)); }; // src/utils/cloudflare/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: "cloudflare", provider: context.provider, fqdn: context.requestUrl.hostname, 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); } if (!result.content) { throw new APIError("no content from api"); } try { const parsedValue = LockValue.omit({ lastCheck: true }).parse( result.content ); debug(`syncing with api...DONE ${JSON.stringify(parsedValue, null, 2)}`); await context.edgeCache.updateValue({ ...parsedValue, lastCheck: Date.now() }); } catch (error) { throw new APIError(`Failed to parse check endpoint result - ${error}`); } } } 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/handlers/maybe-quarantine.ts var resolveLockValue = async (context, options) => { const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context); if (shouldDeleteEdgeValue) { await deleteEdgeValue(context); } if (lockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(lockValue)) { await options.onLocked(); } return lockValue; }; var maybeQuarantine = async (context, options) => { const cachedLockValue = await resolveLockValue(context, { onLocked: options.onLocked }); const shouldRecheck = MemoryCache.isExpired(cachedLockValue); if (shouldRecheck) { if (!cachedLockValue || cachedLockValue.isLocked) { await syncEdgeValue(context); await resolveLockValue(context, { onLocked: options.onLocked }); } else { context.waitUntil(syncEdgeValue(context)); } } }; // src/handlers/reset-cache.ts var isResetCacheRequest = (request) => request.method === "POST" && new URL(request.url).pathname === "/__appwarden/reset-cache" && request.headers.get("content-type") === "application/json"; var handleResetCache = async (keyName, provider, edgeCache, request) => { const { lockValue } = await getLockValue({ keyName, provider, edgeCache }); try { const body = await request.clone().json(); if (body.code === lockValue?.code) { await edgeCache.deleteValue(); } } catch (error) { } }; // src/middlewares/use-appwarden.ts var useAppwarden = (input) => async (context, next) => { await next(); const { request, response } = context; try { const requestUrl = new URL(request.url); const provider = "cloudflare-cache"; const keyName = APPWARDEN_CACHE_KEY; const edgeCache = store.json( { serviceOrigin: requestUrl.origin, cache: await caches.open("appwarden:lock") }, keyName ); if (isResetCacheRequest(request)) { await handleResetCache(keyName, provider, edgeCache, request); return; } const isHTMLRequest = response.headers.get("Content-Type")?.includes("text/html"); const isMonitoringRequest = request.headers.get("User-Agent") === APPWARDEN_USER_AGENT; if (isHTMLRequest && !isMonitoringRequest) { const innerContext = { keyName, request, edgeCache, requestUrl, provider, debug: input.debug, lockPageSlug: input.lockPageSlug, appwardenApiToken: input.appwardenApiToken, waitUntil: (fn) => context.waitUntil(fn) }; await maybeQuarantine(innerContext, { onLocked: async () => { context.response = await renderLockPage(innerContext); } }); } } catch (e) { const message = "Appwarden encountered an unknown error. Please contact Appwarden support at https://appwarden.io/join-community."; console.error( printMessage( e instanceof Error ? `${message} - ${e.message}` : message ) ); } }; // src/middlewares/use-fetch-origin.ts var useFetchOrigin = () => async (context, next) => { context.response = await fetch( new Request(context.request, { ...context.request, redirect: "follow" }) ); await next(); }; // src/runners/appwarden-on-cloudflare.ts var appwardenOnCloudflare = (inputFn) => async (request, env, ctx) => { ctx.passThroughOnException(); const context = { request, hostname: new URL(request.url).host, response: new Response("Unhandled response"), // https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors waitUntil: (fn) => ctx.waitUntil(fn) }; const parsedInput = ConfigFnInputSchema.safeParse(inputFn); if (!parsedInput.success) { return insertErrorLogs(context, parsedInput.error); } try { const input = parsedInput.data({ env, ctx, cf: {} }); const pipeline = [ ...input.middleware.before, useAppwarden(input), useFetchOrigin() ]; await usePipeline(...pipeline).execute(context); } catch (error) { if (error instanceof ZodError) { return insertErrorLogs(context, error); } throw error; } return context.response; }; // src/bundles/cloudflare.ts var withAppwarden = appwardenOnCloudflare; export { useContentSecurityPolicy, withAppwarden };