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
@@ -6,15 +6,103 @@ import fastq from 'fastq';
6
6
  import { nanoid } from 'nanoid';
7
7
 
8
8
  import { AsyncLocalStorage } from 'async_hooks';
9
+ import { isTestEnv } from '../env';
9
10
  import logger from '../logger';
10
- import { sleep, tryWithTimeout } from '../util';
11
+ import { context, withTenant } from '../context';
12
+ import {
13
+ TENANT_CONTEXT_MISSING,
14
+ TENANT_MISMATCH,
15
+ TenantError,
16
+ assertValidInstanceDid,
17
+ getDefaultInstanceDid,
18
+ getTenantMode,
19
+ resolveRowTenant,
20
+ } from '../tenant';
21
+ import { tryWithTimeout } from '../util';
11
22
  import dayjs from '../dayjs';
12
23
  import createQueueStore from './store';
24
+ import { registerQueue, getQueueRuntimeMode, trackPending } from './runtime';
13
25
  import { Job } from '../../store/models/job';
14
26
  import { sequelize } from '../../store/sequelize';
15
27
 
16
28
  const CANCELLED = '__CANCELLED__';
17
- const MIN_DELAY = process.env.NODE_ENV === 'test' ? 2 : 8;
29
+ // lazy so the injected config (set after import) is honored at call time
30
+ const minDelay = (): number => (isTestEnv() ? 2 : 8);
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Phase 5 (W1-4a): generic queue tenant layer.
34
+ // Every queue gets this uniformly — see docs/arc-integration/planning/w1-w2/
35
+ // queue-matrix.md for the per-queue conclusions built on top of it.
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Stamp the active tenant into a job payload at enqueue time.
40
+ * - payload already carries instance_did: validated (and in multi mode it
41
+ * must be a legal DID — a forged/illegal value is refused at the gate)
42
+ * - otherwise: the context tenant (single mode = app DID); multi mode
43
+ * without context -> the push itself is rejected (fail-closed)
44
+ */
45
+ function injectJobTenant(job: any): any {
46
+ if (!job || typeof job !== 'object') return job;
47
+ if (job.instance_did) {
48
+ assertValidInstanceDid(job.instance_did);
49
+ return job;
50
+ }
51
+ return { ...job, instance_did: context.getInstanceDid() };
52
+ }
53
+
54
+ /**
55
+ * Run a job handler inside its payload tenant. Legacy jobs persisted before
56
+ * Phase 5 have no instance_did: single mode falls back to the deployment
57
+ * default, multi mode refuses permanently (non-retryable + structured alert).
58
+ */
59
+ // Warm the tenant identity then run the handler — the queue analogue of the HTTP
60
+ // contextMiddleware warm. Signing queues (payment/refund/payout/...) access the
61
+ // business wallet synchronously; warming inside the tenant span makes `wallet`/
62
+ // `ethWallet` resolve to the job's tenant (no-op on blocklet-server).
63
+ async function warmThenRun<T>(onJob: (job: T) => Promise<any>, job: T): Promise<any> {
64
+ const { warmTenantIdentity } =
65
+ // eslint-disable-next-line global-require
66
+ require('../did-connect/tenant-identity') as typeof import('../did-connect/tenant-identity');
67
+ await warmTenantIdentity();
68
+ return onJob(job);
69
+ }
70
+
71
+ function runJobWithTenant<T>(job: any, onJob: (job: T) => Promise<any>): Promise<any> {
72
+ const tenant = job?.instance_did;
73
+ if (tenant) {
74
+ return withTenant(tenant, () => warmThenRun(onJob, job));
75
+ }
76
+ if (getTenantMode() === 'single') {
77
+ return withTenant(getDefaultInstanceDid(), () => warmThenRun(onJob, job));
78
+ }
79
+ const err = new TenantError(TENANT_CONTEXT_MISSING, 'legacy job without tenant refused in multi mode');
80
+ (err as any).nonRetryable = true;
81
+ logger.error('[queue] legacy job without tenant refused', { code: TENANT_CONTEXT_MISSING, job });
82
+ return Promise.reject(err);
83
+ }
84
+
85
+ /**
86
+ * Handler-entry invariant for object-bound queues: the object loaded by id
87
+ * must belong to the payload tenant the handler is running under. A forged
88
+ * payload (tenant A, object of B) dies here permanently — nothing executes.
89
+ */
90
+ export function assertJobObjectTenant(row: { instance_did?: string | null } | null | undefined): void {
91
+ if (!row) return; // absent objects are the handler's own no-op/warn path
92
+ const rowTenant = resolveRowTenant(row);
93
+ const jobTenant = context.getInstanceDid();
94
+ if (rowTenant !== jobTenant) {
95
+ // Phase 4 (W1-3): emit a structured violation alert BEFORE throwing, so a
96
+ // forged/mixed cross-tenant job is observable in logs (same dedicated code
97
+ // as the W1 §4.1 event-rejection path) even if the queue swallows the
98
+ // thrown error. Loading the object cross-tenant (systemFindByPk) is what
99
+ // makes this violation visible instead of folding into a scoped null.
100
+ logger.error('TENANT_VIOLATION', { code: TENANT_MISMATCH, rowTenant, jobTenant });
101
+ const err = new TenantError(TENANT_MISMATCH, `job object belongs to ${rowTenant} but payload says ${jobTenant}`);
102
+ (err as any).nonRetryable = true;
103
+ throw err;
104
+ }
105
+ }
18
106
 
