payment-kit 1.29.1 → 1.29.2

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 (310) 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 +36 -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 +27 -24
  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/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. 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): the worker's /api business surface now comes from
23
+ // the embedded payment service factory instead of a direct routes import. The
24
+ // factory performs assembly (config slot authoritative via setCoreConfig + model
25
+ // initialize), and exposes `http.resourceRoutes` — the resource routers only,
26
+ // no node app shell, no DID-Connect handlers (the worker registers those through
27
+ // its own Hono `attachDIDConnectRoutes`). The factory's lazy `handler` getter is
28
+ // never touched here, so the node-only app shell never runs under the shim.
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';
@@ -53,6 +68,10 @@ import { withD1Retry } from './shims/sequelize-d1/retry';
53
68
  // DID Connect: login routes proxied to blocklet-service, business actions (pay/subscribe) handled locally
54
69
  import { attachDIDConnectRoutes } from './did-connect-auth';
55
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).
73
+ import { tenantMiddleware } from './tenant-middleware';
74
+
56
75
  FetchRequest.registerGetUrl(async (req: FetchRequest) => {
57
76
  const resp = await fetch(req.url, {
58
77
  method: req.method || 'GET',
@@ -204,6 +223,93 @@ function ensureModelsInit() {
204
223
  }
205
224
  }
206
225
 
226
+ // Phase 12a/12c: build the explicit PaymentCoreConfig from CF env. These keys are
227
+ // read only by the core (via the libs/env.ts readConfig boundary), never by a
228
+ // worker shim, so the factory config slot carries them. Phase 12c removed the
229
+ // last `process.env` mirror: the HTTP path (buildApp) and the scheduled()/queue()
230
+ // path (setupEnv) both call ensurePaymentService(env) -> setCoreConfig, so the
231
+ // config slot is authoritative on every path and no process.env fallback remains.
232
+ function envToPaymentCoreConfig(env: Env): Record<string, any> {
233
+ const config: Record<string, any> = { BLOCKLET_MODE: 'production' };
234
+ if (env.APP_PID) {
235
+ config.BLOCKLET_APP_PID = env.APP_PID;
236
+ config.BLOCKLET_APP_ID = env.APP_PID;
237
+ }
238
+ if (env.APP_URL) {
239
+ config.APP_URL = env.APP_URL;
240
+ config.BLOCKLET_APP_URL = env.APP_URL;
241
+ }
242
+ if (env.APP_NAME) config.BLOCKLET_APP_NAME = env.APP_NAME;
243
+ // Stripe webhook secret: env override for signature verification (DB value may
244
+ // be empty if not configured in the original Blocklet Server).
245
+ if (env.STRIPE_WEBHOOK_SECRET) config.STRIPE_WEBHOOK_SECRET = env.STRIPE_WEBHOOK_SECRET;
246
+ if (env.PAYMENT_CHANGE_LOCKED_PRICE) config.PAYMENT_CHANGE_LOCKED_PRICE = env.PAYMENT_CHANGE_LOCKED_PRICE;
247
+ if (env.SHORT_URL_DOMAIN) config.SHORT_URL_DOMAIN = env.SHORT_URL_DOMAIN;
248
+ // Audience for the Pub/Sub OIDC JWT wrapping Google Play RTDN webhooks; without
249
+ // it googlePlayEndpoint() falls back to the workers.dev origin -> audience mismatch.
250
+ if (env.GOOGLE_PLAY_WEBHOOK_URL) config.GOOGLE_PLAY_WEBHOOK_URL = env.GOOGLE_PLAY_WEBHOOK_URL;
251
+ // Pub/Sub sender binding — without it the google_play webhook can't enforce the
252
+ // push service account on CF and would fail open (PR #1381 P1).
253
+ if (env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT)
254
+ config.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT = env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;
255
+ if (env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER)
256
+ config.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER = env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER;
257
+ return config;
258
+ }
259
+
260
+ // Phase 12a: assemble the embedded payment service once per isolate. The factory
261
+ // performs config/db assembly (setCoreConfig + initialize), so models are bound
262
+ // here and ensureModelsInit() becomes a no-op on the HTTP path. Slots beyond
263
+ // config/db are intentionally omitted in 12a (defaults preserve current worker
264
+ // behavior: single-mode default tenant resolved from BLOCKLET_APP_PID); richer
265
+ // slot bridging is 12b/12c.
266
+ let paymentService: PaymentCoreService | null = null;
267
+ function ensurePaymentService(env: Env): PaymentCoreService {
268
+ if (paymentService) return paymentService;
269
+ const sequelize = new Sequelize();
270
+ const svc = createEmbeddedPaymentService({
271
+ config: envToPaymentCoreConfig(env),
272
+ db: { sequelize },
273
+ });
274
+ // Phase 12c HARD GATE: the CF worker is a host adapter. It mounts only
275
+ // svc.http.resourceRoutes and registers DID-Connect through its own Hono
276
+ // shell — it must NEVER touch svc.handler, whose lazy getter builds the
277
+ // node-only Express app shell (app.set / static / connect attach) that does
278
+ // not belong under workerd. Trap the access so a future regression fails loud
279
+ // at runtime instead of silently constructing the node app in the isolate.
280
+ paymentService = new Proxy(svc, {
281
+ get(target, prop, receiver) {
282
+ if (prop === 'handler') {
283
+ throw new Error(
284
+ '[worker hard-gate] svc.handler is forbidden in the CF worker — mount svc.http.resourceRoutes instead'
285
+ );
286
+ }
287
+ return Reflect.get(target, prop, receiver);
288
+ },
289
+ });
290
+ modelsInitialized = true; // the factory called initialize(sequelize)
291
+ return paymentService;
292
+ }
293
+
294
+ // Phase 2 (W1-1b): static D1 migration SQL cannot know the deployment app
295
+ // DID, so the instance_did backfill + unique-key rebuilds run through the
296
+ // shared runtime routine. Idempotent (NULL-only updates, DDL checks), so a
297
+ // once-per-isolate guard just avoids 38 no-op UPDATEs on every cron tick.
298
+ let tenantBackfillDone = false;
299
+ async function ensureTenantBackfill() {
300
+ if (tenantBackfillDone) return;
301
+ try {
302
+ const { runTenantBackfill } = await import('../api/src/store/tenant-backfill');
303
+ const result = await runTenantBackfill(new Sequelize() as any);
304
+ const touched = Object.entries(result.backfilled).filter(([, n]) => n > 0);
305
+ if (touched.length) console.log('[tenant-backfill] backfilled:', JSON.stringify(Object.fromEntries(touched)));
306
+ tenantBackfillDone = true;
307
+ } catch (e: any) {
308
+ // fail-loud but non-fatal: crons still run; next tick retries
309
+ console.error('[tenant-backfill] failed:', e?.message || e);
310
+ }
311
+ }
312
+
207
313
  function ensureCronsInit() {
208
314
  if (!cronsInitialized) {
209
315
  try {
@@ -234,6 +340,12 @@ function buildApp(env: Env): Hono<HonoEnv> {
234
340
  return cachedApp;
235
341
  }
236
342
 
343
+ // Phase 12a: assemble the embedded payment service (config/db/slots) and take
344
+ // its resource routes for mounting. Only `http.resourceRoutes` is read — the
345
+ // node-only `handler` getter is never touched, so the express app shell + the
346
+ // DID-Connect handlers it carries never run under the workerd shim.
347
+ const service = ensurePaymentService(env);
348
+
237
349
  const app = new Hono<HonoEnv>();
238
350
 
239
351
  // CORS
@@ -255,43 +367,17 @@ function buildApp(env: Env): Hono<HonoEnv> {
255
367
  // In queue consumer/cron, this flag is absent — createEvent uses __cfPendingJobs__ (blocking).
256
368
  (globalThis as any).__cfHttpContext__ = true;
257
369
  setDB(withD1Retry(c.env.DB.withSession('first-primary')));
258
- if (c.env.JOB_QUEUE) setCFQueue(c.env.JOB_QUEUE);
259
370
  ensureModelsInit();
260
371
 
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
- }
372
+ // Phase 12a: CF env now flows through the factory config slot (assembled in
373
+ // ensurePaymentService -> setCoreConfig, made authoritative via the
374
+ // libs/env.ts readConfig boundary). The per-request process.env mirror that
375
+ // lived here is gone. This defensive idempotent call guarantees the config is
376
+ // wired for this isolate regardless of Hono app-cache state; it is a no-op
377
+ // after the first call. (Phase 12c: scheduled()/queue() call the same
378
+ // ensurePaymentService via setupEnv, so the config slot is authoritative
379
+ // there too — no process.env mirror on any path.)
380
+ ensurePaymentService(c.env);
295
381
 
296
382
  // Register Stripe key decrypt overrides from env vars
297
383
  // Fetch EK from AUTH_SERVICE and initialize decrypt capability (first request only)
@@ -351,10 +437,7 @@ function buildApp(env: Env): Hono<HonoEnv> {
351
437
  // --- Append Server-Timing header ---
352
438
  const totalDur = Math.round(performance.now() - t0);
353
439
  const d1 = getD1Timing();
354
- const timings = [
355
- `total;dur=${totalDur}`,
356
- `auth;dur=${authDur};desc="${authSource}"`,
357
- ];
440
+ const timings = [`total;dur=${totalDur}`, `auth;dur=${authDur};desc="${authSource}"`];
358
441
  if (d1.queries > 0) {
359
442
  timings.push(`db;dur=${Math.round(d1.wallMs)};desc="${d1.queries}q ${d1.rowsRead}r"`);
360
443
  if (d1.sqlMs > 0) timings.push(`db_sql;dur=${Math.round(d1.sqlMs)}`);
@@ -366,6 +449,12 @@ function buildApp(env: Env): Hono<HonoEnv> {
366
449
  c.res.headers.append('Server-Timing', timings.join(', '));
367
450
  });
368
451
 
452
+ // Tenant context: resolve Host -> tenant (single point) and wrap the request
453
+ // chain in context.withTenant so every TenantModel query is scoped. Scoped to
454
+ // /api/* so /health and static assets never require a tenant. multi mode:
455
+ // unknown/missing Host -> 400 fail-closed (no default-tenant fallback).
456
+ app.use('/api/*', tenantMiddleware());
457
+
369
458
  // Health check
370
459
  app.get('/health', (c) => c.json({ status: 'ok' }));
371
460
 
@@ -608,10 +697,11 @@ function buildApp(env: Env): Hono<HonoEnv> {
608
697
  // Notification unread count
609
698
  app.get('/api/notifications/unread-count', (c) => c.json({ unReadCount: 0 }));
610
699
 
611
- // Manually trigger job dispatch (same as cron's runAllScheduledJobs)
700
+ // Manually trigger job dispatch (same as cron's scheduled() due-dispatch)
612
701
  app.post('/api/__dev__/dispatch-jobs', async (c) => {
613
- const names = getAllHandlerNames();
614
- const result = await runAllScheduledJobs();
702
+ const names = getAllQueueNames();
703
+ const result = await dispatchDueJobs();
704
+ await flushQueueWork();
615
705
  return c.json({ handlers: names, ...result });
616
706
  });
617
707
 
@@ -640,8 +730,11 @@ function buildApp(env: Env): Hono<HonoEnv> {
640
730
  // for creating PaymentMethods is owner-gated, and staging has no owner yet
641
731
  // (Pengfei's blocklet-service role is `member`). Gated by PAYMENT_LIVEMODE
642
732
  // === 'false' so this only ever touches testmode data.
643
- // === Express-to-Hono Route Adapter ===
644
- mountExpressRoutes(app, '/api', expressRoutes);
733
+ // Phase 4 (express→hono): the resource routes are a native hono app
734
+ // (service.http.resourceRoutes) mounted by the /api/* dispatcher registered
735
+ // AFTER the worker's own /api/__dev__ routes (see below, replacing the old
736
+ // "not implemented" catch-all). The old express-compat mountExpressRoutes shim
737
+ // is gone.
645
738
 
646
739
  // Dev endpoint: D1 admin operations
647
740
  // Test CF Queue send directly
@@ -986,9 +1079,33 @@ function buildApp(env: Env): Hono<HonoEnv> {
986
1079
  }
987
1080
  });
988
1081
 
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);
1082
+ // Phase 4 (express→hono): native hono resource routes. Registered AFTER the
1083
+ // worker's own /api/__dev__ routes so those match first; this dispatcher then
1084
+ // handles every other /api/* path (returning resourceRoutes' own 404 for an
1085
+ // unknown route). The RPC-resolved caller identity is injected as x-user-*
1086
+ // request headers — the native authenticate() reads those — and any
1087
+ // client-supplied x-user-* is overwritten/stripped so it can never be forged
1088
+ // (component auth via x-component-sig is left intact: it is verified
1089
+ // cryptographically downstream). The worker already provides cors + tenant, so
1090
+ // resourceRoutes uses a LITE app-shell (xss only); flushQueueWork() preserves
1091
+ // the workerd flush-before-response the old shim ran after each handler.
1092
+ const resourceRoutes = service.http.resourceRoutes as Hono;
1093
+ const USER_HEADERS = ['x-user-did', 'x-user-role', 'x-user-provider', 'x-user-fullname', 'x-user-wallet-os'];
1094
+ app.all('/api/*', async (c) => {
1095
+ const headers = new Headers(c.req.raw.headers);
1096
+ for (const h of USER_HEADERS) headers.delete(h); // never trust a client-supplied identity header
1097
+ const caller: CallerIdentityDTO | null = c.get('caller');
1098
+ if (caller) {
1099
+ const canonicalDid = caller.did?.startsWith('did:abt:') ? caller.did : `did:abt:${caller.did}`;
1100
+ headers.set('x-user-did', canonicalDid);
1101
+ headers.set('x-user-role', `blocklet-${caller.role || 'guest'}`);
1102
+ headers.set('x-user-provider', caller.authMethod === 'access-key' ? 'access-key' : caller.authMethod || 'wallet');
1103
+ headers.set('x-user-fullname', encodeURIComponent(caller.displayName || ''));
1104
+ headers.set('x-user-wallet-os', '');
1105
+ }
1106
+ const res = await resourceRoutes.fetch(new Request(c.req.raw, { headers }), c.env, c.executionCtx);
1107
+ await flushQueueWork(); // drain workerd deferred queue work before responding
1108
+ return res;
992
1109
  });
993
1110
 
994
1111
  // === Media Kit Proxy ===
@@ -1158,347 +1275,6 @@ function buildApp(env: Env): Hono<HonoEnv> {
1158
1275
  return app;
1159
1276
  }
1160
1277
 
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
1278
 
1503
1279
  // === Shared env setup for scheduled/queue handlers ===
1504
1280
  function setupEnv(env: Env) {
@@ -1509,21 +1285,13 @@ function setupEnv(env: Env) {
1509
1285
  // Queue consumer and cron: NOT HTTP context — createEvent must block (listeners complete before ack/return)
1510
1286
  (globalThis as any).__cfHttpContext__ = false;
1511
1287
  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
- }
1288
+ // Phase 12c: assemble the service so the CONFIG SLOT (setCoreConfig) is
1289
+ // authoritative on the scheduled()/queue() paths too. envToPaymentCoreConfig
1290
+ // carries BLOCKLET_MODE + every key the old setupEnv process.env mirror set,
1291
+ // and the core reads them only through libs/env.ts readConfig — so the worker
1292
+ // env mirror is deleted (no more process.env fallback). Idempotent + cached;
1293
+ // also binds models, so ensureModelsInit() is subsumed.
1294
+ ensurePaymentService(env);
1527
1295
 
1528
1296
  // Security init is handled in the per-request middleware (first request only)
1529
1297
  }
@@ -1531,7 +1299,6 @@ function setupEnv(env: Env) {
1531
1299
  // === Export ===
1532
1300
  export default {
1533
1301
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1534
- setWaitUntil((p) => ctx.waitUntil(p));
1535
1302
  // Expose waitUntil globally for createEvent to use in HTTP context
1536
1303
  (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1537
1304
 
@@ -1542,7 +1309,8 @@ export default {
1542
1309
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
1543
1310
  setupEnv(env);
1544
1311
  ensureCronsInit();
1545
- setWaitUntil((p) => ctx.waitUntil(p));
1312
+ await ensureTenantBackfill();
1313
+ (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1546
1314
 
1547
1315
  console.log('Scheduled event:', event.cron, 'scheduledTime:', event.scheduledTime);
1548
1316
 
@@ -1555,54 +1323,57 @@ export default {
1555
1323
  // execution crosses a minute boundary, matching on wall-clock would miss
1556
1324
  // exact-minute crons like "0 1 * * * *".
1557
1325
  await cronInstance.runAll(new Date(event.scheduledTime));
1558
- await runAllScheduledJobs();
1559
- await flushPendingJobs();
1326
+ // Phase 12b: the workerd trigger for the node queue engine's due-job
1327
+ // re-dispatch. dispatchDueJobs() runs each scheduled queue's redispatchDue()
1328
+ // (the same cancel-then-replay-from-store body the node loop() runs on a
1329
+ // timer), then flushQueueWork() drains the re-pushed executions before the
1330
+ // isolate freezes.
1331
+ await dispatchDueJobs();
1332
+ await flushQueueWork();
1560
1333
  },
1561
1334
 
1562
- // CF Queue consumer — processes jobs sent by push() in the queue shim
1335
+ // CF Queue consumer — resolves the SAME core queue handle by name and runs
1336
+ // the job through the engine's runJobWithTenant/onJob/retry/cancel path. Under
1337
+ // the canonical node-engine model immediate jobs execute in-isolate via fastq
1338
+ // (nothing is sent to CF Queue), so this consumer is the back-compat path for
1339
+ // any message still on JOB_QUEUE — it must never fork the engine.
1563
1340
  async queue(
1564
1341
  batch: MessageBatch<{ queueName: string; jobId: string; job: any; persist?: boolean }>,
1565
1342
  env: Env,
1566
- ctx: ExecutionContext,
1343
+ ctx: ExecutionContext
1567
1344
  ) {
1568
1345
  setupEnv(env);
1569
- setWaitUntil((p) => ctx.waitUntil(p));
1346
+ (globalThis as any).__cfWaitUntil__ = (p: Promise<any>) => ctx.waitUntil(p);
1570
1347
 
1571
1348
  console.log(`[queue:consumer] Received batch of ${batch.messages.length} messages`);
1572
1349
 
1573
1350
  for (const msg of batch.messages) {
1574
- const { queueName, jobId, job, persist } = msg.body;
1351
+ const { queueName, jobId, job } = msg.body;
1575
1352
 
1576
- const handler = getHandler(queueName);
1353
+ const handle = getQueueHandler(queueName);
1577
1354
 
1578
- if (!handler) {
1579
- console.error(`[queue:consumer] No handler registered for queue "${queueName}", acking message`);
1355
+ if (!handle) {
1356
+ console.error(`[queue:consumer] No core queue registered for "${queueName}", acking message`);
1580
1357
  msg.ack();
1581
1358
  continue;
1582
1359
  }
1583
1360
 
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
1361
  try {
1595
- console.log(`[queue:consumer] Processing ${queueName}:${jobId} (persist=${shouldPersist})`);
1596
- await handler.executeJob(jobId, job, shouldPersist);
1362
+ console.log(`[queue:consumer] Processing ${queueName}:${jobId}`);
1363
+ // persist:false — the row already exists in D1 (the original push wrote
1364
+ // it); re-running inline through pushAndWait executes onJob and clears
1365
+ // the row on success without a duplicate-addJob that would hang.
1366
+ // eslint-disable-next-line no-await-in-loop
1367
+ await handle.pushAndWait({ job, id: jobId, persist: false });
1597
1368
  console.log(`[queue:consumer] Completed ${queueName}:${jobId}`);
1598
1369
  msg.ack();
1599
1370
  } catch (err: any) {
1600
1371
  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
1372
+ // Don't retry via CF Queue — job is in D1, scheduled() will re-dispatch
1602
1373
  msg.ack();
1603
1374
  }
1604
1375
  }
1605
1376
 
1606
- await flushPendingJobs();
1377
+ await flushQueueWork();
1607
1378
  },
1608
1379
  };