feat(portal-shell): enable Apollo Client APQ + PQ manifest generator

Task 12-13 of portal-shell data abstraction plan (M3 security).

APQ (Automatic Persisted Queries):
- Add createPersistedQueryLink with sha256 to apollo-client.ts
- Production: client sends only query hash, not plaintext query
- Dev: NEXT_PUBLIC_APOLLO_APQ=false to disable for debugging
- Prevents attackers from crafting arbitrary queries via DevTools

PQ Manifest generator:
- New scripts/generate-pq-manifest.ts iterates operations barrel
- Outputs public/pq-manifest.json (sha256 -> query text whitelist)
- prebuild hook: codegen + generate-pq-manifest before next build
- 51 queries currently registered

- crypto-hash dependency added
- typecheck + lint (0 errors) + test (85/85) verified
This commit is contained in:
SpecialX
2026-07-17 13:31:11 +08:00
parent 1b5781bf42
commit b30d43f983
6 changed files with 178 additions and 5 deletions

View File

@@ -8,16 +8,28 @@
* - 服务端RSCcreateApolloClient() 每次请求新建实例ssrMode=true
* - 客户端getApolloClient() 单例,复用 InMemoryCache
*
* 关联portal-shell spec §5.5 RSC 预取、§5.6 统一 Hook、M8 验收标准
* APQAutomatic Persisted Queriesv2.1 M3 安全加固):
* - 生产环境前端只发 query hashsha256不发明文 query
* - apollo-router 通过 hash 查找 pq-manifest.json 中的 query 文本
* - 防止攻击者通过 DevTools 构造任意查询探测 schema
* - 开发模式可设 NEXT_PUBLIC_APOLLO_APQ=false 关闭 APQ 便于调试
*
* 关联portal-shell spec §4.1 APQ、§5.5 RSC 预取、§5.6 统一 Hook、M8 验收标准
*/
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from "crypto-hash";
const APOLLO_ROUTER_URL =
process.env.NEXT_PUBLIC_APOLLO_ROUTER_URL ||
process.env.APOLLO_ROUTER_URL ||
"http://localhost:3000/graphql";
// 开发模式可关闭 APQ 便于调试NEXT_PUBLIC_APOLLO_APQ=false
// 生产环境默认启用(未设置或设置为 true 均启用)
const APQ_ENABLED = process.env.NEXT_PUBLIC_APOLLO_APQ !== "false";
/**
* 创建 Apollo Client 实例。
*
@@ -41,8 +53,16 @@ export function createApolloClient(
};
});
// Link 链顺序authLink → pqLink → httpLink
// - authLink 注入 Authorization 头
// - pqLink 将 query 替换为 hash启用时
// - httpLink 发送请求
const link = APQ_ENABLED
? from([authLink, createPersistedQueryLink({ sha256 }), httpLink])
: from([authLink, httpLink]);
return new ApolloClient({
link: from([authLink, httpLink]),
link,
cache: new InMemoryCache(),
ssrMode: typeof window === "undefined",
defaultOptions: {