/*
RFC-0624: CDN cache purge helpers for Leitstand commands — collects URLs from behavior snapshot and purges them via Cloudflare API.
- Do not add purge logic to the adapter — purge is at the command level.
- Do not purge zone-level — only URL-level purge is supported.
- RFC-0624: initial cache-purge helpers — collectPurgeUrls, purgeCacheByUrls with internal batching (max 30 per API call).
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1297: BUILD_IDENTITY_PATH re-exported from sternsystem/live-drift.ts (canonical home).
- RFC-1297: keep the worker bundle free of live-drift and engine kernel imports
*/
import type { RouteFact, PurgeResult } from "@warpgogol/werkstatt-engine/schemas";
// RFC-1297: canonical home is the import-free sternsystem/build-identity-path.ts —
// this module is reachable from the worker bundle (agent-gate health route), so it
// must never import live-drift (registry-io → engine kernel → forge → Node-only deps).
import { BUILD_IDENTITY_PATH } from "../sternsystem/build-identity-path.ts";
export { BUILD_IDENTITY_PATH };
const MAX_URLS_PER_BATCH = 30;
export function collectPurgeUrls(deploymentUrl: string, routes: RouteFact[]): string[] {
const base = deploymentUrl.replace(/\/$/, "");
const urls = routes.map((route) => `${base}${route.path}`);
urls.push(`${base}${BUILD_IDENTITY_PATH}`);
return urls;
}
export async function purgeCacheByUrls(
zoneId: string,
apiToken: string,
urls: string[],
): Promise {
if (urls.length === 0) {
return { success: true, purgedUrls: 0 };
}
const batches: string[][] = [];
for (let i = 0; i < urls.length; i += MAX_URLS_PER_BATCH) {
batches.push(urls.slice(i, i + MAX_URLS_PER_BATCH));
}
let totalPurged = 0;
for (const batch of batches) {
let response: Response;
try {
response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ files: batch }),
});
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
return {
success: false,
purgedUrls: totalPurged,
error: `Network error during purge: ${error}`,
};
}
if (!response.ok) {
const errorText = await response.text().catch(() => `HTTP ${response.status}`);
return {
success: false,
purgedUrls: totalPurged,
error: `Cloudflare purge API returned ${response.status}: ${errorText}`,
};
}
totalPurged += batch.length;
}
return { success: true, purgedUrls: totalPurged };
}
export async function verifyCloudflareToken(
zoneId: string,
apiToken: string,
): Promise<{ valid: boolean; error?: string }> {
try {
const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
if (response.status === 401 || response.status === 403) {
const body = await response.text().catch(() => `HTTP ${response.status}`);
return {
valid: false,
error: `Token invalid or expired (HTTP ${response.status}): ${body}`,
};
}
if (!response.ok) {
return {
valid: false,
error: `Unexpected response (HTTP ${response.status})`,
};
}
return { valid: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
return {
valid: false,
error: `Network error during token verification: ${error}`,
};
}
}
export function skippedPurgeResult(reason: string): PurgeResult {
return { success: false, purgedUrls: 0, error: reason };
}