#!/usr/bin/env node import { chmod, readFile, writeFile } from "node:fs/promises"; const DEFAULT_BASE_URL = "https://oa.hyinda.com"; const FLAG_KEY = "payment_batch_v1"; function parseArgs(argv) { const [action, ...rest] = argv; const options = { action, baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL, snapshotFile: "", }; for (let index = 0; index < rest.length; index += 1) { const arg = rest[index]; if (arg === "--snapshot-file") { options.snapshotFile = rest[index + 1] ?? ""; index += 1; } else if (arg.startsWith("--snapshot-file=")) { options.snapshotFile = arg.slice("--snapshot-file=".length); } else if (arg.startsWith("--base-url=")) { options.baseUrl = arg.slice("--base-url=".length); } } if (!new Set(["capture", "restore"]).has(options.action)) { throw new Error("action must be capture or restore"); } if (!options.snapshotFile.trim()) { throw new Error("--snapshot-file is required"); } options.baseUrl = options.baseUrl.trim().replace(/\/$/, ""); options.snapshotFile = options.snapshotFile.trim(); return options; } function getAdminCredential() { const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim(); const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim(); if (!identifier || !password) { throw new Error("missing LIVE_SMOKE_ADMIN_* credentials"); } return { identifier, password }; } async function fetchJson(url, init = {}) { const response = await fetch(url, { ...init, signal: AbortSignal.timeout(20_000), }); const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { ok: response.ok, status: response.status, json }; } function errorCode(json) { return typeof json?.error?.code === "string" ? json.error.code : ""; } async function login(baseUrl, credential) { const result = await fetchJson(`${baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": "payment-batch-flag-window-snapshot", }, body: JSON.stringify(credential), }); const accessToken = result.json?.data?.accessToken; if (!result.ok || typeof accessToken !== "string" || !accessToken) { throw new Error(`admin login failed: HTTP ${result.status} ${errorCode(result.json)}`.trim()); } return accessToken; } async function loadFlag(baseUrl, accessToken) { const result = await fetchJson(`${baseUrl}/api/system/feature-flags`, { headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" }, }); if (!result.ok) { throw new Error(`feature flag list failed: HTTP ${result.status} ${errorCode(result.json)}`.trim()); } const flag = result.json?.data?.items?.find((item) => item.flagKey === FLAG_KEY); if (!flag) { throw new Error(`missing feature flag: ${FLAG_KEY}`); } return flag; } function toSnapshot(flag) { return { flagKey: FLAG_KEY, overrideMode: flag.overrideMode, rolloutType: flag.rolloutType, departmentIds: Array.isArray(flag.departmentIds) ? [...flag.departmentIds] : [], note: typeof flag.note === "string" ? flag.note : "", effectiveEnabled: flag.effectiveEnabled === true, effectiveReason: typeof flag.effectiveReason === "string" ? flag.effectiveReason : "unknown", }; } function validateSnapshot(value) { const overrideModes = new Set(["inherit", "force_on", "force_off"]); const rolloutTypes = new Set(["global", "department_allowlist"]); if ( value?.flagKey !== FLAG_KEY || !overrideModes.has(value.overrideMode) || !rolloutTypes.has(value.rolloutType) || !Array.isArray(value.departmentIds) || !value.departmentIds.every((item) => typeof item === "string") || typeof value.note !== "string" || typeof value.effectiveEnabled !== "boolean" || typeof value.effectiveReason !== "string" ) { throw new Error("invalid payment batch flag snapshot"); } return value; } function sameConfiguration(flag, snapshot) { return flag.overrideMode === snapshot.overrideMode && flag.rolloutType === snapshot.rolloutType && JSON.stringify(flag.departmentIds ?? []) === JSON.stringify(snapshot.departmentIds) && (flag.note ?? "") === snapshot.note; } async function updateFlag(baseUrl, accessToken, current, snapshot) { const result = await fetchJson(`${baseUrl}/api/system/feature-flags/${FLAG_KEY}`, { method: "PUT", headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json", accept: "application/json", }, body: JSON.stringify({ overrideMode: snapshot.overrideMode, rolloutType: snapshot.rolloutType, departmentIds: snapshot.departmentIds, note: snapshot.note, expectedVersion: current.version, }), }); if (!result.ok) { throw new Error(`feature flag restore failed: HTTP ${result.status} ${errorCode(result.json)}`.trim()); } } async function main() { if (process.env.P2_FIN_LIVE_APPROVED !== "1") { throw new Error("P2_FIN_LIVE_APPROVED=1 is required"); } const options = parseArgs(process.argv.slice(2)); const credential = getAdminCredential(); const accessToken = await login(options.baseUrl, credential); if (options.action === "capture") { const current = await loadFlag(options.baseUrl, accessToken); await writeFile(options.snapshotFile, `${JSON.stringify(toSnapshot(current), null, 2)}\n`, { mode: 0o600 }); await chmod(options.snapshotFile, 0o600); console.log(`[payment-flag-snapshot] captured override=${current.overrideMode} effective=${current.effectiveEnabled}`); return; } const snapshot = validateSnapshot(JSON.parse(await readFile(options.snapshotFile, "utf8"))); const current = await loadFlag(options.baseUrl, accessToken); if (!sameConfiguration(current, snapshot)) { await updateFlag(options.baseUrl, accessToken, current, snapshot); } const restored = await loadFlag(options.baseUrl, accessToken); if (!sameConfiguration(restored, snapshot)) { throw new Error("payment batch flag does not match caller snapshot after restore"); } console.log(`[payment-flag-snapshot] restored override=${restored.overrideMode} effective=${restored.effectiveEnabled}`); } main().catch((error) => { console.error(`[payment-flag-snapshot][ERROR] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });