{"version":3,"file":"rollback.cjs","names":["createHash"],"sources":["../src/rollback.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { BosConfigInput } from \"./types\";\n\n/**\n * Rollback safety gate (atomic-deploys 11): before republishing a historical\n * snapshot, every pinned slot's version manifest must still serve and match\n * its SRI — Phase A made bytes additive and retention is keep-everything, so\n * a verify failure means the pointer is broken, never publishable. Snapshots\n * from before the pin existed cannot be verified this way; they are allowed\n * only with an explicit force (their bytes were overwritten in place), with\n * the caller expected to warn loudly.\n */\n\nexport interface RollbackSlotCheck {\n  slot: string;\n  manifest?: string;\n  ok: boolean;\n  reason?: string;\n}\n\nexport interface RollbackVerification {\n  /** false = pre-pin snapshot with no verifiable pins (force required) */\n  verifiable: boolean;\n  ok: boolean;\n  slots: RollbackSlotCheck[];\n}\n\ninterface PinnedSlotRef {\n  production?: unknown;\n  pin?: { manifest?: unknown; integrity?: unknown };\n}\n\nfunction collectPinnedSlots(config: BosConfigInput): Array<{ slot: string; ref: PinnedSlotRef }> {\n  const slots: Array<{ slot: string; ref: PinnedSlotRef }> = [];\n  const app = config.app as Record<string, PinnedSlotRef | undefined> | undefined;\n  const plugins = config.plugins as Record<string, unknown> | undefined;\n\n  const push = (slot: string, ref: unknown): void => {\n    if (ref && typeof ref === \"object\") slots.push({ slot, ref: ref as PinnedSlotRef });\n  };\n\n  if (app) {\n    for (const key of [\"host\", \"ui\", \"api\", \"auth\"] as const) push(`app.${key}`, app[key]);\n    const auth = app.auth as { ui?: unknown } | undefined;\n    if (auth?.ui) push(\"app.auth.ui\", auth.ui);\n  }\n  if (plugins) {\n    for (const [key, plugin] of Object.entries(plugins)) {\n      if (typeof plugin !== \"object\" || plugin === null) continue;\n      push(key, plugin);\n      const ui = (plugin as { ui?: unknown }).ui;\n      if (ui) push(`${key}.ui`, ui);\n    }\n  }\n  return slots;\n}\n\nfunction pinOf(ref: PinnedSlotRef): { manifest: string; integrity: string } | null {\n  const manifest = typeof ref.pin?.manifest === \"string\" ? ref.pin.manifest : undefined;\n  const integrity = typeof ref.pin?.integrity === \"string\" ? ref.pin.integrity : undefined;\n  return manifest && integrity ? { manifest, integrity } : null;\n}\n\nexport interface SlotPinRef {\n  slot: string;\n  /** the slot's production base URL (…/bundles/<account>/<gateway>/<workspace>/) */\n  base: string;\n  pin: { manifest: string; integrity: string };\n}\n\n/** Every pinned slot with both a production base and a verifiable pin. */\nexport function collectSlotPinRefs(config: BosConfigInput): SlotPinRef[] {\n  return collectPinnedSlots(config)\n    .map(({ slot, ref }) => {\n      const pin = pinOf(ref);\n      const base = typeof ref.production === \"string\" ? ref.production : undefined;\n      return pin && base ? { slot, base, pin } : null;\n    })\n    .filter((entry): entry is SlotPinRef => entry !== null);\n}\n\nasync function verifySlot(\n  slot: string,\n  ref: PinnedSlotRef,\n  fetchImpl: typeof fetch,\n): Promise<RollbackSlotCheck> {\n  const base = typeof ref.production === \"string\" ? ref.production : undefined;\n  const pin = pinOf(ref);\n  if (!pin) return { slot, ok: true };\n  const { manifest, integrity } = pin;\n\n  if (!base)\n    return { slot, manifest, ok: false, reason: \"no production URL to fetch the manifest from\" };\n\n  const manifestUrl = `${base.replace(/\\/$/, \"\")}/${manifest.replace(/^\\//, \"\")}`;\n  try {\n    const response = await fetchImpl(manifestUrl);\n    if (!response.ok) {\n      return { slot, manifest, ok: false, reason: `manifest fetch failed: ${response.status}` };\n    }\n    const body = await response.text();\n    const computed = `sha384-${createHash(\"sha384\").update(body).digest(\"base64\")}`;\n    if (computed !== integrity) {\n      return {\n        slot,\n        manifest,\n        ok: false,\n        reason: `integrity mismatch for ${manifestUrl} (expected ${integrity}, computed ${computed})`,\n      };\n    }\n    return { slot, manifest, ok: true };\n  } catch (error) {\n    return {\n      slot,\n      manifest,\n      ok: false,\n      reason: `manifest fetch failed: ${error instanceof Error ? error.message : String(error)}`,\n    };\n  }\n}\n\nexport async function verifyRollbackSnapshot(\n  config: BosConfigInput,\n  opts?: { fetchImpl?: typeof fetch },\n): Promise<RollbackVerification> {\n  const fetchImpl = opts?.fetchImpl ?? fetch;\n  const pinned = collectPinnedSlots(config).filter((entry) => pinOf(entry.ref) !== null);\n\n  if (pinned.length === 0) {\n    return { verifiable: false, ok: false, slots: [] };\n  }\n\n  const slots = await Promise.all(\n    pinned.map((entry) => verifySlot(entry.slot, entry.ref, fetchImpl)),\n  );\n  return { verifiable: true, ok: slots.every((check) => check.ok), slots };\n}\n\nexport function summarizeSlotPins(config: BosConfigInput): string {\n  const pinned = collectPinnedSlots(config)\n    .map((entry) => {\n      const pin = pinOf(entry.ref);\n      return pin ? `${entry.slot}=${pin.manifest}` : null;\n    })\n    .filter(Boolean) as string[];\n  return pinned.length > 0 ? pinned.join(\" \") : \"no version pins (pre-pin snapshot)\";\n}\n\nexport function buildRollbackPayload<T extends Record<string, unknown>>(\n  targetConfig: T,\n  rolledBackFrom: string,\n): T {\n  return { ...targetConfig, rolledBackFrom } as T;\n}\n"],"mappings":"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"}