payment-kit 1.29.1 → 1.29.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,814 @@
1
+ /* eslint-disable global-require, max-classes-per-file */
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
4
+ import { Hono } from 'hono';
5
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
6
+ import { isProduction } from './libs/env';
7
+
8
+ import logger from './libs/logger';
9
+ import { context as requestContext } from './libs/context';
10
+ import { TENANT_CONTEXT_MISSING, TenantError, getTenantMode, getDefaultInstanceDid } from './libs/tenant';
11
+ import type { LocksDriver, QueueHostHooks, CronDriver, IdentityDriver, SecretsDriver } from './libs/drivers';
12
+ import type { DidConnectTokenStorage, DidConnectRuntime } from './libs/auth';
13
+ import type { PaymentFetchOptions, FetchHandler } from './libs/http-fetch-adapter';
14
+
15
+ export type { PaymentFetchOptions } from './libs/http-fetch-adapter';
16
+
17
+ // Phase 7 (W2-0): the embedded payment service factory.
18
+ //
19
+ // Assembly happens HERE (factory call time), never at module import time —
20
+ // the blocklet server shell (./index.ts) listens itself and calls
21
+ // lifecycle.start(); other hosts (arc, standalone worker) mount `handler`
22
+ // under their own prefix and own the lifecycle explicitly.
23
+ //
24
+ // Slot semantics in this phase: validated and carried, with implementations
25
+ // passing through to the current internals (db -> the global sequelize
26
+ // instance, queue/cron/locks -> existing libs). The `config` slot is now
27
+ // authoritative (Phase 8 W2′): setCoreConfig makes the injected config the
28
+ // source of truth for every core read via the libs/env.ts boundary, with
29
+ // process.env as the fallback; the api/src core-env whitelist is zero. NOTE:
30
+ // two factory calls in one process still share the module-level singletons
31
+ // (models, sequelize, queues) — documented transition state, not the end contract.
32
+
33
+ export type TenancySlot =
34
+ | { mode: 'single'; instanceDid: string }
35
+ | {
36
+ mode: 'multi';
37
+ /**
38
+ * D6 (optional): a host-provided enumeration of all known tenant
39
+ * instanceDids. When given, start() bootstraps each at startup; otherwise
40
+ * the host drives bootstrapTenant() per tenant (e.g. on first request).
41
+ * This is the ONLY sanctioned tenant-registry hook — the IdentityDriver
42
+ * (host->tenant + getAppEk) must not be repurposed for enumeration.
43
+ */
44
+ listInstanceDids?: () => Promise<string[]> | string[];
45
+ };
46
+
47
+ export interface PaymentCoreSlots {
48
+ /** all runtime configuration, explicit (Phase 12 makes this exhaustive) */
49
+ config: Record<string, any>;
50
+ /** SQL driver slot — currently the sequelize instance to bind models to */
51
+ db: { sequelize: any };
52
+ queue?: QueueHostHooks;
53
+ cron?: CronDriver;
54
+ locks?: LocksDriver;
55
+ identity?: IdentityDriver;
56
+ secrets?: SecretsDriver;
57
+ tenancy?: TenancySlot;
58
+ /**
59
+ * S3-CF Phase 1 inversion ③: a host-provided DID-Connect token storage (same
60
+ * reversal as db/queue/cron). The CF worker injects a D1-backed store so the
61
+ * token handshake state lands in PAYMENT_DB (strongly consistent); node hosts
62
+ * omit it and keep the file-backed nedb default. The 14 DID-Connect handler
63
+ * routes are unchanged — only the persistence backend swaps.
64
+ */
65
+ storage?: DidConnectTokenStorage;
66
+ /**
67
+ * S3-CF (DID convergence): the host-injected DID-Connect runtime (the host
68
+ * injection entry). blocklet-server injects createBlockletServerDidConnectRuntime
69
+ * (@blocklet/sdk wrapper); CF + arc-node embedded inject the real
70
+ * @arcblock/did-connect-js runtime (AUTH_SERVICE identity + host tokenStorage).
71
+ * Takes precedence over `storage` (the runtime carries its own tokenStorage).
72
+ */
73
+ didConnectRuntime?: DidConnectRuntime;
74
+ /**
75
+ * S3-CF Phase 1 inversion ①: a host-provided static/SPA handler wired onto the
76
+ * full node hono app AFTER the api/connect routes. The node blocklet-server host
77
+ * injects host-node/serve-static's `attachNodeStatic` to replicate today's SPA
78
+ * serving; the CF/standalone worker serves assets via env.ASSETS and omits this,
79
+ * keeping the runtime-neutral `http.fetch` free of node:fs.
80
+ */
81
+ staticHandler?: (app: Hono) => void;
82
+ }
83
+
84
+ export interface PaymentCoreLifecycle {
85
+ start: () => Promise<void>;
86
+ stop: () => Promise<void>;
87
+ }
88
+
89
+ export interface PaymentCoreService {
90
+ /**
91
+ * The full hono app: forked app-shell middleware + DID-Connect handlers +
92
+ * resource routes + static/SPA fallback + unified error handling. For node
93
+ * hosts (blocklet server, arc-node). Built lazily on first access — a workerd
94
+ * host that never touches it never runs the node-only app shell (static
95
+ * serving, fallback), which the CF worker serves through its own Hono pipeline
96
+ * instead. Phase 4: this is the SAME hono instance exposed as `fetch`.
97
+ */
98
+ handler: Hono;
99
+ /**
100
+ * The embeddable HTTP surface for hosts that own their own app shell. The CF
101
+ * worker mounts `resourceRoutes` into its Hono pipeline (Option 3 seam: resource
102
+ * routes only — no app shell static/fallback, no DID-Connect handlers). S3-CF
103
+ * (DID convergence): the worker now mounts the 14 payment DID actions from the
104
+ * shared core `buildConnectRoutesHono` (backed by the injected CF DID-Connect
105
+ * runtime), not a private surface.
106
+ * Phase 4: a hono app (`/api/healthz` + the migrated resource domains, each
107
+ * scoped to its own app-shell pipeline), mounted by the worker via `app.route`.
108
+ */
109
+ http: {
110
+ resourceRoutes: Hono;
111
+ /**
112
+ * D5: the Web-Fetch entry over the FULL hono app. A Fetch-native host
113
+ * (arc-node registerPrefixHandler) forwards a Web Request and gets a Web
114
+ * Response — base-strip, raw-body, multi Set-Cookie / redirect passthrough
115
+ * are all handled inside. The host writes zero express/req-res bridge. Never
116
+ * touched by the workerd host.
117
+ */
118
+ fetch: (request: Request, opts?: PaymentFetchOptions) => Promise<Response>;
119
+ };
120
+ rpc: {
121
+ entitlements: {
122
+ check: (input: { customerDid: string; featureKey: string; livemode?: boolean }) => Promise<any>;
123
+ };
124
+ meterEvents: {
125
+ report: (input: Record<string, any>) => Promise<any>;
126
+ };
127
+ };
128
+ lifecycle: PaymentCoreLifecycle;
129
+ /**
130
+ * D6 — idempotent per-tenant bootstrap (currency logos, overdraft prices,
131
+ * exchange-rate-health schedule, per-tenant integrations), run inside the
132
+ * tenant context. Multi-mode hosts MUST call this per tenant (single mode
133
+ * bootstraps the default tenant from lifecycle.start automatically).
134
+ */
135
+ bootstrapTenant: (instanceDid: string) => Promise<void>;
136
+ /**
137
+ * Idempotent per-tenant base-data provisioning: seeds the default ArcBlock
138
+ * payment methods (main + beta) and their base currencies, scoped to the
139
+ * tenant's instanceDid. The single-tenant `20230911-seeding` migration writes
140
+ * these with NO instance_did, so they are invisible to multi-tenant queries;
141
+ * a multi-tenant host calls this per tenant (e.g. lazily on first request) so
142
+ * the tenant has the payment method/currency every downstream entity (meters,
143
+ * products, prices, subscriptions) depends on. No secrets / no chain calls —
144
+ * pure records. Skips if the tenant already has an arcblock method.
145
+ */
146
+ provisionTenant: (instanceDid: string) => Promise<void>;
147
+ }
148
+
149
+ /**
150
+ * Phase 4 (express→hono): the factory return is the published PaymentCoreService
151
+ * PLUS a top-level `fetch` — the hono app the blocklet server shell serves through
152
+ * `@hono/node-server` `serve({ fetch })`. `fetch` is the same hono instance as
153
+ * `handler` (kept as a convenience entry); `http.fetch` is the base-strip variant
154
+ * for arc consumers (same Request/Response shape, strips the host mount prefix).
155
+ */
156
+ export type EmbeddedPaymentService = PaymentCoreService & {
157
+ fetch: (request: Request) => Response | Promise<Response>;
158
+ };
159
+
160
+ export class PaymentCoreSlotError extends Error {
161
+ code = 'MISSING_SLOT';
162
+
163
+ slot: string;
164
+
165
+ constructor(slot: string) {
166
+ super(`createEmbeddedPaymentService: required slot "${slot}" is missing`);
167
+ this.name = 'PaymentCoreSlotError';
168
+ this.slot = slot;
169
+ }
170
+ }
171
+
172
+ // Phase 8 (W2′): a REQUIRED config field is absent from the injected config (and
173
+ // not present in the process.env fallback). Distinct from a missing slot so the
174
+ // host can tell "you forgot a whole driver" from "you forgot one config key".
175
+ export class MissingConfigError extends Error {
176
+ code = 'MISSING_CONFIG_FIELD';
177
+
178
+ field: string;
179
+
180
+ constructor(field: string) {
181
+ super(`createEmbeddedPaymentService: required config field "${field}" is missing`);
182
+ this.name = 'MissingConfigError';
183
+ this.field = field;
184
+ }
185
+ }
186
+
187
+ // D1 (S3.0): the tenancy slot is the authoritative source of the tenant mode.
188
+ // A tenancy.mode that is neither "single" nor "multi", or that disagrees with an
189
+ // explicit config.PAYMENT_TENANT_MODE, is a construction-time fail-fast (the
190
+ // factory never silently picks one side). Distinct from a missing slot/config
191
+ // field so the host can tell "your tenancy slot is malformed/contradictory"
192
+ // from "you forgot a driver / a config key".
193
+ export class TenancySlotError extends Error {
194
+ code: 'INVALID_TENANCY_MODE' | 'TENANCY_MODE_CONFLICT';
195
+
196
+ constructor(code: 'INVALID_TENANCY_MODE' | 'TENANCY_MODE_CONFLICT', message: string) {
197
+ super(message);
198
+ this.name = 'TenancySlotError';
199
+ this.code = code;
200
+ }
201
+ }
202
+
203
+ const VALID_TENANT_MODES = ['single', 'multi'] as const;
204
+
205
+ // Phase 4 (express→hono): the /api surface is now two hono layers so an embedding
206
+ // host can take only what fits its runtime.
207
+ //
208
+ // buildResourceRoutesHono() — /api/healthz + the migrated resource domains, each
209
+ // scoped to its own forked app-shell pipeline. No DID-Connect handlers, no
210
+ // static/SPA fallback. This is the layer the CF worker mounts into its own
211
+ // Hono pipeline via `app.route('/', resourceRoutes)`.
212
+ // buildHonoApp() — the full node hono app (app-shell pipeline +
213
+ // migrated resources + DID-Connect + static/SPA fallback + error handling).
214
+ // For node hosts (blocklet server, arc-node via svc.http.fetch).
215
+ //
216
+ // All route modules are required lazily so importing THIS module stays
217
+ // side-effect-free for hosts that never build a layer.
218
+
219
+ /**
220
+ * /api/healthz + the migrated resource domains on a standalone hono app — the
221
+ * embeddable surface (no app shell static/fallback, no DID-Connect). The CF
222
+ * worker mounts this whole app via `app.route('/', resourceRoutes)`. Each domain
223
+ * carries its own scoped app-shell pipeline (cors/xss/csrf/cdn/context + livemode
224
+ * + optional baseCurrency) via mountMigratedResources / mountResourceGroup.
225
+ */
226
+ function buildResourceRoutesHono(): Hono {
227
+ const { mountMigratedResources } = require('./routes/hono');
228
+ const app = new Hono();
229
+ app.get('/api/healthz', (c) => c.json({ ok: true }));
230
+ // No appShell arg → mountResourceGroup defaults to the LITE app-shell (xss for
231
+ // sanitizedBody + livemode + baseCurrency). The CF worker (the only consumer of
232
+ // resourceRoutes) owns its own cors + tenant and never ran csrf/cdn/i18n. Keeping
233
+ // this path free of pipeline.ts (csrf/cdn/context) keeps the worker bundle clear
234
+ // of @blocklet/sdk/lib/util/* subpaths its shim does not map.
235
+ mountMigratedResources(app);
236
+ return app;
237
+ }
238
+
239
+ /** The 14 DID-Connect handler modules (each carries its own `action` + callbacks). */
240
+ function connectHandlerModules(): any[] {
241
+ return [
242
+ require('./routes/connect/collect').default,
243
+ require('./routes/connect/collect-batch').default,
244
+ require('./routes/connect/pay').default,
245
+ require('./routes/connect/setup').default,
246
+ require('./routes/connect/subscribe').default,
247
+ require('./routes/connect/change-payment').default,
248
+ require('./routes/connect/change-plan').default,
249
+ require('./routes/connect/recharge').default,
250
+ require('./routes/connect/recharge-account').default,
251
+ require('./routes/connect/delegation').default,
252
+ require('./routes/connect/overdraft-protection').default,
253
+ require('./routes/connect/re-stake').default,
254
+ require('./routes/connect/auto-recharge-auth').default,
255
+ require('./routes/connect/change-payer').default,
256
+ ];
257
+ }
258
+
259
+ /**
260
+ * The DID-Connect handlers attached to a hono app. did-connect-js v4 dispatches
261
+ * by isHonoApp() → native attachHono (design §3.3), so the 14 handlers run with
262
+ * zero rewrite. Mounted on the main hono app alongside the native resource group
263
+ * (NOT under it — DID-Connect carries its own ensureSignedJson, no xss/csrf/cors).
264
+ */
265
+ export function buildConnectRoutesHono(): Hono {
266
+ const { handlers } = require('./libs/auth');
267
+ const connectApp = new Hono();
268
+
269
+ // S3-CF (DID convergence) F: a LIGHTWEIGHT tenant-context-only middleware — NOT
270
+ // the full resource pipeline (no cors/xss/csrf/i18n/cdn; DID-Connect carries its
271
+ // own ensureSignedJson and did-connect-js registers its own CORS). The CF / AUTH_
272
+ // SERVICE runtimes resolve the per-tenant signing identity + scope the token store
273
+ // by the TenantContext instanceDid, so every DID route must run inside one. If the
274
+ // host already established a context (e.g. the CF worker wrapped /api/* in
275
+ // withTenant) we pass through; otherwise we resolve Host→tenant once and wrap.
276
+ connectApp.use('*', async (c, next) => {
277
+ const { context: requestCtx } = require('./libs/context');
278
+ if (requestCtx.peekInstanceDid()) return next();
279
+ const { resolveTenantForHost } = require('./libs/drivers/identity');
280
+ const instanceDid = await resolveTenantForHost(c.req.header('host'));
281
+ return requestCtx.withTenant(instanceDid, () => next());
282
+ });
283
+
284
+ for (const h of connectHandlerModules()) handlers.attach(Object.assign({ app: connectApp }, h));
285
+ return connectApp;
286
+ }
287
+
288
+ /**
289
+ * Phase 4 (express→hono) — the full node hono app. The loopback bridge + express
290
+ * app shell are gone; this hono app is the only entry.
291
+ *
292
+ * Structure (registration order is load-bearing — hono matches in registration
293
+ * order):
294
+ * ① /api/healthz — bare health route (BS liveness probe).
295
+ * ② native — the migrated resource domains + their scoped forked app-shell
296
+ * pipeline (cors/xss/csrf/cdn/context + livemode/baseCurrency),
297
+ * populated by `configureNative`.
298
+ * ③ connectApp — the DID-Connect sub-app, mounted alongside `native` (NOT under
299
+ * it — it carries its own ensureSignedJson, no xss/csrf/cors).
300
+ * ④ host static + SPA fallback (optional) — S3-CF Phase 1 inversion ①: the
301
+ * node-only static/SPA shell is no longer wired HERE (that kept
302
+ * `http.fetch` node-bound). The HOST injects `attachStatic`
303
+ * (node blocklet-server replicates today's serving via
304
+ * host-node/serve-static; the CF/standalone worker serves assets
305
+ * via env.ASSETS and injects nothing). Absent → the app serves
306
+ * no static and carries ZERO node:fs, so node, CF, and the
307
+ * standalone worker share this one surface.
308
+ */
309
+ export function buildHonoApp(
310
+ configureNative?: (native: Hono) => void,
311
+ getConnectApp?: () => Hono,
312
+ attachStatic?: (app: Hono) => void
313
+ ): Hono {
314
+ const app = new Hono();
315
+
316
+ // Unified error handling — the hono equivalent of express-async-errors + the
317
+ // express ErrorRequestHandler: CustomError → its mapped status + formatError;
318
+ // otherwise 500 JSON.
319
+ app.onError((err, c) => {
320
+ logger.error('handle router error', err);
321
+ if (err instanceof CustomError) {
322
+ // getStatusFromError returns a plain number; hono's c.json wants a
323
+ // StatusCode union — cast through the contentful-status type.
324
+ return c.json({ error: formatError(err) }, getStatusFromError(err) as ContentfulStatusCode);
325
+ }
326
+ return c.json({ error: (err as Error).message }, 500);
327
+ });
328
+
329
+ // ① /api/healthz — was on the express resource router; now a bare hono route.
330
+ app.get('/api/healthz', (c) => c.json({ ok: true }));
331
+
332
+ const native = new Hono();
333
+ configureNative?.(native);
334
+ app.route('/', native); // ② native group (forked middleware + migrated resources)
335
+ if (getConnectApp) {
336
+ app.route('/', getConnectApp()); // ③ DID-Connect
337
+ }
338
+
339
+ // ④ host-provided static + SPA fallback, wired LAST (after api/connect routes)
340
+ // so real routes win and only unmatched html navigations hit the fallback.
341
+ // The node:fs implementation lives in host-node/serve-static (node-only).
342
+ attachStatic?.(app);
343
+
344
+ return app;
345
+ }
346
+
347
+ /** rpc calls demand a tenant: explicit context or single-mode default. */
348
+ function requireTenant(): string {
349
+ return requestContext.getInstanceDid(); // throws TENANT_CONTEXT_MISSING in multi mode without context
350
+ }
351
+
352
+ function buildRpc(): PaymentCoreService['rpc'] {
353
+ return {
354
+ entitlements: {
355
+ check(input) {
356
+ // validation errors must surface as rejections, not sync throws
357
+ return Promise.resolve().then(() => {
358
+ const instanceDid = requireTenant();
359
+ const { checkEntitlement } = require('./libs/entitlement');
360
+ return requestContext.withTenant(instanceDid, () =>
361
+ checkEntitlement({
362
+ customer_did: input.customerDid,
363
+ product_id: input.featureKey,
364
+ livemode: input.livemode,
365
+ })
366
+ );
367
+ });
368
+ },
369
+ },
370
+ meterEvents: {
371
+ report(input) {
372
+ return Promise.resolve().then(() => {
373
+ const instanceDid = requireTenant();
374
+ const { MeterEvent } = require('./store/models');
375
+ return requestContext.withTenant(instanceDid, () => MeterEvent.create(input as any));
376
+ });
377
+ },
378
+ },
379
+ };
380
+ }
381
+
382
+ let servicesStarted = false;
383
+
384
+ // D6: a host-provided tenant enumeration hook (tenancy.listInstanceDids). When a
385
+ // multi-mode host wants every known tenant bootstrapped at start(), it injects
386
+ // this; otherwise the host drives bootstrapTenant() per provisioned tenant (e.g.
387
+ // arc-node on first payment request). We do NOT abuse the IdentityDriver as a
388
+ // tenant registry — it only does host->tenant + getAppEk.
389
+ let listInstanceDidsHook: (() => Promise<string[]> | string[]) | undefined;
390
+
391
+ /**
392
+ * D6 — idempotent per-tenant bootstrap. The tenant-scoped startup work that used
393
+ * to run (unscoped) inside startBackgroundServices lives here, executed INSIDE
394
+ * `withTenant(instanceDid)` so every query/push is correctly tenant-scoped:
395
+ * - syncCurrencyLogo() (PaymentMethod logo backfill)
396
+ * - ensureCreateOverdraftProtectionPrices (Price upsert)
397
+ * - scheduleHealthChecks(instanceDid) (exchange-rate-health, tenant in payload)
398
+ * - ensureStakedForGas() / ensureWebhookRegistered() (per-tenant integrations)
399
+ *
400
+ * Multi mode has no default tenant, so a host MUST bootstrap each tenant
401
+ * explicitly (per provisioned host / first request, or all-at-once via
402
+ * tenancy.listInstanceDids). Single mode bootstraps the default tenant from
403
+ * start() automatically (original behavior preserved).
404
+ */
405
+ async function bootstrapTenant(instanceDid: string): Promise<void> {
406
+ if (!instanceDid || typeof instanceDid !== 'string') {
407
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'bootstrapTenant requires an instanceDid');
408
+ }
409
+ const { syncCurrencyLogo } = require('./crons/currency');
410
+ const { ensureStakedForGas } = require('./integrations/arcblock/stake');
411
+ const { ensureWebhookRegistered } = require('./integrations/stripe/setup');
412
+ const { ensureCreateOverdraftProtectionPrices } = require('./libs/overdraft-protection');
413
+ const { scheduleHealthChecks } = require('./queues/exchange-rate-health');
414
+ const { warmTenantIdentity } = require('./libs/did-connect/tenant-identity');
415
+
416
+ // Run the whole bootstrap inside the tenant context. We AWAIT each op so its
417
+ // async continuation stays in-scope (a fire-and-forget would lose the ALS
418
+ // context after the callback returns).
419
+ await requestContext.withTenant(instanceDid, async () => {
420
+ // Warm the tenant's signing identity first — the lifecycle analog of the
421
+ // HTTP/queue warm. ensureStakedForGas reads `wallet.address` synchronously;
422
+ // without a warmed cache it fails-closed on the AUTH_SERVICE runtimes (no-op
423
+ // on blocklet-server). Best-effort, like the other bootstrap steps.
424
+ await warmTenantIdentity(instanceDid);
425
+ await Promise.resolve(syncCurrencyLogo()).catch((error: unknown) =>
426
+ logger.error('bootstrapTenant: syncCurrencyLogo failed', { instanceDid, error })
427
+ );
428
+ await Promise.resolve(ensureCreateOverdraftProtectionPrices()).catch((error: unknown) =>
429
+ logger.error('bootstrapTenant: ensureCreateOverdraftProtectionPrices failed', { instanceDid, error })
430
+ );
431
+ // exchange-rate-health: tenant carried in the job payload (re-schedule safe)
432
+ scheduleHealthChecks(instanceDid);
433
+ if (isProduction()) {
434
+ await ensureWebhookRegistered().catch((error: unknown) =>
435
+ logger.error('bootstrapTenant: ensureWebhookRegistered failed', { instanceDid, error })
436
+ );
437
+ }
438
+ await Promise.resolve(ensureStakedForGas()).catch((error: unknown) =>
439
+ logger.error('bootstrapTenant: ensureStakedForGas failed', { instanceDid, error })
440
+ );
441
+ });
442
+ }
443
+
444
+ // Per-tenant analog of the single-tenant `20230911-seeding` migration: that
445
+ // migration bulk-inserts the default ArcBlock methods/currencies with NO
446
+ // instance_did, so they are invisible to a multi-tenant query. This recreates
447
+ // them scoped to one tenant. Mirrors the proven create order in
448
+ // routes/hono/payment-methods.ts (method → currency → method.default_currency_id).
449
+ async function provisionTenant(instanceDid: string): Promise<void> {
450
+ if (!instanceDid || typeof instanceDid !== 'string') {
451
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'provisionTenant requires an instanceDid');
452
+ }
453
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
454
+ const { fromTokenToUnit } = require('@ocap/util');
455
+ const { PaymentMethod, PaymentCurrency } = require('./store/models');
456
+
457
+ const CHAINS = [
458
+ { chainId: 'main', livemode: true, symbol: 'ABT', label: 'ArcBlock Main', contract: 'z35nNRvYxBoHitx9yZ5ATS88psfShzPPBLxYD' },
459
+ { chainId: 'beta', livemode: false, symbol: 'TBA', label: 'ArcBlock Beta', contract: 'z35n6UoHSi9MED4uaQy6ozFgKPaZj2UKrurBG' },
460
+ ];
461
+
462
+ await requestContext.withTenant(instanceDid, async () => {
463
+ // Idempotent: a tenant that already has an arcblock method is provisioned.
464
+ const existing = await PaymentMethod.findOne({ where: { type: 'arcblock' } });
465
+ if (existing) return;
466
+
467
+ for (const chain of CHAINS) {
468
+ const logo = '/methods/arcblock.png';
469
+ const method = await PaymentMethod.create({
470
+ instance_did: instanceDid,
471
+ active: true,
472
+ livemode: chain.livemode,
473
+ locked: true,
474
+ type: 'arcblock',
475
+ name: chain.label,
476
+ description: `Process payments with tokens on ArcBlock ${chain.chainId} chain`,
477
+ logo,
478
+ confirmation: { type: 'immediate' },
479
+ settings: {
480
+ arcblock: {
481
+ chain_id: chain.chainId,
482
+ api_host: `https://${chain.chainId}.abtnetwork.io/api/`,
483
+ explorer_host: `https://${chain.chainId}.abtnetwork.io/explorer/`,
484
+ },
485
+ },
486
+ features: { recurring: true, refund: true, dispute: false },
487
+ metadata: {},
488
+ });
489
+ const currency = await PaymentCurrency.create({
490
+ instance_did: instanceDid,
491
+ active: true,
492
+ livemode: chain.livemode,
493
+ locked: true,
494
+ is_base_currency: true,
495
+ payment_method_id: method.id,
496
+ type: 'standard',
497
+ name: chain.symbol,
498
+ description: chain.symbol,
499
+ logo,
500
+ symbol: chain.symbol,
501
+ decimal: 18,
502
+ minimum_payment_amount: fromTokenToUnit(0.1, 18).toString(),
503
+ maximum_precision: 6,
504
+ maximum_payment_amount: fromTokenToUnit(100000000, 18).toString(),
505
+ contract: chain.contract,
506
+ metadata: {},
507
+ });
508
+ await method.update({ default_currency_id: currency.id });
509
+ }
510
+ logger.info('provisionTenant: seeded arcblock payment methods + currencies', { instanceDid });
511
+ });
512
+ }
513
+
514
+ async function startBackgroundServices(): Promise<void> {
515
+ if (servicesStarted) {
516
+ logger.info('payment core background services already started, skipping');
517
+ return;
518
+ }
519
+ servicesStarted = true;
520
+
521
+ const crons = require('./crons/index').default;
522
+ const { initResourceHandler } = require('./integrations/blocklet/resource');
523
+ const { initUserHandler } = require('./integrations/blocklet/user');
524
+ const { initEventBroadcast } = require('./libs/ws');
525
+
526
+ // Tenant-AGNOSTIC engine registration: these create the queues + register the
527
+ // consumer handlers. Jobs carry their own tenant (instance_did), so no startup
528
+ // tenant context is needed. The exchange-rate-health SCHEDULE moved to
529
+ // bootstrapTenant (it pushes a tenant-scoped job); the queue itself is created
530
+ // at module import.
531
+ const starters: [string, () => Promise<any> | void][] = [
532
+ ['payment', require('./queues/payment').startPaymentQueue],
533
+ ['invoice', require('./queues/invoice').startInvoiceQueue],
534
+ ['subscription', require('./queues/subscription').startSubscriptionQueue],
535
+ ['event', require('./queues/event').startEventQueue],
536
+ ['payout', require('./queues/payout').startPayoutQueue],
537
+ ['vendor commission', require('./queues/vendors/commission').startVendorCommissionQueue],
538
+ ['vendor fulfillment', require('./queues/vendors/fulfillment').startVendorFulfillmentQueue],
539
+ ['coordinated fulfillment', require('./queues/vendors/fulfillment-coordinator').startCoordinatedFulfillmentQueue],
540
+ ['checkoutSession', require('./queues/checkout-session').startCheckoutSessionQueue],
541
+ ['notification', require('./queues/notification').startNotificationQueue],
542
+ ['refund', require('./queues/refund').startRefundQueue],
543
+ ['credit', require('./queues/credit-consume').startCreditConsumeQueue],
544
+ ['credit grant', require('./queues/credit-grant').startCreditGrantQueue],
545
+ ['token transfer', require('./queues/token-transfer').startTokenTransferQueue],
546
+ ['credit reconciliation', require('./queues/credit-reconciliation').startReconciliationQueue],
547
+ ['discount status', require('./queues/discount-status').startDiscountStatusQueue],
548
+ ];
549
+ for (const [name, start] of starters) {
550
+ Promise.resolve(start()).then(() => logger.info(`${name} queue started`));
551
+ }
552
+
553
+ // cron + global handlers (tenant-agnostic; runOnInit is skipped in multi mode)
554
+ crons.init();
555
+ initEventBroadcast();
556
+ initResourceHandler();
557
+ initUserHandler();
558
+
559
+ // Per-tenant bootstrap. Single mode: the default tenant always exists, so
560
+ // bootstrap it now (preserves the original startup behavior). Multi mode: the
561
+ // host drives bootstrapTenant per tenant — either eagerly via the optional
562
+ // tenancy.listInstanceDids hook, or lazily (per provisioned host / first req).
563
+ if (getTenantMode() === 'single') {
564
+ await bootstrapTenant(getDefaultInstanceDid());
565
+ } else if (listInstanceDidsHook) {
566
+ const dids = (await listInstanceDidsHook()) || [];
567
+ for (const did of dids) {
568
+ // eslint-disable-next-line no-await-in-loop -- bootstraps run sequentially
569
+ await bootstrapTenant(did).catch((error: unknown) =>
570
+ logger.error('startup bootstrapTenant failed', { instanceDid: did, error })
571
+ );
572
+ }
573
+ }
574
+ }
575
+
576
+ // eslint-disable-next-line require-await -- async contract; teardown is synchronous today
577
+ async function stopBackgroundServices(): Promise<void> {
578
+ if (!servicesStarted) return;
579
+
580
+ // D2 teardown surface. In-flight jobs are never lost: the jobs table is the
581
+ // source of truth and re-delivery happens on the next start (queue recovery
582
+ // scan). We tear down the two background trigger sources so the process has no
583
+ // dangling handle after stop (the spec's "active handles 归零"):
584
+ // 1. cron timers — the node-cron driver stops its @abtnode/cron scheduler
585
+ // 2. queue poll loops — each scheduled queue's node loop sleep timer
586
+ // Both are idempotent on the next start(): cron register() clears + restarts,
587
+ // and the queue registry dedupes by name + the old loops are already stopped,
588
+ // so a start()/stop()/start() cycle never double-registers.
589
+ try {
590
+ const { getCronDriver } = require('./libs/drivers/cron');
591
+ getCronDriver().stop();
592
+ } catch (err) {
593
+ logger.error('cron teardown failed on stop', { error: err });
594
+ }
595
+ try {
596
+ const { stopAllQueues } = require('./libs/queue/runtime');
597
+ stopAllQueues();
598
+ } catch (err) {
599
+ logger.error('queue teardown failed on stop', { error: err });
600
+ }
601
+
602
+ // clear the guard so a later start() rebuilds the background services cleanly.
603
+ servicesStarted = false;
604
+ }
605
+
606
+ /**
607
+ * The W2 §1 factory. Synchronous on purpose: hosts get a fully assembled
608
+ * service object and decide when to listen / start. No partial
609
+ * initialization on validation failure.
610
+ */
611
+ export function createEmbeddedPaymentService(slots: PaymentCoreSlots): EmbeddedPaymentService {
612
+ if (!slots || typeof slots !== 'object') throw new PaymentCoreSlotError('config');
613
+ if (!slots.config) throw new PaymentCoreSlotError('config');
614
+ if (!slots.db || !slots.db.sequelize) throw new PaymentCoreSlotError('db');
615
+ // D1 (S3.0): validate the tenancy slot and derive the effective config BEFORE
616
+ // any setCoreConfig — so a malformed/contradictory slot fails fast without
617
+ // writing a half-baked mode into the config boundary.
618
+ let effectiveConfig = slots.config;
619
+ if (slots.tenancy) {
620
+ const slotMode = (slots.tenancy as { mode?: unknown }).mode;
621
+ if (typeof slotMode !== 'string' || !VALID_TENANT_MODES.includes(slotMode as any)) {
622
+ throw new TenancySlotError(
623
+ 'INVALID_TENANCY_MODE',
624
+ `tenancy.mode "${String(slotMode)}" is invalid — expected one of: ${VALID_TENANT_MODES.join(', ')}`
625
+ );
626
+ }
627
+ // slotMode is validated above; use slots.tenancy.mode for the branches below
628
+ // so TypeScript narrows the TenancySlot union (instanceDid only on single).
629
+ if (slots.tenancy.mode === 'single' && !slots.tenancy.instanceDid) {
630
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'tenancy.mode=single requires instanceDid');
631
+ }
632
+ // intending multi must never silently degrade to single: the default
633
+ // identity driver resolves every host to the deployment app DID and the
634
+ // default secrets driver uses a single process key — both are single-tenant
635
+ // behaviors. A multi-mode host that omits either slot fails closed.
636
+ if (slots.tenancy.mode === 'multi') {
637
+ if (!slots.identity) throw new PaymentCoreSlotError('identity');
638
+ if (!slots.secrets) throw new PaymentCoreSlotError('secrets');
639
+ }
640
+ // config.PAYMENT_TENANT_MODE that disagrees with the slot is a conflict —
641
+ // fail fast rather than silently honoring one source.
642
+ const configMode = slots.config.PAYMENT_TENANT_MODE;
643
+ if (configMode !== undefined && configMode !== null && String(configMode) !== slotMode) {
644
+ throw new TenancySlotError(
645
+ 'TENANCY_MODE_CONFLICT',
646
+ `tenancy.mode "${slotMode}" conflicts with config.PAYMENT_TENANT_MODE "${String(configMode)}" — set only one`
647
+ );
648
+ }
649
+ // the slot wins: effectiveConfig carries the slot mode through the boundary
650
+ // so getTenantMode() sees it with no env.
651
+ effectiveConfig = { ...slots.config, PAYMENT_TENANT_MODE: slotMode };
652
+ }
653
+
654
+ // Phase 8 (W2′): make the config slot authoritative — every core read goes
655
+ // through libs/env.ts, which now prefers this injected config over process.env.
656
+ const { setCoreConfig, readConfig } = require('./libs/env');
657
+ setCoreConfig(effectiveConfig);
658
+
659
+ // Fail-fast on a missing REQUIRED config field (not a silent default): a
660
+ // single-mode deployment cannot establish its default tenant without an app
661
+ // DID. Multi mode resolves the tenant per request via the identity slot, so
662
+ // BLOCKLET_APP_PID is not required there.
663
+ const singleMode = !slots.tenancy || slots.tenancy.mode === 'single';
664
+ const hasSingleTenantId = Boolean((slots.tenancy as any)?.instanceDid) || Boolean(readConfig('BLOCKLET_APP_PID'));
665
+ if (singleMode && !hasSingleTenantId) {
666
+ throw new MissingConfigError('BLOCKLET_APP_PID');
667
+ }
668
+
669
+ // model binding — only after validation passed (no partial init)
670
+ const { initialize } = require('./store/models');
671
+ // Make the default-export `sequelize` resolve to the host instance too, so code
672
+ // that imports it directly (Price.expand's `sequelize.models.Product`, etc.) hits
673
+ // the SAME instance the models bind to — in a bare host the default would
674
+ // otherwise build a broken sqlite from an undefined data dir.
675
+ const { setDefaultSequelize } = require('./store/sequelize');
676
+ setDefaultSequelize(slots.db.sequelize);
677
+ initialize(slots.db.sequelize);
678
+
679
+ // locks slot (Phase 8): inject the locks driver if the host provides one;
680
+ // otherwise the lock facade keeps its default in-process memory driver. The
681
+ // worker passes the D1 locks driver here.
682
+ if (slots.locks) {
683
+ const { setLocksDriver } = require('./libs/lock');
684
+ setLocksDriver(slots.locks);
685
+ }
686
+
687
+ // queue / cron slots (Phase 9): inject the host flush hook + cron driver if
688
+ // provided; otherwise the Node defaults apply (no-op flush, node-cron
689
+ // registry). The worker injects flush-before-response hooks and the cf-cron
690
+ // driver here.
691
+ const { setQueueHostHooks, setCronDriver, setIdentityDriver, setSecretsDriver } = require('./libs/drivers');
692
+ if (slots.queue) setQueueHostHooks(slots.queue);
693
+ if (slots.cron) setCronDriver(slots.cron);
694
+
695
+ // secrets slot (Phase 11): the host injects a per-tenant keyring driver for
696
+ // multi-tenant; the default driver wraps the process @blocklet/sdk security
697
+ // (single key — Blocklet Server unchanged, existing ciphertext decryptable).
698
+ if (slots.secrets) setSecretsDriver(slots.secrets);
699
+
700
+ // DID-Connect runtime + storage slots (S3-CF DID convergence): the host injects
701
+ // its runtime (blocklet-server: SDK wrapper; CF/arc-node: real
702
+ // @arcblock/did-connect-js) and/or a token store. Set before buildConnectRoutesHono
703
+ // materializes `handlers`. The runtime is the primary host-injection entry; the
704
+ // storage-only slot remains for hosts that keep the default authenticator/handlers.
705
+ if (slots.didConnectRuntime || slots.storage) {
706
+ const { setDidConnectRuntime, setDidConnectTokenStorage } = require('./libs/auth');
707
+ if (slots.didConnectRuntime) setDidConnectRuntime(slots.didConnectRuntime);
708
+ if (slots.storage) setDidConnectTokenStorage(slots.storage);
709
+ }
710
+
711
+ // identity slot (Phase 10): the host injects a Host->tenant resolver for
712
+ // multi-tenant; the default driver resolves every host to the deployment app
713
+ // DID (single mode, unchanged Blocklet Server behavior).
714
+ if (slots.identity) setIdentityDriver(slots.identity);
715
+
716
+ // tenancy slot (Phase 10): a single-mode host declares its tenant identity
717
+ // explicitly; wire it into the default-tenant getter so it is not silently
718
+ // ignored (env app DID remains the fallback when no slot value is given).
719
+ if (slots.tenancy && slots.tenancy.mode === 'single' && slots.tenancy.instanceDid) {
720
+ const { setDefaultInstanceDid } = require('./libs/tenant');
721
+ setDefaultInstanceDid(slots.tenancy.instanceDid);
722
+ }
723
+
724
+ // D6: wire the optional multi-mode tenant enumeration hook (start() bootstraps
725
+ // each enumerated tenant). Absent => the host bootstraps per tenant on demand.
726
+ listInstanceDidsHook = slots.tenancy && slots.tenancy.mode === 'multi' ? slots.tenancy.listInstanceDids : undefined;
727
+
728
+ // Lazy, memoized layer construction (Option 3 seam). Assembly above already
729
+ // ran (config/db/slots) — that is the factory's eager contract. The HTTP
730
+ // layers are built on first access so a workerd host that only reads
731
+ // `http.resourceRoutes` never constructs the full node app (static/fallback),
732
+ // and a node host that reads `handler`/`fetch` reuses the same hono instance.
733
+ const memo = <T>(build: () => T): (() => T) => {
734
+ let value: T | undefined;
735
+ return () => {
736
+ value ??= build();
737
+ return value;
738
+ };
739
+ };
740
+ // Phase 4: the CF worker's embeddable surface — /api/healthz + migrated
741
+ // resources on a standalone hono app (no app-shell static/fallback, no connect).
742
+ const getResourceRoutes = memo(buildResourceRoutesHono);
743
+ // The DID-Connect handlers on a hono app for the full node shell.
744
+ const getConnectRoutesHono = memo(buildConnectRoutesHono);
745
+ // Phase 4: the full node hono app — the only entry. Memoized so `service.fetch`
746
+ // /`handler` are a stable instance for serve({ fetch }). configureNativePipeline
747
+ // + mountMigratedResources (lazily required to keep this module's import
748
+ // side-effect-free) build the native resource group; getConnectRoutesHono mounts
749
+ // DID-Connect; production static/SPA fallback are wired inside buildHonoApp.
750
+ const getHonoApp = memo(() => {
751
+ // eslint-disable-next-line global-require
752
+ const { configureNativePipeline, fullPipeline } = require('./middlewares/hono/pipeline');
753
+ // eslint-disable-next-line global-require
754
+ const { mountMigratedResources } = require('./routes/hono');
755
+ return buildHonoApp(
756
+ (native: Hono) => {
757
+ configureNativePipeline(native); // forked app-shell middleware (+ test stub)
758
+ mountMigratedResources(native, { appShell: fullPipeline() }); // full app-shell on the node host
759
+ },
760
+ getConnectRoutesHono,
761
+ // S3-CF Phase 1 ①: host-provided static/SPA shell (node injects it; CF omits).
762
+ slots.staticHandler
763
+ );
764
+ });
765
+ // D5: the base-strip Fetch adapter wraps the full hono app (arc consumer). Lazy
766
+ // require + memo so the adapter is built only on the first http.fetch call.
767
+ let fetchHandler: FetchHandler | null = null;
768
+ const getFetchHandler = (): FetchHandler => {
769
+ if (fetchHandler) return fetchHandler;
770
+ // eslint-disable-next-line global-require
771
+ const { createFetchHandler } = require('./libs/http-fetch-adapter');
772
+ const h: FetchHandler = createFetchHandler(getHonoApp());
773
+ fetchHandler = h;
774
+ return h;
775
+ };
776
+
777
+ const rpc = buildRpc();
778
+ const lifecycle: PaymentCoreLifecycle = {
779
+ start: startBackgroundServices,
780
+ async stop() {
781
+ await stopBackgroundServices();
782
+ // Phase 4: the loopback server is gone, so there is no socket to close. The
783
+ // base-strip adapter's close() is a no-op; reset the memo for symmetry.
784
+ if (fetchHandler) {
785
+ const h = fetchHandler;
786
+ fetchHandler = null;
787
+ await h.close();
788
+ }
789
+ },
790
+ };
791
+
792
+ return {
793
+ // Phase 4: handler IS the full node hono app (same instance as `fetch`).
794
+ get handler() {
795
+ return getHonoApp();
796
+ },
797
+ // The hono app the blocklet server shell serves via @hono/node-server.
798
+ fetch(request: Request): Response | Promise<Response> {
799
+ return getHonoApp().fetch(request);
800
+ },
801
+ http: {
802
+ get resourceRoutes() {
803
+ return getResourceRoutes();
804
+ },
805
+ fetch(request: Request, opts?: PaymentFetchOptions): Promise<Response> {
806
+ return getFetchHandler()(request, opts);
807
+ },
808
+ },
809
+ rpc,
810
+ lifecycle,
811
+ bootstrapTenant,
812
+ provisionTenant,
813
+ };
814
+ }