everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
202 lines (200 loc) • 7.76 kB
JavaScript
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