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
@@ -14,15 +14,30 @@ import { setDB } from './shims/sequelize-d1/model';
14
14
  import { initialize } from '../api/src/store/models';
15
15
  import { Sequelize } from './shims/sequelize-d1/sequelize-class';
16
16
 
17
- // Import the original Express routes (esbuild aliases handle all deps)
18
- import expressRoutes from '../api/src/routes/index';
19
- import type { RouteEntry } from './shims/express-compat/index';
17
+ // Phase 12b: pin the core queue engine to 'workerd' mode BEFORE any business
18
+ // queue module loads (createQueue reads it at import to disable the node poll
19
+ // loop). Side-effect import keep it ahead of service/crons/queues below.
20
+ import './queue-runtime-mode';
21
+
22
+ // Phase 12a (Option 3 seam) + S3-CF Phase 1B: the worker's /api surface comes from
23
+ // the embedded payment service factory. The factory performs assembly (config slot
24
+ // authoritative via setCoreConfig + model initialize). Phase 1B: the worker forwards
25
+ // every payment /api/* (business resource routes + DID payment actions) through the
26
+ // runtime-neutral `http.fetch` (the core full app + full pipeline) — a SINGLE
27
+ // surface. The factory's lazy `handler` getter is never touched here (hard-gated),
28
+ // so the worker only ever drives the core via `http.fetch`.
29
+ import { createEmbeddedPaymentService } from '../api/src/service';
30
+ import type { PaymentCoreService } from '../api/src/service';
20
31
 
21
32
  // Import cron instance for scheduled handler
22
33
  import { cronInstance } from './shims/cron';
23
34
 
24
- // Import queue utilities
25
- import { setWaitUntil, setCFQueue, flushPendingJobs, runAllScheduledJobs, getHandler } from './shims/queue';
35
+ // Phase 12b: drive the SAME core queue engine (api/src/libs/queue) through its
36
+ // host-facing runtime surface instead of the legacy cloudflare/shims/queue.ts
37
+ // duplicate engine (dead under the canonical build — its registry is never
38
+ // populated). scheduled() → dispatchDueJobs(); queue() → getQueueHandler();
39
+ // HTTP/scheduled/queue flush → flushQueueWork().
40
+ import { dispatchDueJobs, getQueueHandler, getAllQueueNames, flushQueueWork } from '../api/src/libs/queue/runtime';
26
41
 
27
42
  // Import crons init to register all cron jobs
28
43
  import crons from '../api/src/crons/index';
@@ -51,7 +66,10 @@ import { resetD1Timing, getD1Timing } from './shims/sequelize-d1/timing';
51
66
  import { withD1Retry } from './shims/sequelize-d1/retry';
52
67
 
53
68
  // DID Connect: login routes proxied to blocklet-service, business actions (pay/subscribe) handled locally
54
- import { attachDIDConnectRoutes } from './did-connect-auth';
69
+ import { createCloudflareDidConnectRuntime, createCloudflareIdentityDriver } from './did-connect-runtime';
70
+
71
+ // Phase 7: tenant-context middleware — resolves Host -> tenant (single point)
72
+ // and wraps the request chain in context.withTenant (multi-mode fail-closed).
55
73
 