19
107
  type QueueOptions<T> = {
20
108
  id?: (job: T) => string;
@@ -46,6 +134,18 @@ type PushParams<T> = {
46
134
  delay?: number; // in seconds
47
135
  runAt?: number; // unix timestamp in seconds
48
136
  skipDuplicateCheck?: boolean; // Q1: skip addJob's findOne when caller guarantees no duplicate
137
+ /**
138
+ * Internal: re-delivery of a row already persisted in the jobs table
139
+ * (startup/scheduled recovery). Skips the push-side tenant gate so legacy
140
+ * pre-tenant rows reach runJobWithTenant, which owns the legacy strategy
141
+ * (single -> default tenant, multi -> structured non-retryable refusal).
142
+ *
143
+ * NEVER set this from application code: it bypasses the enqueue tenant
144
+ * gate. Object-bound handlers are still protected by
145
+ * assertJobObjectTenant, but system-operation queues (no object load)
146
+ * would run under whatever tenant the payload claims.
147
+ */
148
+ fromStore?: boolean;
49
149
  };
50
150
 
51
151
  export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
@@ -98,7 +198,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
98
198
  }
99
199
 
100
200
  try {
101
- const result = await tryWithTimeout(() => onJob(job), maxTimeout);
201
+ const result = await tryWithTimeout(() => runJobWithTenant(job, onJob), maxTimeout);
102
202
  logger.info('job finished', { id, result });
103
203
  cb(null, result);
104
204
  } catch (err) {
@@ -108,7 +208,15 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
108
208
  // @ts-ignore
109
209
  }, concurrency);
110
210
 
