UNPKG

@appwarden/middleware

Version:

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

266 lines (259 loc) 7.91 kB
import { MemoryCache, debug, printMessage } from "./chunk-Q7ZBW3WZ.js"; import { APPWARDEN_CACHE_KEY, APPWARDEN_MIDDLEWARE_USER_AGENT, APPWARDEN_TEST_ROUTE, AppwardenApiHostnameSchema, LockValue } from "./chunk-3OXAKJPA.js"; // 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 request = new Request(cacheKey.href); const match = await context.cache.match(request); return match ?? void 0; }; var updateCacheValue = async (context, cacheKey, value, ttl) => { context.debug( "updating cache...", cacheKey.href, value, ttl ? `expires in ${ttl}s` : "" ); const response = new Response(JSON.stringify(value), { headers: { "content-type": "application/json", ...ttl && { "cache-control": `max-age=${ttl}` } } }); const request = new Request(cacheKey.href, { method: "GET" }); await context.cache.put(request, response); }; var clearCache = (context, cacheKey) => { const request = new Request(cacheKey.href); return context.cache.delete(request); }; // 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() }; 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/sync-edge-value.ts var APIError = class extends Error { constructor(message) { super(message); this.name = "APIError"; } }; var DEFAULT_API_HOSTNAME = "https://api.appwarden.io"; var API_TIMEOUT_MS = 1e4; function resolveApiHostname(hostname) { if (!hostname) return DEFAULT_API_HOSTNAME; const parsed = AppwardenApiHostnameSchema.safeParse(hostname); if (!parsed.success) return DEFAULT_API_HOSTNAME; return parsed.data; } var syncEdgeValue = async (context) => { const apiHostname = resolveApiHostname(context.appwardenApiHostname); context.debug(`GET ${apiHostname}`); 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: "cloudflare", provider: context.provider, fqdn: context.requestUrl.hostname, 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); } if (!result.content) { throw new APIError("no content from api"); } try { const parsedValue = LockValue.omit({ lastCheck: true }).parse( result.content ); context.debug(`GET ${apiHostname} succeeded`); await context.edgeCache.updateValue({ ...parsedValue, lastCheck: Date.now() }); } catch (error) { throw new APIError(`Failed to parse check endpoint result - ${error}`); } } } catch (e) { const message = `GET ${apiHostname} failed`; console.error( printMessage( e instanceof APIError ? e.message : e instanceof Error ? `${message}: ${e.message}` : message ) ); } finally { clearTimeout(timeout); } }; // src/core/check-lock-status.ts var createContext = async (config) => { const requestUrl = new URL(config.request.url); const keyName = APPWARDEN_CACHE_KEY; const provider = "cloudflare-cache"; const debugFn = debug(config.debug ?? false); const edgeCache = store.json( { serviceOrigin: requestUrl.origin, cache: await caches.open("appwarden:lock"), debug: debugFn, waitUntil: config.waitUntil }, keyName ); return { keyName, request: config.request, edgeCache, requestUrl, provider, debug: debugFn, lockPageSlug: config.lockPageSlug, appwardenApiToken: config.appwardenApiToken, appwardenApiHostname: config.appwardenApiHostname, waitUntil: config.waitUntil }; }; var resolveLockStatus = async (context) => { const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context); if (shouldDeleteEdgeValue) { context.debug("Deleting corrupted cache value"); await deleteEdgeValue(context); } const isTestRoute = context.requestUrl.pathname === APPWARDEN_TEST_ROUTE; const isTestLock = isTestRoute && !MemoryCache.isTestExpired(lockValue) && !!lockValue; return { isLocked: !!lockValue?.isLocked || isTestLock, isTestLock, lockValue, wasDeleted: shouldDeleteEdgeValue ?? false }; }; var checkLockStatus = async (config) => { const context = await createContext(config); let { isLocked, isTestLock, lockValue, wasDeleted } = await resolveLockStatus(context); const isExpired = MemoryCache.isExpired(lockValue); if (!isExpired && !wasDeleted && lockValue) { context.debug("Lock value resolved from cache"); } if (isExpired || wasDeleted) { if (!lockValue || wasDeleted || lockValue.isLocked) { context.debug( "No fresh cached lock status available - syncing with API synchronously" ); await syncEdgeValue(context); ({ isLocked, isTestLock } = await resolveLockStatus(context)); } else { context.debug( "Cached lock status expired but last known state unlocked - syncing with API in background" ); config.waitUntil(syncEdgeValue(context)); } } return { isLocked, isTestLock }; }; export { checkLockStatus };