@appwarden/middleware
Version:
Instantly shut off access your app deployed on Cloudflare or Vercel
390 lines (372 loc) • 11.2 kB
JavaScript
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
};