Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/demo-pro-seed/cli.ts
T

174 lines
5.8 KiB
TypeScript

export const DEMO_PRO_TENANT_SLUG = "demo-pro-crm" as const;
export type SeedMode = "dry-run" | "apply" | "verify" | "cleanup";
type NonApplySeedCommand = {
mode: "dry-run" | "verify";
tenantSlug: typeof DEMO_PRO_TENANT_SLUG;
};
type CleanupSeedCommand = {
mode: "cleanup";
tenantSlug: typeof DEMO_PRO_TENANT_SLUG;
expectedTenantId: string;
expectedDatabaseName: string;
expectedDatabaseHost: string;
expectedStorageBucket: string;
expectedStorageEndpoint: string;
};
type ApplySeedCommand = {
mode: "apply";
tenantSlug: typeof DEMO_PRO_TENANT_SLUG;
userPassword: string;
expectedTenantId: string;
expectedDatabaseName: string;
expectedDatabaseHost: string;
expectedStorageBucket: string;
expectedStorageEndpoint: string;
};
export type SeedCommand = NonApplySeedCommand | CleanupSeedCommand | ApplySeedCommand;
type SeedEnvironment = Readonly<Record<string, string | undefined>>;
const modeArguments = new Map<string, SeedMode>([
["--dry-run", "dry-run"],
["--apply", "apply"],
["--verify", "verify"],
["--cleanup", "cleanup"],
]);
function parseSingleValueArgument(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 Error(`demo-pro seed: ${name} may only be provided once`);
}
return values[0];
}
function parseMode(argv: readonly string[]): SeedMode {
const modes = argv.flatMap((argument) => {
const mode = modeArguments.get(argument);
return mode ? [mode] : [];
});
if (modes.length > 1) {
throw new Error("demo-pro seed: provide exactly one mode");
}
return modes[0] ?? "dry-run";
}
function assertKnownArguments(argv: readonly string[]): void {
for (const argument of argv) {
if (modeArguments.has(argument) || argument.startsWith("--confirm=") || argument.startsWith("--tenant=")) {
continue;
}
throw new Error("demo-pro seed: unknown argument");
}
}
function assertTargetTenant(argv: readonly string[]): void {
const tenantSlug = parseSingleValueArgument(argv, "--tenant");
if (tenantSlug !== undefined && tenantSlug !== DEMO_PRO_TENANT_SLUG) {
throw new Error(`demo-pro seed: target tenant must be ${DEMO_PRO_TENANT_SLUG}`);
}
}
function assertStagingEnvironment(env: SeedEnvironment): void {
if (env.SENTRY_ENVIRONMENT?.trim() !== "staging") {
throw new Error("demo-pro seed: this mode is restricted to the staging environment");
}
}
function assertMutationAllowed(argv: readonly string[], env: SeedEnvironment): void {
const confirmation = parseSingleValueArgument(argv, "--confirm");
if (confirmation !== DEMO_PRO_TENANT_SLUG) {
throw new Error(`demo-pro seed: this mode requires --confirm=${DEMO_PRO_TENANT_SLUG}`);
}
if (env.SEED_DEMO_PRO_ALLOWED?.trim() !== "true") {
throw new Error("demo-pro seed: this mode requires explicit staging seed authorization");
}
if (env.SEED_DEMO_PRO_WORKERS_DRAINED?.trim() !== "true" || env.SEED_DEMO_PRO_OUTBOUND_SANDBOX?.trim() !== "true") {
throw new Error("demo-pro seed: mutation requires drained workers and an outbound staging sandbox");
}
}
/**
* Parses process arguments passed after the script path and enforces the staging-only seed boundary.
* The function has no side effects and never includes environment values in errors or output.
*/
export function parseSeedCommand(argv: readonly string[], env: SeedEnvironment): SeedCommand {
assertKnownArguments(argv);
const mode = parseMode(argv);
assertTargetTenant(argv);
const confirmation = parseSingleValueArgument(argv, "--confirm");
if ((mode === "dry-run" || mode === "verify") && confirmation !== undefined) {
throw new Error("demo-pro seed: --confirm is only valid for apply and cleanup");
}
if (mode === "dry-run") {
return { mode, tenantSlug: DEMO_PRO_TENANT_SLUG };
}
assertStagingEnvironment(env);
if (mode === "verify") {
return { mode, tenantSlug: DEMO_PRO_TENANT_SLUG };
}
assertMutationAllowed(argv, env);
const expectedTenantId = env.SEED_DEMO_PRO_EXPECTED_TENANT_ID?.trim();
const expectedDatabaseName = env.SEED_DEMO_PRO_EXPECTED_DATABASE?.trim();
const expectedDatabaseHost = env.SEED_DEMO_PRO_EXPECTED_DATABASE_HOST?.trim();
const expectedStorageBucket = env.SEED_DEMO_PRO_EXPECTED_STORAGE_BUCKET?.trim();
const expectedStorageEndpoint = env.SEED_DEMO_PRO_EXPECTED_STORAGE_ENDPOINT?.trim();
if (!expectedTenantId || !expectedDatabaseName || !expectedDatabaseHost || !expectedStorageBucket || !expectedStorageEndpoint) {
throw new Error("demo-pro seed: mutation requires the staging tenant, database, and storage binding secrets");
}
if (mode === "cleanup") {
return {
mode,
tenantSlug: DEMO_PRO_TENANT_SLUG,
expectedTenantId,
expectedDatabaseName,
expectedDatabaseHost,
expectedStorageBucket,
expectedStorageEndpoint,
};
}
const userPassword = env.SEED_DEMO_USER_PASSWORD;
if (!userPassword || userPassword.trim().length === 0) {
throw new Error("demo-pro seed: apply requires the staging demo-user password secret");
}
const explicitlyAllowedSharedStagingPassword =
userPassword === "password" && env.SEED_DEMO_PRO_ALLOW_SHARED_STAGING_PASSWORD?.trim() === "true";
if (
!explicitlyAllowedSharedStagingPassword &&
(userPassword.length < 20 || new Set(userPassword).size < 10 || /^(?:passw(?:or)?t|password|demo|hivegrid)/iu.test(userPassword))
) {
throw new Error("demo-pro seed: the staging demo-user password must be a strong random secret of at least 20 characters");
}
return {
mode,
tenantSlug: DEMO_PRO_TENANT_SLUG,
userPassword,
expectedTenantId,
expectedDatabaseName,
expectedDatabaseHost,
expectedStorageBucket,
expectedStorageEndpoint,
};
}