111
- const push = ({ job, id, persist = true, delay, runAt, skipDuplicateCheck = false }: PushParams<T>) => {
211
+ const push = ({
212
+ job: rawJob,
213
+ id,
214
+ persist = true,
215
+ delay,
216
+ runAt,
217
+ skipDuplicateCheck = false,
218
+ fromStore = false,
219
+ }: PushParams<T>) => {
112
220
  const jobEvents = new EventEmitter();
113
221
  const emit = (e: string, data: any) => {
114
222
  queueEvents.emit(e, data);
@@ -116,20 +224,24 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
116
224
  };
117
225
  const now = dayjs().unix();
118
226
 
119
- if (!job) {
227
+ if (!rawJob) {
120
228
  throw new Error('Can not queue empty job');
121
229
  }
122
230
 
231
+ // fail-closed gate: every NEW payload carries its tenant from here on;
232
+ // store re-deliveries pass through so the execution-side legacy strategy applies
233
+ const job = (fromStore ? rawJob : injectJobTenant(rawJob)) as T;
234
+
123
235
  const jobId = getJobId(id, job);
124
236
 
125
- if ((delay && delay >= MIN_DELAY) || (runAt && runAt > now)) {
237
+ if ((delay && delay >= minDelay()) || (runAt && runAt > now)) {
126
238
  if (!enableScheduledJob) {
127
239
  throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
128
240
  }
129
241
 
130
242
  // 这里不是精确的 delay, 延迟的时间太短没有意义,所以这里限制了最小 delay
131
- if (delay && delay < MIN_DELAY) {
132
- throw new Error(`minimum delay is ${MIN_DELAY}s`);
243
+ if (delay && delay < minDelay()) {
244
+ throw new Error(`minimum delay is ${minDelay()}s`);
133
245
  }
134
246
 
135
247
  const attrs: { delay?: number; will_run_at?: number } = {};
@@ -221,12 +333,31 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
221
333
  queue.push({ id: jobId, job, persist }, onJobComplete);
222
334
  });
223
335
 
336
+ // Phase 12b: workerd flush. Track this immediate execution so a frozen-
337
+ // isolate host can drain it before returning the response — otherwise the
338
+ // fire-and-forget setImmediate/fastq work is dropped when the isolate is
339
+ // torn down. Tracked ONLY on a workerd host: zero overhead and no extra
340
+ // listeners on a long-lived node process.
341
+ let resolveSettled: () => void = () => {};
342
+ if (getQueueRuntimeMode() === 'workerd') {
343
+ const settled = new Promise<void>((resolve) => {
344
+ resolveSettled = resolve;
345
+ });
346
+ jobEvents.on('finished', resolveSettled);
347
+ jobEvents.on('failed', resolveSettled);
348
+ jobEvents.on('cancelled', resolveSettled);
349
+ trackPending(settled);
350
+ }
351
+
224
352
  if (persist) {
225
353
  store
226
354
  .addJob(jobId, job, {}, skipDuplicateCheck)
227
355
  .then(queueJob)
228
356
  .catch((err) => {
229
357
  logger.error('Can not add job to store', { error: err });
358
+ // never enqueued (e.g. duplicate id) → emits nothing; release the
359
+ // workerd flush tracker so flushQueueWork() cannot hang on it.
360
+ resolveSettled();
230
361
  });
231
362
  } else {
232
363
  queueJob();
@@ -246,7 +377,11 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
246
377
 
247
378
  const job = push(params);
248
379
  job.on('finished', (data: { id: string; job: T; result: any }) => resolve(data));
249
- job.on('canceled', (data: { id: string; job: T }) => resolve(data));
380
+ // Phase 12b: the engine emits 'cancelled' (two L); the old 'canceled'
381
+ // listener never fired, so a cancelled job left pushAndWait hanging
382
+ // forever — and the CF queue() consumer now runs through pushAndWait,
383
+ // where that hang would stall the queue batch until CF kills it.
384
+ job.on('cancelled', (data: { id: string; job: T }) => resolve(data));
250
385
  job.on('failed', (data: { id: string; job: T; error: Error }) => reject(data));
251
386
  } catch (err) {
252
387
  reject(err);
@@ -296,60 +431,128 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
296
431
  return updatedJob;
297
432
  };
298
433
 
299
- // Populate the queue on startup
300
- process.nextTick(async () => {
301
- try {
302
- if (!Job.isInitialized()) {
303
- Job.initialize(sequelize);
434
+ // Populate the queue on startup. Phase 12b: this is a long-lived-process
435
+ // recovery — re-queue persisted immediate rows on boot. A workerd host spins
436
+ // up a fresh isolate per request and MUST NOT re-run every persisted row on
437
+ // each cold start; it drives due-job re-dispatch through dispatchDueJobs()
438
+ // from scheduled() instead. So the boot recovery is node-mode only.
439
+ if (getQueueRuntimeMode() === 'node') {
440
+ process.nextTick(async () => {
441
+ try {
442
+ if (!Job.isInitialized()) {
443
+ Job.initialize(sequelize);
444
+ }
445
+ const jobs = await store.getJobs();
446
+ jobs.forEach((x) => {
447
+ if (x.job && x.id) {
448
+ try {
449
+ push({ job: x.job, id: x.id, persist: false, fromStore: true });
450
+ } catch (err: any) {
451
+ // one bad row must never break recovery of the rest
452
+ logger.error('failed to re-queue stored job', { id: x.id, code: err?.code, message: err?.message });
453
+ }
454
+ } else {
455
+ logger.info('skip invalid job from db', { job: x });
456
+ }
457
+ });
458
+ // eslint-disable-next-line no-shadow
459
+ } catch (err) {
460
+ console.error(err);
461
+ logger.error(`Can not load existing ${name} jobs`, { error: err });
304
462
  }
305
- const jobs = await store.getJobs();
306
- jobs.forEach((x) => {
307
- if (x.job && x.id) {
308
- push({ job: x.job, id: x.id, persist: false });
309
- } else {
310
- logger.info('skip invalid job from db', { job: x });
463
+ });
464
+ }
465
+
466
+ // Re-deliver this queue's due delayed rows once — the body shared by the node
467
+ // loop() (timer-driven) and the host dispatchDueJobs() (CF scheduled()-driven).
468
+ // Same cancel-then-replay-from-store path either way, so worker and node agree
469
+ // on exactly how a due delayed job runs.
470
+ const redispatchDue = async (): Promise<{ dispatched: number; failed: number }> => {
471
+ let dispatched = 0;
472
+ let failed = 0;
473
+ if (enableScheduledJob !== true) return { dispatched, failed };
474
+ const jobs = await store.getScheduledJobs();
475
+ for (const x of jobs) {
476
+ if (x.job && x.id) {
477
+ // fix: https://github.com/blocklet/payment-kit/issues/287
478
+ // Intentional cancel-then-replay: marking the row cancelled=true keeps
479
+ // the NEXT due-poll (loop tick / dispatchDueJobs) from re-picking it
480
+ // while this run is in flight; the immediate re-push below clears the
481
+ // in-memory cancel flag for THIS execution, and onJobComplete deletes
482
+ // the row on success. A reader watching raw DB state mid-dispatch will
483
+ // briefly see cancelled=true — that is the de-dupe latch, not an error.
484
+ // eslint-disable-next-line no-await-in-loop
485
+ await cancel(x.id);
486
+ logger.info('reschedule delayed or scheduled job', { id: x.id, job: x.job });
487
+ try {
488
+ push({ job: x.job, id: x.id, persist: false, fromStore: true });
489
+ dispatched += 1;
490
+ } catch (err: any) {
491
+ failed += 1;
492
+ logger.error('failed to reschedule stored job', { id: x.id, code: err?.code, message: err?.message });
311
493
  }
312
- });
313
- // eslint-disable-next-line no-shadow
314
- } catch (err) {
315
- console.error(err);
316
- logger.error(`Can not load existing ${name} jobs`, { error: err });
494
+ } else {
495
+ logger.info('skip invalid job from db', { job: x });
496
+ }
317
497
  }
318
- });
498
+ return { dispatched, failed };
499
+ };
319
500
 
320
- const loop = async () => {
321
- if (enableScheduledJob === true) {
322
- // eslint-disable-next-line no-constant-condition
323
- while (true) {
324
- await sleep((MIN_DELAY * 1000) / 2);
501
+ // D2 teardown: the node poll loop is cancelable. `stopLoop()` flips the flag
502
+ // AND clears the pending sleep timer (so the process has no dangling handle on
503
+ // stop — the spec's "active handles 归零") and resolves the in-flight sleep so
504
+ // the loop observes the flag and returns immediately.
505
+ let loopStopped = false;
506
+ let loopTimer: ReturnType<typeof setTimeout> | null = null;
507
+ let loopWake: (() => void) | null = null;
508
+ const cancelableSleep = (ms: number): Promise<void> =>
509
+ new Promise<void>((resolve) => {
510
+ loopWake = resolve;
511
+ loopTimer = setTimeout(() => {
512
+ loopTimer = null;
513
+ loopWake = null;
514
+ resolve();
515
+ }, ms);
516
+ });
517
+ const stopLoop = (): void => {
518
+ loopStopped = true;
519
+ if (loopTimer) {
520
+ clearTimeout(loopTimer);
521
+ loopTimer = null;
522
+ }
523
+ if (loopWake) {
524
+ const wake = loopWake;
525
+ loopWake = null;
526
+ wake();
527
+ }
528
+ };
325
529
 
326
- try {
327
- /* eslint-disable no-await-in-loop */
328
- const jobs = await store.getScheduledJobs();
329
- for (const x of jobs) {
330
- if (x.job && x.id) {
331
- // fix: https://github.com/blocklet/payment-kit/issues/287
332
- await cancel(x.id);
333
- logger.info('reschedule delayed or scheduled job', {
334
- id: x.id,
335
- job: x.job,
336
- });
337
- push({ job: x.job, id: x.id, persist: false });
338
- } else {
339
- logger.info('skip invalid job from db', { job: x });
340
- }
341
- }
342
- } catch (err) {
343
- console.error(err);
344
- logger.error(`Can not load scheduled ${name} jobs`, { error: err });
345
- }
530
+ // The node poll loop. Disabled on a workerd host: a frozen isolate cannot run
531
+ // a background timer, so the host calls dispatchDueJobs() from scheduled()
532
+ // instead (which runs redispatchDue() above — the same code path).
533
+ const loop = async () => {
534
+ if (enableScheduledJob !== true) return;
535
+ if (getQueueRuntimeMode() !== 'node') return;
536
+ while (!loopStopped) {
537
+ // eslint-disable-next-line no-await-in-loop
538
+ await cancelableSleep((minDelay() * 1000) / 2);
539
+ // stopped during the sleep (teardown) — exit before doing any work.
540
+ if (loopStopped) return;
541
+ // mode can flip after assembly (a host that sets workerd late); stop then.
542
+ if (getQueueRuntimeMode() !== 'node') return;
543
+ try {
544
+ // eslint-disable-next-line no-await-in-loop
545
+ await redispatchDue();
546
+ } catch (err) {
547
+ console.error(err);
548
+ logger.error(`Can not load scheduled ${name} jobs`, { error: err });
346
549
  }
347
550
  }
348
551
  };
349
552
 
350
553
  loop();
351
554
 
352
- return Object.assign(queueEvents, {
555
+ const queueInstance = Object.assign(queueEvents, {
353
556
  store,
354
557
  push,
355
558
  pushAndWait,
@@ -361,6 +564,8 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
361
564
  delete: deleteJob,
362
565
  cancel,
363
566
  update: updateJob,
567
+ /** D2 teardown: stop this queue's node poll loop (no-op if not scheduled). */
568
+ stop: stopLoop,
364
569
  options: {
365
570
  concurrency,
366
571
  maxRetries,
@@ -369,4 +574,18 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
369
574
  enableScheduledJob,
370
575
  },
371
576
  });
577
+
578
+ // Phase 12b: register into the host-facing queue runtime surface so the CF
579
+ // worker can look the handle up by name (queue() consumer) and drive due
580
+ // re-dispatch (scheduled()) through the service/slot boundary — no more
581
+ // direct cloudflare/shims/queue.ts imports.
582
+ registerQueue({
583
+ name,
584
+ enableScheduledJob: enableScheduledJob === true,
585
+ handle: queueInstance,
586
+ redispatchDue,
587
+ stop: stopLoop,
588
+ });
589
+
590
+ return queueInstance;
372
591
  }
@@ -0,0 +1,175 @@
1
+ // Phase 12b (W2′): the core queue RUNTIME surface.
2
+ //
3
+ // Decision (2026-06-12, build-phases 12b, option A): the node engine
4
+ // (api/src/libs/queue) is the ONE canonical queue semantics for every runtime.
5
+ // A host swaps only the TRIGGER (CF scheduled() vs the in-process poll loop),
6
+ // the EXECUTOR (fastq shim vs real fastq) and FLUSH (drain-before-response vs
7
+ // no-op). It NEVER forks the engine — the old cloudflare/shims/queue.ts
8
+ // duplicate engine is dead under the canonical build.ts (nothing populates its
9
+ // registry there) and is removed in 12c.
10
+ //
11
+ // This module is the seam the worker drives instead of reaching into
12
+ // shims/queue internals:
13
+ // - a queue REGISTRY (each createQueue registers its handle + due-dispatch)
14
+ // - dispatchDueJobs() — host-driven scheduled re-dispatch (workerd trigger =
15
+ // CF scheduled(); on node the per-queue loop() does the same on a timer)
16
+ // - flushQueueWork() — drain in-flight push/execution work before the
17
+ // isolate freezes (no-op on a long-lived node process)
18
+ //
19
+ // Runtime modes:
20
+ // 'node' — long-lived process: each scheduled queue's loop() polls D1.
21
+ // 'workerd' — frozen isolate: loop() is disabled; the host calls
22
+ // dispatchDueJobs() from scheduled() and flushQueueWork() before
23
+ // returning the response.
24
+
25
+ export type QueueRuntimeMode = 'node' | 'workerd';
26
+
27
+ let mode: QueueRuntimeMode = 'node';
28
+
29
+ export function setQueueRuntimeMode(next: QueueRuntimeMode): void {
30
+ mode = next;
31
+ }
32
+
33
+ export function getQueueRuntimeMode(): QueueRuntimeMode {
34
+ return mode;
35
+ }
36
+
37
+ export interface RegisteredQueue {
38
+ name: string;
39
+ /** whether this queue accepts delayed/scheduled jobs (gates due-dispatch) */
40
+ enableScheduledJob: boolean;
41
+ /** the public queue handle (push / pushAndWait / cancel / get / ...) */
42
+ handle: any;
43
+ /**
44
+ * Re-deliver this queue's due delayed rows once — the body of the node
45
+ * loop(). The host (CF scheduled()) calls this through dispatchDueJobs();
46
+ * the node loop() calls it on its own timer. Same code path either way.
47
+ */
48
+ redispatchDue: () => Promise<{ dispatched: number; failed: number }>;
49
+ /**
50
+ * D2 teardown: stop this queue's node poll loop (clears its sleep timer). The
51
+ * host (arc/Node) calls stopAllQueues() on lifecycle.stop() so no poll timer
52
+ * survives a stop / ARC_PAYMENT toggle. No-op on workerd (no loop runs).
53
+ */
54
+ stop?: () => void;
55
+ }
56
+
57
+ const registry = new Map<string, RegisteredQueue>();
58
+
59
+ export function registerQueue(entry: RegisteredQueue): void {
60
+ registry.set(entry.name, entry);
61
+ }
62
+
63
+ /** the queue handle the worker's queue() consumer looks up by name (no more shim registry) */
64
+ export function getQueueHandler(name: string): any | undefined {
65
+ return registry.get(name)?.handle;
66
+ }
67
+
68
+ export function getAllQueueNames(): string[] {
69
+ return Array.from(registry.keys());
70
+ }
71
+
72
+ /**
73
+ * Host-driven scheduled dispatch (the workerd trigger is CF scheduled()). Runs
74
+ * every registered scheduled queue's due-row re-dispatch once. On a node host
75
+ * the per-queue loop() does exactly this on a timer; a workerd host calls this
76
+ * explicitly because it cannot run a background timer in a frozen isolate.
77
+ */
78
+ export async function dispatchDueJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
79
+ let dispatched = 0;
80
+ let failed = 0;
81
+ const queues: string[] = [];
82
+ for (const entry of registry.values()) {
83
+ // eslint-disable-next-line no-continue -- skip non-scheduled queues in the dispatch loop
84
+ if (!entry.enableScheduledJob) continue;
85
+ try {
86
+ // eslint-disable-next-line no-await-in-loop -- queues dispatch sequentially within a tick
87
+ const r = await entry.redispatchDue();
88
+ dispatched += r.dispatched;
89
+ failed += r.failed;
90
+ if (r.dispatched > 0 || r.failed > 0) queues.push(entry.name);
91
+ } catch {
92
+ failed += 1;
93
+ }
94
+ }
95
+ return { dispatched, failed, queues };
96
+ }
97
+
98
+ /**
99
+ * D2 teardown: stop every registered queue's node poll loop. The Node host
100
+ * (arc) calls this from lifecycle.stop() so no background poll timer survives a
101
+ * stop / ARC_PAYMENT toggle. The registry entries stay (a later start() rebuilds
102
+ * the loops by recreating the queues). Idempotent — safe to call when stopped.
103
+ */
104
+ export function stopAllQueues(): void {
105
+ for (const entry of registry.values()) {
106
+ try {
107
+ entry.stop?.();
108
+ } catch {
109
+ /* a queue already stopped — ignore */
110
+ }
111
+ }
112
+ }
113
+
114
+ /** stop every queue loop AND clear the registry (full reset). */
115
+ export function disposeQueues(): void {
116
+ stopAllQueues();
117
+ registry.clear();
118
+ }
119
+
120
+ // --- in-flight push tracking (workerd flush) ---
121
+ // On node the process stays alive and fastq drains naturally; nothing is
122
+ // tracked (zero overhead, byte-identical behavior). On workerd the isolate is
123
+ // torn down after the response, so the host must await in-flight executions
124
+ // before it returns — otherwise an immediate push is silently dropped.
125
+ const pending = new Set<Promise<any>>();
126
+
127
+ export function trackPending(p: Promise<any>): void {
128
+ if (mode !== 'workerd') return;
129
+ const wrapped = Promise.resolve(p).catch(() => undefined);
130
+ pending.add(wrapped);
131
+ wrapped.then(() => {
132
+ pending.delete(wrapped);
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Drain in-flight push/execution work. The CF worker calls this before
138
+ * returning the HTTP response and at the end of scheduled()/queue() so no
139
+ * immediate push is lost when the isolate freezes. No-op on a node host.
140
+ */
141
+ export async function flushQueueWork(): Promise<void> {
142
+ if (mode !== 'workerd') return;
143
+ const MAX_ITERATIONS = 10;
144
+ // Wall-clock guard: a misbehaving handler whose fastq callback never fires
145
+ // must not block the isolate's response forever. The D1 jobs row is the
146
+ // source of truth, so anything still pending past the budget is re-dispatched
147
+ // on the next scheduled() tick rather than lost.
148
+ const deadline = Date.now() + 5000;
149
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
150
+ if (pending.size === 0) break;
151
+ const remaining = deadline - Date.now();
152
+ if (remaining <= 0) break;
153
+ const batch = Array.from(pending);
154
+ // eslint-disable-next-line no-await-in-loop
155
+ await Promise.race([
156
+ Promise.allSettled(batch),
157
+ new Promise<void>((resolve) => {
158
+ setTimeout(resolve, remaining);
159
+ }),
160
+ ]);
161
+ }
162
+ }
163
+
164
+ // Exported for unit tests only — not part of the host-facing surface.
165
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- test-only export sentinel
166
+ export const __test__ = {
167
+ reset() {
168
+ registry.clear();
169
+ pending.clear();
170
+ mode = 'node';
171
+ },
172
+ registrySize() {
173
+ return registry.size;
174
+ },
175
+ };
@@ -10,9 +10,9 @@ import { fromTokenToUnit } from '@ocap/util';
10
10
 
11
11
  import { updatePassportExtra } from '../integrations/blocklet/passport';
12
12
  import { replace } from '../locales';
13
- import { createPaymentLink } from '../routes/payment-links';
14
- import { createPrice } from '../routes/prices';
15
- import { createProductAndPrices } from '../routes/products';
13
+ import { createPaymentLink } from '../routes/hono/payment-links';
14
+ import { createPrice } from '../routes/hono/prices';
15
+ import { createProductAndPrices } from '../routes/hono/products';
16
16
  import { PaymentCurrency, PaymentLink, Price, Product, nextPriceId } from '../store/models';
17
17
 
18
18
  export async function getPackResource(type: string) {
@@ -0,0 +1,38 @@
1
+ // Phase 11 (W2-3): tenant-aware secrets facade.
2
+ //
3
+ // Replaces direct `security.encrypt/decrypt` calls in the payment hot path. The
4
+ // tenant is resolved from the TenantContext (single point), so call sites do
5
+ // not change signature. single mode -> the default secrets driver (process
6
+ // key), unchanged; multi mode -> the keyring driver keyed per tenant.
7
+ //
8
+ // Sync surface: the payment loop (PaymentMethod.encrypt/decrypt Settings,
9
+ // getStripeClient, ~50 sync callers) stays synchronous. The keyring driver's
10
+ // sync path requires the tenant key to be warmed (the worker shell warms it at
11
+ // request entry); the default driver's process key is always available.
12
+
13
+ import { getInstanceDid } from './context';
14
+ import { getSecretsDriver } from './drivers/secrets';
15
+
16
+ /** Encrypt a value under the current tenant's key (sync hot path). */
17
+ export function encryptSecret(value: string): string {
18
+ return getSecretsDriver().encryptSync(getInstanceDid(), value);
19
+ }
20
+
21
+ /** Decrypt a value under the current tenant's key (sync hot path). */
22
+ export function decryptSecret(value: string): string {
23
+ return getSecretsDriver().decryptSync(getInstanceDid(), value);
24
+ }
25
+
26
+ /** Async per-tenant variants (lazy EK fetch + retry) for non-hot-path callers. */
27
+ export function encryptSecretAsync(value: string): Promise<string> {
28
+ return getSecretsDriver().encrypt(getInstanceDid(), value);
29
+ }
30
+
31
+ export function decryptSecretAsync(value: string): Promise<string> {
32
+ return getSecretsDriver().decrypt(getInstanceDid(), value);
33
+ }
34
+
35
+ /** Pre-resolve the current tenant's key so the sync path is a cache hit. */
36
+ export function warmupSecrets(instanceDid?: string): Promise<void> {
37
+ return getSecretsDriver().warmup(instanceDid ?? getInstanceDid());
38
+ }
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
6
6
  import isEqual from 'lodash/isEqual';
7
7
  import pAll from 'p-all';
8
8
  import omit from 'lodash/omit';
9
+ import { paymentBillingThreshold, paymentMinStakeAmount } from './env';
9
10
  import dayjs from './dayjs';
10
11
  import { validCoupon } from './discount/coupon';
11
12
  import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
@@ -425,7 +426,7 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
425
426
  }
426
427
  }
427
428
 
428
- const threshold = +(process.env.PAYMENT_BILLING_THRESHOLD as string);
429
+ const threshold = paymentBillingThreshold();
429
430
  if (threshold > 0) {
430
431
  return threshold;
431
432
  }
@@ -441,7 +442,7 @@ export function getMinStakeAmount(config: Record<string, any> = {}) {
441
442
  }
442
443
  }
443
444
 
444
- const threshold = +(process.env.PAYMENT_MIN_STAKE_AMOUNT as string);
445
+ const threshold = paymentMinStakeAmount();
445
446
  if (threshold > 0) {
446
447
  return threshold;
447
448
  }