everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 8.05 kB
Source Map (JSON)
{"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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"}