#!/usr/bin/env bun /** * Compute Subresource Integrity (SRI) hashes from real remote bytes. * * Usage: * bun run security:compute-integrity -- [...] * bun run security:compute-integrity -- --file docs/external-integrity-sources.json * * Example: * bun run security:compute-integrity -- "https://fonts.bunny.net/css?family=inter:400" * * Never invent integrity attributes — always regenerate from this script when a * pinned third-party asset changes. */ import { createHash } from 'node:crypto' import { readFileSync } from 'node:fs' interface IntegritySource { url: string label?: string } function parseArgs(argv: string[]) { const fileFlagIndex = argv.indexOf('--file') if (fileFlagIndex !== -1) { const filePath = argv[fileFlagIndex + 1] if (!filePath) throw new Error('Missing path after --file') const parsed = JSON.parse(readFileSync(filePath, 'utf8')) as IntegritySource[] | { sources: IntegritySource[] } return Array.isArray(parsed) ? parsed : parsed.sources } const urls = argv.filter(arg => arg !== '--' && !arg.startsWith('--')) return urls.map(url => ({ url })) } async function fetchBytes(url: string) { const parsed = new URL(url) if (parsed.protocol !== 'https:') throw new Error(`Only HTTPS sources are allowed: ${url}`) const response = await fetch(url, { redirect: 'follow' }) if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`) const finalUrl = new URL(response.url) if (finalUrl.protocol !== 'https:') throw new Error(`Final URL must stay HTTPS: ${finalUrl.href}`) return Buffer.from(await response.arrayBuffer()) } function toIntegrity(buffer: Buffer, algorithm: 'sha256' | 'sha384' | 'sha512' = 'sha384') { const digest = createHash(algorithm).update(buffer).digest('base64') return `${algorithm}-${digest}` } async function main() { const sources = parseArgs(process.argv.slice(2)) if (sources.length === 0) { console.error('Provide at least one HTTPS URL') process.exit(1) } for (const source of sources) { const bytes = await fetchBytes(source.url) const integrity = toIntegrity(bytes) const label = source.label ? `${source.label}: ` : '' console.log(`${label}${source.url}`) console.log(` integrity="${integrity}"`) console.log(` crossorigin="anonymous"`) console.log(` bytes=${bytes.length}`) console.log('') } } main().catch((error) => { console.error(error instanceof Error ? error.message : error) process.exit(1) })