Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/billing-catalog/contracts.ts
T

134 lines
5.1 KiB
TypeScript

import { hiveGridBillingCatalogVersion } from "@hivegrid/entitlements";
export type BillingCatalogTarget = "staging" | "production";
export type BillingCatalogMode = "plan" | "apply" | "verify";
export type BillingCatalogEnvironment = Readonly<Record<string, string | undefined>>;
export type BillingCatalogCommand = {
readonly target: BillingCatalogTarget;
readonly mode: BillingCatalogMode;
readonly lagoApiUrl: string;
readonly lagoApiKey: string;
readonly expectedOrganizationName: string;
readonly expectedOrganizationIdSha256: string;
readonly snapshotDirectory: string;
};
const STAGING_ORGANIZATION_NAME = "HiveGrid Staging";
const STAGING_ORGANIZATION_ID_SHA256 = "3b1134a731c67b704bb0b3298ce031213079966b348aa762800f9c92556085a8";
const PRODUCTION_ORGANIZATION_NAME = "HiveGrid";
const PRODUCTION_ORGANIZATION_ID_SHA256 = "c54f682e7528f2f1d556a5f539d6d200aa7d9163c9aec31f8fb5b21be632505e";
const MODE_ARGUMENTS = new Map<string, BillingCatalogMode>([
["--plan", "plan"],
["--apply", "apply"],
["--verify", "verify"],
]);
export class BillingCatalogOperatorError extends Error {
constructor(message: string) {
super(message);
this.name = "BillingCatalogOperatorError";
}
}
function required(env: BillingCatalogEnvironment, name: string): string {
const value = env[name]?.trim();
if (!value) throw new BillingCatalogOperatorError(`billing catalog: ${name} is required`);
return value;
}
function valueArgument(argv: readonly string[], name: string): string | undefined {
const prefix = `${name}=`;
const values = argv.filter((argument) => argument.startsWith(prefix)).map((argument) => argument.slice(prefix.length));
if (values.length > 1) throw new BillingCatalogOperatorError(`billing catalog: ${name} may only be provided once`);
return values[0];
}
function parseTarget(argv: readonly string[]): BillingCatalogTarget {
const target = valueArgument(argv, "--target");
if (target !== "staging" && target !== "production") {
throw new BillingCatalogOperatorError("billing catalog: --target=staging|production is required");
}
return target;
}
function parseMode(argv: readonly string[]): BillingCatalogMode {
const modes = argv.flatMap((argument) => {
const mode = MODE_ARGUMENTS.get(argument);
return mode ? [mode] : [];
});
if (modes.length > 1) throw new BillingCatalogOperatorError("billing catalog: provide exactly one mode");
return modes[0] ?? "plan";
}
function assertKnownArguments(argv: readonly string[]): void {
for (const argument of argv) {
if (MODE_ARGUMENTS.has(argument) || argument.startsWith("--target=") || argument.startsWith("--confirm=")) {
continue;
}
throw new BillingCatalogOperatorError("billing catalog: unknown argument");
}
}
function normalizeApiUrl(value: string): string {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new BillingCatalogOperatorError("billing catalog: LAGO_API_URL must be an absolute URL");
}
if (
!new Set(["http:", "https:"]).has(parsed.protocol) ||
parsed.username ||
parsed.password ||
parsed.search ||
parsed.hash ||
(parsed.pathname !== "" && parsed.pathname !== "/")
) {
throw new BillingCatalogOperatorError("billing catalog: LAGO_API_URL must be an HTTP(S) origin");
}
return parsed.origin;
}
function requireOrganizationContract(target: BillingCatalogTarget): { name: string; idSha256: string } {
if (target === "staging") {
return { name: STAGING_ORGANIZATION_NAME, idSha256: STAGING_ORGANIZATION_ID_SHA256 };
}
return {
name: PRODUCTION_ORGANIZATION_NAME,
idSha256: PRODUCTION_ORGANIZATION_ID_SHA256,
};
}
export function parseBillingCatalogCommand(argv: readonly string[], env: BillingCatalogEnvironment): BillingCatalogCommand {
assertKnownArguments(argv);
const target = parseTarget(argv);
const mode = parseMode(argv);
const confirmation = valueArgument(argv, "--confirm");
const expectedConfirmation = `${target}:${hiveGridBillingCatalogVersion}`;
if (mode === "apply" && confirmation !== expectedConfirmation) {
throw new BillingCatalogOperatorError(`billing catalog: apply requires --confirm=${expectedConfirmation}`);
}
if (mode !== "apply" && confirmation !== undefined) {
throw new BillingCatalogOperatorError("billing catalog: --confirm is valid only with --apply");
}
if (env.NODE_ENV?.trim() !== "production") {
throw new BillingCatalogOperatorError("billing catalog: NODE_ENV must be production so the operator path fails closed");
}
const organization = requireOrganizationContract(target);
return {
target,
mode,
lagoApiUrl: normalizeApiUrl(required(env, "LAGO_API_URL")),
lagoApiKey: required(env, "LAGO_API_KEY"),
expectedOrganizationName: organization.name,
expectedOrganizationIdSha256: organization.idSha256,
snapshotDirectory: env.HIVEGRID_BILLING_CATALOG_SNAPSHOT_DIR?.trim() || "tmp/billing-catalog-snapshots",
};
}
export function safeBillingCatalogError(error: unknown): string {
if (error instanceof BillingCatalogOperatorError) return error.message;
return "billing catalog: operator command failed; inspect redacted service logs";
}