ad0430fcd2
- 现状:只把 `/pay`、`/profile/payment` 加进 allowlist 只能让 check:nginx-spa-routes 变绿; payment.rs 生成的 checkoutUrl 是 `/pay/<checkoutToken>`,深链仍落默认 location 的 try_files → 404。同一批漂移里 check:pingora-route-parity 也是红的(Pingora MAIN_SPA_PATHS 缺 /pay、/profile/payment),只是被 lint 链里先失败的门禁掩盖,修一条要跑到链尾 - 真相源:src/routing/activeAppPageRoutes.ts 新增 APP_PREFIX_ROUTE_ENTRIES ('/pay' → payment-checkout),resolveSelectionStageFromPath 改用它 - 门禁:scripts/check-nginx-spa-routes.mjs 要求三份模板都有锚定前缀 location `location ~* "^/pay/[^/]+/?$"`(裸前缀仍由精确 location 负责;前缀 location 必须镜像精确 location 的维护闸与 try_files 回退);新增 scripts/check-nginx-spa-routes.test.mjs 正/反用例 (把前缀写成精确匹配或过宽裸前缀都会红),由 npm run check:nginx-spa-routes 一起执行; check-pingora-route-parity 新增 MAIN_SPA_PREFIX_PATHS 与前端前缀路由的逐条比对 - 模板:deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、 deploy/container/nginx.conf 各加一条锚定前缀 location - Pingora:MAIN_SPA_PATHS 补 /pay、/profile/payment;新增 MAIN_SPA_PREFIX_PATHS 与 is_main_spa_prefix_path(大小写不敏感,只认「前缀 + 恰好一段」),矩阵新增 pay_checkout_spa_fallback 用例,并给网关补一条前缀正/反单测 - 文档:Pingora 试点文档的路由表与门禁说明、deploy/nginx/README 与本地开发/生产运维文档 同步前缀路由口径与线上 curl 复验方式 - 本地实跑:node --test scripts/check-nginx-spa-routes.test.mjs(4 passed)、 node scripts/check-nginx-spa-routes.mjs(OK,14 SPA routes / 1 prefix routes / 3 templates)、 npm run check:pingora-route-parity(OK,25 routes)、 cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix(2 passed)
441 lines
12 KiB
JSON
441 lines
12 KiB
JSON
{
|
|
"version": 1,
|
|
"description": "Pingora shadow gateway routes that must stay aligned with the current Genarrative Nginx production/development templates before any public cutover.",
|
|
"routes": [
|
|
{
|
|
"id": "acme_challenge",
|
|
"samplePath": "/.well-known/acme-challenge/token",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "acme",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location /.well-known/acme-challenge/",
|
|
"root /var/www/html;"
|
|
]
|
|
},
|
|
"docs": ["`/.well-known/acme-challenge/*`"]
|
|
},
|
|
{
|
|
"id": "shadow_probe",
|
|
"samplePath": "/__genarrative_pingora/healthz",
|
|
"expect": {
|
|
"kind": "shadow_probe"
|
|
},
|
|
"docs": ["`/__genarrative_pingora/healthz`"]
|
|
},
|
|
{
|
|
"id": "admin_redirect",
|
|
"samplePath": "/admin",
|
|
"expect": {
|
|
"kind": "redirect_permanent",
|
|
"location": "/admin/"
|
|
},
|
|
"nginx": {
|
|
"production": ["location = /admin", "return 301 /admin/;"],
|
|
"development": ["location = /admin", "return 301 /admin/;"]
|
|
},
|
|
"docs": ["`/admin`", "301 到 `/admin/`"]
|
|
},
|
|
{
|
|
"id": "admin_api_proxy",
|
|
"samplePath": "/admin/api/users",
|
|
"expect": {
|
|
"kind": "proxy",
|
|
"target": "api",
|
|
"bodyLimit": null,
|
|
"protectionClass": "admin_api"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ^~ /admin/api/",
|
|
"proxy_pass http://genarrative_api/admin/api/;",
|
|
"limit_conn genarrative_api_conn 64;",
|
|
"limit_req zone=genarrative_admin_rps burst=16 nodelay;"
|
|
],
|
|
"development": [
|
|
"location ^~ /admin/api/",
|
|
"proxy_pass http://genarrative_api/admin/api/;",
|
|
"limit_conn genarrative_api_conn 64;",
|
|
"limit_req zone=genarrative_admin_rps burst=16 nodelay;"
|
|
]
|
|
},
|
|
"docs": ["`/admin/api/*`"]
|
|
},
|
|
{
|
|
"id": "admin_assets",
|
|
"samplePath": "/admin/assets/index.js",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["location ^~ /admin/assets/", "try_files $uri =404;"],
|
|
"development": ["location ^~ /admin/assets/", "try_files $uri =404;"]
|
|
},
|
|
"docs": ["`/admin/assets/*`"]
|
|
},
|
|
{
|
|
"id": "admin_spa_fallback",
|
|
"samplePath": "/admin/settings",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ^~ /admin/",
|
|
"try_files $uri $uri/ /admin/index.html;"
|
|
],
|
|
"development": [
|
|
"location ^~ /admin/",
|
|
"try_files $uri $uri/ /admin/index.html;"
|
|
]
|
|
},
|
|
"docs": ["`/admin/*`"]
|
|
},
|
|
{
|
|
"id": "web_assets",
|
|
"samplePath": "/assets/app.js",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["location ^~ /assets/", "try_files $uri =404;"],
|
|
"development": ["location ^~ /assets/", "try_files $uri =404;"]
|
|
},
|
|
"docs": ["`/assets/*`"]
|
|
},
|
|
{
|
|
"id": "generic_api_proxy",
|
|
"samplePath": "/api/assets/history",
|
|
"expect": {
|
|
"kind": "proxy",
|
|
"target": "api",
|
|
"bodyLimit": "default",
|
|
"protectionClass": "api"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ ^/api(?:/|$)",
|
|
"client_max_body_size 210m;",
|
|
"limit_conn genarrative_api_conn 64;",
|
|
"limit_req zone=genarrative_api_rps burst=64 nodelay;",
|
|
"add_header X-Accel-Buffering no always;"
|
|
],
|
|
"development": [
|
|
"location ~ ^/api(?:/|$)",
|
|
"client_max_body_size 210m;",
|
|
"limit_conn genarrative_api_conn 64;",
|
|
"limit_req zone=genarrative_api_rps burst=64 nodelay;",
|
|
"add_header X-Accel-Buffering no always;"
|
|
]
|
|
},
|
|
"docs": ["`/api`、`/api/*`"]
|
|
},
|
|
{
|
|
"id": "spacetime_subscribe",
|
|
"samplePath": "/v1/database/genarrative/subscribe",
|
|
"expect": {
|
|
"kind": "proxy",
|
|
"target": "spacetime",
|
|
"bodyLimit": null,
|
|
"protectionClass": "spacetime"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ ^/v1/database/[^/]+/subscribe$",
|
|
"proxy_pass http://127.0.0.1:3101;",
|
|
"proxy_set_header Upgrade $http_upgrade;",
|
|
"proxy_set_header Connection \"Upgrade\";",
|
|
"proxy_read_timeout 3600s;"
|
|
],
|
|
"development": [
|
|
"location ~ ^/v1/database/[^/]+/subscribe$",
|
|
"proxy_pass http://127.0.0.1:3101;",
|
|
"proxy_set_header Upgrade $http_upgrade;",
|
|
"proxy_set_header Connection \"Upgrade\";",
|
|
"proxy_read_timeout 3600s;"
|
|
]
|
|
},
|
|
"docs": ["`/v1/database/{db}/subscribe`"]
|
|
},
|
|
{
|
|
"id": "spacetime_identity",
|
|
"samplePath": "/v1/identity",
|
|
"expect": {
|
|
"kind": "proxy",
|
|
"target": "spacetime",
|
|
"bodyLimit": null,
|
|
"protectionClass": "spacetime"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ^~ /v1/identity",
|
|
"proxy_pass http://127.0.0.1:3101;",
|
|
"proxy_set_header Upgrade $http_upgrade;",
|
|
"proxy_set_header Connection \"Upgrade\";"
|
|
],
|
|
"development": [
|
|
"location ^~ /v1/identity",
|
|
"proxy_pass http://127.0.0.1:3101;",
|
|
"proxy_set_header Upgrade $http_upgrade;",
|
|
"proxy_set_header Connection \"Upgrade\";"
|
|
]
|
|
},
|
|
"docs": ["`/v1/identity*`"]
|
|
},
|
|
{
|
|
"id": "v1_forbidden",
|
|
"samplePath": "/v1/ping",
|
|
"expect": {
|
|
"kind": "not_found"
|
|
},
|
|
"nginx": {
|
|
"production": ["location ^~ /v1/", "return 404;"],
|
|
"development": ["location ^~ /v1/", "return 404;"]
|
|
},
|
|
"docs": ["`/v1/*`"]
|
|
},
|
|
{
|
|
"id": "healthz_forbidden",
|
|
"samplePath": "/healthz",
|
|
"expect": {
|
|
"kind": "not_found"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
],
|
|
"development": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
]
|
|
},
|
|
"docs": ["`/healthz*`"]
|
|
},
|
|
{
|
|
"id": "readyz_forbidden",
|
|
"samplePath": "/readyz",
|
|
"expect": {
|
|
"kind": "not_found"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
],
|
|
"development": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
]
|
|
},
|
|
"docs": ["`/readyz*`"]
|
|
},
|
|
{
|
|
"id": "generated_assets_forbidden",
|
|
"samplePath": "/generated-puzzle-assets/foo.webp",
|
|
"expect": {
|
|
"kind": "not_found"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
],
|
|
"development": [
|
|
"location ~ ^/(generated-|healthz|readyz)",
|
|
"return 404;"
|
|
]
|
|
},
|
|
"docs": ["`/generated-*`"]
|
|
},
|
|
{
|
|
"id": "web_root_spa",
|
|
"samplePath": "/",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": ["location = /", "try_files /index.html =404;"],
|
|
"development": ["location = /", "try_files /index.html =404;"]
|
|
},
|
|
"docs": ["主站 SPA allowlist", "根路径 `/` 精确回退 `/index.html`"]
|
|
},
|
|
{
|
|
"id": "web_spa_fallback",
|
|
"samplePath": "/project",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
],
|
|
"development": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
]
|
|
},
|
|
"docs": ["主站 SPA allowlist", "失败回退 `/index.html`"]
|
|
},
|
|
{
|
|
"id": "profile_spa_fallback",
|
|
"samplePath": "/profile",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
],
|
|
"development": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
]
|
|
},
|
|
"docs": ["主站 SPA allowlist", "失败回退 `/index.html`"]
|
|
},
|
|
{
|
|
"id": "pay_checkout_spa_fallback",
|
|
"samplePath": "/pay/checkout-token",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": ["location ~* \"^/pay/[^/]+/?$\""],
|
|
"development": ["location ~* \"^/pay/[^/]+/?$\""]
|
|
},
|
|
"docs": ["收银台深链 `/pay/<checkoutToken>`", "前缀 + 恰好一个路径段"]
|
|
},
|
|
{
|
|
"id": "games_spa_fallback",
|
|
"samplePath": "/games/detail",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
],
|
|
"development": [
|
|
"# BEGIN GENARRATIVE MAIN SPA ROUTES",
|
|
"try_files $uri /index.html =404;"
|
|
]
|
|
},
|
|
"docs": ["主站 SPA allowlist", "游戏目录 / 详情 / 游玩 / 我的 / 发布深链"]
|
|
},
|
|
{
|
|
"id": "games_release_gateway",
|
|
"samplePath": "/games/game_0123456789abcdef0123456789abcdef/index.html",
|
|
"expect": {
|
|
"kind": "release_gateway",
|
|
"upstreamPath": "/api/game-distribution/releases/game_0123456789abcdef0123456789abcdef/index.html"
|
|
},
|
|
"nginx": {
|
|
"production": [
|
|
"location ~ \"^/games/(?<game_id>game_[0-9a-f]{32})(?<game_path>/.*)?$\"",
|
|
"proxy_set_header Cookie \"\";",
|
|
"proxy_pass http://genarrative_api/api/game-distribution/releases/$game_id$game_path;"
|
|
],
|
|
"development": [
|
|
"location ~ \"^/games/(?<game_id>game_[0-9a-f]{32})(?<game_path>/.*)?$\"",
|
|
"proxy_set_header Cookie \"\";",
|
|
"proxy_pass http://genarrative_api/api/game-distribution/releases/$game_id$game_path;"
|
|
]
|
|
},
|
|
"docs": ["/games/game_<32 位十六进制 id>/…", "平台同源发行入口"]
|
|
},
|
|
{
|
|
"id": "web_spa_case_trailing_slash",
|
|
"samplePath": "/PROJECT/",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "spa_fallback"
|
|
},
|
|
"nginx": {
|
|
"production": ["location ~*", "try_files $uri /index.html =404;"],
|
|
"development": ["location ~*", "try_files $uri /index.html =404;"]
|
|
},
|
|
"docs": ["大小写不敏感", "一个尾部斜杠"]
|
|
},
|
|
{
|
|
"id": "web_unknown_path_exact",
|
|
"samplePath": "/some/spa/path",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["location /", "try_files $uri $uri/ =404;"],
|
|
"development": ["location /", "try_files $uri $uri/ =404;"]
|
|
},
|
|
"docs": ["其它 Web 路径", "缺失时返回真实 404"]
|
|
},
|
|
{
|
|
"id": "creation_unknown_path_exact",
|
|
"samplePath": "/creation/not-exist",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["try_files $uri $uri/ =404;"],
|
|
"development": ["try_files $uri $uri/ =404;"]
|
|
},
|
|
"docs": ["`/creation/not-exist`"]
|
|
},
|
|
{
|
|
"id": "runtime_unknown_path_exact",
|
|
"samplePath": "/runtime/not-exist",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["try_files $uri $uri/ =404;"],
|
|
"development": ["try_files $uri $uri/ =404;"]
|
|
},
|
|
"docs": ["`/runtime/not-exist`"]
|
|
},
|
|
{
|
|
"id": "puzzle_unknown_path_exact",
|
|
"samplePath": "/puzzle/not-exist",
|
|
"expect": {
|
|
"kind": "static",
|
|
"root": "web",
|
|
"mode": "exact"
|
|
},
|
|
"nginx": {
|
|
"production": ["try_files $uri $uri/ =404;"],
|
|
"development": ["try_files $uri $uri/ =404;"]
|
|
},
|
|
"docs": ["`/puzzle/not-exist`"]
|
|
}
|
|
]
|
|
}
|