UNPKG

everything-dev

Version:

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

251 lines (249 loc) 9.84 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_runtime = require('./_virtual/_rolldown/runtime.cjs'); const require_http_client = require('./http-client.cjs'); const require_fastkv = require('./fastkv.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let node_crypto = require("node:crypto"); //#region src/integrity.ts const DEFAULT_MAX_SRI_RESPONSE_BYTES = 20 * 1024 * 1024; 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) { 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`; } 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) { 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 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 sanitizeFilename(name) { return name.replace(/[^A-Za-z0-9_-]/g, "-"); } function writeDeployResult(opts) { const resultDir = process.env.BOS_DEPLOY_RESULT_DIR; if (resultDir) { (0, node_fs.mkdirSync)(resultDir, { recursive: true }); const entry = { label: opts.label, url: opts.url, integrity: opts.integrity ?? void 0, urlField: opts.urlField, integrityField: opts.integrityField }; (0, node_fs.writeFileSync)((0, node_path.join)(resultDir, `${sanitizeFilename(opts.label)}.json`), JSON.stringify(entry, null, 2)); console.log(` ✅ Deploy result: ${opts.urlField}`); return; } try { const config = JSON.parse((0, node_fs.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); (0, node_fs.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 (!(0, node_fs.existsSync)(resultDir)) return []; const results = []; for (const file of (0, node_fs.readdirSync)(resultDir)) { if (!file.endsWith(".json")) continue; try { const content = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(resultDir, file), "utf8")); results.push(content); } catch {} } return results; } function readAllDeployResults(baseDir) { if (!(0, node_fs.existsSync)(baseDir)) return []; const results = []; for (const subdir of (0, node_fs.readdirSync)(baseDir)) { const subdirPath = (0, node_path.join)(baseDir, subdir); try { const stat = (0, node_fs.readdirSync)(subdirPath); for (const file of stat) { if (!file.endsWith(".json")) continue; try { const content = JSON.parse((0, node_fs.readFileSync)((0, node_path.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 ((0, node_fs.existsSync)(baseDir)) (0, node_fs.rmSync)(baseDir, { recursive: true, force: true }); (0, node_fs.mkdirSync)(baseDir, { recursive: true }); } function findPluginKey(bosConfigPath, pluginDir) { const plugins = JSON.parse((0, node_fs.readFileSync)(bosConfigPath, "utf8")).plugins; if (!plugins) return null; const configRoot = (0, node_path.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 ((0, node_path.join)(configRoot, dev.slice(6)).replace(/\\/g, "/").replace(/\/+$/, "") === normalizedPluginDir) return key; } return null; } //#endregion exports.IntegrityRegistry = IntegrityRegistry; exports.applyDeployResults = applyDeployResults; exports.cleanDeployResultDir = cleanDeployResultDir; exports.computeSriHash = computeSriHash; exports.computeSriHashForUrl = computeSriHashForUrl; exports.findPluginKey = findPluginKey; exports.readAllDeployResults = readAllDeployResults; exports.readDeployResults = readDeployResults; exports.resolveEntryUrl = resolveEntryUrl; exports.verifyConfigAgainstChain = verifyConfigAgainstChain; exports.verifySriForUrl = verifySriForUrl; exports.writeDeployResult = writeDeployResult; //# sourceMappingURL=integrity.cjs.map