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