#!/usr/bin/env node
/**
 * csp-hashes.mjs — 构建后为「严格 CSP」补齐内联片段的 sha256 哈希（postbuild）。
 *
 * Astro 的 island 水合会注入少量**内联** <script>（idle/load/visible 引导 + 自定义元素定义）
 * 与一段内联 <style>（astro-island{display:contents}）。在 script-src/style-src 'self' 的
 * 严格 CSP 下，内联片段必须以 'sha256-...' 显式放行 —— 既不放开 'unsafe-inline'，又能跑。
 *
 * 这些内联片段由 Astro 运行时生成、跨页面完全一致、随版本确定，故哈希稳定可复现。
 * 本脚本扫描 dist 下所有 .html，去重收集内联片段，算哈希，写回 dist/_headers 的 CSP。
 * 同时校验：dist/_headers 里 CSP 不得包含 'unsafe-inline'（红线）。
 */
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join, resolve } from 'node:path';

const DIST = resolve(process.argv[2] ?? 'dist');
const HEADERS = join(DIST, '_headers');

if (!existsSync(HEADERS)) {
  console.error(`[csp] 找不到 ${HEADERS}（public/_headers 应被构建复制到 dist）`);
  process.exit(2);
}

function listHtml(dir) {
  const out = [];
  for (const name of readdirSync(dir)) {
    const p = join(dir, name);
    const st = statSync(p);
    if (st.isDirectory()) out.push(...listHtml(p));
    else if (name.endsWith('.html')) out.push(p);
  }
  return out;
}

const sha = (s) => `'sha256-${createHash('sha256').update(s, 'utf8').digest('base64')}'`;

const scripts = new Set();
const styles = new Set();

// 抓取无 src 的内联 <script> 与内联 <style> 的「原始内容」（CSP 哈希基于原始字节）。
const reScript = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
const reStyle = /<style[^>]*>([\s\S]*?)<\/style>/gi;

for (const file of listHtml(DIST)) {
  const html = readFileSync(file, 'utf8');
  for (const m of html.matchAll(reScript)) if (m[1].length) scripts.add(m[1]);
  for (const m of html.matchAll(reStyle)) if (m[1].length) styles.add(m[1]);
}

const scriptHashes = [...scripts].map(sha);
const styleHashes = [...styles].map(sha);

let headers = readFileSync(HEADERS, 'utf8');

// 在 `script-src 'self'` / `style-src 'self'` 之后追加哈希（幂等：若已含则跳过）。
function inject(directive, hashes) {
  if (!hashes.length) return;
  const re = new RegExp(`(${directive} 'self')([^;]*)`);
  headers = headers.replace(re, (_full, head, rest) => {
    const have = new Set(rest.trim().split(/\s+/).filter(Boolean));
    const add = hashes.filter((h) => !have.has(h));
    return `${head}${rest}${add.length ? ' ' + add.join(' ') : ''}`;
  });
}

inject('script-src', scriptHashes);
inject('style-src', styleHashes);

if (/unsafe-inline/.test(headers)) {
  console.error("[csp] 致命：CSP 含 'unsafe-inline'，违反严格策略。");
  process.exit(1);
}

writeFileSync(HEADERS, headers, 'utf8');
console.log(
  `[csp] 已写入 ${HEADERS}：script ${scriptHashes.length} 个内联哈希，style ${styleHashes.length} 个内联哈希。`
);
