UNPKG

everything-dev

Version:

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

238 lines (236 loc) 9.06 kB
import { fetchResponse } from "./http-client.mjs"; import { fetchBosConfigFromFastKv } from "./fastkv.mjs"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; 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 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`; } async function verifySriForUrl(url, expectedIntegrity, options) { const entryUrl = resolveSriTargetUrl(url, options); const response = await fetchResponse(entryUrl, { timeout: "30 seconds" }); 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 }; } 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 sanitizeFilename(name) { return name.replace(/[^A-Za-z0-9_-]/g, "-"); } function writeDeployResult(opts) { const resultDir = process.env.BOS_DEPLOY_RESULT_DIR; if (resultDir) { mkdirSync(resultDir, { recursive: true }); const entry = { label: opts.label, url: opts.url, integrity: opts.integrity ?? void 0, urlField: opts.urlField, integrityField: opts.integrityField }; writeFileSync(join(resultDir, `${sanitizeFilename(opts.label)}.json`), JSON.stringify(entry, null, 2)); console.log(` ✅ Deploy result: ${opts.urlField}`); return; } try { const config = JSON.parse(readFileSync(opts.bosConfigPath, "utf8")); setNestedPath(config, opts.urlField, opts.url); if (opts.integrityField) if (opts.integrity) setNestedPath(config, opts.integrityField, opts.integrity); else deleteNestedPath(config, opts.integrityField); writeFileSync(opts.bosConfigPath, `${JSON.stringify(config, null, 2)}\n`); console.log(` ✅ Updated bos.config.json: ${opts.urlField}`); if (opts.integrityField && opts.integrity) console.log(` ✅ Updated bos.config.json: ${opts.integrityField}`); } catch (err) { console.error(" ❌ Failed to update bos.config.json:", err.message); } } function readDeployResults(resultDir) { if (!existsSync(resultDir)) return []; const results = []; for (const file of readdirSync(resultDir)) { if (!file.endsWith(".json")) continue; try { const content = JSON.parse(readFileSync(join(resultDir, file), "utf8")); results.push(content); } catch {} } return results; } function readAllDeployResults(baseDir) { if (!existsSync(baseDir)) return []; const results = []; for (const subdir of readdirSync(baseDir)) { const subdirPath = join(baseDir, subdir); try { const stat = readdirSync(subdirPath); for (const file of stat) { if (!file.endsWith(".json")) continue; try { const content = JSON.parse(readFileSync(join(subdirPath, file), "utf8")); results.push(content); } catch {} } } catch {} } return results; } 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); } return merged; } function cleanDeployResultDir(baseDir) { if (existsSync(baseDir)) rmSync(baseDir, { recursive: true, force: true }); mkdirSync(baseDir, { recursive: true }); } function findPluginKey(bosConfigPath, pluginDir) { const plugins = JSON.parse(readFileSync(bosConfigPath, "utf8")).plugins; if (!plugins) return null; const configRoot = join(bosConfigPath, ".."); const normalizedPluginDir = pluginDir.replace(/\\/g, "/").replace(/\/+$/, ""); for (const [key, plugin] of Object.entries(plugins)) { const dev = plugin?.development; if (typeof dev !== "string" || !dev.startsWith("local:")) continue; if (join(configRoot, dev.slice(6)).replace(/\\/g, "/").replace(/\/+$/, "") === normalizedPluginDir) return key; } return null; } //#endregion export { IntegrityRegistry, applyDeployResults, cleanDeployResultDir, computeSriHash, computeSriHashForUrl, findPluginKey, readAllDeployResults, readDeployResults, resolveEntryUrl, verifyConfigAgainstChain, verifySriForUrl, writeDeployResult }; //# sourceMappingURL=integrity.mjs.map