{"version":3,"file":"integrity.mjs","names":[],"sources":["../src/integrity.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { fetchBosConfigFromFastKv } from \"./fastkv\";\n\nconst DEFAULT_MAX_SRI_RESPONSE_BYTES = 20 * 1024 * 1024;\n\ninterface SriUrlOptions {\n  resolveEntryUrl?: boolean;\n  maxBytes?: number;\n}\n\nexport function computeSriHash(content: string | Buffer): string {\n  return `sha384-${createHash(\"sha384\").update(content).digest(\"base64\")}`;\n}\n\nfunction resolveSriTargetUrl(url: string, options?: SriUrlOptions): string {\n  return options?.resolveEntryUrl === false ? url : resolveEntryUrl(url);\n}\n\nfunction getMaxSriResponseBytes(options?: SriUrlOptions): number {\n  return options?.maxBytes ?? DEFAULT_MAX_SRI_RESPONSE_BYTES;\n}\n\nasync function computeSriHashFromResponse(\n  response: Response,\n  url: string,\n  options?: SriUrlOptions,\n): Promise<string> {\n  const maxBytes = getMaxSriResponseBytes(options);\n  const contentLengthHeader = response.headers.get(\"content-length\");\n\n  if (contentLengthHeader) {\n    const contentLength = Number(contentLengthHeader);\n    if (Number.isFinite(contentLength) && contentLength > maxBytes) {\n      throw new Error(\n        `[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${contentLength})`,\n      );\n    }\n  }\n\n  if (!response.body) {\n    throw new Error(`[SRI] Missing response body for ${url}`);\n  }\n\n  const hash = createHash(\"sha384\");\n  const reader = response.body.getReader();\n  let totalBytes = 0;\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) {\n      break;\n    }\n\n    totalBytes += value.byteLength;\n    if (totalBytes > maxBytes) {\n      await reader.cancel();\n      throw new Error(\n        `[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${totalBytes})`,\n      );\n    }\n\n    hash.update(value);\n  }\n\n  return `sha384-${hash.digest(\"base64\")}`;\n}\n\nexport async function computeSriHashForUrl(\n  url: string,\n  options?: SriUrlOptions,\n): Promise<string | null> {\n  try {\n    const entryUrl = resolveSriTargetUrl(url, options);\n\n    const response = await fetch(entryUrl);\n    if (!response.ok) {\n      console.warn(`[SRI] Failed to fetch ${entryUrl}: ${response.status} ${response.statusText}`);\n      return null;\n    }\n    return await computeSriHashFromResponse(response, entryUrl, options);\n  } catch (error) {\n    console.warn(\n      `[SRI] Error computing integrity for ${url}:`,\n      error instanceof Error ? error.message : error,\n    );\n    return null;\n  }\n}\n\nexport function resolveEntryUrl(url: string): string {\n  if (url.endsWith(\"/remoteEntry.js\")) return url;\n  if (url.endsWith(\"/mf-manifest.json\"))\n    return `${url.replace(/\\/mf-manifest\\.json$/, \"\")}/remoteEntry.js`;\n  return `${url.replace(/\\/$/, \"\")}/remoteEntry.js`;\n}\n\nexport async function verifySriForUrl(\n  url: string,\n  expectedIntegrity: string,\n  options?: SriUrlOptions,\n): Promise<void> {\n  const entryUrl = resolveSriTargetUrl(url, options);\n\n  const response = await fetch(entryUrl);\n  if (!response.ok) {\n    console.warn(`[SRI] Failed to fetch ${entryUrl} for verification: ${response.status}`);\n    return;\n  }\n\n  const computed = await computeSriHashFromResponse(response, entryUrl, options);\n\n  if (computed !== expectedIntegrity) {\n    throw new Error(\n      `[SRI] Integrity check failed for ${entryUrl}\\n  Expected: ${expectedIntegrity}\\n  Computed: ${computed}`,\n    );\n  }\n}\n\nexport class IntegrityRegistry {\n  private hashes = new Map<string, string>();\n\n  register(url: string, integrity: string): void {\n    this.hashes.set(url, integrity);\n  }\n\n  registerEntry(baseUrl: string, integrity: string): void {\n    this.hashes.set(resolveEntryUrl(baseUrl), integrity);\n  }\n\n  get(url: string): string | undefined {\n    return this.hashes.get(url);\n  }\n\n  has(url: string): boolean {\n    return this.hashes.has(url);\n  }\n\n  entries(): IterableIterator<[string, string]> {\n    return this.hashes.entries();\n  }\n}\n\nfunction extractIntegrityHashes(config: Record<string, unknown>): Map<string, string> {\n  const hashes = new Map<string, string>();\n  const app = config.app as Record<string, Record<string, unknown>> | undefined;\n  const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;\n\n  if (app) {\n    for (const [, entry] of Object.entries(app)) {\n      if (entry?.integrity && entry?.production) {\n        hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);\n      }\n    }\n  }\n\n  if (plugins) {\n    for (const [, entry] of Object.entries(plugins)) {\n      if (entry?.integrity && entry?.production) {\n        hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);\n      }\n    }\n  }\n\n  return hashes;\n}\n\nexport async function verifyConfigAgainstChain(\n  localConfig: Record<string, unknown>,\n  bosUrl: string,\n): Promise<{ verified: boolean; mismatches: string[] }> {\n  const mismatches: string[] = [];\n\n  let chainConfig: Record<string, unknown>;\n  try {\n    chainConfig = await fetchBosConfigFromFastKv<Record<string, unknown>>(bosUrl);\n  } catch (error) {\n    console.warn(\n      `[Attestation] Failed to fetch on-chain config: ${error instanceof Error ? error.message : String(error)}`,\n    );\n    return { verified: false, mismatches: [\"chain-fetch-failed\"] };\n  }\n\n  const localHashes = extractIntegrityHashes(localConfig);\n  const chainHashes = extractIntegrityHashes(chainConfig);\n\n  for (const [url, chainHash] of chainHashes) {\n    const localHash = localHashes.get(url);\n    if (localHash && localHash !== chainHash) {\n      mismatches.push(url);\n      console.error(\n        `[Attestation] Integrity mismatch for ${url}\\n  Local: ${localHash}\\n  Chain: ${chainHash}`,\n      );\n    }\n  }\n\n  if (mismatches.length === 0 && localHashes.size > 0) {\n    console.log(\n      `[Attestation] Local config verified against on-chain anchor (${localHashes.size} entries checked)`,\n    );\n  }\n\n  return { verified: mismatches.length === 0, mismatches };\n}\n"],"mappings":"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"}