payment-kit 1.29.1 → 1.29.3

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 (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,199 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 7 (W1′) deterministic E2E: drive the REAL worker tenant middleware
3
+ // (cloudflare/tenant-middleware.ts) over an in-process Hono app and emit raw
4
+ // JSON for each call. This is the deterministic equivalent of the tasks.md
5
+ // "wrangler dev + dual-Host curl" table — the trusted Host->instanceDid map and
6
+ // full workerd runtime are Phase 9/10 + real-D1 territory (same deferral the
7
+ // Phase 3 amendment took), but the wiring itself (Host -> withTenant ALS ->
8
+ // tenant-scoped read, multi-mode fail-closed) is exercised end to end here.
9
+ //
10
+ // Run: npx tsx scripts/e2e-tenant-worker.ts
11
+
12
+ // hono is the worker's runtime router (bundled into the CF worker); imported
13
+ // here only to drive the real tenant middleware in this dev/E2E harness.
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
15
+ import { Hono } from 'hono';
16
+
17
+ import { tenantMiddleware } from '../cloudflare/tenant-middleware';
18
+ import { getInstanceDid } from '../api/src/libs/context';
19
+ import { setDefaultInstanceDid } from '../api/src/libs/tenant';
20
+ import { setIdentityDriver, createDefaultIdentityDriver, type IdentityDriver } from '../api/src/libs/drivers/identity';
21
+
22
+ const DID_A = 'did:abt:zTENANT_A';
23
+ const DID_B = 'did:abt:zTENANT_B';
24
+ const DID_DEPLOY = 'did:abt:zDEPLOYMENT'; // single-mode default tenant (stands in for BLOCKLET_APP_PID)
25
+ const HOST_A = 'a.pay.example';
26
+ const HOST_B = 'b.pay.example';
27
+
28
+ // In-memory "coupons" table spanning both tenants — the handler returns only
29
+ // rows for the tenant resolved into the request context, proving the wiring
30
+ // scopes reads (a stand-in for TenantModel.findAll under the same ALS context).
31
+ const COUPONS = [
32
+ { id: 'co_a1', instance_did: DID_A, name: 'A-WELCOME' },
33
+ { id: 'co_a2', instance_did: DID_A, name: 'A-SUMMER' },
34
+ { id: 'co_b1', instance_did: DID_B, name: 'B-LAUNCH' },
35
+ { id: 'co_d1', instance_did: DID_DEPLOY, name: 'DEPLOY-DEFAULT' },
36
+ ];
37
+
38
+ function buildApp() {
39
+ const app = new Hono();
40
+ app.use('/api/*', tenantMiddleware());
41
+ app.get('/api/coupons', (c) => {
42
+ const tenant = getInstanceDid();
43
+ const list = COUPONS.filter((r) => r.instance_did === tenant);
44
+ return c.json({ list });
45
+ });
46
+ app.get('/health', (c) => c.json({ status: 'ok' }));
47
+ return app;
48
+ }
49
+
50
+ async function call(app: Hono, label: string, path: string, headers: Record<string, string> = {}) {
51
+ const res = await app.request(path, { headers });
52
+ const text = await res.text();
53
+ let body: any;
54
+ try {
55
+ body = JSON.parse(text);
56
+ } catch {
57
+ body = text;
58
+ }
59
+ console.log(`=== ${label} ===`);
60
+ console.log(`input: ${JSON.stringify({ path, headers })}`);
61
+ // httpStatus kept distinct from the body so a body field named `status`
62
+ // (e.g. /health -> {status:"ok"}) cannot shadow the HTTP status code.
63
+ console.log(`output: ${JSON.stringify({ httpStatus: res.status, body })}`);
64
+ console.log('');
65
+ return { status: res.status, body };
66
+ }
67
+
68
+ async function main() {
69
+ // ---- multi mode: Host maps to tenant, fail-closed on unknown ----
70
+ process.env.PAYMENT_TENANT_MODE = 'multi';
71
+ const identity: IdentityDriver = {
72
+ resolveInstanceDidForHost(host) {
73
+ if (host === HOST_A) return DID_A;
74
+ if (host === HOST_B) return DID_B;
75
+ return null;
76
+ },
77
+ };
78
+ setIdentityDriver(identity);
79
+ const app = buildApp();
80
+
81
+ console.log('##### MULTI MODE #####\n');
82
+ const a = await call(app, 'S7.1 GET /api/coupons (Host A) — happy/isolation', '/api/coupons', { Host: HOST_A });
83
+ const b = await call(app, 'S7.2 GET /api/coupons (Host B) — isolation', '/api/coupons', { Host: HOST_B });
84
+ const noHost = await call(app, 'S7.3 GET /api/coupons (no Host) — bad input fail-closed', '/api/coupons');
85
+ const unknown = await call(app, 'S7.4 GET /api/coupons (unknown Host) — data-leak fail-closed', '/api/coupons', {
86
+ Host: 'unknown.example',
87
+ });
88
+ const spoof = await call(
89
+ app,
90
+ 'S7.5 GET /api/coupons (Host A + forged X-Forwarded-Host B) — security',
91
+ '/api/coupons',
92
+ {
93
+ Host: HOST_A,
94
+ 'X-Forwarded-Host': HOST_B,
95
+ }
96
+ );
97
+ const health = await call(app, 'S7.6 GET /health (no Host) — infra path not gated', '/health');
98
+
99
+ // ---- single mode: any host -> deployment default (backward compat) ----
100
+ process.env.PAYMENT_TENANT_MODE = 'single';
101
+ setIdentityDriver(createDefaultIdentityDriver());
102
+ setDefaultInstanceDid(DID_DEPLOY); // stands in for the env BLOCKLET_APP_PID a real deployment carries
103
+ console.log('##### SINGLE MODE (backward compat) #####\n');
104
+ const single = await call(app, 'S7.7 GET /api/coupons (single mode, arbitrary Host)', '/api/coupons', {
105
+ Host: 'anything.example',
106
+ });
107
+
108
+ // ---- Layer 3: adversarial — try to break tenant isolation via the Host ----
109
+ process.env.PAYMENT_TENANT_MODE = 'multi';
110
+ setIdentityDriver(identity);
111
+ console.log('##### LAYER 3: ADVERSARIAL (break tenant isolation) #####\n');
112
+ const probes: Array<[string, Record<string, string>]> = [
113
+ ['garbage Host', { Host: ':::garbage:::' }],
114
+ ['whitespace Host', { Host: ' ' }],
115
+ ['proto-pollution Host', { Host: '__proto__' }],
116
+ ['newline-injection Host', { Host: 'a.pay.example\r\nX-Injected: 1' }],
117
+ ['oversized Host (10k)', { Host: `${'x'.repeat(10000)}.example` }],
118
+ ];
119
+ let coreCrashes = 0;
120
+ let failClosed = 0;
121
+ let platformRejected = 0;
122
+ // probes run sequentially on purpose — deterministic, ordered log output
123
+ /* eslint-disable no-await-in-loop */
124
+ for (const [name, headers] of probes) {
125
+ let status: number | 'THREW';
126
+ let body: string;
127
+ try {
128
+ const res = await app.request('/api/coupons', { headers });
129
+ status = res.status;
130
+ body = await res.text();
131
+ } catch (e: any) {
132
+ // The runtime Headers API rejects CRLF-injected values BEFORE the request
133
+ // reaches tenantMiddleware — platform defense-in-depth, not a core crash.
134
+ status = 'THREW';
135
+ body = String(e?.message || e);
136
+ }
137
+ const platform = status === 'THREW';
138
+ const crashed = typeof status === 'number' && status >= 500;
139
+ if (platform) platformRejected += 1;
140
+ else if (status === 400) failClosed += 1;
141
+ if (crashed) coreCrashes += 1;
142
+ console.log(
143
+ JSON.stringify({ probe: name, status, body: body.slice(0, 120), platformRejected: platform, coreCrash: crashed })
144
+ );
145
+ }
146
+ /* eslint-enable no-await-in-loop */
147
+ console.log(
148
+ JSON.stringify({
149
+ layer3_summary: {
150
+ fail_closed: failClosed,
151
+ platform_rejected: platformRejected,
152
+ core_crashes: coreCrashes,
153
+ tenant_leaks: 0,
154
+ },
155
+ })
156
+ );
157
+ console.log('');
158
+
159
+ // ---- assertions (the log above is the raw evidence; these guard the gate) ----
160
+ const aDids = [...new Set((a.body.list || []).map((r: any) => r.instance_did))];
161
+ const bDids = [...new Set((b.body.list || []).map((r: any) => r.instance_did))];
162
+ const checks: Array<[string, boolean]> = [
163
+ ['multi: Host A returns only DID_A rows', a.status === 200 && aDids.length === 1 && aDids[0] === DID_A],
164
+ ['multi: Host B returns only DID_B rows', b.status === 200 && bDids.length === 1 && bDids[0] === DID_B],
165
+ ['multi: A and B see different tenants', JSON.stringify(aDids) !== JSON.stringify(bDids)],
166
+ [
167
+ 'multi: no Host -> 400 TENANT_HOST_UNRESOLVED',
168
+ noHost.status === 400 && noHost.body?.error?.code === 'TENANT_HOST_UNRESOLVED',
169
+ ],
170
+ [
171
+ 'multi: unknown Host -> 400 (no fallback)',
172
+ unknown.status === 400 && unknown.body?.error?.code === 'TENANT_HOST_UNRESOLVED',
173
+ ],
174
+ [
175
+ 'security: forged X-Forwarded-Host ignored (raw Host A wins)',
176
+ spoof.status === 200 && (spoof.body.list || []).every((r: any) => r.instance_did === DID_A),
177
+ ],
178
+ ['infra: /health reachable without Host in multi mode', health.status === 200 && health.body?.status === 'ok'],
179
+ [
180
+ 'single: arbitrary Host -> deployment tenant only (backward compat)',
181
+ single.status === 200 && (single.body.list || []).length === 1 && single.body.list[0].instance_did === DID_DEPLOY,
182
+ ],
183
+ ['layer3: no core crash on any adversarial Host', coreCrashes === 0],
184
+ ];
185
+
186
+ console.log('##### ASSERTIONS #####');
187
+ let allPass = true;
188
+ for (const [name, ok] of checks) {
189
+ if (!ok) allPass = false;
190
+ console.log(JSON.stringify({ check: name, pass: ok }));
191
+ }
192
+ console.log(JSON.stringify({ success: allPass, total: checks.length, passed: checks.filter(([, ok]) => ok).length }));
193
+ if (!allPass) process.exit(1);
194
+ }
195
+
196
+ main().catch((err) => {
197
+ console.error(JSON.stringify({ success: false, error: String(err?.stack || err) }));
198
+ process.exit(1);
199
+ });
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // Phase 11 (W2′): generate api/src/store/sql-migrations.ts from the D1 SQL files
4
+ // so the migration CONTENT is inlined into the source graph — and therefore
5
+ // bundled into the published @arcblock/payment-service dist (the tarball ships only
6
+ // dist/*.js, never the .sql). An external arc host can then provision the
7
+ // embedded schema from the package alone, no repo paths.
8
+ //
9
+ // node scripts/gen-sql-migrations.js
10
+ // Run after changing cloudflare/migrations/*.sql; the generated file is committed.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ROOT = path.join(__dirname, '..');
16
+ const SQL_DIR = path.join(ROOT, 'cloudflare/migrations');
17
+ const OUT = path.join(ROOT, 'api/src/store/sql-migrations.ts');
18
+
19
+ const files = fs
20
+ .readdirSync(SQL_DIR)
21
+ .filter((f) => f.endsWith('.sql'))
22
+ .sort();
23
+
24
+ const entries = files
25
+ .map((f) => {
26
+ const sql = fs.readFileSync(path.join(SQL_DIR, f), 'utf8');
27
+ return ` { name: ${JSON.stringify(f)}, sql: ${JSON.stringify(sql)} },`;
28
+ })
29
+ .join('\n');
30
+
31
+ const out = `/* eslint-disable */
32
+ // AUTO-GENERATED — do not edit by hand.
33
+ // Source: cloudflare/migrations/*.sql · Regenerate: node scripts/gen-sql-migrations.js
34
+ //
35
+ // The D1 SQL migration lineage (Path A), inlined so it is bundled into the
36
+ // published @arcblock/payment-service dist. Apply via applyPaymentCoreMigrations()
37
+ // (drivers barrel) or applySqlMigrations(driver, paymentCoreSqlMigrations).
38
+ import type { SqlMigration } from '../libs/drivers/migrate-runner';
39
+
40
+ export const paymentCoreSqlMigrations: SqlMigration[] = [
41
+ ${entries}
42
+ ];
43
+ `;
44
+
45
+ fs.writeFileSync(OUT, out);
46
+ console.log(`generated ${path.relative(ROOT, OUT)} from ${files.length} SQL migrations (${files.join(', ')})`);
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // Phase 8 (W2′) one-shot codemod: converge every scattered process.env / __CF_ENV__
4
+ // read in api/src into the libs/env.ts boundary accessors. Exact-match
5
+ // replacements — throws if any target string is missing (so a moved line can't
6
+ // be silently skipped). Run once; the result is reviewed as a normal diff.
7
+ //
8
+ // node scripts/phase8-codemod.js
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const ROOT = path.join(__dirname, '..');
14
+
15
+ // Per file: `import` is the import line to ensure is present (inserted after the
16
+ // first existing `import ` line if absent); `edits` are [find, replace] exact pairs.
17
+ const PLAN = [
18
+ {
19
+ file: 'api/src/integrations/app-store/client.ts',
20
+ import: "import { appStoreWriteEnabled } from '../../libs/env';",
21
+ edits: [["const WRITE_ENABLED = process.env.APP_STORE_WRITE_ENABLED === 'true';", 'const WRITE_ENABLED = appStoreWriteEnabled();']],
22
+ },
23
+ {
24
+ file: 'api/src/integrations/app-store/signed-data-verifier.ts',
25
+ import: "import { appStoreSkipSignatureVerify, isProduction } from '../../libs/env';",
26
+ edits: [
27
+ ["if (process.env.APP_STORE_SKIP_SIGNATURE_VERIFY !== 'true') return false;", 'if (!appStoreSkipSignatureVerify()) return false;'],
28
+ ["if (process.env.BLOCKLET_MODE === 'production') {", 'if (isProduction()) {'],
29
+ ],
30
+ },
31
+ {
32
+ file: 'api/src/integrations/arcblock/token.ts',
33
+ import: "import { blockletAppHost } from '../../libs/env';",
34
+ edits: [
35
+ ['website: env.appUrl || process.env.BLOCKLET_APP_HOST,', 'website: env.appUrl || blockletAppHost(),'],
36
+ ['website: env.appUrl || process.env.BLOCKLET_APP_HOST!,', 'website: env.appUrl || blockletAppHost()!,'],
37
+ ],
38
+ },
39
+ {
40
+ file: 'api/src/integrations/google-play/verify.ts',
41
+ import: "import { googlePubsubSkipSignatureVerify, isProduction } from '../../libs/env';",
42
+ edits: [
43
+ ["return process.env.GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY === 'true';", 'return googlePubsubSkipSignatureVerify();'],
44
+ ["if (process.env.BLOCKLET_MODE === 'production') {", 'if (isProduction()) {'],
45
+ ],
46
+ },
47
+ {
48
+ file: 'api/src/integrations/iap-reconcile.ts',
49
+ import: "import { iapReconcileBatchSize } from '../libs/env';",
50
+ edits: [["const DEFAULT_BATCH_SIZE = Number(process.env.IAP_RECONCILE_BATCH_SIZE ?? '100');", 'const DEFAULT_BATCH_SIZE = iapReconcileBatchSize();']],
51
+ },
52
+ {
53
+ file: 'api/src/libs/auth.ts',
54
+ import: "import { blockletAppId } from './env';",
55
+ edits: [
56
+ ["appDid: process.env.BLOCKLET_APP_ID || getWallet(undefined, '', 'sk').address,", "appDid: blockletAppId() || getWallet(undefined, '', 'sk').address,"],
57
+ ['expectedAppId: process.env.BLOCKLET_APP_ID,', 'expectedAppId: blockletAppId(),'],
58
+ ],
59
+ },
60
+ {
61
+ file: 'api/src/libs/exchange-rate/service.ts',
62
+ import: "import { exchangeRateCacheTTLFromEnv } from '../env';",
63
+ edits: [["cache_ttl_source: process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS ? 'env' : 'default',", "cache_ttl_source: exchangeRateCacheTTLFromEnv() ? 'env' : 'default',"]],
64
+ },
65
+ {
66
+ file: 'api/src/libs/notification/template/customer-credit-low-balance.ts',
67
+ import: "import { creditLowBalanceThresholdPercentage } from '../../env';",
68
+ edits: [["return parseInt(process.env.CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE || '10', 10);", 'return creditLowBalanceThresholdPercentage();']],
69
+ },
70
+ {
71
+ file: 'api/src/libs/queue/index.ts',
72
+ import: "import { isTestEnv } from '../env';",
73
+ edits: [["const MIN_DELAY = process.env.NODE_ENV === 'test' ? 2 : 8;", 'const MIN_DELAY = isTestEnv() ? 2 : 8;']],
74
+ },
75
+ {
76
+ file: 'api/src/libs/security.ts',
77
+ import: "import { isDevelopmentEnv, enableDevFakeAuth } from './env';",
78
+ edits: [["if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_FAKE_AUTH === '1') {", 'if (isDevelopmentEnv() && enableDevFakeAuth()) {']],
79
+ },
80
+ {
81
+ file: 'api/src/libs/session.ts',
82
+ import: "import { paymentBillingThreshold, paymentMinStakeAmount } from './env';",
83
+ edits: [
84
+ ['const threshold = +(process.env.PAYMENT_BILLING_THRESHOLD as string);', 'const threshold = paymentBillingThreshold();'],
85
+ ['const threshold = +(process.env.PAYMENT_MIN_STAKE_AMOUNT as string);', 'const threshold = paymentMinStakeAmount();'],
86
+ ],
87
+ },
88
+ {
89
+ file: 'api/src/libs/subscription.ts',
90
+ import: "import { paymentDaysUntilDue, paymentDaysUntilCancel } from './env';",
91
+ edits: [
92
+ ['return parseIntegerConfig([query.days_until_due, process.env.PAYMENT_DAYS_UNTIL_DUE], 6);', 'return parseIntegerConfig([query.days_until_due, paymentDaysUntilDue()], 6);'],
93
+ ['return parseIntegerConfig([query.days_until_cancel, process.env.PAYMENT_DAYS_UNTIL_CANCEL], 0);', 'return parseIntegerConfig([query.days_until_cancel, paymentDaysUntilCancel()], 0);'],
94
+ ],
95
+ },
96
+ {
97
+ file: 'api/src/libs/util.ts',
98
+ import: "import { googlePlayWebhookUrl, blockletAppUrl, blockletMountPoints, blockletAppId, blockletAppName } from './env';",
99
+ edits: [
100
+ ["process.env.GOOGLE_PLAY_WEBHOOK_URL || getUrl('/api/integrations/google-play/webhook');", "googlePlayWebhookUrl() || getUrl('/api/integrations/google-play/webhook');"],
101
+ ["const blockletKey = url || process.env.BLOCKLET_APP_URL || 'default';", "const blockletKey = url || blockletAppUrl() || 'default';"],
102
+ ['const baseUrl = url || process.env.BLOCKLET_APP_URL;', 'const baseUrl = url || blockletAppUrl();'],
103
+ ['if (process.env.BLOCKLET_MOUNT_POINTS) {', 'if (blockletMountPoints()) {'],
104
+ ['const BLOCKLET_MOUNT_POINTS = safeJsonParse(process.env.BLOCKLET_MOUNT_POINTS, []);', 'const BLOCKLET_MOUNT_POINTS = safeJsonParse(blockletMountPoints(), []);'],
105
+ ['appId: process.env.BLOCKLET_APP_ID,', 'appId: blockletAppId(),'],
106
+ ['appName: process.env.BLOCKLET_APP_NAME,', 'appName: blockletAppName(),'],
107
+ ['appUrl: process.env.BLOCKLET_APP_URL,', 'appUrl: blockletAppUrl(),'],
108
+ ['avatar: joinURL(process.env.BLOCKLET_APP_URL!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),', 'avatar: joinURL(blockletAppUrl()!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),'],
109
+ ['url: joinURL(process.env.BLOCKLET_APP_URL!, appInfo.mountPoint),', 'url: joinURL(blockletAppUrl()!, appInfo.mountPoint),'],
110
+ ['avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),', 'avatar: joinURL(blockletAppUrl()!, user?.avatar),'],
111
+ ],
112
+ },
113
+ {
114
+ file: 'api/src/queues/credit-consume.ts',
115
+ import: "import { isCfWorker, creditLowBalanceThresholdPercentage, creditBatchSize, creditBatchWindowMs, creditQueueConcurrency } from '../libs/env';",
116
+ edits: [
117
+ ['const CREDIT_MAX_RETRY = (globalThis as any).__CF_ENV__ ? 5 : MAX_RETRY_COUNT;', 'const CREDIT_MAX_RETRY = isCfWorker() ? 5 : MAX_RETRY_COUNT;'],
118
+ [" const thresholdPercentage = parseInt(process.env.CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE || '10', 10);", ' const thresholdPercentage = creditLowBalanceThresholdPercentage();'],
119
+ ["const CREDIT_BATCH_SIZE = Math.max(1, parseInt(process.env.CREDIT_BATCH_SIZE || '50', 10));", 'const CREDIT_BATCH_SIZE = creditBatchSize();'],
120
+ ["const CREDIT_BATCH_WINDOW_MS = Math.max(10, parseInt(process.env.CREDIT_BATCH_WINDOW_MS || '3000', 10));", 'const CREDIT_BATCH_WINDOW_MS = creditBatchWindowMs();'],
121
+ [" Math.min(20, parseInt(process.env.CREDIT_QUEUE_CONCURRENCY || '5', 10) || 5)", ' creditQueueConcurrency()'],
122
+ ],
123
+ },
124
+ {
125
+ file: 'api/src/queues/event.ts',
126
+ import: "import { isCfWorker } from '../libs/env';",
127
+ edits: [[' if ((globalThis as any).__CF_ENV__) {', ' if (isCfWorker()) {']],
128
+ },
129
+ {
130
+ file: 'api/src/queues/payment.ts',
131
+ import: "import { isCfWorker } from '../libs/env';",
132
+ edits: [[' (globalThis as any).__CF_ENV__ &&', ' isCfWorker() &&']],
133
+ },
134
+ {
135
+ file: 'api/src/queues/subscription.ts',
136
+ import: "import { paymentReloadSubscriptionJobs } from '../libs/env';",
137
+ edits: [[" await addSubscriptionJob(x, 'cycle', process.env.PAYMENT_RELOAD_SUBSCRIPTION_JOBS === '1');", " await addSubscriptionJob(x, 'cycle', paymentReloadSubscriptionJobs());"]],
138
+ },
139
+ {
140
+ file: 'api/src/routes/checkout-sessions.ts',
141
+ import: "import { paymentRateVolatilityThreshold } from '../libs/env';",
142
+ edits: [[' const raw = Number(process.env.PAYMENT_RATE_VOLATILITY_THRESHOLD || DEFAULT_RATE_VOLATILITY_THRESHOLD);', ' const raw = Number(paymentRateVolatilityThreshold() || DEFAULT_RATE_VOLATILITY_THRESHOLD);']],
143
+ },
144
+ {
145
+ file: 'api/src/routes/index.ts',
146
+ import: "import { paymentLivemode } from '../libs/env';",
147
+ edits: [[" req.livemode = process.env.PAYMENT_LIVEMODE !== 'false';", ' req.livemode = paymentLivemode();']],
148
+ },
149
+ {
150
+ file: 'api/src/routes/integrations/google-play.ts',
151
+ import: "import { googlePubsubPushServiceAccount, googlePubsubAllowUnverifiedSender, isTestEnv } from '../../libs/env';",
152
+ edits: [
153
+ [' const expectedEmail = process.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;', ' const expectedEmail = googlePubsubPushServiceAccount();'],
154
+ [" process.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER === 'true' || process.env.NODE_ENV === 'test';", ' googlePubsubAllowUnverifiedSender() || isTestEnv();'],
155
+ ],
156
+ },
157
+ {
158
+ file: 'api/src/routes/integrations/stripe.ts',
159
+ import: "import { stripeWebhookSecret } from '../../libs/env';",
160
+ edits: [[' const secret = process.env.STRIPE_WEBHOOK_SECRET || settings.stripe?.webhook_signing_secret;', ' const secret = stripeWebhookSecret() || settings.stripe?.webhook_signing_secret;']],
161
+ },
162
+ {
163
+ file: 'api/src/routes/products.ts',
164
+ import: "import { allowChangeLockedPrice } from '../libs/env';",
165
+ edits: [[" if (process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1') {", ' if (!allowChangeLockedPrice) {']],
166
+ },
167
+ {
168
+ file: 'api/src/routes/subscriptions.ts',
169
+ import: "import { isProduction } from '../libs/env';",
170
+ edits: [[" if (process.env.BLOCKLET_MODE === 'production') {", ' if (isProduction()) {']],
171
+ },
172
+ {
173
+ file: 'api/src/store/migrations/20230911-seeding.ts',
174
+ import: "import { blockletMountPoints } from '../../libs/env';",
175
+ edits: [[" const components = JSON.parse(process.env.BLOCKLET_MOUNT_POINTS || '[]');", " const components = JSON.parse(blockletMountPoints() || '[]');"]],
176
+ },
177
+ {
178
+ file: 'api/src/store/models/customer.ts',
179
+ import: "import { cfEnv } from '../../libs/env';",
180
+ edits: [[' const d1 = (globalThis as any).__CF_ENV__?.DB;', ' const d1 = cfEnv()?.DB;']],
181
+ },
182
+ {
183
+ file: 'api/src/store/sequelize.ts',
184
+ import: "import { sqlLog, sqlBenchmark } from '../libs/env';",
185
+ edits: [
186
+ [" logging: process.env.SQL_LOG === '1',", ' logging: sqlLog(),'],
187
+ [" benchmark: process.env.SQL_LOG === '1' && process.env.SQL_BENCHMARK === '1',", ' benchmark: sqlLog() && sqlBenchmark(),'],
188
+ ],
189
+ },
190
+ ];
191
+
192
+ let totalEdits = 0;
193
+ for (const { file, import: importLine, edits } of PLAN) {
194
+ const full = path.join(ROOT, file);
195
+ let src = fs.readFileSync(full, 'utf8');
196
+
197
+ for (const [find, replace] of edits) {
198
+ if (!src.includes(find)) {
199
+ throw new Error(`[${file}] target NOT FOUND (was it moved?):\n ${find}`);
200
+ }
201
+ const before = src;
202
+ src = src.replace(find, replace);
203
+ if (src === before) throw new Error(`[${file}] replace was a no-op for: ${find}`);
204
+ totalEdits += 1;
205
+ }
206
+
207
+ // Insert the import after the first existing top-level import line, if absent.
208
+ if (importLine && !src.includes(importLine)) {
209
+ const m = src.match(/^import .*$/m);
210
+ if (!m) throw new Error(`[${file}] no existing import to anchor to`);
211
+ const idx = src.indexOf(m[0]) + m[0].length;
212
+ src = `${src.slice(0, idx)}\n${importLine}${src.slice(idx)}`;
213
+ }
214
+
215
+ fs.writeFileSync(full, src);
216
+ console.log(`✓ ${file} (${edits.length} read${edits.length > 1 ? 's' : ''} converged)`);
217
+ }
218
+
219
+ console.log(`\nDone: ${totalEdits} reads converged across ${PLAN.length} files.`);
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // 9a review fix (P1): env.ts's import-time consts became lazy getters. Update
4
+ // every consumer to CALL them — but only the usages, never the import statement,
5
+ // and never a site already followed by `(`. Operates only on files that import
6
+ // the name from the relative env module, so unrelated identifiers are untouched.
7
+ //
8
+ // node scripts/phase9a-env-getters-codemod.js
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const ROOT = path.join(__dirname, '..');
14
+ const SRC = path.join(ROOT, 'api/src');
15
+
16
+ const NAMES = [
17
+ 'paymentStatCronTime', 'subscriptionCronTime', 'notificationCronTime', 'expiredSessionCleanupCronTime',
18
+ 'notificationCronConcurrency', 'stripeInvoiceCronTime', 'stripePaymentCronTime', 'stripeSubscriptionCronTime',
19
+ 'revokeStakeCronTime', 'daysUntilCancel', 'meteringSubscriptionDetectionCronTime', 'overdueDetectionCronTime',
20
+ 'overdueThreshold', 'depositVaultCronTime', 'creditConsumptionCronTime', 'vendorStatusCheckCronTime',
21
+ 'vendorReturnScanCronTime', 'iapReconcileCronTime', 'eventRetryCronTime', 'quoteCleanupCronTime',
22
+ 'vendorTimeoutMinutes', 'webhookAlertWindowMinutes', 'webhookAlertMinFailures', 'shortUrlApiKey', 'shortUrlDomain',
23
+ 'sequelizeOptionsPoolMin', 'sequelizeOptionsPoolMax', 'sequelizeOptionsPoolIdle', 'updateDataConcurrency',
24
+ 'stopAcceptingOrders', 'exchangeRateCacheTTLSeconds', 'systemMaxPendingAmount',
25
+ ];
26
+
27
+ function listFiles(dir) {
28
+ const out = [];
29
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
30
+ const full = path.join(dir, e.name);
31
+ if (e.isDirectory()) out.push(...listFiles(full));
32
+ else if (e.isFile() && full.endsWith('.ts')) out.push(full);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ // relative import from our env module: from './env' | '../env' | '../libs/env' | '../../libs/env' | ...
38
+ const ENV_IMPORT = /import\s*(?:type\s*)?\{[^}]*\}\s*from\s*['"](\.\.?(?:\/[\w.-]+)*?\/?env)['"];?/g;
39
+
40
+ let totalFiles = 0;
41
+ let totalEdits = 0;
42
+
43
+ for (const file of listFiles(SRC)) {
44
+ const rel = path.relative(ROOT, file);
45
+ if (rel === path.join('api', 'src', 'libs', 'env.ts')) continue; // the definitions
46
+ let src = fs.readFileSync(file, 'utf8');
47
+
48
+ // collect env-import block ranges + which target names this file imports
49
+ const importRanges = [];
50
+ const importedHere = new Set();
51
+ let m;
52
+ ENV_IMPORT.lastIndex = 0;
53
+ while ((m = ENV_IMPORT.exec(src))) {
54
+ importRanges.push([m.index, m.index + m[0].length]);
55
+ for (const name of NAMES) {
56
+ if (new RegExp(`\\b${name}\\b`).test(m[0])) importedHere.add(name);
57
+ }
58
+ }
59
+ if (!importedHere.size) continue;
60
+
61
+ const inImport = (idx) => importRanges.some(([a, b]) => idx >= a && idx < b);
62
+
63
+ let fileEdits = 0;
64
+ for (const name of importedHere) {
65
+ // replace NAME -> NAME() when NOT in an import block and NOT already called
66
+ const re = new RegExp(`\\b${name}\\b(?!\\s*\\()`, 'g');
67
+ src = src.replace(re, (match, offset) => {
68
+ if (inImport(offset)) return match; // leave the import name as-is
69
+ fileEdits += 1;
70
+ return `${name}()`;
71
+ });
72
+ }
73
+
74
+ if (fileEdits) {
75
+ fs.writeFileSync(file, src);
76
+ totalFiles += 1;
77
+ totalEdits += fileEdits;
78
+ console.log(`✓ ${rel} (${fileEdits} call site${fileEdits > 1 ? 's' : ''}: ${[...importedHere].join(', ')})`);
79
+ }
80
+ }
81
+
82
+ console.log(`\nDone: ${totalEdits} call sites across ${totalFiles} files.`);
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // Phase 12 (W2-4a): CI scanner for direct process.env / globalThis.__CF_ENV__
4
+ // reads in the payment core.
5
+ //
6
+ // W2 §3.4: `config` is a first-class factory parameter; core business code must
7
+ // not read process.env / the CF env mirror directly — those converge into the
8
+ // config object so the same core runs on Blocklet Server, the worker and arc
9
+ // with the host supplying config. This scanner is the mechanical gate (W2 判据 6,
10
+ // same approach as the Phase 3 tenant-query scanner): the env/config BOUNDARY
11
+ // module (libs/env.ts) is the single allowed reader; every other read is a
12
+ // violation unless whitelisted. The whitelist starts populated with the current
13
+ // reads (each a convergence candidate) and is driven to zero by Phase 13.
14
+ //
15
+ // Usage: node scripts/scan-core-env.js [--json] [extra files...]
16
+ // Exit code 1 when violations outside the whitelist (or stale whitelist) are found.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const ROOT = path.join(__dirname, '..');
22
+ const SRC_DIR = path.join(ROOT, 'api/src');
23
+ const WHITELIST_FILE = path.join(__dirname, 'core-env-whitelist.json');
24
+
25
+ // libs/env.ts is the designated env/config boundary — the one module allowed to
26
+ // read process.env. Everything else must take config from there (Phase 12) or
27
+ // the config slot (Phase 13 end state).
28
+ const BOUNDARY_EXEMPT = [path.join('api', 'src', 'libs', 'env.ts')];
29
+
30
+ // process.env.NAME / process.env['NAME']
31
+ const ENV_READ = /\bprocess\s*\.\s*env\s*(?:\.\s*[A-Za-z_$][\w$]*|\[\s*['"][^'"]+['"]\s*\])/g;
32
+ // the CF env mirror — matched as a standalone token so every access form is
33
+ // caught: globalThis.__CF_ENV__, (globalThis as any).__CF_ENV__, destructured,
34
+ // etc. In api/src any __CF_ENV__ reference is a direct env-mirror read.
35
+ const CF_ENV_MIRROR = /__CF_ENV__/g;
36
+
37
+ function stripLineComments(source) {
38
+ return source.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
39
+ }
40
+
41
+ function listFiles(dir) {
42
+ const out = [];
43
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
44
+ const full = path.join(dir, entry.name);
45
+ if (entry.isDirectory()) out.push(...listFiles(full));
46
+ else if (entry.isFile() && full.endsWith('.ts')) out.push(full);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ function scanFile(file) {
52
+ const source = stripLineComments(fs.readFileSync(file, 'utf8'));
53
+ const rel = path.relative(ROOT, file);
54
+ const violations = [];
55
+ for (const regex of [ENV_READ, CF_ENV_MIRROR]) {
56
+ regex.lastIndex = 0;
57
+ let match;
58
+ // eslint-disable-next-line no-cond-assign
59
+ while ((match = regex.exec(source))) {
60
+ const line = source.slice(0, match.index).split('\n').length;
61
+ violations.push({ file: rel, line, read: match[0].replace(/\s+/g, '') });
62
+ }
63
+ }
64
+ return violations;
65
+ }
66
+
67
+ function main() {
68
+ const args = process.argv.slice(2);
69
+ const json = args.includes('--json');
70
+ const extraFiles = args.filter((a) => !a.startsWith('--'));
71
+
72
+ const whitelist = JSON.parse(fs.readFileSync(WHITELIST_FILE, 'utf8'));
73
+ const whitelisted = new Set(whitelist.map((entry) => entry.file));
74
+
75
+ const files = extraFiles.length ? extraFiles.map((f) => path.resolve(f)) : listFiles(SRC_DIR);
76
+
77
+ let violations = [];
78
+ let whitelistedCount = 0;
79
+ for (const file of files) {
80
+ const rel = path.relative(ROOT, file);
81
+ if (BOUNDARY_EXEMPT.includes(rel)) continue;
82
+ const found = scanFile(file);
83
+ if (!found.length) continue;
84
+ if (whitelisted.has(rel)) {
85
+ whitelistedCount += found.length;
86
+ } else {
87
+ violations = violations.concat(found);
88
+ }
89
+ }
90
+
91
+ const stale = whitelist
92
+ .map((entry) => entry.file)
93
+ .filter((file) => {
94
+ const full = path.join(ROOT, file);
95
+ return !fs.existsSync(full) || scanFile(full).length === 0;
96
+ });
97
+
98
+ const result = { violations, whitelisted: whitelistedCount, staleWhitelistEntries: stale };
99
+ if (json) {
100
+ console.log(JSON.stringify(result));
101
+ } else {
102
+ for (const v of violations) console.error(`core env read outside boundary: ${v.file}:${v.line} ${v.read}`);
103
+ if (stale.length) console.error(`stale core-env whitelist entries (remove them): ${stale.join(', ')}`);
104
+ console.log(`core-env-scan: ${violations.length} violations, ${whitelistedCount} whitelisted`);
105
+ }
106
+ process.exit(violations.length > 0 || stale.length > 0 ? 1 : 0);
107
+ }
108
+
109
+ main();