payment-kit 1.29.1 → 1.29.2

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