UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

127 lines (125 loc) 4.86 kB
import { fetchBosConfigFromFastKv } from "./fastkv.mjs"; import { createHash } from "node:crypto"; //#region src/integrity.ts const DEFAULT_MAX_SRI_RESPONSE_BYTES = 20 * 1024 * 1024; function computeSriHash(content) { return `sha384-${createHash("sha384").update(content).digest("base64")}`; } function resolveSriTargetUrl(url, options) { return options?.resolveEntryUrl === false ? url : resolveEntryUrl(url); } function getMaxSriResponseBytes(options) { return options?.maxBytes ?? DEFAULT_MAX_SRI_RESPONSE_BYTES; } async function computeSriHashFromResponse(response, url, options) { const maxBytes = getMaxSriResponseBytes(options); const contentLengthHeader = response.headers.get("content-length"); if (contentLengthHeader) { const contentLength = Number(contentLengthHeader); if (Number.isFinite(contentLength) && contentLength > maxBytes) throw new Error(`[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${contentLength})`); } if (!response.body) throw new Error(`[SRI] Missing response body for ${url}`); const hash = createHash("sha384"); const reader = response.body.getReader(); let totalBytes = 0; while (true) { const { done, value } = await reader.read(); if (done) break; totalBytes += value.byteLength; if (totalBytes > maxBytes) { await reader.cancel(); throw new Error(`[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${totalBytes})`); } hash.update(value); } return `sha384-${hash.digest("base64")}`; } async function computeSriHashForUrl(url, options) { try { const entryUrl = resolveSriTargetUrl(url, options); const response = await fetch(entryUrl); if (!response.ok) { console.warn(`[SRI] Failed to fetch ${entryUrl}: ${response.status} ${response.statusText}`); return null; } return await computeSriHashFromResponse(response, entryUrl, options); } catch (error) { console.warn(`[SRI] Error computing integrity for ${url}:`, error instanceof Error ? error.message : error); return null; } } function resolveEntryUrl(url) { if (url.endsWith("/remoteEntry.js")) return url; if (url.endsWith("/mf-manifest.json")) return `${url.replace(/\/mf-manifest\.json$/, "")}/remoteEntry.js`; return `${url.replace(/\/$/, "")}/remoteEntry.js`; } async function verifySriForUrl(url, expectedIntegrity, options) { const entryUrl = resolveSriTargetUrl(url, options); const response = await fetch(entryUrl); if (!response.ok) { console.warn(`[SRI] Failed to fetch ${entryUrl} for verification: ${response.status}`); return; } const computed = await computeSriHashFromResponse(response, entryUrl, options); if (computed !== expectedIntegrity) throw new Error(`[SRI] Integrity check failed for ${entryUrl}\n Expected: ${expectedIntegrity}\n Computed: ${computed}`); } var IntegrityRegistry = class { hashes = /* @__PURE__ */ new Map(); register(url, integrity) { this.hashes.set(url, integrity); } registerEntry(baseUrl, integrity) { this.hashes.set(resolveEntryUrl(baseUrl), integrity); } get(url) { return this.hashes.get(url); } has(url) { return this.hashes.has(url); } entries() { return this.hashes.entries(); } }; function extractIntegrityHashes(config) { const hashes = /* @__PURE__ */ new Map(); const app = config.app; const plugins = config.plugins; if (app) { for (const [, entry] of Object.entries(app)) if (entry?.integrity && entry?.production) hashes.set(resolveEntryUrl(entry.production), entry.integrity); } if (plugins) { for (const [, entry] of Object.entries(plugins)) if (entry?.integrity && entry?.production) hashes.set(resolveEntryUrl(entry.production), entry.integrity); } return hashes; } async function verifyConfigAgainstChain(localConfig, bosUrl) { const mismatches = []; let chainConfig; try { chainConfig = await fetchBosConfigFromFastKv(bosUrl); } catch (error) { console.warn(`[Attestation] Failed to fetch on-chain config: ${error instanceof Error ? error.message : String(error)}`); return { verified: false, mismatches: ["chain-fetch-failed"] }; } const localHashes = extractIntegrityHashes(localConfig); const chainHashes = extractIntegrityHashes(chainConfig); for (const [url, chainHash] of chainHashes) { const localHash = localHashes.get(url); if (localHash && localHash !== chainHash) { mismatches.push(url); console.error(`[Attestation] Integrity mismatch for ${url}\n Local: ${localHash}\n Chain: ${chainHash}`); } } if (mismatches.length === 0 && localHashes.size > 0) console.log(`[Attestation] Local config verified against on-chain anchor (${localHashes.size} entries checked)`); return { verified: mismatches.length === 0, mismatches }; } //#endregion export { IntegrityRegistry, computeSriHash, computeSriHashForUrl, resolveEntryUrl, verifyConfigAgainstChain, verifySriForUrl }; //# sourceMappingURL=integrity.mjs.map