UNPKG

everything-dev

Version:

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

1 lines • 14.5 kB
{"version":3,"file":"integrity.cjs","names":["createHash","fetchResponse","Data","fetchBosConfigFromFastKv"],"sources":["../src/integrity.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { Data } from \"effect\";\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 const attempts = 3;\n for (let attempt = 1; attempt < attempts; attempt++) {\n const hash = await computeSriHashOnce(url, options);\n if (hash) return hash;\n await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));\n }\n return computeSriHashOnce(url, options);\n}\n\nasync function computeSriHashOnce(url: string, options?: SriUrlOptions): 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 class SriVerificationError extends Data.TaggedError(\"SriVerificationError\")<{\n readonly detail: string;\n}> {\n constructor(message: string) {\n super({ detail: message });\n }\n\n override get message() {\n return this.detail;\n }\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 throw new SriVerificationError(\n `Failed to fetch ${entryUrl} for verification: ${response.status}`,\n );\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 for (const entry of [...Object.values(app ?? {}), ...Object.values(plugins ?? {})]) {\n if (!entry) continue;\n const slotHash = slotHashOf(entry);\n if (slotHash) hashes.set(slotHash.key, slotHash.hash);\n }\n\n return hashes;\n}\n\n/**\n * The integrity anchor a slot contributes to local↔chain attestation: pinned\n * slots anchor at the pinned manifest document (the pin's SRI), unpinned\n * slots at their fixed-name entry (the direct entry SRI).\n */\nfunction slotHashOf(entry: Record<string, unknown>): { key: string; hash: string } | null {\n const production = entry.production;\n if (typeof production !== \"string\") return null;\n const pin = entry.pin as { manifest?: unknown; integrity?: unknown } | undefined;\n if (pin && typeof pin.manifest === \"string\" && typeof pin.integrity === \"string\") {\n return {\n key: `${production.replace(/\\/$/, \"\")}/${pin.manifest.replace(/^\\//, \"\")}`,\n hash: pin.integrity,\n };\n }\n if (typeof entry.integrity === \"string\") {\n return { key: resolveEntryUrl(production), hash: entry.integrity };\n }\n return null;\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 url: string;\n integrity?: string;\n urlField: string;\n integrityField?: string;\n /** A plain pipeline-state value (e.g. the built MF container name). */\n value?: string;\n valueField?: string;\n /** Dotted paths deleted after applying this entry — clears stale\n * pipeline-state fields (e.g. the retired flat `manifest` pointer). */\n removeFields?: 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\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 if (result.valueField && result.value !== undefined && result.value !== null) {\n setNestedPath(merged, result.valueField, result.value);\n }\n for (const field of result.removeFields ?? []) {\n deleteNestedPath(merged, field);\n }\n }\n return merged;\n}\n"],"mappings":"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"}