56
74
  FetchRequest.registerGetUrl(async (req: FetchRequest) => {
57
75
  const resp = await fetch(req.url, {
@@ -81,7 +99,6 @@ interface CallerIdentityDTO {
81
99
 
82
100
  interface Env {
83
101
  DB: D1Database;
84
- DID_CONNECT_KV: KVNamespace;
85
102
  JOB_QUEUE: Queue;
86
103
  ASSETS: { fetch: (request: Request | string) => Promise<Response> };
87
104
  APP_SK: string;
@@ -109,6 +126,17 @@ interface Env {
109
126
  role?: string;
110
127
  approved?: number;
111
128
  } | null>;
129
+ // S3-CF (DID convergence): did-connect-service@4.0.3 — the per-instance app
130
+ // signing identity for the DID-Connect authenticator. The RPC resolves the arc
131
+ // run-mode internally (instance app:sk → instance identity; else auth-service
132
+ // root APP_SK/APP_PSK; else fail-closed), so payment-core never reimplements it.
133
+ getInstanceAppIdentity: (instanceDid: string) => Promise<{
134
+ appSk: string;
135
+ appPsk?: string;
136
+ appInfo?: { name?: string; description?: string; icon?: string; link?: string };
137
+ }>;
138
+ getAppEk?: (instanceDid: string) => Promise<string | null>;
139
+ resolveInstanceDidForHost?: (host: string) => Promise<string | null>;
112
140
  };
113
141
  HYPERDRIVE: { connectionString: string };
114
142
  [key: string]: any;
@@ -204,6 +232,101 @@ function ensureModelsInit() {
204
232
  }
205
233
  }
206
234
 
235
+ // Phase 12a/12c: build the explicit PaymentCoreConfig from CF env. These keys are
236
+ // read only by the core (via the libs/env.ts readConfig boundary), never by a
237
+ // worker shim, so the factory config slot carries them. Phase 12c removed the
238
+ // last `process.env` mirror: the HTTP path (buildApp) and the scheduled()/queue()
239
+ // path (setupEnv) both call ensurePaymentService(env) -> setCoreConfig, so the
240
+ // config slot is authoritative on every path and no process.env fallback remains.
241
+ function envToPaymentCoreConfig(env: Env): Record<string, any> {
242
+ const config: Record<string, any> = { BLOCKLET_MODE: 'production' };
243
+ if (env.APP_PID) {
244
+ config.BLOCKLET_APP_PID = env.APP_PID;
245
+ config.BLOCKLET_APP_ID = env.APP_PID;
246
+ }
247
+ if (env.APP_URL) {
248
+ config.APP_URL = env.APP_URL;
249
+ config.BLOCKLET_APP_URL = env.APP_URL;
250
+ }
251
+ if (env.APP_NAME) config.BLOCKLET_APP_NAME = env.APP_NAME;
252
+ // Stripe webhook secret: env override for signature verification (DB value may
253
+ // be empty if not configured in the original Blocklet Server).
254
+ if (env.STRIPE_WEBHOOK_SECRET) config.STRIPE_WEBHOOK_SECRET = env.STRIPE_WEBHOOK_SECRET;
255
+ if (env.PAYMENT_CHANGE_LOCKED_PRICE) config.PAYMENT_CHANGE_LOCKED_PRICE = env.PAYMENT_CHANGE_LOCKED_PRICE;
256
+ if (env.SHORT_URL_DOMAIN) config.SHORT_URL_DOMAIN = env.SHORT_URL_DOMAIN;
257
+ // Audience for the Pub/Sub OIDC JWT wrapping Google Play RTDN webhooks; without
258
+ // it googlePlayEndpoint() falls back to the workers.dev origin -> audience mismatch.
259
+ if (env.GOOGLE_PLAY_WEBHOOK_URL) config.GOOGLE_PLAY_WEBHOOK_URL = env.GOOGLE_PLAY_WEBHOOK_URL;
260
+ // Pub/Sub sender binding — without it the google_play webhook can't enforce the
261
+ // push service account on CF and would fail open (PR #1381 P1).
262
+ if (env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT)
263
+ config.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT = env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;
264
+ if (env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER)
265
+ config.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER = env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER;
266
+ return config;
267
+ }
268
+
269
+ // Phase 12a: assemble the embedded payment service once per isolate. The factory
270
+ // performs config/db assembly (setCoreConfig + initialize), so models are bound
271
+ // here and ensureModelsInit() becomes a no-op on the HTTP path. Slots beyond
272
+ // config/db are intentionally omitted in 12a (defaults preserve current worker
273
+ // behavior: single-mode default tenant resolved from BLOCKLET_APP_PID); richer
274
+ // slot bridging is 12b/12c.
275
+ let paymentService: PaymentCoreService | null = null;
276
+ function ensurePaymentService(env: Env): PaymentCoreService {
277
+ if (paymentService) return paymentService;
278
+ const sequelize = new Sequelize();
279
+ const svc = createEmbeddedPaymentService({
280
+ config: envToPaymentCoreConfig(env),
281
+ db: { sequelize },
282
+ // S3-CF (DID convergence): inject the CF DID-Connect runtime (real
283
+ // @arcblock/did-connect-js + CF chain/txEncoder/timeout + tenant-aware D1 token
284
+ // store) and the AUTH_SERVICE-backed identity driver (getInstanceAppIdentity).
285
+ // The core buildConnectRoutesHono then registers the 14 payment DID actions with
286
+ // a per-tenant signing identity — the worker no longer owns a private DID surface.
287
+ identity: createCloudflareIdentityDriver(),
288
+ didConnectRuntime: createCloudflareDidConnectRuntime(),
289
+ });
290
+ // Phase 12c HARD GATE (updated S3-CF Phase 1B): the CF worker is a host adapter
291
+ // that drives the core via the runtime-neutral `svc.http.fetch`. It must NEVER
292
+ // read `svc.handler` directly — that lazy getter is the node-host convenience
293
+ // entry (it would wire the node staticHandler if one were injected). The worker
294
+ // forwards through `svc.http.fetch`, which builds the same full hono app but is
295
+ // the sanctioned runtime-neutral seam. Trap `handler` so a future regression
296
+ // fails loud instead of silently taking the node-convenience path.
297
+ paymentService = new Proxy(svc, {
298
+ get(target, prop, receiver) {
299
+ if (prop === 'handler') {
300
+ throw new Error(
301
+ '[worker hard-gate] svc.handler is forbidden in the CF worker — use svc.http.fetch instead'
302
+ );
303
+ }
304
+ return Reflect.get(target, prop, receiver);
305
+ },
306
+ });
307
+ modelsInitialized = true; // the factory called initialize(sequelize)
308
+ return paymentService;
309
+ }
310
+
311
+ // Phase 2 (W1-1b): static D1 migration SQL cannot know the deployment app
312
+ // DID, so the instance_did backfill + unique-key rebuilds run through the
313
+ // shared runtime routine. Idempotent (NULL-only updates, DDL checks), so a
314
+ // once-per-isolate guard just avoids 38 no-op UPDATEs on every cron tick.
315
+ let tenantBackfillDone = false;
316
+ async function ensureTenantBackfill() {
317
+ if (tenantBackfillDone) return;
318
+ try {
319
+ const { runTenantBackfill } = await import('../api/src/store/tenant-backfill');
320
+ const result = await runTenantBackfill(new Sequelize() as any);
321
+ const touched = Object.entries(result.backfilled).filter(([, n]) => n > 0);
322
+ if (touched.length) console.log('[tenant-backfill] backfilled:', JSON.stringify(Object.fromEntries(touched)));
323
+ tenantBackfillDone = true;
324
+ } catch (e: any) {
325
+ // fail-loud but non-fatal: crons still run; next tick retries
326
+ console.error('[tenant-backfill] failed:', e?.message || e);
327
+ }
328
+ }
329
+
207
330
  function ensureCronsInit() {
208
331
  if (!cronsInitialized) {
209
332
  try {
@@ -234,10 +357,19 @@ function buildApp(env: Env): Hono<HonoEnv> {
234
357
  return cachedApp;
235
358
  }
236
359
 
360
+ // Phase 12a + S3-CF Phase 1B: assemble the embedded payment service
361
+ // (config/db/slots incl. the injected CF DID-Connect runtime). The worker drives
362
+ // it through `service.http.fetch` (the single runtime-neutral surface); the
363
+ // node-only `handler` getter is hard-gated and never touched.
364
+ const service = ensurePaymentService(env);
365
+
237
366
  const app = new Hono<HonoEnv>();
238
367
 
239
368
  // CORS
240
- app.use('/api/*', cors());
369
+ // S3-CF Phase 1B: payment `/api/*` CORS is owned by the CORE full pipeline
370
+ // (cors/xss/csrf/i18n/cdn/context), reached via service.http.fetch. The worker no
371
+ // longer pre-applies cors() to /api/* — that would double the CORS headers on
372
+ // every payment route. Worker-owned cross-origin surfaces keep their own cors:
241
373
  app.use('/.well-known/*', cors());
242
374
  // /__blocklet__.js is fetched by external wallets (e.g. abtwallet.io, localhost
243
375
  // dev wallets) to resolve app metadata + chain info before starting DID Connect.
@@ -255,43 +387,17 @@ function buildApp(env: Env): Hono<HonoEnv> {
255
387
  // In queue consumer/cron, this flag is absent — createEvent uses __cfPendingJobs__ (blocking).
256
388
  (globalThis as any).__cfHttpContext__ = true;
257
389
  setDB(withD1Retry(c.env.DB.withSession('first-primary')));
258
- if (c.env.JOB_QUEUE) setCFQueue(c.env.JOB_QUEUE);
259
390
  ensureModelsInit();
260
391
 
261
- // Sync CF env vars to process.env for source code compatibility
262
- if (typeof process !== 'undefined' && process.env) {
263
- // Stripe webhook secret: kept as env var override for webhook signature verification
264
- // (DB value may be empty if not configured in original Blocklet Server)
265
- if (c.env.STRIPE_WEBHOOK_SECRET) process.env.STRIPE_WEBHOOK_SECRET = c.env.STRIPE_WEBHOOK_SECRET;
266
- if (c.env.APP_URL) {
267
- process.env.APP_URL = c.env.APP_URL;
268
- process.env.BLOCKLET_APP_URL = c.env.APP_URL;
269
- }
270
- if (c.env.APP_PID) {
271
- process.env.BLOCKLET_APP_PID = c.env.APP_PID;
272
- process.env.BLOCKLET_APP_ID = c.env.APP_PID;
273
- }
274
- if (c.env.APP_NAME) process.env.BLOCKLET_APP_NAME = c.env.APP_NAME;
275
- if (c.env.PAYMENT_CHANGE_LOCKED_PRICE) process.env.PAYMENT_CHANGE_LOCKED_PRICE = c.env.PAYMENT_CHANGE_LOCKED_PRICE;
276
- if (c.env.SHORT_URL_DOMAIN) process.env.SHORT_URL_DOMAIN = c.env.SHORT_URL_DOMAIN;
277
- // Audience for the Pub/Sub OIDC JWT that wraps Google Play RTDN webhooks.
278
- // Without this mirror, libs/util.ts googlePlayEndpoint() falls back to
279
- // getUrl('/api/integrations/google-play/webhook') — which resolves to
280
- // the *.workers.dev origin, not the custom domain Pub/Sub actually
281
- // POSTs to. Every webhook then dies with "audience mismatch".
282
- if (c.env.GOOGLE_PLAY_WEBHOOK_URL) {
283
- process.env.GOOGLE_PLAY_WEBHOOK_URL = c.env.GOOGLE_PLAY_WEBHOOK_URL;
284
- }
285
- // Pub/Sub sender binding — without mirroring these, the google_play webhook
286
- // can't enforce the push service account on CF and would fail open (PR #1381 P1).
287
- if (c.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT) {
288
- process.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT = c.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;
289
- }
290
- if (c.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER) {
291
- process.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER = c.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER;
292
- }
293
- process.env.BLOCKLET_MODE = 'production';
294
- }
392
+ // Phase 12a: CF env now flows through the factory config slot (assembled in
393
+ // ensurePaymentService -> setCoreConfig, made authoritative via the
394
+ // libs/env.ts readConfig boundary). The per-request process.env mirror that
395
+ // lived here is gone. This defensive idempotent call guarantees the config is
396
+ // wired for this isolate regardless of Hono app-cache state; it is a no-op
397
+ // after the first call. (Phase 12c: scheduled()/queue() call the same
398
+ // ensurePaymentService via setupEnv, so the config slot is authoritative
399
+ // there too — no process.env mirror on any path.)
400
+ ensurePaymentService(c.env);
295
401
 
296
402
  // Register Stripe key decrypt overrides from env vars
297
403
  // Fetch EK from AUTH_SERVICE and initialize decrypt capability (first request only)
@@ -327,7 +433,23 @@ function buildApp(env: Env): Hono<HonoEnv> {
327
433
  if (caller) {
328
434
  authSource = 'cache';
329
435
  } else {
330
- caller = await authService.resolveIdentity(jwt, authHeader, c.env.APP_PID);
436
+ // S3-CF Phase 1B: parameterize the caller RPC with the request's actual
437
+ // tenant via the SAME Host→tenant resolver the core uses — not a fixed
438
+ // env.APP_PID — so a multi-tenant embed verifies the caller against the
439
+ // right instance. Non-throwing: an unresolved host (multi) keeps the
440
+ // APP_PID fallback. Lazily required (NOT a top-level import) to avoid
441
+ // pulling the identity module into the worker's eager startup graph. This
442
+ // does NOT wrap the business handler's ALS context (the core owns that).
443
+ let callerInstanceDid: string | undefined = c.env.APP_PID;
444
+ try {
445
+ // eslint-disable-next-line global-require
446
+ const { resolveTenantForHost } = require('../api/src/libs/drivers/identity');
447
+ const resolved = await resolveTenantForHost(c.req.header('host'));
448
+ if (resolved) callerInstanceDid = resolved;
449
+ } catch {
450
+ /* unresolved host → keep the APP_PID fallback for the caller RPC param */
451
+ }
452
+ caller = await authService.resolveIdentity(jwt, authHeader, callerInstanceDid);
331
453
  authSource = 'rpc';
332
454
  if (caller && cacheKey) {
333
455
  cacheIdentity(cacheKey, caller);
@@ -351,10 +473,7 @@ function buildApp(env: Env): Hono<HonoEnv> {
351
473
  // --- Append Server-Timing header ---
352
474
  const totalDur = Math.round(performance.now() - t0);
353
475
  const d1 = getD1Timing();
354
- const timings = [
355
- `total;dur=${totalDur}`,
356
- `auth;dur=${authDur};desc="${authSource}"`,
357
- ];
476
+ const timings = [`total;dur=${totalDur}`, `auth;dur=${authDur};desc="${authSource}"`];
358
477
  if (d1.queries > 0) {
359
478
  timings.push(`db;dur=${Math.round(d1.wallMs)};desc="${d1.queries}q ${d1.rowsRead}r"`);
360
479
  if (d1.sqlMs > 0) timings.push(`db_sql;dur=${Math.round(d1.sqlMs)}`);
@@ -366,6 +485,14 @@ function buildApp(env: Env): Hono<HonoEnv> {
366
485
  c.res.headers.append('Server-Timing', timings.join(', '));
367
486
  });
368
487
 
488
+ // S3-CF Phase 1B: the ALS tenant context for payment `/api/*` business requests
489
+ // is owned by the CORE full pipeline (contextMiddleware), reached via
490
+ // service.http.fetch. The worker no longer wraps /api/* in tenantMiddleware() —
491
+ // that would double-resolve + double-wrap the tenant. The few worker-owned /api/*
492
+ // endpoints (dev/debug/proxies) run in single-mode default and never query under
493
+ // a per-request tenant. (DID payment actions get their context from the core
494
+ // buildConnectRoutesHono tenant middleware, also via http.fetch.)
495
+
369
496
  // Health check
370
497
  app.get('/health', (c) => c.json({ status: 'ok' }));
371
498
 
@@ -567,8 +694,9 @@ function buildApp(env: Env): Hono<HonoEnv> {
567
694
 
568
695
  // === DID Auth Login routes ===
569
696
  // Only proxy login-related /api/did/* paths to blocklet-service.
570
- // Other /api/did/* paths (subscription, pay, collect, etc.) are Payment Kit's
571
- // own DID Connect actions handled by attachDIDConnectRoutes below.
697
+ // Other /api/did/* paths (subscription, pay, collect, etc.) are Payment Kit's own
698
+ // DID Connect payment actions they fall through to the /api/* → service.http.fetch
699
+ // dispatcher (the core full app's DID payment actions).
572
700
  const DID_AUTH_PROXY_PATHS = [
573
701
  '/api/did/login/',
574
702
  '/api/did/session',
@@ -579,7 +707,10 @@ function buildApp(env: Env): Hono<HonoEnv> {
579
707
  app.all('/api/did/*', async (c, next) => {
580
708
  const path = new URL(c.req.url).pathname;
581
709
  const shouldProxy = DID_AUTH_PROXY_PATHS.some((p) => path.startsWith(p) || path === p);
582
- if (!shouldProxy) return next(); // Fall through to attachDIDConnectRoutes or Express routes
710
+ // S3-CF Phase 1B: non-proxy payment DID actions fall through to the /api/*
711
+ // dispatcher below → service.http.fetch (the core full app includes the DID
712
+ // payment actions via buildConnectRoutesHono).
713
+ if (!shouldProxy) return next();
583
714
 
584
715
  if (!c.env.AUTH_SERVICE) {
585
716
  return c.json({ error: 'AUTH_SERVICE not configured' }, 503);
@@ -596,22 +727,19 @@ function buildApp(env: Env): Hono<HonoEnv> {
596
727
  });
597
728
 
598
729
  // === DID Connect business actions (subscription, pay, collect, etc.) ===
599
- if (env.APP_SK && env.DID_CONNECT_KV) {
600
- try {
601
- attachDIDConnectRoutes(app, env.DID_CONNECT_KV, env.APP_SK);
602
- console.log('[CF Worker] DID Connect routes attached');
603
- } catch (e: any) {
604
- console.error('DID Connect init error:', e?.message || e);
605
- }
606
- }
730
+ // S3-CF Phase 1B: the 14 payment DID actions are part of the CORE full app and
731
+ // are reached through the /api/* → service.http.fetch dispatcher below (they were
732
+ // converged onto buildConnectRoutesHono in Phase 1A, backed by the CF DID-Connect
733
+ // runtime injected into the service above). No separate worker mount — one surface.
607
734
 
608
735
  // Notification unread count
609
736
  app.get('/api/notifications/unread-count', (c) => c.json({ unReadCount: 0 }));
610
737
 
611
- // Manually trigger job dispatch (same as cron's runAllScheduledJobs)
738
+ // Manually trigger job dispatch (same as cron's scheduled() due-dispatch)
612
739
  app.post('/api/__dev__/dispatch-jobs', async (c) => {
613
- const names = getAllHandlerNames();
614
- const result = await runAllScheduledJobs();
740
+ const names = getAllQueueNames();
741
+ const result = await dispatchDueJobs();
742
+ await flushQueueWork();
615
743
  return c.json({ handlers: names, ...result });
616
744
  });
617
745
 
@@ -640,8 +768,9 @@ function buildApp(env: Env): Hono<HonoEnv> {
640
768
  // for creating PaymentMethods is owner-gated, and staging has no owner yet
641
769
  // (Pengfei's blocklet-service role is `member`). Gated by PAYMENT_LIVEMODE
642
770
  // === 'false' so this only ever touches testmode data.
643
- // === Express-to-Hono Route Adapter ===
644
- mountExpressRoutes(app, '/api', expressRoutes);
771
+ // S3-CF Phase 1B: payment business routes (and DID payment actions) are served by
772
+ // the core full app via the /api/* → service.http.fetch dispatcher, registered
773
+ // AFTER the worker's own /api/__dev__ routes so those match first.
645
774
 
646
775
  // Dev endpoint: D1 admin operations
647
776
  // Test CF Queue send directly
@@ -986,9 +1115,39 @@ function buildApp(env: Env): Hono<HonoEnv> {
986
1115
  }
987
1116
  });
988
1117
 
989
- // Catch-all for unimplemented API routes
990
- app.all('/api/*', (c) => {
991
- return c.json({ error: `Not yet implemented: ${c.req.method} ${c.req.path}` }, 501);
1118
+ // S3-CF Phase 1B: the single payment HTTP surface. Every payment `/api/*` request
1119
+ // (business resource routes + non-proxy DID payment actions) that wasn't handled
1120
+ // by a worker-owned route above is forwarded to the CORE full app via
1121
+ // service.http.fetch. The core full pipeline owns cors/xss/csrf/i18n/cdn/context
1122
+ // + the resource routes + the DID payment actions — there is no more LITE
1123
+ // resourceRoutes dispatcher and no second surface.
1124
+ //
1125
+ // The worker stays the HOST GLUE: it injects the caller identity it resolved
1126
+ // (caller RPC) as canonical x-user-* request headers — the core authenticate()
1127
+ // reads those — and STRIPS any client-supplied x-user-* first so it can never be
1128
+ // forged (component auth via x-component-sig is verified cryptographically
1129
+ // downstream and left intact). Raw body bytes are forwarded unconsumed (the
1130
+ // worker never reads the body here), preserving Stripe webhook signature fidelity
1131
+ // through to the core webhook route. flushQueueWork() drains the workerd deferred
1132
+ // queue work before responding.
1133
+ const USER_HEADERS = ['x-user-did', 'x-user-role', 'x-user-provider', 'x-user-fullname', 'x-user-wallet-os'];
1134
+ app.all('/api/*', async (c) => {
1135
+ const headers = new Headers(c.req.raw.headers);
1136
+ for (const h of USER_HEADERS) headers.delete(h); // never trust a client-supplied identity header
1137
+ const caller: CallerIdentityDTO | null = c.get('caller');
1138
+ if (caller) {
1139
+ const canonicalDid = caller.did?.startsWith('did:abt:') ? caller.did : `did:abt:${caller.did}`;
1140
+ headers.set('x-user-did', canonicalDid);
1141
+ headers.set('x-user-role', `blocklet-${caller.role || 'guest'}`);
1142
+ headers.set('x-user-provider', caller.authMethod === 'access-key' ? 'access-key' : caller.authMethod || 'wallet');
1143
+ headers.set('x-user-fullname', encodeURIComponent(caller.displayName || ''));
1144
+ headers.set('x-user-wallet-os', '');
1145
+ }
1146
+ // No basePath: the standalone worker serves payment routes at the root /api/*,
1147
+ // so the core full app matches them directly (raw bytes/headers carried verbatim).
1148
+ const res = await service.http.fetch(new Request(c.req.raw, { headers }));
1149
+ await flushQueueWork(); // drain workerd deferred queue work before responding
1150
+ return res;
992
1151
  });
993
1152
 
994
1153
  // === Media Kit Proxy ===
@@ -1158,347 +1317,6 @@ function buildApp(env: Env): Hono<HonoEnv> {
1158
1317
  return app;
1159
1318
  }
1160
1319
 
1161
- // === Express-to-Hono Route Adapter ===
1162
-
1163
- function normalizeRoutePath(prefix: string, routePath: string): string {
1164
- let full = (prefix + routePath).replace(/\/+/g, '/');
1165
- if (!full.startsWith('/')) full = `/${full}`;
1166
- if (full.length > 1 && full.endsWith('/')) full = full.slice(0, -1);
1167
- return full;
1168
- }
1169
-
1170
- function createExpressReq(c: any, routeParams: Record<string, string>): any {
1171
- const url = new URL(c.req.url);
1172
- const query: Record<string, any> = {};
1173
- url.searchParams.forEach((v, k) => {
1174
- query[k] = v;
1175
- });
1176
-
1177
- const headers: Record<string, string> = {};
1178
- c.req.raw.headers.forEach((v: string, k: string) => {
1179
- headers[k.toLowerCase()] = v;
1180
- });
1181
-
1182
- const req: any = {
1183
- method: c.req.method,
1184
- url: url.pathname + url.search,
1185
- path: url.pathname,
1186
- originalUrl: url.pathname + url.search,
1187
- query,
1188
- params: { ...routeParams },
1189
- body: null,
1190
- headers,
1191
- user: null,
1192
- // Worker-wide livemode is driven by the PAYMENT_LIVEMODE env var (set on
1193
- // each deployment). Routes that filter PaymentMethod by livemode rely on
1194
- // this value matching what we stored when bootstrapping the methods.
1195
- livemode: c.env?.PAYMENT_LIVEMODE !== 'false',
1196
- baseCurrency: null,
1197
- ip: headers['cf-connecting-ip'] || headers['x-forwarded-for'] || '127.0.0.1',
1198
- get(name: string) {
1199
- return headers[name.toLowerCase()];
1200
- },
1201
- header(name: string) {
1202
- return headers[name.toLowerCase()];
1203
- },
1204
- };
1205
-
1206
- return req;
1207
- }
1208
-
1209
- function createExpressRes(): any {
1210
- const res: any = {
1211
- _statusCode: 200,
1212
- _headers: {} as Record<string, string>,
1213
- _body: null as any,
1214
- _sent: false,
1215
- _redirectUrl: null as string | null,
1216
- headersSent: false,
1217
-
1218
- status(code: number) {
1219
- res._statusCode = code;
1220
- return res;
1221
- },
1222
- json(data: any) {
1223
- if (res._sent) return res;
1224
- res._sent = true;
1225
- res.headersSent = true;
1226
- res._body = data;
1227
- res._headers['content-type'] = 'application/json';
1228
- return res;
1229
- },
1230
- send(data: any) {
1231
- if (res._sent) return res;
1232
- res._sent = true;
1233
- res.headersSent = true;
1234
- res._body = data;
1235
- return res;
1236
- },
1237
- redirect(urlOrStatus: any, url?: string) {
1238
- res._sent = true;
1239
- res.headersSent = true;
1240
- if (typeof urlOrStatus === 'number') {
1241
- res._statusCode = urlOrStatus;
1242
- res._redirectUrl = url;
1243
- } else {
1244
- res._statusCode = 302;
1245
- res._redirectUrl = urlOrStatus;
1246
- }
1247
- return res;
1248
- },
1249
- set(key: string, value: string) {
1250
- res._headers[key.toLowerCase()] = value;
1251
- return res;
1252
- },
1253
- setHeader(key: string, value: string) {
1254
- res._headers[key.toLowerCase()] = value;
1255
- return res;
1256
- },
1257
- cookie(_name: string, _value: string, _options?: any) {
1258
- return res;
1259
- },
1260
- end() {
1261
- if (!res._sent) {
1262
- res._sent = true;
1263
- res.headersSent = true;
1264
- }
1265
- },
1266
- type(t: string) {
1267
- res._headers['content-type'] = t;
1268
- return res;
1269
- },
1270
- };
1271
-
1272
- Object.defineProperty(res, 'statusCode', {
1273
- get() {
1274
- return res._statusCode;
1275
- },
1276
- set(v: number) {
1277
- res._statusCode = v;
1278
- },
1279
- });
1280
-
1281
- return res;
1282
- }
1283
-
1284
- function expressResToResponse(res: any): Response {
1285
- if (res._redirectUrl) {
1286
- return Response.redirect(res._redirectUrl, res._statusCode || 302);
1287
- }
1288
-
1289
- const headers = new Headers(res._headers);
1290
-
1291
- if (res._body === null || res._body === undefined) {
1292
- return new Response(null, { status: res._statusCode, headers });
1293
- }
1294
-
1295
- if (typeof res._body === 'string') {
1296
- return new Response(res._body, { status: res._statusCode, headers });
1297
- }
1298
-
1299
- if (res._body instanceof ArrayBuffer || res._body instanceof Uint8Array) {
1300
- return new Response(res._body, { status: res._statusCode, headers });
1301
- }
1302
-
1303
- if (!headers.has('content-type')) {
1304
- headers.set('content-type', 'application/json');
1305
- }
1306
- let jsonStr = JSON.stringify(res._body);
1307
- // Rewrite legacy blocklet server URLs to CF Workers domain
1308
- // Old format: https://old-domain/payment/methods/x.png -> https://cf-domain/methods/x.png
1309
- const cfAppUrl = ((globalThis as any).__CF_ENV__?.APP_URL || '').replace(/\/$/, '');
1310
- if (cfAppUrl) {
1311
- jsonStr = jsonStr
1312
- .split('https://bbqa7swuuaze4l2y5salvngyjyohlhq5fs5j42eokni.did.abtnet.io/payment/')
1313
- .join(`${cfAppUrl}/`);
1314
- jsonStr = jsonStr.split('https://bbqa7swuuaze4l2y5salvngyjyohlhq5fs5j42eokni.did.abtnet.io').join(cfAppUrl);
1315
- }
1316
- return new Response(jsonStr, { status: res._statusCode, headers });
1317
- }
1318
-
1319
- async function runExpressHandlers(handlers: Function[], req: any, res: any): Promise<void> {
1320
- let idx = 0;
1321
-
1322
- async function runNext(err?: any): Promise<void> {
1323
- if (err) {
1324
- console.error('[CF Worker] Express middleware error:', err?.message || err);
1325
- if (!res._sent) {
1326
- res.status(500).json({ error: err?.message || 'Internal Server Error' });
1327
- }
1328
- return;
1329
- }
1330
- if (idx >= handlers.length || res._sent) return;
1331
-
1332
- const handler = handlers[idx++];
1333
- if (!handler) return runNext();
1334
-
1335
- if (handler.length === 4) {
1336
- return runNext();
1337
- }
1338
-
1339
- return new Promise<void>((resolve) => {
1340
- let nextCalled = false;
1341
-
1342
- try {
1343
- const result = handler(req, res, (nextErr?: any) => {
1344
- nextCalled = true;
1345
- runNext(nextErr)
1346
- .then(resolve)
1347
- .catch((e: any) => {
1348
- if (!res._sent) res.status(500).json({ error: e?.message || 'Internal Server Error' });
1349
- resolve();
1350
- });
1351
- });
1352
-
1353
- if (result && typeof result.then === 'function') {
1354
- result
1355
- .then(() => {
1356
- if (!nextCalled) {
1357
- resolve();
1358
- }
1359
- })
1360
- .catch((e: any) => {
1361
- console.error('[CF Worker] Async handler error:', e?.message || e);
1362
- if (!res._sent) res.status(500).json({ error: e?.message || 'Internal Server Error' });
1363
- resolve();
1364
- });
1365
- } else if (!nextCalled) {
1366
- resolve();
1367
- }
1368
- } catch (e: any) {
1369
- console.error('[CF Worker] Sync handler error:', e?.message || e);
1370
- if (!res._sent) res.status(500).json({ error: e?.message || 'Internal Server Error' });
1371
- resolve();
1372
- }
1373
- });
1374
- }
1375
-
1376
- await runNext();
1377
- }
1378
-
1379
- function mountExpressRoutes(honoApp: Hono<HonoEnv>, prefix: string, expressRouter: any) {
1380
- const routes: RouteEntry[] = expressRouter._routes || [];
1381
-
1382
- console.log(`[CF Worker] Mounting ${routes.length} Express routes under ${prefix}`);
1383
-
1384
- for (const route of routes) {
1385
- const fullPath = normalizeRoutePath(prefix, route.path);
1386
- const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete';
1387
-
1388
- if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
1389
- console.warn(`[CF Worker] Skipping unsupported method: ${route.method} ${fullPath}`);
1390
- continue;
1391
- }
1392
-
1393
- honoApp[method](fullPath, async (c) => {
1394
- const req = createExpressReq(c, c.req.param());
1395
- const res = createExpressRes();
1396
-
1397
- if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method)) {
1398
- try {
1399
- const contentType = c.req.header('content-type') || '';
1400
- const isStripeWebhook = fullPath.includes('/integrations/stripe/webhook');
1401
-
1402
- if (isStripeWebhook) {
1403
- const rawBody = await c.req.arrayBuffer();
1404
- req.body = Buffer.from(rawBody);
1405
- req.rawBody = req.body;
1406
- } else if (contentType.includes('application/json')) {
1407
- req.body = await c.req.json();
1408
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
1409
- const text = await c.req.text();
1410
- req.body = Object.fromEntries(new URLSearchParams(text));
1411
- } else if (contentType.includes('text/')) {
1412
- req.body = await c.req.text();
1413
- } else {
1414
- try {
1415
- req.body = await c.req.json();
1416
- } catch {
1417
- try {
1418
- req.body = await c.req.text();
1419
- } catch {
1420
- req.body = null;
1421
- }
1422
- }
1423
- }
1424
- } catch {
1425
- req.body = {};
1426
- }
1427
- }
1428
-
1429
- // Debug logging for webhook
1430
- if (fullPath.includes('stripe/webhook')) {
1431
- console.log('[CF Worker] Stripe webhook request received:', {
1432
- method: c.req.method,
1433
- path: fullPath,
1434
- hasSignature: !!req.headers['stripe-signature'],
1435
- bodyType: typeof req.body,
1436
- bodyLength: req.body?.length || 0,
1437
- isBuffer: Buffer.isBuffer(req.body),
1438
- handlersCount: route.handlers.length,
1439
- });
1440
- }
1441
-
1442
- // Inject caller identity resolved by AUTH_SERVICE RPC (or mock fallback).
1443
- // AUTH_SERVICE returns the bare base58 address; Customer/Subscription queries
1444
- // and entitlement lookups expect the canonical `did:abt:…` form, so we
1445
- // normalize once here at the boundary instead of in every downstream call site.
1446
- const caller: CallerIdentityDTO | null = c.get('caller');
1447
- if (caller) {
1448
- const canonicalDid = caller.did?.startsWith('did:abt:') ? caller.did : `did:abt:${caller.did}`;
1449
- req.user = {
1450
- did: canonicalDid,
1451
- role: caller.role || 'guest',
1452
- provider: caller.authMethod === 'access-key' ? 'access-key' : 'wallet',
1453
- fullName: caller.displayName || '',
1454
- walletOS: '',
1455
- via: 'dashboard',
1456
- };
1457
- req.headers['x-user-did'] = canonicalDid;
1458
- req.headers['x-user-role'] = `blocklet-${caller.role || 'guest'}`;
1459
- req.headers['x-user-provider'] = caller.authMethod || 'wallet';
1460
- req.headers['x-user-fullname'] = encodeURIComponent(caller.displayName || '');
1461
- req.headers['x-user-wallet-os'] = '';
1462
- } else {
1463
- req.user = { did: '', role: 'guest', provider: '', fullName: '', walletOS: '', via: '' };
1464
- }
1465
-
1466
- try {
1467
- await runExpressHandlers(route.handlers, req, res);
1468
- } catch (e: any) {
1469
- console.error(
1470
- `[CF Worker] Unhandled error in ${route.method} ${fullPath}:`,
1471
- e?.message || e,
1472
- '\n',
1473
- e?.stack?.split('\n').slice(0, 8).join('\n')
1474
- );
1475
- if (!res._sent) {
1476
- res.status(500).json({ error: e?.message || 'Internal Server Error' });
1477
- }
1478
- }
1479
-
1480
- // Debug logging for webhook response
1481
- if (fullPath.includes('stripe/webhook')) {
1482
- console.log('[CF Worker] Stripe webhook handler result:', {
1483
- sent: res._sent,
1484
- statusCode: res._statusCode,
1485
- body:
1486
- typeof res._body === 'string' ? res._body.substring(0, 200) : JSON.stringify(res._body)?.substring(0, 200),
1487
- });
1488
- }
1489
-
1490
- // Ensure all async push() jobs complete before returning
1491
- await flushPendingJobs();
1492
-
1493
- if (!res._sent) {
1494
- console.warn(`[CF Worker] No response sent for ${route.method} ${fullPath}`);
1495
- return c.json({ error: 'No response from handler' }, 500);
1496
- }
1497
-
1498
- return expressResToResponse(res);
1499
- });
1500
- }
1501
- }
1502
1320
 
1503
1321
  // === Shared env setup for scheduled/queue handlers ===
1504
1322
  function setupEnv(env: Env) {
@@ -1509,21 +1327,13 @@ function setupEnv(env: Env) {
1509
1327
  // Queue consumer and cron: NOT HTTP context — createEvent must block (listeners complete before ack/return)
1510
1328
  (globalThis as any).__cfHttpContext__ = false;
1511
1329
  setDB(env.DB.withSession('first-primary'));
1512
- if (env.JOB_QUEUE) setCFQueue(env.JOB_QUEUE);
1513
- ensureModelsInit();
1514
-
1515
- if (typeof process !== 'undefined' && process.env) {
1516
- if (env.APP_URL) {
1517
- process.env.APP_URL = env.APP_URL;
1518
- process.env.BLOCKLET_APP_URL = env.APP_URL;
1519
- }
1520
- if (env.APP_PID) {
1521
- process.env.BLOCKLET_APP_PID = env.APP_PID;
1522
- process.env.BLOCKLET_APP_ID = env.APP_PID;
1523
- }
1524
- if (env.APP_NAME) process.env.BLOCKLET_APP_NAME = env.APP_NAME;
1525
- process.env.BLOCKLET_MODE = 'production';
1526
- }
1330
+ // Phase 12c: assemble the service so the CONFIG SLOT (setCoreConfig) is
1331
+ // authoritative on the scheduled()/queue() paths too. envToPaymentCoreConfig
1332
+ // carries BLOCKLET_MODE + every key the old setupEnv process.env mirror set,
1333
+ // and the core reads them only through libs/env.ts readConfig — so the worker
1334
+ // env mirror is deleted (no more process.env fallback). Idempotent + cached;
1335
+ // also binds models, so ensureModelsInit() is subsumed.
1336
+ ensurePaymentService(env);
1527
1337
 
1528
1338
  // Security init is handled in the per-request middleware (first request only)
1529
1339
  }
@@ -1531,7 +1341,6 @@ function setupEnv(env: Env) {
1531
1341
  // === Export ===
1532
1342
  export default {
1533
1343
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1534
- setWaitUntil((p) => ctx.waitUntil(p));
1535
1344
  // Expose waitUntil globally for createEvent to use in HTTP context
1536
1345
  (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1537
1346
 
@@ -1542,7 +1351,8 @@ export default {
1542
1351
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
1543
1352
  setupEnv(env);
1544
1353
  ensureCronsInit();
1545
- setWaitUntil((p) => ctx.waitUntil(p));
1354
+ await ensureTenantBackfill();
1355
+ (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1546
1356
 
1547
1357
  console.log('Scheduled event:', event.cron, 'scheduledTime:', event.scheduledTime);
1548
1358
 
@@ -1555,54 +1365,57 @@ export default {
1555
1365
  // execution crosses a minute boundary, matching on wall-clock would miss
1556
1366
  // exact-minute crons like "0 1 * * * *".
1557
1367
  await cronInstance.runAll(new Date(event.scheduledTime));
1558
- await runAllScheduledJobs();
1559
- await flushPendingJobs();
1368
+ // Phase 12b: the workerd trigger for the node queue engine's due-job
1369
+ // re-dispatch. dispatchDueJobs() runs each scheduled queue's redispatchDue()
1370
+ // (the same cancel-then-replay-from-store body the node loop() runs on a
1371
+ // timer), then flushQueueWork() drains the re-pushed executions before the
1372
+ // isolate freezes.
1373
+ await dispatchDueJobs();
1374
+ await flushQueueWork();
1560
1375
  },
1561
1376
 
1562
- // CF Queue consumer — processes jobs sent by push() in the queue shim
1377
+ // CF Queue consumer — resolves the SAME core queue handle by name and runs
1378
+ // the job through the engine's runJobWithTenant/onJob/retry/cancel path. Under
1379
+ // the canonical node-engine model immediate jobs execute in-isolate via fastq
1380
+ // (nothing is sent to CF Queue), so this consumer is the back-compat path for
1381
+ // any message still on JOB_QUEUE — it must never fork the engine.
1563
1382
  async queue(
1564
1383
  batch: MessageBatch<{ queueName: string; jobId: string; job: any; persist?: boolean }>,
1565
1384
  env: Env,
1566
- ctx: ExecutionContext,
1385
+ ctx: ExecutionContext
1567
1386
  ) {
1568
1387
  setupEnv(env);
1569
- setWaitUntil((p) => ctx.waitUntil(p));
1388
+ (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1570
1389
 
1571
1390
  console.log(`[queue:consumer] Received batch of ${batch.messages.length} messages`);
1572
1391
 
1573
1392
  for (const msg of batch.messages) {
1574
- const { queueName, jobId, job, persist } = msg.body;
1393
+ const { queueName, jobId, job } = msg.body;
1575
1394
 
1576
- const handler = getHandler(queueName);
1395
+ const handle = getQueueHandler(queueName);
1577
1396
 
1578
- if (!handler) {
1579
- console.error(`[queue:consumer] No handler registered for queue "${queueName}", acking message`);
1397
+ if (!handle) {
1398
+ console.error(`[queue:consumer] No core queue registered for "${queueName}", acking message`);
1580
1399
  msg.ack();
1581
1400
  continue;
1582
1401
  }
1583
1402
 
1584
- // persist defaults to true for backward compatibility and for direct
1585
- // push() immediate jobs (where addJob wrote the row, so we must delete
1586
- // it after onJob succeeds).
1587
- //
1588
- // Scheduled dispatches from runAllScheduledJobs set persist=false —
1589
- // the dispatcher already deleted the D1 row before sending, and
1590
- // onJob may have re-pushed a fresh row with the same id; deleting
1591
- // again would wipe out that new row.
1592
- const shouldPersist = persist !== false;
1593
-
1594
1403
  try {
1595
- console.log(`[queue:consumer] Processing ${queueName}:${jobId} (persist=${shouldPersist})`);
1596
- await handler.executeJob(jobId, job, shouldPersist);
1404
+ console.log(`[queue:consumer] Processing ${queueName}:${jobId}`);
1405
+ // persist:false — the row already exists in D1 (the original push wrote
1406
+ // it); re-running inline through pushAndWait executes onJob and clears
1407
+ // the row on success without a duplicate-addJob that would hang.
1408
+ // eslint-disable-next-line no-await-in-loop
1409
+ await handle.pushAndWait({ job, id: jobId, persist: false });
1597
1410
  console.log(`[queue:consumer] Completed ${queueName}:${jobId}`);
1598
1411
  msg.ack();
1599
1412
  } catch (err: any) {
1600
1413
  console.error(`[queue:consumer] Failed ${queueName}:${jobId}:`, err?.message || err);
1601
- // Don't retry via CF Queue — job is in D1, cron will re-dispatch
1414
+ // Don't retry via CF Queue — job is in D1, scheduled() will re-dispatch
1602
1415
  msg.ack();
1603
1416
  }
1604
1417
  }
1605
1418
 
1606
- await flushPendingJobs();
1419
+ await flushQueueWork();
1607
1420
  },
1608
1421
  };