152 lines
6.4 KiB
TypeScript
152 lines
6.4 KiB
TypeScript
#!/usr/bin/env tsx
|
|
import "dotenv/config";
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
|
|
import { createPrismaPlatformLeadService } from "@hivegrid/db/platform-leads-admin.server";
|
|
import { disconnectDbClients, getDbClient } from "@hivegrid/db/client.server";
|
|
import { runWithRlsContext } from "@hivegrid/db/rls-context.server";
|
|
|
|
import { closeSharedStores } from "../../apps/web/app/shared-store.server.js";
|
|
import { userHasPermission } from "../../apps/web/app/utils/db/permissions/userRoles.db.server.js";
|
|
|
|
import {
|
|
isSuccessfulPlatformLeadSourceBootstrapManifest,
|
|
PlatformLeadSourceBootstrapError,
|
|
runPlatformLeadSourceBootstrap,
|
|
type PlatformLeadSourceBootstrapMode,
|
|
} from "./source-bootstrap.js";
|
|
|
|
export type PlatformLeadSourceBootstrapCliOptions = {
|
|
mode: PlatformLeadSourceBootstrapMode;
|
|
actorAdminUserId: string;
|
|
routingAdminUserId: string;
|
|
manifestPath: string;
|
|
runId: string;
|
|
expectedPlanSha256?: string;
|
|
};
|
|
|
|
export function parsePlatformLeadSourceBootstrapCliArgs(argv: readonly string[]): PlatformLeadSourceBootstrapCliOptions {
|
|
const values = parseNamedArguments(argv);
|
|
const allowed = new Set(["mode", "actor-admin-user-id", "routing-admin-user-id", "manifest", "run-id", "expected-plan-sha256"]);
|
|
if ([...values.keys()].some((key) => !allowed.has(key))) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
const modeValue = required(values.get("mode"));
|
|
if (modeValue !== "dry-run" && modeValue !== "apply") throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
const options: PlatformLeadSourceBootstrapCliOptions = {
|
|
mode: modeValue,
|
|
actorAdminUserId: identifier(values.get("actor-admin-user-id")),
|
|
routingAdminUserId: identifier(values.get("routing-admin-user-id")),
|
|
manifestPath: required(values.get("manifest")),
|
|
runId: values.get("run-id")?.trim() || randomUUID(),
|
|
};
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,190}$/u.test(options.runId)) {
|
|
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
}
|
|
if (modeValue === "apply") {
|
|
const expected = required(values.get("expected-plan-sha256")).toLowerCase();
|
|
if (!/^[a-f0-9]{64}$/u.test(expected)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
options.expectedPlanSha256 = expected;
|
|
} else if (values.has("expected-plan-sha256")) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
return options;
|
|
}
|
|
|
|
export async function runPlatformLeadSourceBootstrapCli(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
|
|
let options: PlatformLeadSourceBootstrapCliOptions;
|
|
try {
|
|
options = parsePlatformLeadSourceBootstrapCliArgs(argv);
|
|
} catch (error) {
|
|
writeSafeError(error);
|
|
return 1;
|
|
}
|
|
|
|
try {
|
|
const manifest = await runWithRlsContext({ userId: options.actorAdminUserId }, async () => {
|
|
if (!(await userHasPermission(options.actorAdminUserId, null, "admin.leads.admin"))) {
|
|
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
}
|
|
const [canReadOwn, canWrite] = await Promise.all([
|
|
userHasPermission(options.routingAdminUserId, null, "admin.leads.read_own"),
|
|
userHasPermission(options.routingAdminUserId, null, "admin.leads.write"),
|
|
]);
|
|
if (!canReadOwn || !canWrite) {
|
|
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
}
|
|
const db = getDbClient();
|
|
const routingOperator = await db.adminUser.findUnique({
|
|
where: { userId: options.routingAdminUserId },
|
|
select: { user: { select: { active: true, pendingDeletion: true, pseudonymized: true } } },
|
|
});
|
|
if (
|
|
!routingOperator?.user.active ||
|
|
routingOperator.user.pendingDeletion ||
|
|
routingOperator.user.pseudonymized
|
|
) {
|
|
throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
}
|
|
const service = createPrismaPlatformLeadService();
|
|
const existingSources = await service.listSources();
|
|
return runPlatformLeadSourceBootstrap({
|
|
mode: options.mode,
|
|
runId: options.runId,
|
|
actorAdminUserId: options.actorAdminUserId,
|
|
routingAdminUserId: options.routingAdminUserId,
|
|
existingSources,
|
|
expectedPlanSha256: options.expectedPlanSha256,
|
|
service,
|
|
});
|
|
});
|
|
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
await writeFile(options.manifestPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
process.stdout.write(serialized);
|
|
return isSuccessfulPlatformLeadSourceBootstrapManifest(manifest) ? 0 : 2;
|
|
} catch (error) {
|
|
writeSafeError(error);
|
|
return 1;
|
|
} finally {
|
|
await Promise.allSettled([closeSharedStores(), disconnectDbClients()]);
|
|
}
|
|
}
|
|
|
|
function parseNamedArguments(argv: readonly string[]): Map<string, string> {
|
|
const values = new Map<string, string>();
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const argument = argv[index]!;
|
|
if (argument === "--" && index === 0) continue;
|
|
if (!argument.startsWith("--")) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
const equalsIndex = argument.indexOf("=");
|
|
const key = argument.slice(2, equalsIndex === -1 ? undefined : equalsIndex);
|
|
const value = equalsIndex === -1 ? argv[++index] : argument.slice(equalsIndex + 1);
|
|
if (!key || !value || value.startsWith("--") || values.has(key)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
values.set(key, value);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function required(value: string | undefined): string {
|
|
const normalized = value?.trim();
|
|
if (!normalized || /[\u0000-\u001f\u007f]/u.test(normalized)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
return normalized;
|
|
}
|
|
|
|
function identifier(value: string | undefined): string {
|
|
const normalized = required(value);
|
|
if (normalized.length > 191 || !/^[A-Za-z0-9_-]+$/u.test(normalized)) throw new PlatformLeadSourceBootstrapError("INVALID_CONFIGURATION");
|
|
return normalized;
|
|
}
|
|
|
|
function writeSafeError(error: unknown): void {
|
|
const code = error instanceof PlatformLeadSourceBootstrapError ? error.code : "OPERATION_FAILED";
|
|
process.stderr.write(`${JSON.stringify({ ok: false, code })}\n`);
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
runPlatformLeadSourceBootstrapCli()
|
|
.then((exitCode) => {
|
|
process.exitCode = exitCode;
|
|
})
|
|
.catch(() => {
|
|
process.exitCode = 1;
|
|
});
|
|
}
|