// Operator-run remediation of an existing multi-tenant install. Registers every // existing D1 database and R2 bucket to its owning account, relocates the master // token into the house-only config file, and strips every account-wide token // from every sub-account secrets file. Fails closed on any enumerated resource // the operator did not map: ownership is never guessed. // // runRemediation is the testable core (all effects injected or against a given // directory). main() wires the real deps and is the on-device entry the shell // wrapper calls. import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { validateMapping, stripAccountWideTokens } from "../remediate.js"; import { registerResource, type StorageResource } from "../registry.js"; import { readHouseCredential } from "../house-credential.js"; import { makeCfExec } from "../cf-exec.js"; import { resolveHouseScopedToken } from "../house-scoped-token.js"; export interface RemediationDeps { cfD1(): Promise>; cfR2(): Promise>; mapping: Record; register(r: StorageResource): Promise; accountsDir: string; master: { apiToken: string; accountId: string }; configPath: string; } export interface RemediationResult { registered: number; strippedFiles: string[]; } export async function runRemediation(deps: RemediationDeps): Promise { const d1 = await deps.cfD1(); const r2 = await deps.cfR2(); const enumerated = [...d1.map((x) => x.name), ...r2.map((x) => x.name)]; // Fail closed BEFORE any write: an enumerated resource with no mapped owner // aborts the whole run, leaving the install untouched. const validation = validateMapping(enumerated, deps.mapping); if (!validation.ok) { throw new Error(`storage-broker remediation: unmapped resources: ${validation.missing.join(", ")}`); } for (const x of d1) { await deps.register({ accountId: deps.mapping[x.name], kind: "d1", name: x.name, cfResourceId: x.cfResourceId }); } for (const x of r2) { await deps.register({ accountId: deps.mapping[x.name], kind: "r2", name: x.name, cfResourceId: x.cfResourceId }); } // Relocate the master into the house-only config file (read before stripping). mkdirSync(dirname(deps.configPath), { recursive: true }); writeFileSync( deps.configPath, `CLOUDFLARE_API_TOKEN=${deps.master.apiToken}\nCLOUDFLARE_ACCOUNT_ID=${deps.master.accountId}\n`, { mode: 0o600 }, ); // Strip every account-wide token from every sub-account secrets file. const strippedFiles: string[] = []; if (existsSync(deps.accountsDir)) { for (const accountId of readdirSync(deps.accountsDir)) { const p = join(deps.accountsDir, accountId, "secrets", "cloudflare.env"); if (!existsSync(p)) continue; const before = readFileSync(p, "utf8"); const after = stripAccountWideTokens(before); if (after !== before) { writeFileSync(p, after, { mode: 0o600 }); strippedFiles.push(p); } } } return { registered: d1.length + r2.length, strippedFiles }; } // --- on-device CLI (untested glue) ---------------------------------------- function findMasterFromAccounts( accountsDir: string, ): { apiToken: string; accountId: string; path: string } | null { if (!existsSync(accountsDir)) return null; for (const accountId of readdirSync(accountsDir)) { const p = join(accountsDir, accountId, "secrets", "cloudflare.env"); if (!existsSync(p)) continue; const env: Record = {}; for (const line of readFileSync(p, "utf8").split("\n")) { const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); if (m) env[m[1]] = m[2].trim(); } if (env.CLOUDFLARE_API_TOKEN && env.CLOUDFLARE_ACCOUNT_ID) { return { apiToken: env.CLOUDFLARE_API_TOKEN, accountId: env.CLOUDFLARE_ACCOUNT_ID, path: p }; } } return null; } async function main(): Promise { const mappingFile = process.argv[2]; if (!mappingFile || !existsSync(mappingFile)) { console.error(`[remediate] error: mapping file not found: ${mappingFile ?? "(none)"}`); process.exit(2); } const platformRoot = process.env.PLATFORM_ROOT; if (!platformRoot) { console.error("[remediate] error: PLATFORM_ROOT not set"); process.exit(2); } const accountsDir = resolve(platformRoot, "..", "data", "accounts"); const configPath = join(platformRoot, "config", "cloudflare-house.env"); const mapping = JSON.parse(readFileSync(mappingFile, "utf8")) as Record; // The master is wherever it lives today: the house file if already relocated, // else the first sub-account secrets file that still carries it. masterFile is // the file cf-token.sh mints the scoped enumeration token against. let master: { apiToken: string; accountId: string }; let masterFile: string; try { master = readHouseCredential(platformRoot); masterFile = configPath; } catch { const found = findMasterFromAccounts(accountsDir); if (!found) { console.error("[remediate] error: no Cloudflare master token found in house config or account secrets"); process.exit(2); return; } master = { apiToken: found.apiToken, accountId: found.accountId }; masterFile = found.path; } // Task 1670 — the master is a minter; enumerate D1/R2 through a scoped storage // token minted from it, never the minter directly. `master` (the raw minter) // is still what runRemediation relocates into the house file below. const cfTokenSh = join(platformRoot, "plugins/cloudflare/bin/cf-token.sh"); const scoped = await resolveHouseScopedToken("storage", masterFile, cfTokenSh); const cf = makeCfExec({ apiToken: scoped, accountId: master.accountId }); const neo4j = (await import("neo4j-driver")) as any; const driver = neo4j.default.driver( process.env.NEO4J_URI, neo4j.default.auth.basic(process.env.NEO4J_USER ?? "neo4j", process.env.NEO4J_PASSWORD ?? ""), ); const session = driver.session(); try { const result = await runRemediation({ cfD1: async () => (await cf.d1List()).map((d) => ({ name: d.name, cfResourceId: d.uuid })), cfR2: async () => (await cf.r2List()).map((b) => ({ name: b.name, cfResourceId: b.name })), mapping, register: (r) => registerResource(session, r), accountsDir, master, configPath, }); console.error(`[remediate] done registered=${result.registered} stripped=${result.strippedFiles.length}`); } finally { await session.close(); await driver.close(); } } if (require.main === module) { main().catch((err) => { console.error(`[remediate] error: ${(err as Error).message}`); process.exit(1); }); }