Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/migrate-api-keys.ts
T
2026-07-08 16:15:37 +02:00

153 lines
4.3 KiB
TypeScript

#!/usr/bin/env tsx
import { backfillLegacyApiKeyHashes, DEFAULT_LEGACY_API_KEY_HASH_BACKFILL_BATCH_SIZE } from "@hivegrid/db/api-keys.server";
import type { LegacyApiKeyHashBackfillResult } from "@hivegrid/db/api-keys.server";
export type ApiKeyMigrationCliEnv = NodeJS.ProcessEnv & {
API_KEY_HASH_SECRET?: string;
JWT_SECRET?: string;
};
export type ApiKeyMigrationCliDependencies = {
backfillLegacyApiKeyHashes?: typeof backfillLegacyApiKeyHashes;
stdout?: Pick<typeof process.stdout, "write">;
stderr?: Pick<typeof process.stderr, "write">;
};
type ApiKeyMigrationCliOptions = {
dryRun: boolean;
batchSize: number;
};
function parseApiKeyMigrationArgs(argv: string[]): ApiKeyMigrationCliOptions {
let dryRun = true;
let batchSize = DEFAULT_LEGACY_API_KEY_HASH_BACKFILL_BATCH_SIZE;
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--dry-run") {
dryRun = true;
continue;
}
if (arg === "--execute") {
dryRun = false;
continue;
}
if (arg === "--batch-size") {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error("Invalid --batch-size: expected a positive integer.");
}
batchSize = parseBatchSize(value);
index += 1;
continue;
}
if (arg.startsWith("--batch-size=")) {
batchSize = parseBatchSize(arg.slice("--batch-size=".length));
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
return { dryRun, batchSize };
}
function parseBatchSize(value: string): number {
if (!/^\d+$/.test(value)) {
throw new Error("Invalid --batch-size: expected a positive integer.");
}
const batchSize = Number(value);
if (!Number.isSafeInteger(batchSize) || batchSize < 1) {
throw new Error("Invalid --batch-size: expected a positive integer.");
}
return batchSize;
}
function resolveHashSecret(env: ApiKeyMigrationCliEnv): string | undefined {
return env.API_KEY_HASH_SECRET?.trim() || env.JWT_SECRET?.trim() || undefined;
}
function writeLine(stream: Pick<typeof process.stdout, "write">, value: string): void {
stream.write(`${value}\n`);
}
function summarizeResult(result: LegacyApiKeyHashBackfillResult): Omit<LegacyApiKeyHashBackfillResult, "failures"> & { failureIds: string[] } {
return {
scanned: result.scanned,
updated: result.updated,
skipped: result.skipped,
failed: result.failed,
failureIds: result.failures.map((failure) => failure.id),
};
}
function redactCliError(error: unknown): string {
if (error instanceof Error) {
return error.name || "Error";
}
return "UnknownError";
}
export async function runApiKeyMigrationCli(
argv: string[] = process.argv.slice(2),
env: ApiKeyMigrationCliEnv = process.env,
dependencies: ApiKeyMigrationCliDependencies = {},
): Promise<number> {
const stdout = dependencies.stdout ?? process.stdout;
const stderr = dependencies.stderr ?? process.stderr;
const migrate = dependencies.backfillLegacyApiKeyHashes ?? backfillLegacyApiKeyHashes;
let options: ApiKeyMigrationCliOptions;
try {
options = parseApiKeyMigrationArgs(argv);
} catch (error) {
writeLine(stderr, error instanceof Error ? error.message : "Invalid arguments.");
return 1;
}
if (options.dryRun) {
writeLine(
stdout,
JSON.stringify({
mode: "dry-run",
batchSize: options.batchSize,
scanned: 0,
updated: 0,
skipped: 0,
failed: 0,
message: "Dry run only; no database writes performed. Use --execute to apply the backfill.",
}),
);
return 0;
}
try {
const result = await migrate({ batchSize: options.batchSize, hashSecret: resolveHashSecret(env) });
writeLine(stdout, JSON.stringify({ mode: "execute", batchSize: options.batchSize, ...summarizeResult(result) }));
return result.failed > 0 ? 1 : 0;
} catch (error) {
writeLine(
stderr,
JSON.stringify({ mode: "execute", batchSize: options.batchSize, scanned: 0, updated: 0, skipped: 0, failed: 1, error: redactCliError(error) }),
);
return 1;
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
runApiKeyMigrationCli()
.then((exitCode) => {
process.exitCode = exitCode;
})
.catch(() => {
process.exitCode = 1;
});
}