/** * Persisted Query Manifest 生成脚本(v2.1 M3 安全加固) * * 构建时遍历 src/lib/api/operations/ 中所有 gql 文档,生成 * sha256(query) → query 文本 的白名单 manifest。 * * 部署到 apollo-router 容器,生产模式(APOLLO_REQUIRE_PQ_MANIFEST=true) * 拒绝 manifest 之外的查询,防止攻击者构造任意 query 探测 schema。 * * 产物:apps/portal-shell/public/pq-manifest.json * 关联:portal-shell spec §4.2 PQ Manifest */ import { print } from "graphql"; import { sha256 } from "crypto-hash"; import * as fs from "node:fs"; import * as path from "node:path"; import * as url from "node:url"; // 使用 fileURLToPath 兼容 ESM 下 __dirname 缺失 const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 动态 import operations barrel(含 51 个 gql DocumentNode) // Windows 下动态 import 需 file:// URL(ESM 限制) const operationsPath = path.resolve( __dirname, "../src/lib/api/operations/index.ts", ); const operationsUrl = url.pathToFileURL(operationsPath).href; async function generateManifest(): Promise { // tsx 运行时支持直接 import .ts const operationsModule = (await import(operationsUrl)) as Record< string, unknown >; const manifest: Record = {}; let skipped = 0; for (const [, doc] of Object.entries(operationsModule)) { // 仅处理 DocumentNode 对象(含 loc.source.body) if ( typeof doc === "object" && doc !== null && "loc" in doc && // eslint-disable-next-line @typescript-eslint/no-explicit-any (doc as any).loc !== null && typeof (doc as { loc: { source: { body: string } } }).loc.source?.body === "string" ) { const query = print(doc as never); const hash = await sha256(query); manifest[hash] = query; } else { skipped++; } } const outDir = path.resolve(__dirname, "../public"); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } const outPath = path.resolve(outDir, "pq-manifest.json"); fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2)); const count = Object.keys(manifest).length; console.log( `✓ PQ manifest generated: ${count} queries` + (skipped > 0 ? ` (${skipped} non-document exports skipped)` : ""), ); console.log(` Output: ${outPath}`); } generateManifest().catch((err) => { console.error("✗ Failed to generate PQ manifest:", err); process.exit(1); });