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