UNPKG

everything-dev

Version:

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

1 lines 17.7 kB
{"version":3,"file":"integrity.mjs","names":[],"sources":["../src/integrity.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fetchBosConfigFromFastKv } from \"./fastkv\";\nimport { fetchResponse } from \"./http-client\";\n\nconst DEFAULT_MAX_SRI_RESPONSE_BYTES = 20 * 1024 * 1024;\n\ninterface SriUrlOptions {\n resolveEntryUrl?: boolean;\n maxBytes?: number;\n}\n\nexport function computeSriHash(content: string | Buffer): string {\n return `sha384-${createHash(\"sha384\").update(content).digest(\"base64\")}`;\n}\n\nfunction resolveSriTargetUrl(url: string, options?: SriUrlOptions): string {\n return options?.resolveEntryUrl === false ? url : resolveEntryUrl(url);\n}\n\nfunction getMaxSriResponseBytes(options?: SriUrlOptions): number {\n return options?.maxBytes ?? DEFAULT_MAX_SRI_RESPONSE_BYTES;\n}\n\nasync function computeSriHashFromResponse(\n response: Response,\n url: string,\n options?: SriUrlOptions,\n): Promise<string> {\n const maxBytes = getMaxSriResponseBytes(options);\n const contentLengthHeader = response.headers.get(\"content-length\");\n\n if (contentLengthHeader) {\n const contentLength = Number(contentLengthHeader);\n if (Number.isFinite(contentLength) && contentLength > maxBytes) {\n throw new Error(\n `[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${contentLength})`,\n );\n }\n }\n\n if (!response.body) {\n throw new Error(`[SRI] Missing response body for ${url}`);\n }\n\n const hash = createHash(\"sha384\");\n const reader = response.body.getReader();\n let totalBytes = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(\n `[SRI] Response for ${url} exceeds max size of ${maxBytes} bytes (${totalBytes})`,\n );\n }\n\n hash.update(value);\n }\n\n return `sha384-${hash.digest(\"base64\")}`;\n}\n\nexport async function computeSriHashForUrl(\n url: string,\n options?: SriUrlOptions,\n): Promise<string | null> {\n try {\n const entryUrl = resolveSriTargetUrl(url, options);\n\n const response = await fetchResponse(entryUrl, { timeout: \"30 seconds\" });\n if (!response.ok) {\n console.warn(`[SRI] Failed to fetch ${entryUrl}: ${response.status} ${response.statusText}`);\n return null;\n }\n return await computeSriHashFromResponse(response, entryUrl, options);\n } catch (error) {\n console.warn(\n `[SRI] Error computing integrity for ${url}:`,\n error instanceof Error ? error.message : error,\n );\n return null;\n }\n}\n\nexport function resolveEntryUrl(url: string): string {\n if (url.endsWith(\"/remoteEntry.js\")) return url;\n if (url.endsWith(\"/mf-manifest.json\"))\n return `${url.replace(/\\/mf-manifest\\.json$/, \"\")}/remoteEntry.js`;\n return `${url.replace(/\\/$/, \"\")}/remoteEntry.js`;\n}\n\nexport async function verifySriForUrl(\n url: string,\n expectedIntegrity: string,\n options?: SriUrlOptions,\n): Promise<void> {\n const entryUrl = resolveSriTargetUrl(url, options);\n\n const response = await fetchResponse(entryUrl, { timeout: \"30 seconds\" });\n if (!response.ok) {\n console.warn(`[SRI] Failed to fetch ${entryUrl} for verification: ${response.status}`);\n return;\n }\n\n const computed = await computeSriHashFromResponse(response, entryUrl, options);\n\n if (computed !== expectedIntegrity) {\n throw new Error(\n `[SRI] Integrity check failed for ${entryUrl}\\n Expected: ${expectedIntegrity}\\n Computed: ${computed}`,\n );\n }\n}\n\nexport class IntegrityRegistry {\n private hashes = new Map<string, string>();\n\n register(url: string, integrity: string): void {\n this.hashes.set(url, integrity);\n }\n\n registerEntry(baseUrl: string, integrity: string): void {\n this.hashes.set(resolveEntryUrl(baseUrl), integrity);\n }\n\n get(url: string): string | undefined {\n return this.hashes.get(url);\n }\n\n has(url: string): boolean {\n return this.hashes.has(url);\n }\n\n entries(): IterableIterator<[string, string]> {\n return this.hashes.entries();\n }\n}\n\nfunction extractIntegrityHashes(config: Record<string, unknown>): Map<string, string> {\n const hashes = new Map<string, string>();\n const app = config.app as Record<string, Record<string, unknown>> | undefined;\n const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;\n\n if (app) {\n for (const [, entry] of Object.entries(app)) {\n if (entry?.integrity && entry?.production) {\n hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);\n }\n }\n }\n\n if (plugins) {\n for (const [, entry] of Object.entries(plugins)) {\n if (entry?.integrity && entry?.production) {\n hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);\n }\n }\n }\n\n return hashes;\n}\n\nexport async function verifyConfigAgainstChain(\n localConfig: Record<string, unknown>,\n bosUrl: string,\n): Promise<{ verified: boolean; mismatches: string[] }> {\n const mismatches: string[] = [];\n\n let chainConfig: Record<string, unknown>;\n try {\n chainConfig = await fetchBosConfigFromFastKv<Record<string, unknown>>(bosUrl);\n } catch (error) {\n console.warn(\n `[Attestation] Failed to fetch on-chain config: ${error instanceof Error ? error.message : String(error)}`,\n );\n return { verified: false, mismatches: [\"chain-fetch-failed\"] };\n }\n\n const localHashes = extractIntegrityHashes(localConfig);\n const chainHashes = extractIntegrityHashes(chainConfig);\n\n for (const [url, chainHash] of chainHashes) {\n const localHash = localHashes.get(url);\n if (localHash && localHash !== chainHash) {\n mismatches.push(url);\n console.error(\n `[Attestation] Integrity mismatch for ${url}\\n Local: ${localHash}\\n Chain: ${chainHash}`,\n );\n }\n }\n\n if (mismatches.length === 0 && localHashes.size > 0) {\n console.log(\n `[Attestation] Local config verified against on-chain anchor (${localHashes.size} entries checked)`,\n );\n }\n\n return { verified: mismatches.length === 0, mismatches };\n}\n\nexport interface DeployResultEntry {\n label: string;\n url: string;\n integrity?: string;\n urlField: string;\n integrityField?: string;\n}\n\nfunction setNestedPath(obj: Record<string, unknown>, dottedPath: string, value: unknown): void {\n const keys = dottedPath.split(\".\");\n let current = obj;\n for (let i = 0; i < keys.length - 1; i += 1) {\n const key = keys[i]!;\n if (current[key] === undefined || current[key] === null || typeof current[key] !== \"object\") {\n current[key] = {};\n }\n current = current[key] as Record<string, unknown>;\n }\n current[keys[keys.length - 1]!] = value;\n}\n\nfunction deleteNestedPath(obj: Record<string, unknown>, dottedPath: string): void {\n const keys = dottedPath.split(\".\");\n let current = obj;\n for (let i = 0; i < keys.length - 1; i += 1) {\n const key = keys[i]!;\n if (current[key] === undefined || typeof current[key] !== \"object\") return;\n current = current[key] as Record<string, unknown>;\n }\n delete current[keys[keys.length - 1]!];\n}\n\nfunction sanitizeFilename(name: string): string {\n return name.replace(/[^A-Za-z0-9_-]/g, \"-\");\n}\n\nexport function writeDeployResult(opts: {\n url: string;\n integrity?: string | null;\n bosConfigPath: string;\n urlField: string;\n integrityField?: string;\n label: string;\n}): void {\n const resultDir = process.env.BOS_DEPLOY_RESULT_DIR;\n\n if (resultDir) {\n mkdirSync(resultDir, { recursive: true });\n const entry: DeployResultEntry = {\n label: opts.label,\n url: opts.url,\n integrity: opts.integrity ?? undefined,\n urlField: opts.urlField,\n integrityField: opts.integrityField,\n };\n const resultFile = join(resultDir, `${sanitizeFilename(opts.label)}.json`);\n writeFileSync(resultFile, JSON.stringify(entry, null, 2));\n console.log(` ✅ Deploy result: ${opts.urlField}`);\n return;\n }\n\n try {\n const config = JSON.parse(readFileSync(opts.bosConfigPath, \"utf8\")) as Record<string, unknown>;\n setNestedPath(config, opts.urlField, opts.url);\n if (opts.integrityField) {\n if (opts.integrity) {\n setNestedPath(config, opts.integrityField, opts.integrity);\n } else {\n deleteNestedPath(config, opts.integrityField);\n }\n }\n writeFileSync(opts.bosConfigPath, `${JSON.stringify(config, null, 2)}\\n`);\n console.log(` ✅ Updated bos.config.json: ${opts.urlField}`);\n if (opts.integrityField && opts.integrity) {\n console.log(` ✅ Updated bos.config.json: ${opts.integrityField}`);\n }\n } catch (err) {\n console.error(\" ❌ Failed to update bos.config.json:\", (err as Error).message);\n }\n}\n\nexport function readDeployResults(resultDir: string): DeployResultEntry[] {\n if (!existsSync(resultDir)) return [];\n const results: DeployResultEntry[] = [];\n for (const file of readdirSync(resultDir)) {\n if (!file.endsWith(\".json\")) continue;\n try {\n const content = JSON.parse(readFileSync(join(resultDir, file), \"utf8\")) as DeployResultEntry;\n results.push(content);\n } catch {\n // skip malformed files\n }\n }\n return results;\n}\n\nexport function readAllDeployResults(baseDir: string): DeployResultEntry[] {\n if (!existsSync(baseDir)) return [];\n const results: DeployResultEntry[] = [];\n for (const subdir of readdirSync(baseDir)) {\n const subdirPath = join(baseDir, subdir);\n try {\n const stat = readdirSync(subdirPath);\n for (const file of stat) {\n if (!file.endsWith(\".json\")) continue;\n try {\n const content = JSON.parse(\n readFileSync(join(subdirPath, file), \"utf8\"),\n ) as DeployResultEntry;\n results.push(content);\n } catch {\n // skip malformed files\n }\n }\n } catch {\n // not a directory, skip\n }\n }\n return results;\n}\n\nexport function applyDeployResults(\n config: Record<string, unknown>,\n results: DeployResultEntry[],\n): Record<string, unknown> {\n const merged = structuredClone(config);\n for (const result of results) {\n setNestedPath(merged, result.urlField, result.url);\n if (result.integrityField) {\n if (result.integrity) {\n setNestedPath(merged, result.integrityField, result.integrity);\n } else {\n deleteNestedPath(merged, result.integrityField);\n }\n }\n }\n return merged;\n}\n\nexport function cleanDeployResultDir(baseDir: string): void {\n if (existsSync(baseDir)) {\n rmSync(baseDir, { recursive: true, force: true });\n }\n mkdirSync(baseDir, { recursive: true });\n}\n\nexport function findPluginKey(bosConfigPath: string, pluginDir: string): string | null {\n const config = JSON.parse(readFileSync(bosConfigPath, \"utf8\")) as Record<string, unknown>;\n const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;\n if (!plugins) return null;\n const configRoot = join(bosConfigPath, \"..\");\n const normalizedPluginDir = pluginDir.replace(/\\\\/g, \"/\").replace(/\\/+$/, \"\");\n for (const [key, plugin] of Object.entries(plugins)) {\n const dev = plugin?.development;\n if (typeof dev !== \"string\" || !dev.startsWith(\"local:\")) continue;\n const resolved = join(configRoot, dev.slice(\"local:\".length))\n .replace(/\\\\/g, \"/\")\n .replace(/\\/+$/, \"\");\n if (resolved === normalizedPluginDir) return key;\n }\n return null;\n}\n"],"mappings":"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"}