UNPKG

everything-dev

Version:

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

202 lines (200 loc) • 7.76 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_http_client = require('./http-client.cjs'); const require_fastkv = require('./fastkv.cjs'); let effect = require("effect"); let node_crypto = require("node:crypto"); //#region src/integrity.ts const DEFAULT_MAX_SRI_RESPONSE_BYTES = 20971520; function computeSriHash(content) { return `sha384-${(0, node_crypto.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 = (0, node_crypto.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) { const attempts = 3; for (let attempt = 1; attempt < attempts; attempt++) { const hash = await computeSriHashOnce(url, options); if (hash) return hash; await new Promise((resolve) => setTimeout(resolve, 1e3 * attempt)); } return computeSriHashOnce(url, options); } async function computeSriHashOnce(url, options) { try { const entryUrl = resolveSriTargetUrl(url, options); const response = await require_http_client.fetchResponse(entryUrl, { timeout: "30 seconds" }); 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`; } var SriVerificationError = class extends effect.Data.TaggedError("SriVerificationError") { constructor(message) { super({ detail: message }); } get message() { return this.detail; } }; async function verifySriForUrl(url, expectedIntegrity, options) { const entryUrl = resolveSriTargetUrl(url, options); const response = await require_http_client.fetchResponse(entryUrl, { timeout: "30 seconds" }); if (!response.ok) throw new SriVerificationError(`Failed to fetch ${entryUrl} for verification: ${response.status}`); 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; for (const entry of [...Object.values(app ?? {}), ...Object.values(plugins ?? {})]) { if (!entry) continue; const slotHash = slotHashOf(entry); if (slotHash) hashes.set(slotHash.key, slotHash.hash); } return hashes; } /** * The integrity anchor a slot contributes to local↔chain attestation: pinned * slots anchor at the pinned manifest document (the pin's SRI), unpinned * slots at their fixed-name entry (the direct entry SRI). */ function slotHashOf(entry) { const production = entry.production; if (typeof production !== "string") return null; const pin = entry.pin; if (pin && typeof pin.manifest === "string" && typeof pin.integrity === "string") return { key: `${production.replace(/\/$/, "")}/${pin.manifest.replace(/^\//, "")}`, hash: pin.integrity }; if (typeof entry.integrity === "string") return { key: resolveEntryUrl(production), hash: entry.integrity }; return null; } async function verifyConfigAgainstChain(localConfig, bosUrl) { const mismatches = []; let chainConfig; try { chainConfig = await require_fastkv.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 }; } function setNestedPath(obj, dottedPath, value) { const keys = dottedPath.split("."); let current = obj; for (let i = 0; i < keys.length - 1; i += 1) { const key = keys[i]; if (current[key] === void 0 || current[key] === null || typeof current[key] !== "object") current[key] = {}; current = current[key]; } current[keys[keys.length - 1]] = value; } function deleteNestedPath(obj, dottedPath) { const keys = dottedPath.split("."); let current = obj; for (let i = 0; i < keys.length - 1; i += 1) { const key = keys[i]; if (current[key] === void 0 || typeof current[key] !== "object") return; current = current[key]; } delete current[keys[keys.length - 1]]; } function applyDeployResults(config, results) { const merged = structuredClone(config); for (const result of results) { setNestedPath(merged, result.urlField, result.url); if (result.integrityField) { if (result.integrity) setNestedPath(merged, result.integrityField, result.integrity); else deleteNestedPath(merged, result.integrityField); } if (result.valueField && result.value !== void 0 && result.value !== null) setNestedPath(merged, result.valueField, result.value); for (const field of result.removeFields ?? []) deleteNestedPath(merged, field); } return merged; } //#endregion exports.IntegrityRegistry = IntegrityRegistry; exports.SriVerificationError = SriVerificationError; exports.applyDeployResults = applyDeployResults; exports.computeSriHash = computeSriHash; exports.computeSriHashForUrl = computeSriHashForUrl; exports.resolveEntryUrl = resolveEntryUrl; exports.verifyConfigAgainstChain = verifyConfigAgainstChain; exports.verifySriForUrl = verifySriForUrl; //# sourceMappingURL=integrity.cjs.map