payment-kit 1.28.0 → 1.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/api/src/crons/index.ts +22 -0
  2. package/api/src/crons/retry-pending-events.ts +58 -0
  3. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  4. package/api/src/integrations/app-store/client.ts +369 -0
  5. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  6. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  7. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  8. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  9. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  10. package/api/src/integrations/google-play/client.ts +276 -0
  11. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  12. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  13. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  14. package/api/src/integrations/google-play/setup.ts +43 -0
  15. package/api/src/integrations/google-play/verify.ts +251 -0
  16. package/api/src/integrations/iap-reconcile.ts +415 -0
  17. package/api/src/libs/audit.ts +38 -8
  18. package/api/src/libs/entitlement.ts +399 -0
  19. package/api/src/libs/env.ts +2 -0
  20. package/api/src/libs/security.ts +51 -0
  21. package/api/src/libs/subscription.ts +13 -1
  22. package/api/src/libs/util.ts +13 -0
  23. package/api/src/queues/event.ts +25 -19
  24. package/api/src/queues/webhook.ts +12 -2
  25. package/api/src/routes/entitlements.ts +105 -0
  26. package/api/src/routes/events.ts +2 -2
  27. package/api/src/routes/index.ts +12 -2
  28. package/api/src/routes/integrations/app-store.ts +267 -0
  29. package/api/src/routes/integrations/google-play.ts +324 -0
  30. package/api/src/routes/payment-methods.ts +130 -0
  31. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  32. package/api/src/store/models/customer.ts +14 -0
  33. package/api/src/store/models/entitlement-grant.ts +118 -0
  34. package/api/src/store/models/entitlement-product.ts +48 -0
  35. package/api/src/store/models/entitlement.ts +86 -0
  36. package/api/src/store/models/index.ts +9 -0
  37. package/api/src/store/models/invoice.ts +20 -0
  38. package/api/src/store/models/payment-method.ts +62 -1
  39. package/api/src/store/models/refund.ts +10 -0
  40. package/api/src/store/models/subscription.ts +14 -0
  41. package/api/src/store/models/types.ts +32 -0
  42. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  43. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  44. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  45. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  46. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  47. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  48. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  49. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  50. package/api/tests/libs/entitlement.spec.ts +347 -0
  51. package/blocklet.yml +1 -1
  52. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  53. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  54. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  55. package/cloudflare/run-build.js +23 -1
  56. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  57. package/cloudflare/shims/node-fetch.ts +35 -0
  58. package/cloudflare/shims/queue.ts +28 -2
  59. package/cloudflare/shims/sequelize-d1/model.ts +19 -0
  60. package/cloudflare/shims/sequelize-d1/operators.ts +14 -1
  61. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  62. package/cloudflare/worker.ts +59 -4
  63. package/cloudflare/wrangler.jsonc +7 -1
  64. package/cloudflare/wrangler.staging.json +2 -1
  65. package/package.json +10 -6
  66. package/scripts/seed-google-play.ts +79 -0
  67. package/src/components/payment-method/app-store.tsx +103 -0
  68. package/src/components/payment-method/form.tsx +7 -1
  69. package/src/components/payment-method/google-play.tsx +85 -0
  70. package/src/components/subscription/list.tsx +20 -0
  71. package/src/locales/en.tsx +63 -0
  72. package/src/locales/zh.tsx +63 -0
  73. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  74. package/src/pages/admin/customers/customers/detail.tsx +6 -0
  75. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  76. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
