{"version":3,"file":"integrity.mjs","names":[],"sources":["../src/integrity.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fetchBosConfigFromFastKv } from \"./fastkv\";\nimport { fetchResponse } from \"./http-client\";\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 fetchResponse(entryUrl, { timeout: \"30 seconds\" });\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 fetchResponse(entryUrl, { timeout: \"30 seconds\" });\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\nexport interface DeployResultEntry {\n  label: string;\n  url: string;\n  integrity?: string;\n  urlField: string;\n  integrityField?: string;\n}\n\nfunction setNestedPath(obj: Record<string, unknown>, dottedPath: string, value: unknown): void {\n  const keys = dottedPath.split(\".\");\n  let current = obj;\n  for (let i = 0; i < keys.length - 1; i += 1) {\n    const key = keys[i]!;\n    if (current[key] === undefined || current[key] === null || typeof current[key] !== \"object\") {\n      current[key] = {};\n    }\n    current = current[key] as Record<string, unknown>;\n  }\n  current[keys[keys.length - 1]!] = value;\n}\n\nfunction deleteNestedPath(obj: Record<string, unknown>, dottedPath: string): void {\n  const keys = dottedPath.split(\".\");\n  let current = obj;\n  for (let i = 0; i < keys.length - 1; i += 1) {\n    const key = keys[i]!;\n    if (current[key] === undefined || typeof current[key] !== \"object\") return;\n    current = current[key] as Record<string, unknown>;\n  }\n  delete current[keys[keys.length - 1]!];\n}\n\nfunction sanitizeFilename(name: string): string {\n  return name.replace(/[^A-Za-z0-9_-]/g, \"-\");\n}\n\nexport function writeDeployResult(opts: {\n  url: string;\n  integrity?: string | null;\n  bosConfigPath: string;\n  urlField: string;\n  integrityField?: string;\n  label: string;\n}): void {\n  const resultDir = process.env.BOS_DEPLOY_RESULT_DIR;\n\n  if (resultDir) {\n    mkdirSync(resultDir, { recursive: true });\n    const entry: DeployResultEntry = {\n      label: opts.label,\n      url: opts.url,\n      integrity: opts.integrity ?? undefined,\n      urlField: opts.urlField,\n      integrityField: opts.integrityField,\n    };\n    const resultFile = join(resultDir, `${sanitizeFilename(opts.label)}.json`);\n    writeFileSync(resultFile, JSON.stringify(entry, null, 2));\n    console.log(`   ✅ Deploy result: ${opts.urlField}`);\n    return;\n  }\n\n  try {\n    const config = JSON.parse(readFileSync(opts.bosConfigPath, \"utf8\")) as Record<string, unknown>;\n    setNestedPath(config, opts.urlField, opts.url);\n    if (opts.integrityField) {\n      if (opts.integrity) {\n        setNestedPath(config, opts.integrityField, opts.integrity);\n      } else {\n        deleteNestedPath(config, opts.integrityField);\n      }\n    }\n    writeFileSync(opts.bosConfigPath, `${JSON.stringify(config, null, 2)}\\n`);\n    console.log(`   ✅ Updated bos.config.json: ${opts.urlField}`);\n    if (opts.integrityField && opts.integrity) {\n      console.log(`   ✅ Updated bos.config.json: ${opts.integrityField}`);\n    }\n  } catch (err) {\n    console.error(\"   ❌ Failed to update bos.config.json:\", (err as Error).message);\n  }\n}\n\nexport function readDeployResults(resultDir: string): DeployResultEntry[] {\n  if (!existsSync(resultDir)) return [];\n  const results: DeployResultEntry[] = [];\n  for (const file of readdirSync(resultDir)) {\n    if (!file.endsWith(\".json\")) continue;\n    try {\n      const content = JSON.parse(readFileSync(join(resultDir, file), \"utf8\")) as DeployResultEntry;\n      results.push(content);\n    } catch {\n      // skip malformed files\n    }\n  }\n  return results;\n}\n\nexport function readAllDeployResults(baseDir: string): DeployResultEntry[] {\n  if (!existsSync(baseDir)) return [];\n  const results: DeployResultEntry[] = [];\n  for (const subdir of readdirSync(baseDir)) {\n    const subdirPath = join(baseDir, subdir);\n    try {\n      const stat = readdirSync(subdirPath);\n      for (const file of stat) {\n        if (!file.endsWith(\".json\")) continue;\n        try {\n          const content = JSON.parse(\n            readFileSync(join(subdirPath, file), \"utf8\"),\n          ) as DeployResultEntry;\n          results.push(content);\n        } catch {\n          // skip malformed files\n        }\n      }\n    } catch {\n      // not a directory, skip\n    }\n  }\n  return results;\n}\n\nexport function applyDeployResults(\n  config: Record<string, unknown>,\n  results: DeployResultEntry[],\n): Record<string, unknown> {\n  const merged = structuredClone(config);\n  for (const result of results) {\n    setNestedPath(merged, result.urlField, result.url);\n    if (result.integrityField) {\n      if (result.integrity) {\n        setNestedPath(merged, result.integrityField, result.integrity);\n      } else {\n        deleteNestedPath(merged, result.integrityField);\n      }\n    }\n  }\n  return merged;\n}\n\nexport function cleanDeployResultDir(baseDir: string): void {\n  if (existsSync(baseDir)) {\n    rmSync(baseDir, { recursive: true, force: true });\n  }\n  mkdirSync(baseDir, { recursive: true });\n}\n\nexport function findPluginKey(bosConfigPath: string, pluginDir: string): string | null {\n  const config = JSON.parse(readFileSync(bosConfigPath, \"utf8\")) as Record<string, unknown>;\n  const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;\n  if (!plugins) return null;\n  const configRoot = join(bosConfigPath, \"..\");\n  const normalizedPluginDir = pluginDir.replace(/\\\\/g, \"/\").replace(/\\/+$/, \"\");\n  for (const [key, plugin] of Object.entries(plugins)) {\n    const dev = plugin?.development;\n    if (typeof dev !== \"string\" || !dev.startsWith(\"local:\")) continue;\n    const resolved = join(configRoot, dev.slice(\"local:\".length))\n      .replace(/\\\\/g, \"/\")\n      .replace(/\\/+$/, \"\");\n    if (resolved === normalizedPluginDir) return key;\n  }\n  return null;\n}\n"],"mappings":"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"}