Files
eSIM-Tools/netlify.toml
Neo Vern 7298ae4a0d feat: QR 码本地打包 + Edge Function 直接生成 (#86)
*  feat: 打包 qrcode-generator@1.4.4 到本地 ES 模块

* ♻️ refactor(qrcode): 移除 CDN 加载逻辑,改用本地 import 引入 qrcode-generator

* 🔧 chore: 移除 CDN preconnect 提示,QR 码库已内联

* 🔧 chore: 清理 CSP 配置,移除不再需要的 CDN 域名

*  feat: 将 QR 码生成迁移到 Edge Function,消除后端冷启动延迟

- 在 Edge Function 中内联 qrcode-generator 库(~20KB),直接生成 QR 码
- 删除废弃的 Netlify Function (qrcode-generate.js)
- 更新 server.js 移除对已删除函数的引用
- 更新测试文件适配新的 Edge 内联架构

* ♻️ refactor: 代码质量修复 — 移除死代码、消除变量遮蔽、添加交叉引用注释

* 🔧 chore: 修复非阻塞风险 — 补充 strict 模式、移除废弃 qrcode 依赖

- Edge Function qrcode-lib.js 补充 'use strict' 声明,与浏览器版保持一致
- 移除已废弃的 qrcode npm 依赖(原用于已删除的 Netlify Function)

* ♻️ refactor: 消除魔法数字、补充脆耦合和 async 技术债注释

- Edge Function 中 QR margin 魔法数字 8 替换为 QR_MARGIN_MODULES 常量
- generateQRCodeLocal 补充 async 无 await 的技术债说明
- error.message.startsWith 条件补充校验函数耦合关系注释

* 🐛 fix: 修复边界条件 — null JSON body、vendor 字符串异常、CDN preconnect 残留

- Edge Function: null JSON body 解构移到 try/catch 内,添加 null/非对象检查
- qrcode-generator: 库 throw 字符串时统一转换为 Error 对象,避免 .startsWith 崩溃
- index.html: 移除不再需要的 CDN preconnect 提示(jsdelivr/cdnjs)

* ♻️ refactor: 修复 CSP 恢复、异常归一化、自定义 Error 类、负面路径测试

- netlify.toml: 恢复 script-src 中 cdn.jsdelivr.net(Bootstrap JS 仍依赖)
- Edge Function catch: 归一化非 Error 异常(库可能 throw 字符串)
- Edge Function QR margin: createDataURL(cellSize, 2) → createDataURL(cellSize, cellSize * 2) 对齐模块边距
- Edge Function: 补充认证模型和 QR 格式变更注释
- qrcode-generator.js: 引入 QRCodeValidationError 替代 error.message.startsWith 脆耦合
- tests/bff-proxy: 添加 6 个 Edge QR 负面路径测试(无效 JSON、空/超长/非字符串 data、超范围 size)
- tests/qrcode-generator: 添加库 throw 字符串异常的测试
- scripts/sync-qrcode-lib.js: 新增库代码同步验证脚本

* 🐛 fix: 修复 server.js CSP 缺少 jsdelivr、同步脚本 CRLF 归一化、fallback 拦截校验错误

- server.js: 恢复 script-src 中 cdn.jsdelivr.net(本地开发 Bootstrap JS 依赖)
- sync-qrcode-lib.js: 归一化 CRLF 换行符避免跨平台误报
- qrcode-generator.js: generateQRCodeWithFallback 入口处拦截 QRCodeValidationError,避免无效输入触发无意义的后端降级
2026-06-25 22:15:17 +08:00

195 lines
6.6 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[build]
# 构建命令
command = "npm run build --silent"
# 发布目录:仅暴露构建后的 dist 输出
publish = "dist"
# Functions目录
functions = "netlify/functions"
# Edge Functions 目录
edge_functions = "netlify/edge-functions"
[build.environment]
# 固定 Node 版本,避免二进制依赖(如 sharp在 Node 22 下编译耗时
NODE_VERSION = "20.12.2"
# 安装时减少无关输出与提示
NPM_FLAGS = "--no-audit --no-fund --omit=optional"
# 重定向和重写规则
[[redirects]]
# Giffgaff eSIM页面 - 模块化版本
from = "/giffgaff"
to = "/src/giffgaff/giffgaff_modular.html"
status = 200
[[redirects]]
# Simyo eSIM页面 - 模块化版本
from = "/simyo"
to = "/src/simyo/simyo_modular.html"
status = 200
[[redirects]]
# 根路径重定向到选择页面
from = "/"
to = "/index.html"
status = 200
# API代理重定向用于解决CORS问题
# 通用 Simyo API 代理 (webapi - 新版API)
[[redirects]]
from = "/api/simyo/*"
to = "https://appapi.simyo.nl/webapi/api/v1/:splat"
status = 200
force = true
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
# v2 API 代理(用于特定端点如可用验证方式查询)
[[redirects]]
from = "/api/simyo/esim.availableValidationMethods"
to = "https://appapi.simyo.nl/webapi/api/v2/esim.availableValidationMethods"
status = 200
force = true
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
# 保留旧版 simyoapi 路径的兼容性(如果需要)
[[redirects]]
from = "/api/simyo/legacy/*"
to = "https://appapi.simyo.nl/simyoapi/api/v1/:splat"
status = 200
force = true
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
[[headers]]
# 限制 Simyo 代理仅允许指定站点跨域访问
for = "/api/simyo/*"
[headers.values]
Access-Control-Allow-Origin = "https://esim.cosr.eu.org"
Access-Control-Allow-Headers = "Content-Type, Authorization"
Access-Control-Allow-Methods = "GET, POST, OPTIONS"
Vary = "Origin"
# 将 /bff/* 映射到 Edge Function bff-proxy
[[edge_functions]]
path = "/bff/*"
function = "bff-proxy"
# Markdown for Agents检测 Accept: text/markdown 请求
[[edge_functions]]
path = "/"
function = "markdown-negotiation"
[[redirects]]
from = "/api/giffgaff/*"
to = "https://api.giffgaff.com/:splat"
status = 200
force = true
headers = {X-Forwarded-Host = "api.giffgaff.com"}
[[headers]]
for = "/api/giffgaff/*"
[headers.values]
# giffgaff 直连不受此限制,保持通用放行
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Headers = "Content-Type, Authorization"
Access-Control-Allow-Methods = "GET, POST, OPTIONS"
[[redirects]]
from = "/api/giffgaff-id/*"
to = "https://id.giffgaff.com/:splat"
status = 200
force = true
headers = {X-Forwarded-Host = "id.giffgaff.com"}
[[headers]]
for = "/api/giffgaff-id/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Headers = "Content-Type, Authorization"
Access-Control-Allow-Methods = "GET, POST, OPTIONS"
[[redirects]]
from = "/api/giffgaff-public/*"
to = "https://publicapi.giffgaff.com/:splat"
status = 200
force = true
headers = {X-Forwarded-Host = "publicapi.giffgaff.com"}
[[headers]]
for = "/api/giffgaff-public/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Headers = "Content-Type, Authorization"
Access-Control-Allow-Methods = "GET, POST, OPTIONS"
# favicon 根路径重定向(浏览器默认请求 /favicon.ico
[[redirects]]
from = "/favicon.ico"
to = "/src/assets/favicon.ico"
status = 200
# OG 社交分享图片重定向
[[redirects]]
from = "/og-image.png"
to = "/src/assets/og-image.png"
status = 200
# 处理SPA路由兜底 404
[[redirects]]
from = "/*"
to = "/index.html"
status = 404
# 静态资源长期缓存CSS/JS/字体/图片)
[[headers]]
for = "/src/styles/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/src/js/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/src/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/dist/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
# HTML 页面:短缓存 + stale-while-revalidate
[[headers]]
for = "/*.html"
[headers.values]
Cache-Control = "public, max-age=300, stale-while-revalidate=86400"
# 安全头部
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
# 生产 CSP包含 unsafe-inline 是因为部分页面仍有内联样式和脚本(如内联 CSS、runtime 兜底脚本),
# 迁移至 nonce/hash 策略需模板注入机制支持,当前暂保留 unsafe-inline
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google.com https://www.gstatic.com https://browser.sentry-cdn.com https://sentry.io https://*.sentry.io https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com; img-src 'self' data: https:; connect-src 'self' https://browser.sentry-cdn.com https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://www.google.com https://www.gstatic.com https://challenges.cloudflare.com https://sentry.io https://*.sentry.io; font-src 'self' data: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.gstatic.com; frame-src 'self' https://challenges.cloudflare.com https://www.google.com https://*.sentry.io; worker-src 'self' blob:; child-src 'self' blob:; manifest-src 'self';"
# Agent 发现 Link 响应头 (RFC 8288)
[[headers]]
for = "/"
[headers.values]
Link = "</.well-known/api-catalog>; rel=\"api-catalog\", </.well-known/oauth-protected-resource>; rel=\"oauth-protected-resource\", </.well-known/mcp/server-card.json>; rel=\"mcp-server-card\", </.well-known/agent-skills/index.json>; rel=\"agent-skills\""
# Markdown for Agents 支持头部
[[headers]]
for = "/"
[headers.values]
Vary = "Accept"
[[headers]]
for = "/manifest.webmanifest"
[headers.values]
Content-Type = "application/manifest+json; charset=utf-8"