/* 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 }; }