Files
cfw-autumn/scripts/migrations/validate-fullsubject-shebangs.ts
John Yeo 1d35a6180b cleanup
2026-05-17 10:26:49 +01:00

187 lines
6.1 KiB
TypeScript

/**
* Validates that every FullSubject Lua script reachable via the Upstash
* redisV2 path is prefixed with the `#!lua flags=allow-key-locking` shebang,
* AND that Upstash is actually enforcing the flag on the live connection.
*
* Two layers:
* 1. Static — reproduces the resolution path in initRedisV2.ts and asserts
* every FullSubject command registers with the shebang prepended.
* 2. Live probe — connects to Upstash and exploits the fact that under
* `allow-key-locking`, calling `redis.call` on a key not in `KEYS[]`
* fails with a specific error. If the shebang-flagged probe errors and
* the unflagged control probe succeeds, key-locking is confirmed active.
*/
import { UPSTASH_KEY_LOCKING_SHEBANG } from "@server/_luaScriptsV2/luaScriptsV2";
import { getRedisV2ConnectionConfig } from "@server/external/redis/initUtils/redisV2Config";
import { registerRedisCommands } from "@server/external/redis/initUtils/registerRedisCommands";
import { Redis } from "ioredis";
// Commands whose Lua bodies read/write FullSubject cache keys. Any command
// that touches the FullSubject cache on redisV2 MUST appear here.
const FULL_SUBJECT_COMMANDS = [
"deductFromSubjectBalances",
"updateSubjectBalances",
"setCachedFullSubject",
"adjustSubjectBalance",
"updateFullSubjectCustomerDataV2",
"updateFullSubjectEntityDataV2",
"updateFullSubjectCustomerProductV2",
"upsertInvoiceInFullSubjectV2",
] as const;
type CapturedCommand = { lua: string; numberOfKeys?: number };
const captured = new Map<string, CapturedCommand>();
const stubRedis = {
defineCommand(
name: string,
definition: { lua: string; numberOfKeys?: number },
) {
captured.set(name, definition);
},
on() {},
} as unknown as Parameters<typeof registerRedisCommands>[0]["redisInstance"];
const v2Config = getRedisV2ConnectionConfig({
cacheV2Url: process.env.CACHE_V2_UPSTASH_URL,
currentRegion: "validate",
instanceName: "upstash",
});
if (!v2Config) {
console.error(
"❌ No distinct CACHE_V2_UPSTASH_URL configured — redisV2 is falling back to the primary cache, so Upstash key-locking is NOT in effect.",
);
console.error(
" Set CACHE_V2_UPSTASH_URL to a value different from CACHE_URL and re-run.",
);
process.exit(1);
}
const { supportsUpstashShebang } = v2Config;
console.log(
`Resolved redisV2 path: supportsUpstashShebang=${supportsUpstashShebang} (region=${v2Config.region})`,
);
registerRedisCommands({
redisInstance: stubRedis,
supportsUpstashShebang,
});
const failures: string[] = [];
for (const commandName of FULL_SUBJECT_COMMANDS) {
const definition = captured.get(commandName);
if (!definition) {
failures.push(` - ${commandName}: NOT REGISTERED`);
continue;
}
if (!definition.lua.startsWith(UPSTASH_KEY_LOCKING_SHEBANG)) {
const firstLine = definition.lua.split("\n")[0];
failures.push(
` - ${commandName}: missing shebang (first line: "${firstLine}")`,
);
continue;
}
console.log(`${commandName}: shebang present`);
}
if (failures.length > 0) {
console.error("\n❌ FullSubject shebang validation failed:");
for (const line of failures) console.error(line);
process.exit(1);
}
console.log(
`\n✅ All ${FULL_SUBJECT_COMMANDS.length} FullSubject scripts carry the Upstash key-locking shebang.`,
);
// -----------------------------------------------------------------------------
// Live probe against Upstash
// -----------------------------------------------------------------------------
//
// Proves Upstash is ENFORCING the flag, not just accepting the shebang.
//
// flagged probe — shebang present, reads a key not in KEYS[] → must fail
// with "Dynamic keys are not allowed ..."
// control probe — same script, no shebang → must succeed
//
// Both probes are read-only (`GET` on a random namespaced key) and pass 0
// KEYS, so they never mutate Upstash state.
const upstashUrl = process.env.CACHE_V2_UPSTASH_URL?.trim();
if (!upstashUrl) {
console.error("\n❌ CACHE_V2_UPSTASH_URL missing for live probe.");
process.exit(1);
}
console.log("\nRunning live Upstash probe...");
const probeClient = new Redis(upstashUrl, {
tls: process.env.CACHE_CERT ? { ca: process.env.CACHE_CERT } : undefined,
family: 4,
maxRetriesPerRequest: 2,
commandTimeout: 10_000,
});
const probeKey = `autumn:shebang_probe:${Date.now()}:${Math.random().toString(36).slice(2)}`;
const probeBody = `redis.call('GET', '${probeKey}')\nreturn 'OK'`;
const flaggedScript = `${UPSTASH_KEY_LOCKING_SHEBANG}${probeBody}`;
const controlScript = probeBody;
const DYNAMIC_KEYS_ERROR_FRAGMENT = "Dynamic keys are not allowed";
try {
// Flagged probe — expect rejection.
let flaggedResult: unknown;
let flaggedError: Error | undefined;
try {
flaggedResult = await probeClient.eval(flaggedScript, 0);
} catch (err) {
flaggedError = err as Error;
}
if (!flaggedError) {
console.error(
`❌ Flagged probe unexpectedly succeeded (returned ${JSON.stringify(flaggedResult)}). Upstash did NOT enforce allow-key-locking — either the connection is not an Upstash database, or the flag was stripped somewhere in the pipeline.`,
);
process.exit(1);
}
if (!flaggedError.message.includes(DYNAMIC_KEYS_ERROR_FRAGMENT)) {
console.error(
`❌ Flagged probe errored, but not with the expected "${DYNAMIC_KEYS_ERROR_FRAGMENT}" message. Got: ${flaggedError.message}`,
);
process.exit(1);
}
console.log(
`✅ Flagged probe rejected by Upstash: "${flaggedError.message.trim()}"`,
);
// Control probe — same script without shebang must succeed.
const controlResult = await probeClient.eval(controlScript, 0);
if (controlResult !== "OK") {
console.error(
`❌ Control probe returned unexpected value: ${JSON.stringify(controlResult)}`,
);
process.exit(1);
}
console.log("✅ Control probe (no shebang) succeeded — baseline confirmed.");
console.log(
"\n✅ 100% verified: Upstash is enforcing allow-key-locking on this connection, and every FullSubject script carries the shebang.",
);
await probeClient.quit();
process.exit(0);
} catch (err) {
console.error("❌ Live probe failed:", err);
try {
await probeClient.quit();
} catch {}
process.exit(1);
}