{"version":3,"file":"version-manifest.mjs","names":[],"sources":["../../../src/services/deploy/version-manifest.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport path, { join } from \"node:path\";\nimport { type BuildEntryReport, BuildEntryReportSchema } from \"every-plugin/build/artifact-names\";\nimport {\n  composeVersionManifest,\n  type SourceRef,\n  type WorkspaceVersionManifest,\n  WorkspaceVersionManifestSchema,\n} from \"every-plugin/version-manifest\";\nimport { bundleUrlToStagedPath, resolveContained } from \"../resolution/resolver\";\nimport { fetchVerifiedManifest, manifestDocumentUrl } from \"./manifest-verify\";\n\n/** Read a workspace dist's build-report.json (ticket 01's entry-name report). */\nexport function readBuildReport(distRoot: string): BuildEntryReport | null {\n  const reportPath = join(distRoot, \"build-report.json\");\n  if (!existsSync(reportPath)) return null;\n  try {\n    return BuildEntryReportSchema.parse(JSON.parse(readFileSync(reportPath, \"utf8\")));\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Compose the workspace's version manifest from the dist build reports and\n * the **server-computed** SRI map of the upload response (MAP decision 6 —\n * SRI is authoritative from the storage boundary, never computed\n * client-side). Returns null when the dist predates hashed entry names\n * (no report, or the SRI map lacks the entry) — the deploy leg then aborts:\n * uploaded workspaces must pin.\n */\nexport function composeWorkspaceVersionManifest(input: {\n  report: BuildEntryReport;\n  ssrReport?: BuildEntryReport | null;\n  integrityMap: Record<string, string>;\n  /** full per-file SRI map of the dist — recorded for the next deploy's diff */\n  files?: Record<string, string>;\n  /** source archive reference (publish.sourcePublish) — server-computed SRI */\n  source?: SourceRef;\n  /** the repo HEAD the build was produced from — best-effort, may be absent */\n  gitSha?: string;\n}): WorkspaceVersionManifest | null {\n  const { report, ssrReport, integrityMap, files, source, gitSha } = input;\n\n  const entryIntegrity = integrityMap[report.entry];\n  if (!entryIntegrity) return null;\n\n  const ssrEntry = ssrReport?.entry;\n  const ssrIntegrity = ssrEntry ? integrityMap[`ssr/${ssrEntry}`] : undefined;\n\n  return composeVersionManifest({\n    entry: report.entry,\n    entryIntegrity,\n    ...(ssrEntry && ssrIntegrity\n      ? { ssr: { entry: `ssr/${ssrEntry}`, integrity: ssrIntegrity } }\n      : {}),\n    ...(report.browserManifest && integrityMap[report.browserManifest]\n      ? {\n          browserManifest: {\n            file: report.browserManifest,\n            integrity: integrityMap[report.browserManifest]!,\n          },\n        }\n      : {}),\n    ...(report.css && integrityMap[report.css]\n      ? { assets: { css: integrityMap[report.css]! } }\n      : {}),\n    // Shared-singleton semantics (#546): the web report's record is primary —\n    // the node env resolves the same workspace's node_modules, so the two\n    // agree; an ssr-only report still carries the record.\n    ...((report.shared ?? ssrReport?.shared)\n      ? { shared: (report.shared ?? ssrReport?.shared)! }\n      : {}),\n    ...(files ? { files } : {}),\n    ...(source ? { source } : {}),\n    ...(gitSha ? { gitSha } : {}),\n  });\n}\n\n/**\n * Version-manifest slot resolution (atomic-deploys 04): a config slot's\n * `pin` ({ manifest, integrity }) resolves to the derived entry-level fields\n * every consumer needs. The cache is keyed by the full pin — the pinned\n * bytes are immutable, so a verified entry is cached for the process\n * lifetime and serves as last-known-good if the origin later fails; a fetch\n * or SRI failure with nothing cached propagates (a boot-time resolution\n * failure fails the deploy, which is the platform's rollback).\n */\n\nexport interface ResolvedSlotVersion {\n  /** Absolute URL of the content-hashed container entry. */\n  entryUrl: string;\n  entryIntegrity: string;\n  /** Absolute URL of the hashed browser manifest (mf-manifest), when present. */\n  browserManifestUrl?: string;\n  /** Absolute URL of the content-hashed SSR entry, when present. */\n  ssrEntryUrl?: string;\n  ssrIntegrity?: string;\n  /** Shared-singleton `name → version` the pinned build embedded (#546) —\n   * stamped onto UI slots so the host's composition guard can detect a\n   * platform skew before loading. */\n  sharedVersions?: Record<string, string>;\n  /** Full per-file digest map of the dist (object path → SRI), when the\n   * pinned manifest carries one — the diff base for skip-unchanged uploads. */\n  files?: Record<string, string>;\n  /** Source archive reference from the pinned manifest (publish.sourcePublish). */\n  source?: SourceRef;\n}\n\nexport interface SlotPin {\n  /** versioned manifest filename, relative to the slot's production base */\n  manifest: string;\n  /** the manifest document's own SRI */\n  integrity: string;\n}\n\nconst cache = new Map<string, ResolvedSlotVersion>();\n\nexport function clearSlotVersionCache(): void {\n  cache.clear();\n}\n\nfunction slotVersionFromManifest(\n  base: string,\n  manifest: WorkspaceVersionManifest,\n): ResolvedSlotVersion {\n  const trimmedBase = base.replace(/\\/$/, \"\");\n  return {\n    entryUrl: `${trimmedBase}/${manifest.entry}`,\n    entryIntegrity: manifest.entryIntegrity,\n    ...(manifest.browserManifest\n      ? { browserManifestUrl: `${trimmedBase}/${manifest.browserManifest.file}` }\n      : {}),\n    ...(manifest.ssr\n      ? {\n          ssrEntryUrl: `${trimmedBase}/${manifest.ssr.entry}`,\n          ssrIntegrity: manifest.ssr.integrity,\n        }\n      : {}),\n    ...(manifest.shared ? { sharedVersions: manifest.shared } : {}),\n    ...(manifest.files ? { files: manifest.files } : {}),\n    ...(manifest.source ? { source: manifest.source } : {}),\n  };\n}\n\n/**\n * Staged-slot preference (ADR 0020/0021 self-contained tier): when a runtime\n * stages its own namespace under `BOS_BUNDLE_DIR`, a slot resolves from the\n * staged dist's own version manifest instead of the config's pin — pinned\n * manifest bytes that predate the image (deploy lag after a release bump)\n * cannot exist in the staged namespace, so their hashed files would 404 the\n * disk and fall through to the network, where the stale immutable bytes\n * still answer 200 and boot a foreign build. The staged manifest is trusted\n * (staged by the image build itself) and skipped entirely when the slot is\n * absent from the staged namespace or the pinned manifest is present (the\n * normal healthy case, which keeps the pin's SRI check).\n */\nfunction readStagedSlotVersion(base: string, pin: SlotPin): ResolvedSlotVersion | null {\n  const bundleDir = process.env.BOS_BUNDLE_DIR;\n  if (!bundleDir) return null;\n\n  const slotDir = bundleUrlToStagedPath(base, bundleDir);\n  if (!slotDir || !existsSync(slotDir)) return null;\n\n  const pinnedManifestPath = resolveContained(slotDir, pin.manifest);\n  if (!pinnedManifestPath || existsSync(pinnedManifestPath)) return null;\n\n  const versionsDir = path.join(slotDir, \"versions\");\n  if (!existsSync(versionsDir)) return null;\n  const manifests = readdirSync(versionsDir)\n    .filter((name) => name.endsWith(\".json\"))\n    .map((name) => path.join(versionsDir, name))\n    .filter((file) => statSync(file).isFile())\n    .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs || (a < b ? -1 : 1));\n  if (manifests.length === 0) return null;\n  const manifestPath = manifests[0];\n  if (!manifestPath) return null;\n\n  try {\n    const manifest = WorkspaceVersionManifestSchema.parse(\n      JSON.parse(readFileSync(manifestPath, \"utf8\")),\n    );\n    return slotVersionFromManifest(base, manifest);\n  } catch {\n    return null;\n  }\n}\n\nexport async function resolveSlotVersion(input: {\n  base: string;\n  pin: SlotPin;\n  fetchImpl?: typeof fetch;\n}): Promise<ResolvedSlotVersion> {\n  const key = `${input.base}::${input.pin.manifest}::${input.pin.integrity}`;\n  // content-addressed: a verified pin is cached for the process lifetime and\n  // never refetched — the cache IS the last-known-good\n  const cached = cache.get(key);\n  if (cached) return cached;\n\n  const staged = readStagedSlotVersion(input.base, input.pin);\n  if (staged) {\n    cache.set(key, staged);\n    return staged;\n  }\n\n  const manifestUrl = manifestDocumentUrl(input.base, input.pin.manifest);\n  const verified = await fetchVerifiedManifest({\n    base: input.base,\n    pin: input.pin,\n    fetchImpl: input.fetchImpl,\n  });\n  if (!verified.ok) {\n    throw new Error(\n      `[SRI] version manifest verification failed for ${manifestUrl}: ${verified.reason}`,\n    );\n  }\n  const manifest: WorkspaceVersionManifest = WorkspaceVersionManifestSchema.parse(\n    JSON.parse(verified.text),\n  );\n\n  const resolved = slotVersionFromManifest(input.base, manifest);\n  cache.set(key, resolved);\n  return resolved;\n}\n"],"mappings":"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"}