@appwarden/middleware
Version:
Instantly disable all user interaction with your app deployed on Cloudflare or Vercel
266 lines (259 loc) • 7.91 kB
JavaScript
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
};