174 lines
5.8 KiB
TypeScript
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,
|
|
};
|
|
}
|