import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import postgres from "postgres"; import { repoRoot, rootPath } from "./config"; import { runCommand, type CommandResult } from "./command"; import type { RuntimeSecretSpec } from "./hwlab-node/entry"; import { runtimeSecretSpec } from "./hwlab-node/public-exposure"; const defaultConfigPath = "config/hwlab-test-accounts.yaml"; const apiKeyTargetKey = "api-key"; type AccountTargetKind = "kubernetes-secret" | "user-billing-api-key"; type Action = "status" | "sync" | "host-env-status" | "host-env-sync"; interface Options { action: Action; configPath: string; node: string; lane: string; confirm: boolean; createSources: boolean; timeoutSeconds: number; } interface SourceRef { sourceRef: string; sourceKey: string; } interface HostEnvTarget { path: string; envKey: string; mode: string; } interface Account extends SourceRef { logicalId: string; kind: string; userId: string; username: string; email: string | null; displayName: string; role: "admin" | "user"; status: "active" | "disabled" | "pending"; planId: string; initialCredits: number; permissions: string[]; workbench: Record | null; createIfMissing: { enabled: boolean; randomBase64Url: { bytes: number; prefix: string } | null }; hostEnvTargets: HostEnvTarget[]; target: { kind: AccountTargetKind; namespace: string | null; secretName: string | null; serviceId: string | null; keyId: string | null; keyName: string; targetKey: string; rolloutDeployment: string | null; scopes: string[]; }; } interface Target { id: string; node: string; lane: string; namespace: string; publicUrl: string; userBilling: { serviceId: string; databaseUrlSource: SourceRef }; accounts: Account[]; } interface LoadedConfig { configPath: string; version: number; kind: "hwlab-test-accounts"; sourceRoot: string; targets: Target[]; } interface SourceMaterial { ok: boolean; sourceRef: string; sourceKey: string; sourcePath: string; exists: boolean; byteCount: number | null; keyPrefix: string | null; serviceKeyPrefix: string | null; fingerprint: string | null; sha256Hex: string | null; value: string | null; blocker: Record | null; } interface DatabaseAccess { kind: "platform-service" | "local-k3s"; runtimeStoreMode: string | null; databaseUrlSource: SourceMaterial; runtimeSecretSpec: RuntimeSecretSpec | null; } export async function runHwlabTestAccountsCommand(args: string[]): Promise> { if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return hwlabTestAccountsHelp(); const options = parseOptions(args); const config = loadConfig(options.configPath); const target = selectTarget(config, options); if (options.action === "host-env-status") return hostEnvStatus(config, target, options); if (options.action === "host-env-sync") return hostEnvSync(config, target, options); if (options.action === "sync") return sync(config, target, options); return status(config, target, options); } export function hwlabTestAccountsHelp(): Record { return { ok: true, command: "hwlab nodes test-accounts", description: "Inspect or sync UniDesk YAML-declared HWLAB test/admin accounts without printing API keys or database URLs.", configPath: defaultConfigPath, examples: [ "bun scripts/cli.ts hwlab nodes test-accounts status --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes test-accounts sync --node D601 --lane v03 --confirm", "bun scripts/cli.ts hwlab nodes test-accounts host-env status --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes test-accounts host-env sync --node D601 --lane v03 --confirm", ], valuesRedacted: true, }; } function parseOptions(args: string[]): Options { let action = args[0] as Action | "host-env" | undefined; let optionStart = 1; if (action === "host-env") { const subaction = args[1]; if (subaction !== "status" && subaction !== "sync") throw new Error("test-accounts host-env usage: host-env status|sync --node NODE --lane vNN [--config PATH] [--confirm]"); action = subaction === "status" ? "host-env-status" : "host-env-sync"; optionStart = 2; } if (action !== "status" && action !== "sync" && action !== "host-env-status" && action !== "host-env-sync") throw new Error("test-accounts usage: status|sync|host-env status|host-env sync --node NODE --lane vNN [--config PATH] [--confirm]"); const options: Options = { action, configPath: defaultConfigPath, node: "", lane: "", confirm: false, createSources: true, timeoutSeconds: 30 }; for (let index = optionStart; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--config") options.configPath = requiredValue(args, index += 1, arg); else if (arg === "--node") options.node = requiredValue(args, index += 1, arg); else if (arg === "--lane") options.lane = requiredValue(args, index += 1, arg); else if (arg === "--confirm") options.confirm = true; else if (arg === "--no-create-sources") options.createSources = false; else if (arg === "--timeout-seconds") options.timeoutSeconds = positiveInteger(requiredValue(args, index += 1, arg), arg); else throw new Error(`unknown test-accounts option: ${arg}`); } if (!options.node) throw new Error("--node is required"); if (!options.lane) throw new Error("--lane is required"); return options; } async function hostEnvStatus(config: LoadedConfig, target: Target, options: Options): Promise> { const accounts = target.accounts.map((account) => { const source = readAccountSource(account, config.sourceRoot, false); return hostEnvAccountStatus(target, account, source, options); }); return hostEnvReport("host-env status", config, target, options, accounts, { mutation: false }); } async function hostEnvSync(config: LoadedConfig, target: Target, options: Options): Promise> { if (!options.confirm) { return hostEnvReport("host-env sync", config, target, options, [], { mutation: false, blockers: [{ code: "confirm_required", message: "host-env sync requires --confirm" }] }); } const synced: Record[] = []; const sourceBlockers: Record[] = []; for (const account of target.accounts) { const source = readAccountSource(account, config.sourceRoot, options.createSources); if (!source.ok || !source.value) { if (source.blocker) sourceBlockers.push(source.blocker); continue; } for (const hostTarget of account.hostEnvTargets) { synced.push(syncHostEnvTarget(target, account, hostTarget, source, options)); } } const accounts = target.accounts.map((account) => { const source = readAccountSource(account, config.sourceRoot, false); return hostEnvAccountStatus(target, account, source, options); }); return hostEnvReport("host-env sync", config, target, options, accounts, { mutation: synced.some((item) => item.mutation === true), synced, blockers: sourceBlockers }); } async function status(config: LoadedConfig, target: Target, options: Options): Promise> { const database = databaseAccessForTarget(config, target); const accounts = await accountStatuses(config, target, options, database); return report("status", config, target, options, accounts, database, { mutation: false }); } async function sync(config: LoadedConfig, target: Target, options: Options): Promise> { const database = databaseAccessForTarget(config, target); if (!options.confirm) { return report("sync", config, target, options, [], database, { mutation: false, blockers: [{ code: "confirm_required", message: "sync requires --confirm" }] }); } const sources = target.accounts.map((account) => readAccountSource(account, config.sourceRoot, options.createSources)); const sourceBlockers = [ database.kind === "platform-service" && !database.databaseUrlSource.ok ? database.databaseUrlSource.blocker : null, ...sources.map((source) => source.ok ? null : source.blocker), ].filter(Boolean) as Record[]; if (sourceBlockers.length > 0 || (database.kind === "platform-service" && !database.databaseUrlSource.value)) { const accounts = await accountStatuses(config, target, options, database, sources); return report("sync", config, target, options, accounts, database, { mutation: false, blockers: sourceBlockers }); } const synced: Record[] = []; for (const account of target.accounts.filter((item) => item.target.kind === "kubernetes-secret")) { const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey); if (!source?.value) throw new Error(`${account.logicalId} source material missing after preflight`); synced.push(syncKubernetesSecretIfNeeded(target, account, source, options)); } const userBillingAccounts = target.accounts.filter((item) => item.target.kind === "user-billing-api-key"); if (userBillingAccounts.length === 0) { const accounts = await accountStatuses(config, target, options, database); return report("sync", config, target, options, accounts, database, { mutation: synced.some((item) => item.mutation === true), synced }); } if (database.kind === "local-k3s") { const syncBlockers: Record[] = []; for (const account of userBillingAccounts) { const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey); if (!source?.value || !source.sha256Hex || !source.serviceKeyPrefix) throw new Error(`${account.logicalId} source material missing after preflight`); const targetStatus = userBillingApiKeyStatusLocalK3s(database.runtimeSecretSpec, account, source, options); const statusBlockers = targetStatusAvailabilityBlockers(account, targetStatus); if (statusBlockers.length > 0) { syncBlockers.push(...statusBlockers); continue; } if (userBillingAccountMatches(account, targetStatus)) { synced.push(skippedUserBillingAccountSync(account, source, targetStatus)); continue; } synced.push(syncUserBillingAccountLocalK3s(database.runtimeSecretSpec, account, source, options)); } if (syncBlockers.length > 0) { const accounts = await accountStatuses(config, target, options, database, sources); return report("sync", config, target, options, accounts, database, { mutation: synced.some((item) => item.mutation === true), synced, blockers: syncBlockers }); } const accounts = await accountStatuses(config, target, options, database); return report("sync", config, target, options, accounts, database, { mutation: synced.some((item) => item.mutation === true), synced }); } const sql = postgres(database.databaseUrlSource.value ?? "", { max: 1 }); try { await sql.begin(async (tx: any) => { for (const account of userBillingAccounts) { const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey); if (!source?.value || !source.sha256Hex || !source.serviceKeyPrefix) throw new Error(`${account.logicalId} source material missing after preflight`); const targetStatus = await userBillingApiKeyStatus(tx, account, source); if (userBillingAccountMatches(account, targetStatus)) { synced.push(skippedUserBillingAccountSync(account, source, targetStatus)); continue; } await syncUserBillingAccount(tx, account, source); synced.push({ logicalId: account.logicalId, targetKind: account.target.kind, mutation: true, userId: account.userId, keyId: account.target.keyId, keyPrefix: source.serviceKeyPrefix, fingerprint: source.fingerprint, valuesRedacted: true }); } }); } finally { await sql.end({ timeout: 5 }); } const accounts = await accountStatuses(config, target, options, database); return report("sync", config, target, options, accounts, database, { mutation: synced.some((item) => item.mutation === true), synced }); } async function accountStatuses(config: LoadedConfig, target: Target, options: Options, database: DatabaseAccess, preloadedSources?: SourceMaterial[]): Promise[]> { const sql = database.kind === "platform-service" && database.databaseUrlSource.value ? postgres(database.databaseUrlSource.value, { max: 1 }) : null; try { const statuses: Record[] = []; for (const account of target.accounts) { const source = preloadedSources?.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey) ?? readAccountSource(account, config.sourceRoot, false); const targetStatus = account.target.kind === "kubernetes-secret" ? runtimeSecretStatus(target, account, source, options) : database.kind === "local-k3s" ? userBillingApiKeyStatusLocalK3s(database.runtimeSecretSpec, account, source, options) : await userBillingApiKeyStatus(sql, account, source); const blockers = [ ...validateAccount(account, source), ...targetStatusAvailabilityBlockers(account, targetStatus), ...targetFingerprintBlockers(account, targetStatus), ]; statuses.push({ logicalId: account.logicalId, account: publicAccount(account), source: publicSource(source), target: targetStatus, blockers, valuesRedacted: true, }); } return statuses; } finally { if (sql) await sql.end({ timeout: 5 }); } } function targetFingerprintBlockers(account: Account, targetStatus: Record): Record[] { if (targetStatus.matchesSourceFingerprint === false) { return [{ code: "target_fingerprint_mismatch", logicalId: account.logicalId, targetKind: account.target.kind }]; } const apiKey = targetStatus.apiKey; if (typeof apiKey === "object" && apiKey !== null && (apiKey as Record).matchesSourceFingerprint === false) { return [{ code: "target_fingerprint_mismatch", logicalId: account.logicalId, targetKind: account.target.kind }]; } return []; } function targetStatusAvailabilityBlockers(account: Account, targetStatus: Record): Record[] { if (targetStatus.checked === false || typeof targetStatus.error === "string") { return [{ code: "target_status_unavailable", logicalId: account.logicalId, targetKind: account.target.kind, reason: targetStatus.error ?? targetStatus.reason ?? "unknown" }]; } return []; } function readAccountSource(account: Account, sourceRoot: string, allowCreate: boolean): SourceMaterial { if (allowCreate && account.createIfMissing.enabled && account.createIfMissing.randomBase64Url !== null) { const sourcePath = resolveSourcePath(account.sourceRef, sourceRoot); if (!existsSync(sourcePath)) createSourceFile(sourcePath, account.sourceKey, account.createIfMissing.randomBase64Url); } return readSource(account, sourceRoot, false); } function readSource(source: SourceRef, sourceRoot: string, _allowCreate: boolean): SourceMaterial { const sourcePath = resolveSourcePath(source.sourceRef, sourceRoot); if (!existsSync(sourcePath)) { return sourceBlocker(source, sourcePath, false, "source_ref_missing"); } const value = parseEnvFile(readFileSync(sourcePath, "utf8")).get(source.sourceKey); if (!value) return sourceBlocker(source, sourcePath, true, "source_key_missing"); const sha256Hex = sha256(value); return { ok: true, sourceRef: source.sourceRef, sourceKey: source.sourceKey, sourcePath: displaySourcePath(sourcePath), exists: true, byteCount: Buffer.byteLength(value, "utf8"), keyPrefix: value.slice(0, Math.min(12, value.length)), serviceKeyPrefix: value.slice(0, Math.min(16, value.length)), fingerprint: `sha256:${sha256Hex.slice(0, 16)}`, sha256Hex, value, blocker: null, }; } function sourceBlocker(source: SourceRef, sourcePath: string, exists: boolean, code: string): SourceMaterial { return { ok: false, sourceRef: source.sourceRef, sourceKey: source.sourceKey, sourcePath: displaySourcePath(sourcePath), exists, byteCount: null, keyPrefix: null, serviceKeyPrefix: null, fingerprint: null, sha256Hex: null, value: null, blocker: { code, sourceRef: source.sourceRef, sourceKey: source.sourceKey }, }; } function createSourceFile(sourcePath: string, sourceKey: string, spec: { bytes: number; prefix: string }): void { mkdirSync(dirname(sourcePath), { recursive: true, mode: 0o700 }); const value = `${spec.prefix}${randomBytes(spec.bytes).toString("base64url")}`; writeFileSync(sourcePath, `${sourceKey}=${value}\n`, { flag: "wx", mode: 0o600 }); chmodSync(sourcePath, 0o600); } async function syncUserBillingAccount(tx: any, account: Account, source: SourceMaterial): Promise { const metadata = { source: "unidesk-hwlab-test-accounts", logicalId: account.logicalId, sourceRef: account.sourceRef }; const conflicts = await tx`SELECT id FROM hwlab_users WHERE id <> ${account.userId} AND (lower(username) = lower(${account.username}) OR lower(email) = lower(${account.email ?? ""})) LIMIT 1`; if (conflicts.length > 0) throw new Error(`${account.logicalId} username/email is already owned by another user`); const keyHashConflicts = await tx`SELECT id FROM hwlab_api_keys WHERE key_hash = ${source.sha256Hex ?? ""} AND id <> ${account.target.keyId ?? ""} LIMIT 1`; if (keyHashConflicts.length > 0) throw new Error(`${account.logicalId} API key hash is already owned by another key`); await tx` INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role, email_verified) VALUES (${account.userId}, ${account.email ?? ""}, ${account.username}, ${account.displayName}, 'owner-only-api-key-account', ${account.status}, ${account.role}, true) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, display_name = EXCLUDED.display_name, status = EXCLUDED.status, role = EXCLUDED.role, email_verified = true, updated_at = now()`; await tx` INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES (${account.userId}, ${account.initialCredits}, 0, ${account.planId}) ON CONFLICT (user_id) DO UPDATE SET balance_credits = GREATEST(hwlab_credit_accounts.balance_credits, EXCLUDED.balance_credits), plan_id = EXCLUDED.plan_id, updated_at = now()`; if (account.initialCredits > 0) { await tx` INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, idempotency_key, operator_user_id, operator_username, metadata) VALUES (${`led_${account.logicalId.replaceAll("-", "_")}_initial`}, ${account.userId}, ${account.initialCredits}, 0, ${account.initialCredits}, 'admin_grant', 'test_account_initial_credit', 'test-account-sync', 'applied', ${`test-account-sync:${account.logicalId}:initial-credit`}, 'usr_v03_admin', 'admin', ${tx.json(metadata)}) ON CONFLICT DO NOTHING`; } await tx` INSERT INTO hwlab_api_keys (id, user_id, name, key_prefix, key_hash, scopes_json, status, revoked_at) VALUES (${account.target.keyId ?? ""}, ${account.userId}, ${account.target.keyName}, ${source.serviceKeyPrefix ?? ""}, ${source.sha256Hex ?? ""}, ${tx.json(account.target.scopes)}, 'active', NULL) ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, key_prefix = EXCLUDED.key_prefix, key_hash = EXCLUDED.key_hash, scopes_json = EXCLUDED.scopes_json, status = 'active', revoked_at = NULL`; } function syncKubernetesSecretIfNeeded(target: Target, account: Account, source: SourceMaterial, options: Options): Record { const targetStatus = runtimeSecretStatus(target, account, source, options); if (targetStatus.matchesSourceFingerprint === true) return skippedKubernetesSecretSync(target, account, source, targetStatus); return syncKubernetesSecret(target, account, source, options); } function skippedKubernetesSecretSync(target: Target, account: Account, source: SourceMaterial, targetStatus: Record): Record { const namespace = account.target.namespace ?? target.namespace; const secretName = account.target.secretName ?? ""; return { logicalId: account.logicalId, targetKind: account.target.kind, namespace, secretName, targetKey: account.target.targetKey, rolloutDeployment: account.target.rolloutDeployment, mutation: false, skipped: "already-matches-source", target: compactTargetStatus(targetStatus), fingerprint: source.fingerprint, valuesRedacted: true, }; } function skippedUserBillingAccountSync(account: Account, source: SourceMaterial, targetStatus: Record): Record { return { logicalId: account.logicalId, targetKind: account.target.kind, mutation: false, skipped: "already-matches-source", userId: account.userId, keyId: account.target.keyId, keyPrefix: source.serviceKeyPrefix, fingerprint: source.fingerprint, target: compactTargetStatus(targetStatus), valuesRedacted: true, }; } function databaseAccessForTarget(config: LoadedConfig, target: Target): DatabaseAccess { const spec = runtimeSecretSpec({ node: target.node, lane: target.lane }); const runtimeStoreMode = spec.runtimeLaneSpec?.runtimeStore?.postgres?.mode ?? null; if (runtimeStoreMode === "local-k3s") { return { kind: "local-k3s", runtimeStoreMode, databaseUrlSource: inactiveDatabaseSource(target.userBilling.databaseUrlSource, config.sourceRoot), runtimeSecretSpec: spec, }; } return { kind: "platform-service", runtimeStoreMode, databaseUrlSource: readSource(target.userBilling.databaseUrlSource, config.sourceRoot, false), runtimeSecretSpec: spec, }; } function inactiveDatabaseSource(source: SourceRef, sourceRoot: string): SourceMaterial { const sourcePath = resolveSourcePath(source.sourceRef, sourceRoot); return { ok: true, sourceRef: source.sourceRef, sourceKey: source.sourceKey, sourcePath: displaySourcePath(sourcePath), exists: existsSync(sourcePath), byteCount: null, keyPrefix: null, serviceKeyPrefix: null, fingerprint: null, sha256Hex: null, value: null, blocker: null, }; } function userBillingApiKeyStatusLocalK3s(spec: RuntimeSecretSpec | null, account: Account, source: SourceMaterial, options: Options): Record { if (spec === null) return { checked: false, kind: account.target.kind, reason: "runtime_secret_spec_missing", valuesRedacted: true }; const result = runLocalK3sUserBillingSql(spec, userBillingStatusSql(account, source), options); const parsed = parseJsonObjectLine(result.stdout); if (result.exitCode !== 0 || parsed === null) { return { checked: false, kind: account.target.kind, error: compactText(result.stderr || result.stdout || "local-k3s-user-billing-status-failed"), result: compactCommandResult(result), valuesRedacted: true, }; } if (parsed.ok === false) { return { checked: false, kind: account.target.kind, error: String(parsed.error ?? "local-k3s-user-billing-status-failed"), result: compactCommandResult(result), valuesRedacted: true }; } return { checked: true, kind: account.target.kind, exists: parsed.exists === true, userId: account.userId, keyId: account.target.keyId, user: parsed.user ?? null, apiKey: normalizeApiKeyStatus(parsed.apiKey, account), result: compactCommandResult(result), valuesRedacted: true, }; } function syncUserBillingAccountLocalK3s(spec: RuntimeSecretSpec | null, account: Account, source: SourceMaterial, options: Options): Record { if (spec === null) throw new Error(`${account.logicalId} local-k3s runtime secret spec missing`); if (!source.sha256Hex || !source.serviceKeyPrefix) throw new Error(`${account.logicalId} source material missing hash after preflight`); const result = runLocalK3sUserBillingSql(spec, syncUserBillingAccountSql(account, source), options); const parsed = parseJsonObjectLine(result.stdout); if (result.exitCode !== 0 || parsed === null || parsed.ok === false) { throw new Error(`${account.logicalId} local-k3s user-billing sync failed: ${compactText(result.stderr || result.stdout || String(parsed?.error ?? "unknown"))}`); } return { logicalId: account.logicalId, targetKind: account.target.kind, mutation: parsed.mutation === true, userId: account.userId, keyId: account.target.keyId, keyPrefix: source.serviceKeyPrefix, fingerprint: source.fingerprint, result: compactCommandResult(result), valuesRedacted: true, }; } function runLocalK3sUserBillingSql(spec: RuntimeSecretSpec, sql: string, options: Options): CommandResult { const script = [ "set -eu", `namespace=${shellQuote(spec.namespace)}`, `postgres_secret=${shellQuote(spec.postgresSecret)}`, `postgres_statefulset=${shellQuote(spec.postgresStatefulSet)}`, `postgres_admin_user=${shellQuote(spec.postgresAdminUser)}`, `db_name=${shellQuote(spec.cloudApiDbName)}`, "secret_b64_key() { kubectl -n \"$namespace\" get secret \"$1\" -o \"go-template={{ index .data \\\"$2\\\" }}\" 2>/dev/null || true; }", "postgres_admin_b64=$(secret_b64_key \"$postgres_secret\" POSTGRES_PASSWORD)", "if [ -z \"$postgres_admin_b64\" ]; then printf '{\"ok\":false,\"error\":\"postgres-admin-secret-missing\"}\\n'; exit 0; fi", "postgres_admin_password=$(printf '%s' \"$postgres_admin_b64\" | base64 -d 2>/dev/null || true)", "if [ -z \"$postgres_admin_password\" ]; then printf '{\"ok\":false,\"error\":\"postgres-admin-secret-decode-failed\"}\\n'; exit 0; fi", "kubectl -n \"$namespace\" exec -i \"statefulset/$postgres_statefulset\" -c postgres -- env PGPASSWORD=\"$postgres_admin_password\" psql -X -q -t -A -v ON_ERROR_STOP=1 -U \"$postgres_admin_user\" -d \"$db_name\" <<'SQL'", sql, "SQL", ].join("\n"); return runCommand([transPath(), `${spec.node}:k3s`, "sh"], repoRoot, { input: script, timeoutMs: options.timeoutSeconds * 1000 }); } function userBillingStatusSql(account: Account, source: SourceMaterial): string { const expectedHash = source.ok && source.sha256Hex ? source.sha256Hex : ""; return ` SELECT jsonb_build_object( 'ok', true, 'checked', true, 'kind', ${sqlLiteral(account.target.kind)}, 'exists', u.id IS NOT NULL, 'user', CASE WHEN u.id IS NULL THEN NULL ELSE jsonb_build_object( 'id', u.id, 'email', u.email, 'username', u.username, 'displayName', u.display_name, 'role', u.role, 'status', u.status, 'planId', COALESCE(a.plan_id, ''), 'balanceCredits', COALESCE(a.balance_credits, 0), 'reservedCredits', COALESCE(a.reserved_credits, 0) ) END, 'apiKey', CASE WHEN k.id IS NULL THEN jsonb_build_object( 'exists', false, 'keyId', ${sqlLiteral(account.target.keyId ?? "")}, 'keyPrefix', NULL, 'status', NULL, 'scopes', '[]'::jsonb, 'matchesSourceFingerprint', NULL ) ELSE jsonb_build_object( 'exists', true, 'keyId', ${sqlLiteral(account.target.keyId ?? "")}, 'keyPrefix', k.key_prefix, 'status', k.status, 'scopes', COALESCE(k.scopes_json, '[]'::jsonb), 'matchesSourceFingerprint', CASE WHEN ${sqlLiteral(expectedHash)} = '' OR k.key_hash IS NULL THEN NULL ELSE k.key_hash = ${sqlLiteral(expectedHash)} END ) END )::text FROM (SELECT 1) seed LEFT JOIN hwlab_users u ON u.id = ${sqlLiteral(account.userId)} LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id LEFT JOIN hwlab_api_keys k ON k.id = ${sqlLiteral(account.target.keyId ?? "")} AND k.user_id = u.id LIMIT 1;`.trim(); } function syncUserBillingAccountSql(account: Account, source: SourceMaterial): string { const metadata = JSON.stringify({ source: "unidesk-hwlab-test-accounts", logicalId: account.logicalId, sourceRef: account.sourceRef }); return ` BEGIN; DO $$ BEGIN IF EXISTS (SELECT 1 FROM hwlab_users WHERE id <> ${sqlLiteral(account.userId)} AND (lower(username) = lower(${sqlLiteral(account.username)}) OR lower(email) = lower(${sqlLiteral(account.email ?? "")})) LIMIT 1) THEN RAISE EXCEPTION 'test account username/email is already owned by another user'; END IF; IF EXISTS (SELECT 1 FROM hwlab_api_keys WHERE key_hash = ${sqlLiteral(source.sha256Hex ?? "")} AND id <> ${sqlLiteral(account.target.keyId ?? "")} LIMIT 1) THEN RAISE EXCEPTION 'test account API key hash is already owned by another key'; END IF; END $$; INSERT INTO hwlab_users (id, email, username, display_name, password_hash, status, role, email_verified) VALUES (${sqlLiteral(account.userId)}, ${sqlLiteral(account.email ?? "")}, ${sqlLiteral(account.username)}, ${sqlLiteral(account.displayName)}, 'owner-only-api-key-account', ${sqlLiteral(account.status)}, ${sqlLiteral(account.role)}, true) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, display_name = EXCLUDED.display_name, status = EXCLUDED.status, role = EXCLUDED.role, email_verified = true, updated_at = now(); INSERT INTO hwlab_credit_accounts (user_id, balance_credits, reserved_credits, plan_id) VALUES (${sqlLiteral(account.userId)}, ${account.initialCredits}, 0, ${sqlLiteral(account.planId)}) ON CONFLICT (user_id) DO UPDATE SET balance_credits = GREATEST(hwlab_credit_accounts.balance_credits, EXCLUDED.balance_credits), plan_id = EXCLUDED.plan_id, updated_at = now(); ${account.initialCredits > 0 ? `INSERT INTO hwlab_credit_ledger (id, user_id, delta_credits, balance_before, balance_after, kind, reason, source, status, idempotency_key, operator_user_id, operator_username, metadata) VALUES (${sqlLiteral(`led_${account.logicalId.replaceAll("-", "_")}_initial`)}, ${sqlLiteral(account.userId)}, ${account.initialCredits}, 0, ${account.initialCredits}, 'admin_grant', 'test_account_initial_credit', 'test-account-sync', 'applied', ${sqlLiteral(`test-account-sync:${account.logicalId}:initial-credit`)}, 'usr_v03_admin', 'admin', ${sqlLiteral(metadata)}::jsonb) ON CONFLICT DO NOTHING;` : ""} INSERT INTO hwlab_api_keys (id, user_id, name, key_prefix, key_hash, scopes_json, status, revoked_at) VALUES (${sqlLiteral(account.target.keyId ?? "")}, ${sqlLiteral(account.userId)}, ${sqlLiteral(account.target.keyName)}, ${sqlLiteral(source.serviceKeyPrefix ?? "")}, ${sqlLiteral(source.sha256Hex ?? "")}, ${sqlLiteral(JSON.stringify(account.target.scopes))}::jsonb, 'active', NULL) ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, key_prefix = EXCLUDED.key_prefix, key_hash = EXCLUDED.key_hash, scopes_json = EXCLUDED.scopes_json, status = 'active', revoked_at = NULL; COMMIT; SELECT jsonb_build_object('ok', true, 'mutation', true, 'userId', ${sqlLiteral(account.userId)}, 'keyId', ${sqlLiteral(account.target.keyId ?? "")}, 'keyPrefix', ${sqlLiteral(source.serviceKeyPrefix ?? "")}, 'valuesRedacted', true)::text;`.trim(); } function normalizeApiKeyStatus(value: unknown, account: Account): Record { const apiKey = plainObject(value); if (apiKey === null) return { exists: false, keyId: account.target.keyId, keyPrefix: null, status: null, scopes: [], matchesSourceFingerprint: null }; return { exists: apiKey.exists === true, keyId: apiKey.keyId ?? account.target.keyId, keyPrefix: apiKey.keyPrefix ?? null, status: apiKey.status ?? null, scopes: parseJsonArray(apiKey.scopes), matchesSourceFingerprint: apiKey.matchesSourceFingerprint === true ? true : apiKey.matchesSourceFingerprint === false ? false : null, }; } function syncKubernetesSecret(target: Target, account: Account, source: SourceMaterial, options: Options): Record { const namespace = account.target.namespace ?? target.namespace; const secretName = account.target.secretName ?? ""; const rolloutDeployment = account.target.rolloutDeployment; const script = [ "set -eu", "tmp=\"$(mktemp -d)\"", "trap 'rm -rf \"$tmp\"' EXIT", "key_file=\"$tmp/value\"", "cat >\"$key_file\"", `kubectl -n ${shellQuote(namespace)} create secret generic ${shellQuote(secretName)} --from-file=${shellQuote(`${account.target.targetKey}=`)}"$key_file" --dry-run=client -o yaml | kubectl apply -f - >/dev/null`, ...(rolloutDeployment ? [ `kubectl -n ${shellQuote(namespace)} rollout restart deploy/${shellQuote(rolloutDeployment)} >/dev/null`, `kubectl -n ${shellQuote(namespace)} rollout status deploy/${shellQuote(rolloutDeployment)} --timeout=${shellQuote(String(options.timeoutSeconds))}s >/dev/null`, ] : []), `printf 'kind=%s namespace=%s secret=%s key=%s rolloutDeployment=%s mutation=true\\n' ${shellQuote(account.target.kind)} ${shellQuote(namespace)} ${shellQuote(secretName)} ${shellQuote(account.target.targetKey)} ${shellQuote(rolloutDeployment ?? "")}`, ].join("\n"); const result = runCommand([transPath(), `${target.node}:k3s`, "sh", "--", script], repoRoot, { input: source.value ?? "", timeoutMs: options.timeoutSeconds * 1000 + 15000 }); if (result.exitCode !== 0) throw new Error(`${account.logicalId} kubernetes secret sync failed: ${(result.stderr || result.stdout).slice(0, 500)}`); return { logicalId: account.logicalId, targetKind: account.target.kind, namespace, secretName, targetKey: account.target.targetKey, rolloutDeployment, mutation: true, result: compactCommandResult(result), fingerprint: source.fingerprint, valuesRedacted: true, }; } function hostEnvAccountStatus(target: Target, account: Account, source: SourceMaterial, options: Options): Record { const targets = account.hostEnvTargets.map((hostTarget) => hostEnvTargetStatus(target, account, hostTarget, source, options)); const blockers = [ ...(source.ok ? [] : source.blocker ? [source.blocker] : []), ...targets.flatMap((item) => { if (item.exists === false) return [{ code: "host_env_target_missing", logicalId: account.logicalId, path: item.path, envKey: item.envKey }]; if (item.keyPresent === false) return [{ code: "host_env_key_missing", logicalId: account.logicalId, path: item.path, envKey: item.envKey }]; if (item.matchesSourceFingerprint === false) return [{ code: "host_env_fingerprint_mismatch", logicalId: account.logicalId, path: item.path, envKey: item.envKey }]; return []; }), ]; return { logicalId: account.logicalId, account: publicHostEnvAccount(account), source: publicSource(source), hostEnvTargets: targets, blockers, valuesRedacted: true, }; } function hostEnvTargetStatus(target: Target, _account: Account, hostTarget: HostEnvTarget, source: SourceMaterial, options: Options): Record { const script = [ "set -eu", `target_path=${shellQuote(hostTarget.path)}`, `env_key=${shellQuote(hostTarget.envKey)}`, `expected_sha=${shellQuote(source.sha256Hex ?? "")}`, "if [ ! -f \"$target_path\" ]; then printf 'exists=false keyPresent=false path=%s envKey=%s mode= byteCount=0 fingerprint= matchesSourceFingerprint=unknown\\n' \"$target_path\" \"$env_key\"; exit 0; fi", "line=$(grep -E \"^(export[[:space:]]+)?${env_key}=\" \"$target_path\" | tail -n 1 || true)", "mode=$(stat -c '%a' \"$target_path\" 2>/dev/null || printf unknown)", "if [ -z \"$line\" ]; then printf 'exists=true keyPresent=false path=%s envKey=%s mode=%s byteCount=0 fingerprint= matchesSourceFingerprint=unknown\\n' \"$target_path\" \"$env_key\" \"$mode\"; exit 0; fi", "value=${line#*=}", "value=${value%$'\\r'}", "sha=$(printf '%s' \"$value\" | sha256sum | awk '{print $1}')", "bytes=$(printf '%s' \"$value\" | wc -c | tr -d ' ')", "prefix=$(printf '%s' \"$value\" | cut -c1-12)", "matches=unknown", "if [ -n \"$expected_sha\" ]; then if [ \"$sha\" = \"$expected_sha\" ]; then matches=true; else matches=false; fi; fi", "printf 'exists=true keyPresent=true path=%s envKey=%s mode=%s byteCount=%s keyPrefix=%s fingerprint=sha256:%s matchesSourceFingerprint=%s\\n' \"$target_path\" \"$env_key\" \"$mode\" \"$bytes\" \"$prefix\" \"$(printf '%s' \"$sha\" | cut -c1-16)\" \"$matches\"", ].join("\n"); const result = runCommand([transPath(), target.node, "sh", "--", script], repoRoot, { timeoutMs: options.timeoutSeconds * 1000 }); const fields = keyValueFields(result.stdout); return { checked: true, kind: "host-env", path: fields.path || hostTarget.path, envKey: fields.envKey || hostTarget.envKey, exists: fields.exists === "true", keyPresent: fields.keyPresent === "true", mode: fields.mode || null, byteCount: numericField(fields.byteCount), keyPrefix: fields.keyPrefix || null, fingerprint: fields.fingerprint || null, matchesSourceFingerprint: fields.matchesSourceFingerprint === "true" ? true : fields.matchesSourceFingerprint === "false" ? false : null, result: compactCommandResult(result), valuesRedacted: true, }; } function syncHostEnvTarget(target: Target, account: Account, hostTarget: HostEnvTarget, source: SourceMaterial, options: Options): Record { const before = hostEnvTargetStatus(target, account, hostTarget, source, options); if (before.matchesSourceFingerprint === true && before.mode === hostTarget.mode) { return { logicalId: account.logicalId, targetKind: "host-env", mutation: false, skipped: "already-matches-source", target: before, fingerprint: source.fingerprint, valuesRedacted: true }; } const script = [ "set -eu", `target_path=${shellQuote(hostTarget.path)}`, `env_key=${shellQuote(hostTarget.envKey)}`, `mode=${shellQuote(hostTarget.mode)}`, "tmp=$(mktemp)", "trap 'rm -f \"$tmp\"' EXIT", "value=$(cat)", "mkdir -p \"$(dirname \"$target_path\")\"", "chmod 700 \"$(dirname \"$target_path\")\" 2>/dev/null || true", "printf '%s=%s\\n' \"$env_key\" \"$value\" >\"$tmp\"", "install -m \"$mode\" \"$tmp\" \"$target_path\"", "sha=$(printf '%s' \"$value\" | sha256sum | awk '{print $1}')", "bytes=$(printf '%s' \"$value\" | wc -c | tr -d ' ')", "prefix=$(printf '%s' \"$value\" | cut -c1-12)", "actual_mode=$(stat -c '%a' \"$target_path\" 2>/dev/null || printf unknown)", "printf 'kind=host-env path=%s envKey=%s mode=%s byteCount=%s keyPrefix=%s fingerprint=sha256:%s mutation=true\\n' \"$target_path\" \"$env_key\" \"$actual_mode\" \"$bytes\" \"$prefix\" \"$(printf '%s' \"$sha\" | cut -c1-16)\"", ].join("\n"); const result = runCommand([transPath(), target.node, "sh", "--", script], repoRoot, { input: source.value ?? "", timeoutMs: options.timeoutSeconds * 1000 + 15000 }); if (result.exitCode !== 0) throw new Error(`${account.logicalId} host env sync failed: ${(result.stderr || result.stdout).slice(0, 500)}`); const fields = keyValueFields(result.stdout); return { logicalId: account.logicalId, targetKind: "host-env", path: fields.path || hostTarget.path, envKey: fields.envKey || hostTarget.envKey, mode: fields.mode || hostTarget.mode, byteCount: numericField(fields.byteCount), keyPrefix: fields.keyPrefix || null, fingerprint: fields.fingerprint || source.fingerprint, mutation: true, result: compactCommandResult(result), valuesRedacted: true, }; } function runtimeSecretStatus(target: Target, account: Account, source: SourceMaterial, options: Options): Record { const namespace = account.target.namespace ?? target.namespace; const secretName = account.target.secretName ?? ""; const result = runCommand([transPath(), `${target.node}:k3s`, "kubectl", "-n", namespace, "get", "secret", secretName, "-o", `jsonpath={.data.${account.target.targetKey.replaceAll(".", "\\.")}}`], repoRoot, { timeoutMs: options.timeoutSeconds * 1000 }); if (result.exitCode !== 0 || result.stdout.trim().length === 0) { return { checked: true, kind: account.target.kind, namespace, secretName, targetKey: account.target.targetKey, exists: false, result: compactCommandResult(result), valuesRedacted: true }; } const value = Buffer.from(result.stdout.trim(), "base64").toString("utf8"); const sha256Hex = sha256(value); return { checked: true, kind: account.target.kind, namespace, secretName, targetKey: account.target.targetKey, exists: true, byteCount: Buffer.byteLength(value, "utf8"), keyPrefix: value.slice(0, Math.min(12, value.length)), fingerprint: `sha256:${sha256Hex.slice(0, 16)}`, matchesSourceFingerprint: source.ok && source.sha256Hex ? source.sha256Hex === sha256Hex : null, result: compactCommandResult(result), valuesRedacted: true, }; } function hostEnvReport(action: string, config: LoadedConfig, target: Target, options: Options, accounts: Record[], extra: Record = {}): Record { const blockers = [ ...accounts.flatMap((account) => Array.isArray(account.blockers) ? account.blockers : []), ...((extra.blockers as Record[] | undefined) ?? []), ].filter(Boolean); return { ok: blockers.length === 0, command: `hwlab nodes test-accounts ${action}`, configPath: config.configPath, version: config.version, target: { id: target.id, node: target.node, lane: target.lane, namespace: target.namespace, publicUrl: target.publicUrl }, sourceRoot: config.sourceRoot, accounts, blockers, next: blockers.length === 0 ? undefined : { sync: `bun scripts/cli.ts hwlab nodes test-accounts host-env sync --node ${options.node} --lane ${options.lane} --confirm` }, ...extra, valuesRedacted: true, }; } async function userBillingApiKeyStatus(sql: any, account: Account, source: SourceMaterial): Promise> { if (!sql) return { checked: false, kind: account.target.kind, reason: "database_url_source_missing", valuesRedacted: true }; const rows = await sql` SELECT u.id AS user_id, u.email, u.username, u.display_name, u.role, u.status AS user_status, COALESCE(a.balance_credits, 0) AS balance_credits, COALESCE(a.reserved_credits, 0) AS reserved_credits, COALESCE(a.plan_id, '') AS plan_id, k.id AS key_id, k.key_prefix, k.key_hash, k.scopes_json::text AS scopes_json, k.status AS key_status FROM hwlab_users u LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id LEFT JOIN hwlab_api_keys k ON k.id = ${account.target.keyId ?? ""} AND k.user_id = u.id WHERE u.id = ${account.userId} LIMIT 1`; const row = rows[0] as Record | undefined; if (!row) return { checked: true, kind: account.target.kind, exists: false, userId: account.userId, keyId: account.target.keyId, valuesRedacted: true }; return { checked: true, kind: account.target.kind, exists: true, user: { id: row.user_id, email: row.email, username: row.username, displayName: row.display_name, role: row.role, status: row.user_status, planId: row.plan_id, balanceCredits: Number(row.balance_credits ?? 0), reservedCredits: Number(row.reserved_credits ?? 0) }, apiKey: { exists: Boolean(row.key_id), keyId: account.target.keyId, keyPrefix: row.key_prefix || null, status: row.key_status || null, scopes: parseJsonArray(row.scopes_json), matchesSourceFingerprint: source.ok && source.sha256Hex && row.key_hash ? row.key_hash === source.sha256Hex : null }, valuesRedacted: true, }; } function userBillingAccountMatches(account: Account, targetStatus: Record): boolean { const user = plainObject(targetStatus.user); const apiKey = plainObject(targetStatus.apiKey); return targetStatus.exists === true && user !== null && apiKey !== null && user.id === account.userId && (user.email ?? "") === (account.email ?? "") && user.username === account.username && user.displayName === account.displayName && user.role === account.role && user.status === account.status && user.planId === account.planId && Number(user.balanceCredits ?? 0) >= account.initialCredits && apiKey.exists === true && apiKey.keyId === account.target.keyId && apiKey.status === "active" && apiKey.matchesSourceFingerprint === true && stringArraysEqual(apiKey.scopes, account.target.scopes); } function compactTargetStatus(status: Record): Record { const target: Record = { valuesRedacted: true }; for (const key of ["checked", "kind", "namespace", "secretName", "targetKey", "exists", "byteCount", "keyPrefix", "fingerprint", "matchesSourceFingerprint", "user", "apiKey"]) { if (status[key] !== undefined) target[key] = status[key]; } return target; } function report(action: string, config: LoadedConfig, target: Target, options: Options, accounts: Record[], database: DatabaseAccess, extra: Record = {}): Record { const blockers = [ ...(database.kind === "platform-service" && !database.databaseUrlSource.ok ? [database.databaseUrlSource.blocker] : []), ...accounts.flatMap((account) => Array.isArray(account.blockers) ? account.blockers : []), ...((extra.blockers as Record[] | undefined) ?? []), ].filter(Boolean); return { ok: blockers.length === 0, command: `hwlab nodes test-accounts ${action}`, configPath: config.configPath, version: config.version, target: { id: target.id, node: target.node, lane: target.lane, namespace: target.namespace, publicUrl: target.publicUrl, userBillingServiceId: target.userBilling.serviceId }, sourceRoot: config.sourceRoot, databaseAccess: publicDatabaseAccess(database), databaseUrlSource: publicSource(database.databaseUrlSource), accounts, blockers, next: blockers.length === 0 ? undefined : { sync: `bun scripts/cli.ts hwlab nodes test-accounts sync --node ${options.node} --lane ${options.lane} --confirm` }, ...extra, valuesRedacted: true, }; } function validateAccount(account: Account, source: SourceMaterial): Record[] { const blockers: Record[] = []; if (!source.ok && source.blocker) blockers.push(source.blocker); if (account.target.targetKey !== apiKeyTargetKey) blockers.push({ code: "target_key_mismatch", logicalId: account.logicalId, expected: apiKeyTargetKey, actual: account.target.targetKey }); if (account.target.kind === "kubernetes-secret" && (!account.target.namespace || !account.target.secretName)) blockers.push({ code: "kubernetes_secret_target_incomplete", logicalId: account.logicalId }); if (account.target.kind === "user-billing-api-key" && (!account.email || !account.target.keyId)) blockers.push({ code: "user_billing_target_incomplete", logicalId: account.logicalId }); return blockers; } function loadConfig(configPath: string): LoadedConfig { const raw = asRecord(Bun.YAML.parse(readFileSync(rootPath(configPath), "utf8")) as unknown, configPath); const kind = stringField(raw, "kind", configPath); if (kind !== "hwlab-test-accounts") throw new Error(`${configPath}.kind must be hwlab-test-accounts`); return { configPath, version: numberField(raw, "version", configPath), kind, sourceRoot: stringField(raw, "sourceRoot", configPath), targets: arrayField(raw, "targets", configPath).map((item, index) => parseTarget(item, `${configPath}.targets[${index}]`)) }; } function parseTarget(raw: Record, path: string): Target { const userBilling = asRecord(raw.userBilling, `${path}.userBilling`); const databaseUrlSource = asRecord(userBilling.databaseUrlSource, `${path}.userBilling.databaseUrlSource`); return { id: stringField(raw, "id", path), node: stringField(raw, "node", path), lane: stringField(raw, "lane", path), namespace: stringField(raw, "namespace", path), publicUrl: stringField(raw, "publicUrl", path), userBilling: { serviceId: stringField(userBilling, "serviceId", `${path}.userBilling`), databaseUrlSource: parseSourceRef(databaseUrlSource, `${path}.userBilling.databaseUrlSource`) }, accounts: arrayField(raw, "accounts", path).map((item, index) => parseAccount(item, `${path}.accounts[${index}]`)), }; } function parseAccount(raw: Record, path: string): Account { const role = stringField(raw, "role", path); if (role !== "admin" && role !== "user") throw new Error(`${path}.role must be admin or user`); const status = optionalStringField(raw, "status", path) ?? "active"; if (status !== "active" && status !== "disabled" && status !== "pending") throw new Error(`${path}.status is invalid`); const target = asRecord(raw.target, `${path}.target`); const targetKind = stringField(target, "kind", `${path}.target`); if (targetKind !== "kubernetes-secret" && targetKind !== "user-billing-api-key") throw new Error(`${path}.target.kind is unsupported`); const createIfMissing = raw.createIfMissing === undefined ? {} : asRecord(raw.createIfMissing, `${path}.createIfMissing`); const randomBase64Url = createIfMissing.randomBase64Url === undefined ? null : asRecord(createIfMissing.randomBase64Url, `${path}.createIfMissing.randomBase64Url`); return { logicalId: stringField(raw, "logicalId", path), kind: stringField(raw, "kind", path), userId: stringField(raw, "userId", path), username: stringField(raw, "username", path), email: optionalStringField(raw, "email", path) ?? null, displayName: optionalStringField(raw, "displayName", path) ?? stringField(raw, "username", path), role, status, planId: optionalStringField(raw, "planId", path) ?? "default", initialCredits: optionalNumberField(raw, "initialCredits", path) ?? 0, permissions: optionalStringArray(raw.permissions, `${path}.permissions`), workbench: raw.workbench === undefined ? null : asRecord(raw.workbench, `${path}.workbench`), ...parseSourceRef(raw, path), createIfMissing: { enabled: createIfMissing.enabled === true, randomBase64Url: randomBase64Url === null ? null : { bytes: numberField(randomBase64Url, "bytes", `${path}.createIfMissing.randomBase64Url`), prefix: optionalStringField(randomBase64Url, "prefix", `${path}.createIfMissing.randomBase64Url`) ?? "" } }, hostEnvTargets: raw.hostEnvTargets === undefined ? [] : arrayField(raw, "hostEnvTargets", path).map((item, index) => parseHostEnvTarget(item, `${path}.hostEnvTargets[${index}]`)), target: { kind: targetKind, namespace: optionalStringField(target, "namespace", `${path}.target`) ?? null, secretName: optionalStringField(target, "secretName", `${path}.target`) ?? null, serviceId: optionalStringField(target, "serviceId", `${path}.target`) ?? null, keyId: optionalStringField(target, "keyId", `${path}.target`) ?? null, keyName: optionalStringField(target, "keyName", `${path}.target`) ?? "default", targetKey: stringField(target, "targetKey", `${path}.target`), rolloutDeployment: optionalStringField(target, "rolloutDeployment", `${path}.target`) ?? null, scopes: optionalStringArray(target.scopes, `${path}.target.scopes`, ["api"]) }, }; } function parseSourceRef(raw: Record, path: string): SourceRef { return { sourceRef: stringField(raw, "sourceRef", path), sourceKey: stringField(raw, "sourceKey", path) }; } function parseHostEnvTarget(raw: Record, path: string): HostEnvTarget { const mode = optionalStringField(raw, "mode", path) ?? "0600"; if (!/^[0-7]{3,4}$/u.test(mode)) throw new Error(`${path}.mode must be an octal file mode string`); const envKey = stringField(raw, "envKey", path); if (!/^[A-Z_][A-Z0-9_]*$/u.test(envKey)) throw new Error(`${path}.envKey must be an uppercase env var name`); return { path: stringField(raw, "path", path), envKey, mode }; } function publicAccount(account: Account): Record { return { logicalId: account.logicalId, kind: account.kind, userId: account.userId, username: account.username, email: account.email, displayName: account.displayName, role: account.role, status: account.status, planId: account.planId, initialCredits: account.initialCredits, permissions: account.permissions, workbench: account.workbench, sourceRef: account.sourceRef, sourceKey: account.sourceKey, hostEnvTargets: account.hostEnvTargets, target: account.target }; } function publicHostEnvAccount(account: Account): Record { return { logicalId: account.logicalId, kind: account.kind, userId: account.userId, username: account.username, role: account.role, sourceRef: account.sourceRef, sourceKey: account.sourceKey, hostEnvTargets: account.hostEnvTargets }; } function publicSource(source: SourceMaterial): Record { return { sourceRef: source.sourceRef, sourceKey: source.sourceKey, sourcePath: source.sourcePath, exists: source.exists, byteCount: source.byteCount, keyPrefix: source.keyPrefix, fingerprint: source.fingerprint, valuesRedacted: true }; } function publicDatabaseAccess(database: DatabaseAccess): Record { const spec = database.runtimeSecretSpec; return { mode: database.kind, runtimeStoreMode: database.runtimeStoreMode, databaseUrlSource: { ...publicSource(database.databaseUrlSource), active: database.kind === "platform-service" }, localK3s: database.kind === "local-k3s" && spec !== null ? { namespace: spec.namespace, postgresStatefulSet: spec.postgresStatefulSet, postgresSecret: spec.postgresSecret, database: spec.cloudApiDbName, user: spec.cloudApiDbUser, cloudApiDbSecret: spec.cloudApiDbSecret, cloudApiDbKey: spec.cloudApiDbKey, } : null, valuesRedacted: true, }; } function selectTarget(config: LoadedConfig, options: Options): Target { const target = config.targets.find((item) => item.node === options.node && item.lane === options.lane); if (!target) throw new Error(`${config.configPath} has no target for node=${options.node} lane=${options.lane}`); return target; } function resolveSourcePath(sourceRef: string, sourceRoot: string): string { return isAbsolute(sourceRef) ? sourceRef : join(sourceRoot, sourceRef); } function displaySourcePath(path: string): string { return path.startsWith(`${repoRoot}/`) ? path.slice(repoRoot.length + 1) : path; } function parseEnvFile(text: string): Map { const values = new Map(); for (const rawLine of text.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith("#")) continue; const clean = line.startsWith("export ") ? line.slice("export ".length).trim() : line; const match = clean.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u); if (!match) continue; values.set(match[1] ?? "", unquoteEnvValue((match[2] ?? "").trim())); } return values; } function unquoteEnvValue(value: string): string { if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1); return value; } function keyValueFields(text: string): Record { const fields: Record = {}; for (const token of text.trim().split(/\s+/u)) { const index = token.indexOf("="); if (index <= 0) continue; fields[token.slice(0, index)] = token.slice(index + 1); } return fields; } function numericField(raw: string | undefined): number | null { if (raw === undefined || raw.length === 0) return null; const value = Number(raw); return Number.isFinite(value) ? value : null; } function parseJsonArray(value: unknown): string[] { if (Array.isArray(value)) return value.map((item) => String(item)); if (typeof value !== "string" || value.length === 0) return []; try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.map((item) => String(item)) : []; } catch { return []; } } function plainObject(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; } function stringArraysEqual(left: unknown, right: string[]): boolean { return Array.isArray(left) && left.length === right.length && left.every((item, index) => item === right[index]); } function sha256(value: string): string { return createHash("sha256").update(value).digest("hex"); } function transPath(): string { return rootPath("scripts/trans"); } function compactCommandResult(result: CommandResult): Record { return { command: result.command, exitCode: result.exitCode, stdoutBytes: Buffer.byteLength(result.stdout ?? "", "utf8"), stderr: (result.stderr ?? "").trim().slice(0, 2000), timedOut: result.timedOut }; } function shellQuote(value: string): string { return `'${value.replace(/'/gu, "'\\''")}'`; } function sqlLiteral(value: string): string { return `'${value.replace(/'/gu, "''")}'`; } function parseJsonObjectLine(text: string): Record | null { for (const line of text.split(/\r?\n/u)) { const trimmed = line.trim(); if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) continue; try { const parsed = JSON.parse(trimmed) as unknown; return plainObject(parsed); } catch { continue; } } return null; } function compactText(value: string): string { return value.replace(/\b([A-Za-z0-9_.-]*(?:TOKEN|PASSWORD|SECRET|API_KEY|DATABASE_URL)[A-Za-z0-9_.-]*)=([^\s"'`]+)/giu, "$1=").trim().slice(0, 1000); } function requiredValue(args: string[], index: number, name: string): string { const value = args[index]; if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`); return value; } function positiveInteger(raw: string, name: string): number { const value = Number(raw); if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`); return value; } function asRecord(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`); return value as Record; } function stringField(obj: Record, key: string, path: string): string { const value = obj[key]; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function optionalStringField(obj: Record, key: string, path: string): string | undefined { const value = obj[key]; if (value === undefined) return undefined; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function numberField(obj: Record, key: string, path: string): number { const value = obj[key]; if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${path}.${key} must be a number`); return value; } function optionalNumberField(obj: Record, key: string, path: string): number | undefined { const value = obj[key]; if (value === undefined) return undefined; if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${path}.${key} must be a non-negative number`); return value; } function arrayField(obj: Record, key: string, path: string): Record[] { const value = obj[key]; if (!Array.isArray(value)) throw new Error(`${path}.${key} must be an array`); return value.map((item, index) => asRecord(item, `${path}.${key}[${index}]`)); } function optionalStringArray(value: unknown, path: string, fallback: string[] = []): string[] { if (value === undefined) return fallback; if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be an array of non-empty strings`); return value as string[]; }