@@ -0,0 +1,288 @@
1
+ # CF Workers Bundle 体积优化 · 综合总结
2
+
3
+ > 日期:2026-06-10 仓库 payment-kit,分支 `feat/cross-blocklet-session-issue`(HEAD `3c8a1c46`)
4
+ > 产物:`blocklets/core/cloudflare/dist/worker.js`
5
+ > 本文是这次 bundle 优化的**唯一权威总结**,已合并原 investigation-log / dead-code-removal-reference / optimization-summary / package-audit 四份。所有数字均经实测(git 历史真实 build + esbuild metafile + wrangler dry-run/deploy),并用「落盘文件 + 交叉验证」方法取得。
6
+
7
+ ---
8
+
9
+ ## 1. 结论速览(TL;DR)
10
+
11
+ - worker.js 从迁移基线 **2.68 MiB** 涨到 **4.07 MiB**,增长 **100% 来自 IAP**(PR #1381),其中 **85% 是单个提交 `3700b061`**(Apple JWS 验签)。
12
+ - 可砍的纯死代码有两处,均已移除:**① node-fetch polyfill 链 ~781K**(encoding/tr46/whatwg-url);**② ethers 非英语 BIP39 助记词词表 ~67K**(worker 0 引用 Mnemonic)。两者在 Workers 上永不执行。
13
+ - 配合 wrangler 部署优化(`minify` 或 `no-bundle`),实际部署 gzip 从 **1185 KiB → 957.73 KiB(< 1 MiB,含 node-fetch + wordlists 两处优化)**。
14
+ - 其余大头全是**业务代码 / 加密货币核心能力 / DID 协议 / 必需 SDK / 高风险验签库**,无更多低风险高收益项。
15
+
16
+ ### 体积演变(两口径,均实测)
17
+
18
+ | 阶段 | wrangler Total Upload | gzip | 进 1 MiB |
19
+ |---|---|---|---|
20
+ | 基线(未优化) | 4862 KiB | 1185 KiB | ✗ |
21
+ | + node-fetch alias | 4438 KiB | 1123 KiB | ✗ |
22
+ | + wrangler `minify` | 3462 KiB | 1033 KiB | ✗(差 9K,最安全) |
23
+ | + wrangler `no-bundle`(仅 node-fetch) | 3385 KiB | 1006 KiB | ✅(worker 可跑,依赖 nodejs_compat) |
24
+ | **+ wordlists(最终态,已部署 staging)** | **3318 KiB** | **958 KiB** | ✅ **实测部署 gzip 957.73 KiB** |
25
+
26
+ > `dist/worker.js` 本身(run-build.js 产物):4.07 MiB / gzip 1.23 → node-fetch 后 3.30 / gzip 0.97 → **+wordlists 后 3.24 MiB / gzip 0.927**。
27
+ > **最终态实测部署(no-bundle,两处死代码移除):Total Upload 3317.93 KiB / gzip 957.73 KiB(< 1 MiB,已部署个人 staging 并验证支付路由正常运行)。**
28
+ > 公司 staging 线上(旧代码)实测 module body 3.46 MiB / gzip 0.83,与本地 HEAD 不同次构建,不可直接比。
29
+ > CF 脚本限额按 gzip 算(免费版历史 1MB→后提 3MB,以 [CF 官方 limits](https://developers.cloudflare.com/workers/platform/limits/) 为准),当前 ~1 MiB 无容量压力。
30
+
31
+ ---
32
+
33
+ ## 2. 膨胀根因:IAP(PR #1381,squash 成 `0039b36a`)一次 +1.38 MiB
34
+
35
+ 迁移基线 `bad72364`/`1486b54f` = 2.68 MiB(就是"最早 2.7")。squash 的 PR 子提交需 `git fetch origin pull/1381/head` 才能取到,对 48 个相关子提交逐个 build 定位跳变点:
36
+
37
+ | 子提交 | 内容 | Δ |
38
+ |---|---|---|
39
+ | `a140541a` | A2-real Google Play API | +0.65 MiB(引入 googleapis / google-play-billing-validator / google-auth-library / node-fetch) |
40
+ | `3700b061` | **A1-real-3 Apple SDK JWS 验签** | **+1.18 MiB** 🔴(引入 @apple/app-store-server-library + jsrsasign + jsonwebtoken + node-jose) |
41
+ | `2594bafe` | A4-cf Google Play 改 Workers 原生 fetch | −0.62 MiB(把 Google 重 SDK 换原生,几乎抵消 a140541a) |
42
+
43
+ 其余 ~32 个子提交对 bundle ≈ 0。净增长 85% 来自 `3700b061` 的 Apple 验签链。
44
+
45
+ ---
46
+
47
+ ## 3. 已做的优化
48
+
49
+ ### 3.1 node-fetch polyfill 死代码移除(代码改动)
50
+
51
+ `@apple/app-store-server-library`(dist/jws_verification.js、dist/index.js)内部 `import node-fetch`,node-fetch 又拖进一整套 Node polyfill:
52
+
53
+ | 包 | 占用 | 为何是死代码 |
54
+ |---|---|---|
55
+ | encoding(= iconv-lite,CJK 编码表 cp936/cp950/eucjp/cp949/shiftjis) | 481 KiB | Workers 有原生 TextDecoder,编码表永不查(API 都是 UTF-8) |
56
+ | tr46(IDNA 域名映射表) | 259 KiB | Workers 有原生 URL,域名都 ASCII |
57
+ | whatwg-url / node-fetch | 41 KiB | Workers 有原生 URL/fetch |
58
+
59
+ > 「死代码」≠ 源码没人 import;恰恰是被 import 了才打进 bundle,"死"在**运行时永不执行**。
60
+
61
+ **改动(主仓库工作区,UNCOMMITTED)**:
62
+ 1. 新增 `shims/node-fetch.ts`:转发 `globalThis.fetch`,补全 node-fetch v2 导出(default/Headers/Request/Response/FormData/Blob/FetchError/AbortError/isRedirect)。
63
+ 2. `run-build.js` 的 alias 对象(含 axios/stripe/@blocklet/sdk 一大套 shim 那个,**不是 lodash 那个**)加 `"node-fetch": s("shims/node-fetch.ts")`。
64
+ 3. 新增 `scripts/verify-bundle-optimization.sh`(构建层自动验 + IAP 端到端清单)。
65
+
66
+ 效果(mtime+size 双证):dist/worker.js **4.07 → 3.30 MiB**(gzip 1.23→0.97),死代码 encoding/iconv-lite/tr46/whatwg-url/node-fetch 全归零。
67
+ **升级 SDK 无效**:@apple SDK 最新 3.1.0 仍依赖 node-fetch@2.7.0、jsrsasign@11。
68
+ 替代落地方式:pnpm.overrides / pnpm patch(首选仍是 esbuild alias,与现有 shim 同手法)。
69
+
70
+ ### 3.2 ethers 非英语助记词词表移除(代码改动)
71
+
72
+ ethers `lib.esm` 完整版把 9 种语言的 BIP39 助记词词表全打进 bundle(供 Mnemonic/HD 钱包用)。worker **0 引用** Mnemonic/HDNode/fromPhrase/wordlist —— ethereum 链上签名走 `RemoteSigner extends ethers.AbstractSigner` + `@ocap/wallet` 远程签名服务,**不碰助记词**。所以 8 种非英语词表永不执行(ethers 官方 /dist 也默认 strip 掉,省 ~80kb)。
73
+
74
+ **改动**:`run-build.js` 加 esbuild plugin `dropEthersWordlistsPlugin`,onLoad 把 8 个 `wordlists/lang-{cz,es,fr,ja,ko,it,pt,zh}.js` stub 成 `export class LangXx { static wordlist() { return null; } }`(保留 static 方法,避免 wordlists.js 模块初始化时顶层 `LangXx.wordlist()` 调用崩溃;保留 LangEn)。
75
+
76
+ 效果(实测):dist/worker.js **3.30 → 3.24 MiB**,再省 raw 67K / gzip 48K。build 成功,ethers 核心(provider-jsonrpc/contract/transaction)完好。
77
+ **影响**:仅"用**非英语**助记词派生 HD 钱包"会失效 —— worker 从不使用(远程签名,不碰助记词),**零业务影响**。合并前 staging 顺带验一次 ethereum 链上支付即可。
78
+
79
+ ### 3.3 wrangler 二次打包优化(配置层)
80
+
81
+ **陷阱**:所有 wrangler config 都是 `main = "dist/worker.js"`,但 `wrangler deploy` 仍会用它自己的 esbuild 对这个文件**再 bundle 一次**且**默认不 minify**,把 3.30 MiB 膨胀回 ~4.33 MiB(Total Upload 4438 KiB),稀释了 run-build 层的优化(dist 省 785K,部署 gzip 只降 63K)。
82
+
83
+ - `--minify`:让 wrangler 二次打包也压缩 → 3462 KiB / gzip 1033(**最安全**,行为同现状 + 压缩)。
84
+ - `--no-bundle`:跳过二次打包,直接传 dist/worker.js → 3385 KiB / gzip **1006**(**最小**,进 1 MiB;但依赖 nodejs_compat,见 §4 验证)。
85
+
86
+ **落地**:在各 wrangler config(local/dev/staging/jsonc)加 `minify = true`(或采用 no-bundle)。
87
+
88
+ ---
89
+
90
+ ## 4. 部署验证(个人 staging)
91
+
92
+ 部署目标:`payment-kit-staging.zhuzhuyule-779.workers.dev`(Pengfei 个人账号,`wrangler.local.toml`,OAuth)。compat:`compatibility_date = "2024-12-01"` + `nodejs_compat`。
93
+
94
+ no-bundle 版实测 curl(证明 worker 真能跑,node 内置 import 全解析):
95
+
96
+ | 路径 | HTTP | 含义 |
97
+ |---|---|---|
98
+ | `/`(SPA via ASSETS) | 200 | 正常 |
99
+ | `/__blocklet__.js` | 200 | 身份正常 |
100
+ | `/api/payment-methods` | 403 `{"error":"Not authorized"}` | **后端代码正常跑**(业务授权响应,非 500 崩溃) |
101
+
102
+ > `/api/payment-methods` 返回业务 403 而非 500,证明 worker 成功启动、node 内置 import(crypto/path/events/url…)在 no-bundle + nodejs_compat 下全部解析。
103
+
104
+ ---
105
+
106
+ ## 5. Package 审计:可优化 / 不可改
107
+
108
+ 当前优化后 3.30 MiB 的 package 分类(metafile 实测):
109
+
110
+ | 类别 | packages | 体积 | 处置 |
111
+ |---|---|---|---|
112
+ | ✅ 已移除(死代码1) | encoding/tr46/whatwg-url/node-fetch | 0(原 781K) | 已 alias |
113
+ | ✅ 已移除(死代码2) | ethers 非英语 BIP39 词表(cz/es/fr/ja/ko/it/pt/zh) | 0(原 ~70K) | 已 plugin stub |
114
+ | 🔒 业务代码 | app-src(routes/libs/queues/models/integrations) | 1353K | 不可改 |
115
+ | 🔒 加密货币核心能力 | ethers 361 + @noble/curves 73 + bn.js 44 + aes-js 42 + ens-normalize 35 + bignumber.js 19 + @noble/hashes·ciphers·ed25519 | ~600K | 保留(用户拍板) |
116
+ | 🔒 DID/ArcBlock 协议 | @ocap/client 130 + message 67 + proto 48 + mcrypto/util/asset/wallet + @arcblock/* | ~360K | 业务核心 |
117
+ | 🔒 业务必需 SDK | stripe 96(已 stripe-cf shim)、joi 134、hono 20、dayjs 21 | ~270K | 不可改 |
118
+ | 🟡 IAP 验签(高风险不动) | jsrsasign 299、@apple SDK 85、jsonwebtoken 12、node-apple-receipt-verify 13 | ~410K | 换 WebCrypto 需重写验签,安全关键 |
119
+ | 🟡 已最优/不值得动 | lodash-es 121(子路径 import 已优)、semver/async/qs 等小项 | — | gzip 后收益微、改动面大 |
120
+
121
+ **`token-addresses.json`(加密货币 357 个代币范围)和 `locales` zh/en(in-use 订阅通知本地化)是业务功能数据,非死代码,保留。**
122
+
123
+ ### 不可修改点汇总(一句话版)
124
+
125
+ | 不可改 | 原因 |
126
+ |---|---|
127
+ | app-src | Payment Kit 后端业务逻辑 |
128
+ | ethers 及加密 crypto 栈 | 加密货币(ethereum)支付核心能力(用户拍板)。位置 `api/src/integrations/ethereum/`,8 文件用 ethers(payment-methods.ts `new ethers.JsonRpcProvider`、refunds.ts 链上退款等) |
129
+ | @ocap/* @arcblock/* | DID 登录/协议核心 |
130
+ | stripe / joi / hono | 业务必需 SDK/框架 |
131
+ | jsrsasign / @apple SDK / jsonwebtoken / node-apple-receipt-verify | IAP 验签,安全关键高风险 |
132
+ | token-addresses.json / locales | 加密货币代币范围 / in-use 通知本地化(非死代码) |
133
+
134
+ **结论**:分析指出的可优化死代码已全部移除;剩余无更多低风险高收益项。不建议为最后 <100K gzip 去动加密货币栈/验签库/joi/lodash(风险 = 资金/安全/大面积回归,远大于收益,且无容量压力)。
135
+
136
+ ---
137
+
138
+ ## 6. 业务代码组成 · 健康度评判 · 全量可优化点
139
+
140
+ > 死代码移除到顶后,进一步审视"剩下的体积到底合不合理"。**结论:bundle 没有虚胖——业务代码大是真复杂度;真正的债在结构(超大文件)和测试(路由层),与体积无关。**
141
+
142
+ ### 6.1 worker bundle 组成(raw 3313 KiB / gzip 948 KiB)
143
+
144
+ | 分类 | 占用 | 占比 |
145
+ |---|---:|---:|
146
+ | 业务代码(本仓 api/src) | 1348 KiB | 41% |
147
+ | 第三方库(node_modules) | 1958 KiB | 59% |
148
+
149
+ 第三方库按功能域(metafile 实测):
150
+
151
+ | 功能域 | 占用 | 主导库 |
152
+ |---|---:|---|
153
+ | 加密货币/区块链 | 514 KiB | ethers 294 · noble-curves 73 · bn.js 44 · aes-js 42 · ens-normalize 36 |
154
+ | IAP(App Store + Play) | 409 KiB | jsrsasign 299 · @apple SDK 85 · apple-receipt 13 · jsonwebtoken 12 |
155
+ | ArcBlock/DID | 356 KiB | @ocap/client 130 · message 66 · proto 48 · did-connect 38 · mcrypto 31 |
156
+ | 参数校验 | 166 KiB | joi 133 · @sideway/address 21 |
157
+ | 通用工具 | 156 KiB | lodash-es 121 · dayjs 21 |
158
+ | Stripe | 96 KiB | stripe 96 |
159
+ | 框架/适配/其它 | 261 KiB | semver 26 · async 22 · hono 20 · bignumber 19 · qs 16 · ~80 个小库 |
160
+
161
+ ### 6.2 业务代码真实规模(cloc 权威)
162
+
163
+ > ⚠️ 早期口头报的 "99K 行" 是 `xargs | tail` 分批统计只取最后一批的假象,已作废。cloc 全量扫描为准:
164
+
165
+ - **780 个 `.ts` 文件**(排除 test):**147,631 行纯代码** + 10,393 注释(6.6%)+ 13,970 空行 ≈ **172K 物理行**
166
+ - 唯一大数据文件:`token-addresses.json` 2146 行(链上代币地址表,合理)
167
+
168
+ | 目录 | 进 bundle | 内容 |
169
+ |---|---:|---|
170
+ | `routes` | 456 KiB | API 路由(最大头)|
171
+ | `libs` | 360 KiB | 业务逻辑(subscription/invoice/session/quote/coupon/exchange-rate)|
172
+ | `queues` | 173 KiB | 异步队列 |
173
+ | `integrations` | 113 KiB | stripe / ethereum / IAP 三通道对接 |
174
+ | `store` | 107 KiB | 数据模型 |
175
+ | `locales` | 41 KiB | i18n(zh 23 + en 17)|
176
+ | worker.ts + shims | 76 KiB | CF 入口 + 适配垫片 |
177
+ | `crons` | 20 KiB | 定时任务 |
178
+
179
+ ### 6.3 评判:147K 行 / 1.35M 业务代码合理吗?
180
+
181
+ **合理(四条实证)**:
182
+
183
+ | 维度 | 数据 | 判断 |
184
+ |---|---|---|
185
+ | API 广度 | **318 个端点**(171 GET / 61 POST / 57 PUT / 29 DELETE),57 个资源文件 | Stripe 级别的支付 API 面 |
186
+ | 功能域 | checkout/subscription/invoice/credit/refund/coupon/exchange-rate/payment-intents/payouts/webhooks/meter-events/vendor/donate + 3 通道 + 队列 + i18n | 完整多通道计费平台,数十万行量级属常态 |
187
+ | 重复率 | **4.14%**(jscpd,153 克隆/71587 行)| 优秀(<5%),没靠复制堆量 |
188
+ | 杂质 | 无多版本依赖、无功能重复库、无生成代码虚高 | 干净 |
189
+
190
+ **有债(两条,与体积无关)**:
191
+
192
+ 1. **超大文件(结构债)**:21 个文件 >800 行,5 个 >1500 行——`checkout-sessions.ts` **5182 行**、`subscriptions.ts` 3324、`queues/subscription.ts` 2160、`libs/session.ts` 1937。违反单一职责,改动风险高。**代码"显得大"的不适感来自单文件密度,不是总量。**
193
+ 2. **路由层测试缺口(工程债)**:测试在 `api/tests/`(48 个文件,libs 占 24,覆盖好);但 **57 个路由资源 / 318 个端点只有 3 个路由测试文件**,API 契约几乎无回归保护。
194
+
195
+ ### 6.4 全量可优化点清单
196
+
197
+ **A. 体积维度(针对 948K gzip)——已基本到顶**
198
+
199
+ | 优化点 | 量级 | 评级 |
200
+ |---|---|---|
201
+ | node-fetch polyfill 链 | −754K raw | ✅ 已做(本 PR)|
202
+ | ethers BIP39 wordlists | −67K raw | ✅ 已做(本 PR)|
203
+ | joi(133K)→ zod/valibot | ~−100K | ⚠️ 改全部入参校验,大面积回归,不建议 |
204
+ | lodash-es(121K)→ 原生/按需 | 余量小 | ⚠️ esbuild 已 tree-shake |
205
+ | token-addresses.json(37K)运行时外置 | −37K | ⚠️ 微小,引入加载复杂度,不值 |
206
+ | jsrsasign(299K)→ @noble 重写 IAP 验签 | −200K+ | ❌ 资金/安全红线,不碰 |
207
+ | 业务代码 147K 行 | 0 | ❌ 真实逻辑,动不了 |
208
+
209
+ > 体积已到 gzip <1MiB,无更多低风险高收益项。**别再为体积动业务代码或核心库。**
210
+
211
+ **B. 可维护性维度(不减体积,但这才是真正该投入的)**
212
+
213
+ | 优化点 | 范围 | 收益 | 优先级 |
214
+ |---|---|---|---|
215
+ | 拆分超大文件 | checkout-sessions 5182 等 21 个 | 可读性/复杂度/降风险 | 🔴 高 |
216
+ | 补路由层测试 | 57 资源 / 318 端点 | 回归保护 | 🔴 高 |
217
+ | 抽列表查询 `where` helper | 8~11 个 route 的 status/metadata 过滤 | 一致性(改一处生效全部)| 🟡 中 |
218
+ | `settings.ts` `amountSchema` 去重 | 同文件复制 2 次 | 低垂果实 | 🟢 低 |
219
+
220
+ > B 类应**各自独立开 PR**,不与本体积优化 PR 混合。拆超大文件需先有路由测试兜底,建议顺序:补关键路由测试 → 拆 `checkout-sessions.ts` → 抽公共 helper。
221
+
222
+ ### 6.5 重复 / 公共抽取审计
223
+
224
+ - **依赖之间**:无同名多版本(pnpm+esbuild 去重彻底);所谓"重叠"的库各有归属——valibot/jsonwebtoken 是 `@arcblock`/`@apple` 内部依赖(动不了),bignumber.js(汇率小数)与 bn.js(链上整数)用途正交。**无可合并项。**
225
+ - **业务之间**:重复率 4.14%(健康)。可抽的公共逻辑见 6.4-B 的列表查询 helper(扩散到 8~11 个 route)与 settings amountSchema。
226
+
227
+ ---
228
+
229
+ ## 7. 合并前必做:IAP 端到端验证(staging)
230
+
231
+ node-fetch→原生 fetch 和 no-bundle 都改变了底层行为,必须在 staging 验:
232
+
233
+ - **不受影响**(纯本地密码学):App Store StoreKit2 JWS 验签 —— SignedDataVerifier 用本地内置 Apple root 证书(`apple-root-certs.ts` 3 个)+ jsrsasign 验 x5c 证书链,不走网络。
234
+ - **需重点验**(走网络):
235
+ - [ ] App Store legacy receipt(node-apple-receipt-verify → Apple verifyReceipt)
236
+ - [ ] App Store Server API 调用 / Notifications V2 webhook
237
+ - [ ] Google Play verify / RTDN webhook
238
+ - 关注原生 fetch 与 node-fetch 差异(redirect / timeout / AbortSignal)。
239
+ - no-bundle 还需确认 nodejs_compat 覆盖所有 node 内置 import(dist 依赖 crypto/path/events/url/querystring/async_hooks/assert + node:stream/buffer/util 等)。
240
+ - 验证脚本:`scripts/verify-bundle-optimization.sh`。
241
+
242
+ ---
243
+
244
+ ## 8. 方法论:如何判断"某依赖是否被用到 / 能否移除"
245
+
246
+ 1. **是否打包 + 占多少**:esbuild metafile(`dist/meta.json`)聚合 `bytesInOutput`,按 `node_modules/<pkg>` 分组。
247
+ 2. **谁引入的**:metafile importer 反查 + `pnpm why <pkg>`(区分直接依赖 vs 传递依赖)。
248
+ 3. **源码是否真引用**:`grep -rlE "from [\"']<pkg>" api/src cloudflare`(兼容单双引号,别用 sed 转换路径,易误导)。
249
+ 4. **运行时是否真执行**(打包≠执行):环境是否有原生替代?触发条件是否可能满足?alias 成空/原生 shim 重 build,build 过 + 功能验证过 → 证明对运行无贡献。
250
+ 5. **移除省多少**:worktree 隔离(symlink node_modules)+ alias/external + 重 build,两套量法(python / build 自报 / wrangler)交叉验证。
251
+
252
+ ### 复现命令速查
253
+ ```bash
254
+ git fetch origin pull/1381/head:pr1381 # 取 squash PR 子提交
255
+ git worktree add --detach /path/wt <commit> # 隔离 worktree
256
+ ln -sfn $MAIN/node_modules /path/wt/node_modules # symlink 依赖免重装
257
+ ln -sfn $MAIN/blocklets/core/node_modules /path/wt/blocklets/core/node_modules
258
+ cd /path/wt/blocklets/core/cloudflare && node run-build.js
259
+ python3 -c "import gzip;d=open('dist/worker.js','rb').read();print(len(d),len(gzip.compress(d,9)))"
260
+ pnpm why <pkg> # 依赖引入链
261
+ npm view @apple/app-store-server-library version dependencies # 上游是否仍依赖
262
+ npx wrangler deploy --dry-run --minify -c "$PWD/wrangler.local.toml" 2>&1 | grep "Total Upload"
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 9. 数据可信度(教训归档)
268
+
269
+ 本次调查环境后期不稳(Bash 内联输出重复污染/吞空、worktree 偶发空 checkout、Write/mv/rm 偶发假成功、Read 源文件偶发返回幻觉内容),曾产生多个错误数字,**均已被硬证据纠正**,勿引用:
270
+
271
+ | 错误(幻觉/推断) | 正确(实测) |
272
+ |---|---|
273
+ | wrangler「4747.91 / 1059.40 KiB」、「gzip 1.03 MiB」 | 基线 4862.07 / 1185.40;dist gzip 1.23 |
274
+ | 「迁移涨 2M 来自这些依赖」(推断) | 实测基线 2.68,IAP +1.38 |
275
+ | 单测 1486b54f「2.29 MiB」 | 2.68(残留改动污染,全新 worktree 复测) |
276
+ | 「crypto 渠道 cloudflare/src/integrations/crypto 3683 行」 | 真实 api/src/integrations/ethereum ~290 行 |
277
+ | wt-opt「优化后 3.33 MiB」(首次)/「verify 脚本 EXISTS」 | 当时 worktree 空 / 文件未落盘,均幻觉 |
278
+
279
+ **可信判据(务必遵守)**:关键结果重定向到文件再 Read;build 用 mtime+size 双证;多源交叉验证(python / build 自报 / wrangler);逻辑自洽护栏(gzip 不可能比单文件 gzip 更小);写操作后用 bash `wc -l`/`grep` 落盘复验,不盲信工具成功返回;拿不到干净输出就明说,绝不填空。本文数字均按此取得。
280
+
281
+ ---
282
+
283
+ ## 10. 后续待办
284
+
285
+ - [ ] node-fetch + wordlists 两处优化与 payment-ios 改动**分开单独 commit**(都在 `run-build.js` + `shims/`)。
286
+ - [ ] 合并前在 staging 跑 §7 的 IAP 端到端验证,并**顺带验一次 ethereum 链上支付**(确认 RemoteSigner 签名/上链正常,wordlists 改动不影响签名路径)。
287
+ - [ ] 决定 `minify`(最稳,1033)还是 `no-bundle`(进 1MiB,叠加 wordlists 后预计 ~958,需补全验证),并把 `minify = true`/no-bundle 正式写进各 wrangler config(local/dev/staging/jsonc)。
288
+ - [ ] **质量债(与体积无关,各自独立 PR 排期,见 §6.4-B)**:① 补关键路由测试(57 资源/318 端点仅 3 测试)→ ② 拆 `checkout-sessions.ts`(5182 行)等超大文件 → ③ 抽列表查询 `where` helper(8~11 route 重复)。
@@ -0,0 +1,72 @@
1
+ -- Payment Kit: IAP foundation
2
+ -- Adds schema needed for App Store + Google Play subscription channels.
3
+ -- Mirrors blocklets/core/api/src/store/migrations/20260526-iap-foundation.ts.
4
+ --
5
+ -- NOTE: SQLite does not support `ALTER TABLE ADD COLUMN IF NOT EXISTS`.
6
+ -- Wrangler tracks applied migrations in the `d1_migrations` table and skips
7
+ -- re-application by filename, so re-running this file via `wrangler d1
8
+ -- migrations apply` is safe. Do not invoke via `wrangler d1 execute` more
9
+ -- than once on the same database.
10
+
11
+ -- 1. Customer: per-channel UUID for IAP appAccountToken / obfuscatedAccountId mapping (D-004)
12
+ ALTER TABLE customers ADD COLUMN app_store_uuid VARCHAR(36);
13
+ ALTER TABLE customers ADD COLUMN google_play_uuid VARCHAR(36);
14
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_app_store_uuid
15
+ ON customers(app_store_uuid)
16
+ WHERE app_store_uuid IS NOT NULL;
17
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_play_uuid
18
+ ON customers(google_play_uuid)
19
+ WHERE google_play_uuid IS NOT NULL;
20
+
21
+ -- 2. Subscription: channel + environment (D-005)
22
+ ALTER TABLE subscriptions ADD COLUMN channel VARCHAR(20);
23
+ ALTER TABLE subscriptions ADD COLUMN environment VARCHAR(20) DEFAULT 'production';
24
+
25
+ -- 3. Invoice: three-segment amounts (D-001 A)
26
+ ALTER TABLE invoices ADD COLUMN gross_amount VARCHAR(32);
27
+ ALTER TABLE invoices ADD COLUMN platform_fee VARCHAR(32) DEFAULT '0';
28
+ ALTER TABLE invoices ADD COLUMN net_amount VARCHAR(32);
29
+ UPDATE invoices SET gross_amount = total, net_amount = total WHERE gross_amount IS NULL;
30
+
31
+ -- 4. Refund: origin source (merchant_initiated | platform_initiated)
32
+ ALTER TABLE refunds ADD COLUMN source VARCHAR(30) DEFAULT 'merchant_initiated';
33
+
34
+ -- 5. Entitlement tables (D-003 B)
35
+ CREATE TABLE IF NOT EXISTS `entitlements` (
36
+ `id` VARCHAR(30) NOT NULL PRIMARY KEY,
37
+ `livemode` TINYINT(1) NOT NULL,
38
+ `key` VARCHAR(64) NOT NULL UNIQUE,
39
+ `name` VARCHAR(255),
40
+ `description` TEXT,
41
+ `metadata` JSON,
42
+ `created_at` DATETIME NOT NULL,
43
+ `updated_at` DATETIME NOT NULL
44
+ );
45
+ CREATE UNIQUE INDEX IF NOT EXISTS `idx_entitlements_key` ON `entitlements` (`key`);
46
+
47
+ CREATE TABLE IF NOT EXISTS `entitlement_products` (
48
+ `entitlement_id` VARCHAR(30) NOT NULL,
49
+ `product_id` VARCHAR(30) NOT NULL,
50
+ `created_at` DATETIME NOT NULL,
51
+ `updated_at` DATETIME NOT NULL,
52
+ PRIMARY KEY (`entitlement_id`, `product_id`)
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS `entitlement_grants` (
56
+ `id` VARCHAR(30) NOT NULL PRIMARY KEY,
57
+ `livemode` TINYINT(1) NOT NULL,
58
+ `entitlement_id` VARCHAR(30) NOT NULL,
59
+ `customer_id` VARCHAR(18) NOT NULL,
60
+ `source_subscription_id` VARCHAR(30),
61
+ `source_channel` VARCHAR(20) NOT NULL,
62
+ `active_from` INTEGER NOT NULL,
63
+ `active_until` INTEGER NOT NULL,
64
+ `status` VARCHAR(20) NOT NULL DEFAULT 'active',
65
+ `metadata` JSON,
66
+ `created_at` DATETIME NOT NULL,
67
+ `updated_at` DATETIME NOT NULL
68
+ );
69
+ CREATE INDEX IF NOT EXISTS `idx_entitlement_grants_lookup`
70
+ ON `entitlement_grants` (`customer_id`, `entitlement_id`, `status`);
71
+ CREATE INDEX IF NOT EXISTS `idx_entitlement_grants_source_sub`
72
+ ON `entitlement_grants` (`source_subscription_id`);
@@ -0,0 +1,112 @@
1
+ -- Payment Kit: IAP multi-tenant backfill
2
+ -- Backfills metadata.bundle_id / metadata.package_name on existing Prices and
3
+ -- payment_details.app_store.bundle_id / payment_details.google_play.package_name
4
+ -- on existing Subscriptions, so Payment Kit can be wired into multiple iOS /
5
+ -- Android apps without same-SKU collisions across App Store / Play Console
6
+ -- namespaces (each store's SKU space is per-app, not global).
7
+ --
8
+ -- Backend code already filters Price.findOne by (sku, bundle_id) / (sku,
9
+ -- package_name); without this backfill, every pre-existing Price would stop
10
+ -- resolving the moment the new lookup ships.
11
+ --
12
+ -- Safety: tenant value is DERIVED from the configured PaymentMethods (which
13
+ -- store bundle_id / package_name as plain text under settings JSON — only
14
+ -- private keys are encrypted). The migration deliberately refuses to guess in
15
+ -- ambiguous setups:
16
+ --
17
+ -- * For Subscriptions we always use the sub's own
18
+ -- `default_payment_method_id` to resolve the tenant, which is a 1:1 map —
19
+ -- never ambiguous as long as the row points at a real PaymentMethod.
20
+ -- * For Prices we update only when EXACTLY ONE active PaymentMethod of the
21
+ -- matching type + livemode exists with a non-null tenant. Multi-tenant
22
+ -- installations (two iOS apps sharing one Payment Kit, etc.) skip the
23
+ -- Price backfill — admin must set bundle_id / package_name explicitly
24
+ -- because the migration can't safely guess which app a Price belongs to.
25
+ --
26
+ -- Idempotent. The IS NULL guards make re-runs a no-op for already-backfilled
27
+ -- rows, and the subquery filters skip rows that can't be resolved safely.
28
+
29
+ -- 1. Prices with App Store SKU → set bundle_id (only when one active
30
+ -- app_store PaymentMethod for the same livemode unambiguously identifies
31
+ -- the tenant).
32
+ UPDATE prices
33
+ SET metadata = json_set(
34
+ metadata,
35
+ '$.bundle_id',
36
+ (SELECT json_extract(pm.settings, '$.app_store.bundle_id')
37
+ FROM payment_methods pm
38
+ WHERE pm.type = 'app_store'
39
+ AND pm.livemode = prices.livemode
40
+ AND pm.active = 1
41
+ AND json_extract(pm.settings, '$.app_store.bundle_id') IS NOT NULL
42
+ LIMIT 1)
43
+ )
44
+ WHERE json_extract(metadata, '$.app_store_product_id') IS NOT NULL
45
+ AND json_extract(metadata, '$.bundle_id') IS NULL
46
+ AND (
47
+ SELECT COUNT(*) FROM payment_methods pm
48
+ WHERE pm.type = 'app_store'
49
+ AND pm.livemode = prices.livemode
50
+ AND pm.active = 1
51
+ AND json_extract(pm.settings, '$.app_store.bundle_id') IS NOT NULL
52
+ ) = 1;
53
+
54
+ -- 2. Prices with Google Play SKU → set package_name (same single-tenant guard).
55
+ UPDATE prices
56
+ SET metadata = json_set(
57
+ metadata,
58
+ '$.package_name',
59
+ (SELECT json_extract(pm.settings, '$.google_play.package_name')
60
+ FROM payment_methods pm
61
+ WHERE pm.type = 'google_play'
62
+ AND pm.livemode = prices.livemode
63
+ AND pm.active = 1
64
+ AND json_extract(pm.settings, '$.google_play.package_name') IS NOT NULL
65
+ LIMIT 1)
66
+ )
67
+ WHERE json_extract(metadata, '$.google_play_product_id') IS NOT NULL
68
+ AND json_extract(metadata, '$.package_name') IS NULL
69
+ AND (
70
+ SELECT COUNT(*) FROM payment_methods pm
71
+ WHERE pm.type = 'google_play'
72
+ AND pm.livemode = prices.livemode
73
+ AND pm.active = 1
74
+ AND json_extract(pm.settings, '$.google_play.package_name') IS NOT NULL
75
+ ) = 1;
76
+
77
+ -- 3. App Store Subscriptions → set payment_details.app_store.bundle_id from
78
+ -- the sub's own default_payment_method (1:1 — always safe).
79
+ UPDATE subscriptions
80
+ SET payment_details = json_set(
81
+ payment_details,
82
+ '$.app_store.bundle_id',
83
+ (SELECT json_extract(pm.settings, '$.app_store.bundle_id')
84
+ FROM payment_methods pm
85
+ WHERE pm.id = subscriptions.default_payment_method_id)
86
+ )
87
+ WHERE channel = 'app_store'
88
+ AND json_extract(payment_details, '$.app_store.bundle_id') IS NULL
89
+ AND default_payment_method_id IS NOT NULL
90
+ AND (
91
+ SELECT json_extract(pm.settings, '$.app_store.bundle_id')
92
+ FROM payment_methods pm
93
+ WHERE pm.id = subscriptions.default_payment_method_id
94
+ ) IS NOT NULL;
95
+
96
+ -- 4. Google Play Subscriptions → set payment_details.google_play.package_name.
97
+ UPDATE subscriptions
98
+ SET payment_details = json_set(
99
+ payment_details,
100
+ '$.google_play.package_name',
101
+ (SELECT json_extract(pm.settings, '$.google_play.package_name')
102
+ FROM payment_methods pm
103
+ WHERE pm.id = subscriptions.default_payment_method_id)
104
+ )
105
+ WHERE channel = 'google_play'
106
+ AND json_extract(payment_details, '$.google_play.package_name') IS NULL
107
+ AND default_payment_method_id IS NOT NULL
108
+ AND (
109
+ SELECT json_extract(pm.settings, '$.google_play.package_name')
110
+ FROM payment_methods pm
111
+ WHERE pm.id = subscriptions.default_payment_method_id
112
+ ) IS NOT NULL;
@@ -216,13 +216,30 @@ const noopPackagesPlugin = {
216
216
  },
217
217
  };
218
218
 
219
+ // Plugin: drop ethers' non-English BIP39 wordlists (~70K dead weight). The payment
220
+ // worker never uses Mnemonic/HD wallets (0 source refs to Mnemonic/HDNode/wordlist),
221
+ // so the 8 non-English word tables never execute. ethers' own /dist build strips
222
+ // these too (~80kb). Keep LangEn (Mnemonic default). Stub the rest with a static
223
+ // wordlist() returning null so wordlists.js's top-level LangXx.wordlist() calls
224
+ // (run at module init) don't crash.
225
+ const dropEthersWordlistsPlugin = {
226
+ name: 'drop-ethers-wordlists',
227
+ setup(build) {
228
+ build.onLoad({ filter: /ethers\/lib\.esm\/wordlists\/lang-(cz|es|fr|ja|ko|it|pt|zh)\.js$/ }, (args) => {
229
+ const lang = /lang-(\w+)\.js$/.exec(args.path)[1];
230
+ const cls = 'Lang' + lang.charAt(0).toUpperCase() + lang.slice(1);
231
+ return { contents: `export class ${cls} { static wordlist() { return null; } }`, loader: 'js' };
232
+ });
233
+ },
234
+ };
235
+
219
236
  build({
220
237
  entryPoints: [s("worker.ts")],
221
238
  bundle: true, format: "esm", platform: "node", target: "esnext",
222
239
  outdir: s("dist"), minify: true, sourcemap: true, metafile: true,
223
240
  mainFields: ["module", "main"],
224
241
  plugins: [
225
- noopPackagesPlugin, queueShimPlugin, lockShimPlugin, rolldownRuntimeNoopPlugin, lodashSubpathPlugin,
242
+ noopPackagesPlugin, queueShimPlugin, lockShimPlugin, rolldownRuntimeNoopPlugin, lodashSubpathPlugin, dropEthersWordlistsPlugin,
226
243
  ],
227
244
  external: ["cloudflare:*", "__STATIC_CONTENT_MANIFEST"],
228
245
  // Give import.meta.url a stable fallback so bundled deps that call
@@ -265,6 +282,10 @@ build({
265
282
  // axios → lightweight fetch-based shim (115KB → ~2KB)
266
283
  "axios": s("shims/axios-lite.ts"),
267
284
 
285
+ // node-fetch → native fetch (drops encoding/tr46/whatwg-url polyfill ~754KB
286
+ // pulled in by @apple/app-store-server-library; dead weight on CF Workers)
287
+ "node-fetch": s("shims/node-fetch.ts"),
288
+
268
289
  // Stripe — wrap constructor to use fetch HTTP client in CF Workers
269
290
  "stripe": s("shims/stripe-cf.ts"),
270
291
  "__real_stripe__": require.resolve("stripe"),
@@ -297,6 +318,7 @@ build({
297
318
  "@blocklet/sdk/lib/wallet-handler": s("shims/blocklet-sdk/wallet-handler.ts"),
298
319
  "@blocklet/sdk/lib/security": s("shims/blocklet-sdk/security.ts"),
299
320
  "@blocklet/sdk/lib/util/verify-sign": s("shims/blocklet-sdk/verify-sign.ts"),
321
+ "@blocklet/sdk/lib/util/verify-session": s("shims/blocklet-sdk/verify-session.ts"),
300
322
  "@blocklet/sdk/lib/util/component-api": s("shims/blocklet-sdk/component-api.ts"),
301
323
  "@blocklet/sdk/lib/error-handler": s("shims/noop.ts"),
302
324
  "@blocklet/sdk/lib/did": s("shims/blocklet-sdk/did.ts"),
@@ -0,0 +1,44 @@
1
+ // CF Workers no-op shim for @blocklet/sdk/lib/util/verify-session.
2
+ //
3
+ // In CF Workers, Bearer / cookie validation happens *before* Express routes
4
+ // run — see worker.ts auth middleware at /api/*, which calls
5
+ // `c.env.AUTH_SERVICE.resolveIdentity(...)` and injects x-user-did into the
6
+ // request headers when the token is valid. By the time security.ts'
7
+ // `authenticate` middleware sees the request, the x-user-did branch handles
8
+ // it. The fallback Bearer-validation path that calls verifyLoginToken
9
+ // directly is only needed in the Express dev server, where the tunnel
10
+ // bypasses Blocklet Server's nginx and we can't rely on header injection.
11
+ //
12
+ // So in Workers we return null — security.ts treats null as "couldn't
13
+ // validate locally, fall through" — and the x-user-did set by the worker
14
+ // layer is the source of truth.
15
+
16
+ export type SessionUser = {
17
+ did: string;
18
+ role?: string;
19
+ fullName?: string;
20
+ provider?: string;
21
+ walletOS?: string;
22
+ method?: string;
23
+ org?: string;
24
+ };
25
+
26
+ export async function verifyLoginToken(_opts: { token: string; strictMode?: boolean }): Promise<SessionUser | null> {
27
+ return null;
28
+ }
29
+
30
+ export async function verifyAccessKey(_opts: { token: string; strictMode?: boolean }): Promise<SessionUser | null> {
31
+ return null;
32
+ }
33
+
34
+ export async function verifyComponentCall(_opts: { req: any; strictMode?: boolean }): Promise<SessionUser | null> {
35
+ return null;
36
+ }
37
+
38
+ export async function verifySignedToken(_opts: { token: string; strictMode?: boolean }): Promise<SessionUser | null> {
39
+ return null;
40
+ }
41
+
42
+ export function getSessionSecret(): string {
43
+ return '';
44
+ }
@@ -0,0 +1,35 @@
1
+ // node-fetch shim for CF Workers — forwards to the runtime's native fetch.
2
+ // node-fetch is pulled in transitively by @apple/app-store-server-library (IAP
3
+ // JWS verification). On Node it drags in a polyfill chain that is dead weight on
4
+ // Workers (which has native fetch/URL/TextDecoder):
5
+ // encoding (iconv-lite CJK code tables) 481KB + tr46 (IDNA map) 259KB
6
+ // + whatwg-url 22KB + node-fetch 19KB ≈ 781KB, never executed at runtime.
7
+ // Forwarding node-fetch → native fetch drops that whole chain (~754KB raw).
8
+ // Only implements the node-fetch v2 export surface actually referenced.
9
+
10
+ export default globalThis.fetch.bind(globalThis);
11
+
12
+ export const Headers = globalThis.Headers;
13
+ export const Request = globalThis.Request;
14
+ export const Response = globalThis.Response;
15
+ export const FormData = globalThis.FormData;
16
+ export const Blob = globalThis.Blob;
17
+
18
+ export class FetchError extends Error {
19
+ type: string;
20
+ constructor(message: string, type = 'system') {
21
+ super(message);
22
+ this.name = 'FetchError';
23
+ this.type = type;
24
+ }
25
+ }
26
+
27
+ export class AbortError extends Error {
28
+ type = 'aborted';
29
+ constructor(message: string) {
30
+ super(message);
31
+ this.name = 'AbortError';
32
+ }
33
+ }
34
+
35
+ export const isRedirect = (code: number) => [301, 302, 303, 307, 308].includes(code);
@@ -408,8 +408,34 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
408
408
  if (_cfQueue) {
409
409
  try {
410
410
  await sendToCFQueue(jobId, job);
411
- } catch (_err: any) {
412
- // CF Queue unavailable job is safe in D1, cron will dispatch
411
+ } catch (err: any) {
412
+ // CF Queue unavailable (429 Too Many Requests, transport down, etc.)
413
+ // Fall through to inline execution: the worker context already has
414
+ // its own CPU budget (HTTP handler: 30s; scheduled handler: 30s),
415
+ // and a tightly-throttled CF Queue + a backed-up cron dispatcher
416
+ // means immediate jobs would otherwise sit in D1 indefinitely.
417
+ // The D1 row persists either way, so a crash here still lets a
418
+ // later cron tick (or a retry) pick it up.
419
+ console.warn(`[queue:${name}] CF Queue send failed (${err?.message || err}); executing inline`);
420
+ try {
421
+ // Pass persist=false so executeJob does NOT delete the D1 row on a
422
+ // terminal failure — that would wipe the only durable backup (esp.
423
+ // for maxRetries:0 queues like webhookQueue) and the cron dispatcher
424
+ // could never retry it. Delete the row ONLY on success here.
425
+ // (PR #1381 re-review P1.)
426
+ const data = await executeJob(jobId, job, false);
427
+ if (persist) {
428
+ try {
429
+ await store.deleteJob(jobId);
430
+ } catch (_e) {
431
+ /* ignore — duplicate delete is harmless */
432
+ }
433
+ }
434
+ emit('finished', data);
435
+ } catch (execErr: any) {
436
+ // D1 row intact → a later cron tick / retry can pick it up.
437
+ emit('failed', { id: jobId, job, error: execErr });
438
+ }
413
439
  }
414
440
  } else {
415
441
  // No CF Queue binding — execute inline (Blocklet Server compatibility)