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
@@ -5,9 +5,58 @@ import type { EventType } from '../store/models';
5
5
  import { Event } from '../store/models/event';
6
6
  import { events } from './event';
7
7
  import { context } from './context';
8
+ import logger from './logger';
9
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH, TenantError } from './tenant';
8
10
 
9
11
  const API_VERSION = '2023-09-05';
10
12
 
13
+ /**
14
+ * Phase 4 (W1-3): tenant of an event, fail-closed.
15
+ * Order: model.instance_did, then TenantContext. Both missing or mutually
16
+ * contradictory -> reject. The rejection only kills the EVENT (the business
17
+ * transaction that triggered the hook is never blocked — see
18
+ * reportAuditFailure and the fire-and-forget catch sites in store/models).
19
+ */
20
+ function resolveEventTenant(model?: { instance_did?: string | null } | null): string {
21
+ const fromModel = model?.instance_did || undefined;
22
+ // the EXPLICIT context tenant (withTenant), not the single-mode default
23
+ // fill — otherwise every model row of a non-default tenant would falsely
24
+ // conflict with the deployment app DID in single mode
25
+ const explicitContext = context.getContext().instanceDid || undefined;
26
+ if (fromModel && explicitContext && fromModel !== explicitContext) {
27
+ throw new TenantError(
28
+ TENANT_MISMATCH,
29
+ `event tenant conflict: model carries ${fromModel} but context is ${explicitContext}`
30
+ );
31
+ }
32
+ const tenant = fromModel || explicitContext;
33
+ if (tenant) return tenant;
34
+ // neither source explicit: single mode falls back to the app DID,
35
+ // multi mode fails closed
36
+ return context.getInstanceDid();
37
+ }
38
+
39
+ /**
40
+ * Create an event row under its resolved tenant. The event belongs to the
41
+ * model's tenant, which may differ from the ambient context (e.g. a system /
42
+ * cross-tenant path acting on another tenant's row). Now that Event extends
43
+ * TenantModel, the create must run under that tenant so stampTenant stamps the
44
+ * matching instance_did instead of rejecting the explicit one as a conflict.
45
+ */
46
+ function createEventUnderTenant(instanceDid: string, values: any): Promise<any> {
47
+ return context.withTenant(instanceDid, () => Event.create(values));
48
+ }
49
+
50
+ /**
51
+ * Structured fire-and-forget alert for failed event creation. Tenant
52
+ * rejections keep their dedicated codes so monitoring/tests can assert them;
53
+ * everything else is EVENT_CREATE_FAILED. Mode-independent and never throws.
54
+ */
55
+ export function reportAuditFailure(err: any): void {
56
+ const code = err?.code === TENANT_MISMATCH || err?.code === TENANT_CONTEXT_MISSING ? err.code : 'EVENT_CREATE_FAILED';
57
+ logger.error('[audit] event creation failed', { code, message: err?.message || String(err) });
58
+ }
59
+
11
60
  /**
12
61
  * Invoke every registered listener for `eventName` and await any Promise
13
62
  * results. EventEmitter.emit() returns sync — listener async work would
@@ -58,6 +107,7 @@ export function createEvent(
58
107
  }
59
108
 
60
109
  async function doCreateEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any = {}) {
110
+ const instanceDid = resolveEventTenant(model);
61
111
  const data: any = {
62
112
  object: model.dataValues,
63
113
  };
@@ -65,8 +115,9 @@ async function doCreateEvent(scope: string, type: LiteralUnion<EventType, string
65
115
  data.previous_attributes = pick(model._previousDataValues, options.fields);
66
116
  }
67
117
 
68
- const event = await Event.create({
118
+ const event = await createEventUnderTenant(instanceDid, {
69
119
  type,
120
+ instance_did: instanceDid,
70
121
  api_version: API_VERSION,
71
122
  livemode: !!model.livemode,
72
123
  object_id: model.id,
@@ -113,8 +164,10 @@ export async function createStatusEvent(
113
164
  }
114
165
 
115
166
  const suffix = config[data.object.status];
116
- const event = await Event.create({
167
+ const instanceDid = resolveEventTenant(model);
168
+ const event = await createEventUnderTenant(instanceDid, {
117
169
  type: [prefix, suffix].join('.'),
170
+ instance_did: instanceDid,
118
171
  api_version: API_VERSION,
119
172
  livemode: !!model.livemode,
120
173
  object_id: model.id,
@@ -150,8 +203,10 @@ export async function createCustomEvent(
150
203
  return;
151
204
  }
152
205
 
153
- const event = await Event.create({
206
+ const instanceDid = resolveEventTenant(model);
207
+ const event = await createEventUnderTenant(instanceDid, {
154
208
  type: [prefix, suffix].join('.'),
209
+ instance_did: instanceDid,
155
210
  api_version: API_VERSION,
156
211
  livemode: !!model.livemode,
157
212
  object_id: model.id,
@@ -185,9 +240,11 @@ export async function createFlexibleEvent(
185
240
  } = {}
186
241
  ) {
187
242
  const { livemode = false, requestedBy, metadata = {} } = options;
243
+ const instanceDid = resolveEventTenant(null);
188
244
 
189
- const event = await Event.create({
245
+ const event = await createEventUnderTenant(instanceDid, {
190
246
  type,
247
+ instance_did: instanceDid,
191
248
  api_version: API_VERSION,
192
249
  livemode,
193
250
  object_id: objectId,
@@ -1,18 +1,53 @@
1
+ import os from 'os';
1
2
  import path from 'path';
2
3
 
3
- import AuthStorage from '@arcblock/did-connect-storage-nedb';
4
- // @ts-ignore
5
- import { BlockletService } from '@blocklet/sdk/service/auth';
6
- import { getWallet, getAccessWallet } from '@blocklet/sdk/lib/wallet';
7
- import { WalletAuthenticator } from '@blocklet/sdk/lib/wallet-authenticator';
8
- import { WalletHandlers } from '@blocklet/sdk/lib/wallet-handler';
9
- import type { Request } from 'express';
10
4
  import type { LiteralUnion } from 'type-fest';
11
5
  import type { WalletObject } from '@ocap/wallet';
6
+ import env, { blockletAppId } from './env';
7
+ import { getIdentityDriver } from './drivers';
12
8
 
13
- import env from './env';
14
9
  import logger from './logger';
15
10
 
11
+ // Phase 13b: every blocklet-runtime binding below is constructed LAZILY, on first
12
+ // property access, instead of at module import. Importing this module — and the
13
+ // modules that transitively pull it in (libs/util, libs/payment, the models) —
14
+ // therefore runs NO blocklet side effects, so createEmbeddedPaymentService +
15
+ // rpc.entitlements.check work in a bare host with only the explicit config/db/
16
+ // tenancy slots: no BLOCKLET_APP_SK/PK, no BLOCKLET_DATA_DIR, no ABT_NODE_*, no
17
+ // notification patch. The node/full-handler and background-lifecycle paths still
18
+ // materialize the real bindings on first use — transparently, through the proxies —
19
+ // so every existing consumer (`wallet`, `ethWallet`, `blocklet`, `handlers`,
20
+ // `authenticator`) keeps working unchanged.
21
+
22
+ /** Transparent lazy proxy: defer factory() until the first property access. */
23
+ function lazyProxy<T extends object>(factory: () => T): T {
24
+ let instance: T | undefined;
25
+ const resolve = (): T => {
26
+ instance ??= factory();
27
+ return instance;
28
+ };
29
+ return new Proxy({} as T, {
30
+ get(_t, prop) {
31
+ const obj = resolve();
32
+ const value: any = (obj as any)[prop];
33
+ if (typeof value !== 'function') return value;
34
+ // Don't re-bind jest mock functions: binding returns a plain wrapper that
35
+ // strips jest's mock helpers (.mockImplementation / .mockResolvedValue),
36
+ // which would make `jest.spyOn(blocklet, 'method')` unusable. Mocks ignore
37
+ // `this`, so leaving them unbound is safe. No-op in production (no mocks).
38
+ if (value._isMockFunction) return value;
39
+ return value.bind(obj);
40
+ },
41
+ set(_t, prop, value) {
42
+ (resolve() as any)[prop] = value;
43
+ return true;
44
+ },
45
+ has(_t, prop) {
46
+ return prop in (resolve() as any);
47
+ },
48
+ });
49
+ }
50
+
16
51
  // Workaround #2: @blocklet/sdk's notification.getSender() uses
17
52
  // `getWallet().address` (BLOCKLET_APP_SK derived) as the sender appDid for
18
53
  // relay/EventBus broadcasts. On migrated blocklets that address (e.g. zNKti3…)
@@ -28,51 +63,214 @@ import logger from './logger';
28
63
  // Path must be `@blocklet/sdk/service/notification` (no `lib/`) — in Node it
29
64
  // is a thin re-export that resolves via the module cache to the same object
30
65
  // as `@blocklet/sdk/lib/service/notification`; in CF Workers the esbuild
31
- // config aliases it to a no-op shim (patching a no-op is harmless). Using
32
- // the `lib/` path breaks the CF build because it does not have an alias
33
- // and falls through to `@blocklet/sdk` `shims/blocklet-sdk/index.ts/lib/...`.
34
- // Same root cause as the WalletAuthenticator override below remove once the
35
- // upstream sdk is patched.
36
- // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
37
- const notificationExports = require('@blocklet/sdk/service/notification');
38
-
39
- notificationExports.getSender = () => ({
40
- appDid: process.env.BLOCKLET_APP_ID || getWallet(undefined, '', 'sk').address,
41
- wallet: getAccessWallet(),
42
- });
43
- logger.info('[sdk-patch] notification.getSender overridden', {
44
- appDid: notificationExports.getSender().appDid,
45
- expectedAppId: process.env.BLOCKLET_APP_ID,
46
- });
66
+ // config aliases it to a no-op shim (patching a no-op is harmless).
67
+ //
68
+ // Phase 13b: applied LAZILY (once) the first time a blocklet binding is
69
+ // materialized, instead of at module import so a bare host that never touches a
70
+ // binding never requires the wallet/notification modules. The override closure
71
+ // still reads getWallet()/getAccessWallet() at send time, exactly as before.
72
+ let notificationPatched = false;
73
+ function ensureNotificationPatch(): void {
74
+ if (notificationPatched) return;
75
+ notificationPatched = true;
76
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
77
+ const { getWallet, getAccessWallet } = require('@blocklet/sdk/lib/wallet');
78
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
79
+ const notificationExports = require('@blocklet/sdk/service/notification');
80
+ notificationExports.getSender = () => ({
81
+ appDid: blockletAppId() || getWallet(undefined, '', 'sk').address,
82
+ wallet: getAccessWallet(),
83
+ });
84
+ // Note: do NOT invoke getSender() here — that would force a wallet read at patch
85
+ // time. Log only the expected appId (config-derived, env-free).
86
+ logger.info('[sdk-patch] notification.getSender overridden', { expectedAppId: blockletAppId() });
87
+ }
88
+
89
+ function makeWallet(...args: any[]): WalletObject {
90
+ ensureNotificationPatch();
91
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
92
+ const { getWallet } = require('@blocklet/sdk/lib/wallet');
93
+ return getWallet(...args);
94
+ }
47
95
 
48
- export const wallet: WalletObject = getWallet();
49
- export const ethWallet: WalletObject = getWallet('ethereum');
96
+ // The blocklet-server business wallets — env-derived (BLOCKLET_APP_SK), lazy as
97
+ // before. They are the FALLBACK when the active IdentityDriver provides no
98
+ // getBusinessWallet (blocklet-server / tests); the @blocklet/sdk getWallet alone
99
+ // handles the remote-sign / delegation / migration cases a bare appSk cannot.
100
+ const envWallet: WalletObject = lazyProxy(() => makeWallet());
101
+ const envEthWallet: WalletObject = lazyProxy(() => makeWallet('ethereum'));
50
102
 
51
- // Workaround for migrated blocklets where `BLOCKLET_APP_SK` is a rotating session
52
- // key whose derived address appId. Upstream @blocklet/sdk's WalletAuthenticator
53
- // passes `wallet: getWallet()` to did-connect-js, so outer.agentDid becomes that
54
- // rotating address. Meanwhile the federated cert (signed by master) sets
55
- // agentDid = `did:abt:${verifySite.appId}` (permanent), so the wallet-side strict
56
- // check `cert.agentDid === outer.agentDid` fails with "Agent did does not match
57
- // with certificate issuer."
103
+ // THE SINGLE SEAM (wallet-authenticator-dynamic.md §0 "差异收敛在 IdentityDriver"):
104
+ // the active business wallet is whatever the active IdentityDriver provides, else
105
+ // the env wallet. arc-node + CF inject a driver whose getBusinessWallet resolves the
106
+ // current request/job tenant's wallet from the warmed cache (fail-closed outside a
107
+ // warmed scope); blocklet-server's default driver provides none → env wallet,
108
+ // unchanged. No consumer branches on the runtime only on driver capability. The
109
+ // driver is consulted on EVERY access so two concurrent tenants never share a wallet.
110
+ function activeBusinessWallet(chain: 'arcblock' | 'ethereum'): WalletObject {
111
+ const driver = getIdentityDriver();
112
+ if (typeof driver.getBusinessWallet === 'function') {
113
+ return driver.getBusinessWallet(chain);
114
+ }
115
+ return chain === 'ethereum' ? envEthWallet : envWallet;
116
+ }
117
+
118
+ /** Per-access proxy onto the active tenant's business wallet (see activeBusinessWallet). */
119
+ function businessWalletProxy(chain: 'arcblock' | 'ethereum'): WalletObject {
120
+ return new Proxy({} as WalletObject, {
121
+ get(_t, prop) {
122
+ const w = activeBusinessWallet(chain) as any;
123
+ const value = w[prop];
124
+ if (typeof value !== 'function') return value;
125
+ if (value._isMockFunction) return value; // keep jest spies usable (parity with lazyProxy)
126
+ return value.bind(w);
127
+ },
128
+ set(_t, prop, value) {
129
+ (activeBusinessWallet(chain) as any)[prop] = value;
130
+ return true;
131
+ },
132
+ has(_t, prop) {
133
+ return prop in (activeBusinessWallet(chain) as any);
134
+ },
135
+ });
136
+ }
137
+
138
+ export const wallet: WalletObject = businessWalletProxy('arcblock');
139
+ export const ethWallet: WalletObject = businessWalletProxy('ethereum');
140
+
141
+ // S3-CF Phase 1 (DID convergence): the DID-Connect token storage is a host slot.
142
+ export interface DidConnectTokenStorage {
143
+ read(token: string): Promise<any> | any;
144
+ create(token: string, status?: string): Promise<any> | any;
145
+ update(token: string, updates: Record<string, any>): Promise<any> | any;
146
+ delete(token: string): Promise<any> | any;
147
+ [key: string]: any;
148
+ }
149
+
150
+ // S3-CF Phase 1 (DID convergence) — the DID-Connect RUNTIME is host-injectable.
58
151
  //
59
- // Force the authenticator to derive its signing wallet from BLOCKLET_APP_PK
60
- // (permanent app pk address = appId), aligning with the fix in
61
- // @blocklet/sdk PR #12810 that already corrected getDelegatee. Remove once the
62
- // upstream wallet-authenticator.ts is patched accordingly.
63
- export const authenticator = new WalletAuthenticator({
64
- wallet: getWallet(undefined, '', 'sk'),
65
- });
66
- export const handlers = new WalletHandlers({
67
- authenticator,
68
- tokenStorage: new AuthStorage({
69
- dbPath: path.join(env.dataDir, 'auth.db'),
152
+ // The boundary is by HOST, not by node-vs-CF:
153
+ // - blocklet-server → the @blocklet/sdk WalletAuthenticator/WalletHandlers
154
+ // wrapper (autoConnect / notification relay /
155
+ // federated login). The ONLY runtime that uses SDK.
156
+ // - arc-node embedded + CF → the REAL @arcblock/did-connect-js stack, identity
157
+ // from AUTH_SERVICE.getInstanceAppIdentity, host-
158
+ // injected tokenStorage. They differ ONLY in the host
159
+ // adapter (storage/DB/event/waitUntil, CF chain
160
+ // config/txEncoder/timeout), NOT in SDK-vs-non-SDK.
161
+ // Unified = the host-injection + identity-resolution mechanism; SDK is one runtime
162
+ // implementation, not the node default. Every host injects via setDidConnectRuntime
163
+ // BEFORE `handlers` first materializes (lazyProxy → buildConnectRoutesHono).
164
+ export interface DidConnectRuntime {
165
+ /** Build the DID-Connect authenticator (signs sessions/certs). */
166
+ createAuthenticator(): any;
167
+ /** Wrap the authenticator + tokenStorage into the WalletHandlers used to attach routes. */
168
+ createHandlers(opts: { authenticator: any; tokenStorage: DidConnectTokenStorage }): any;
169
+ /** The DID-Connect token store; falls back to the blocklet-server nedb default when omitted. */
170
+ tokenStorage?: DidConnectTokenStorage;
171
+ }
172
+
173
+ let injectedRuntime: DidConnectRuntime | null = null;
174
+ let injectedTokenStorage: DidConnectTokenStorage | null = null;
175
+
176
+ /** Inject the full DID-Connect runtime (CF: real @arcblock/did-connect-js stack). */
177
+ export function setDidConnectRuntime(runtime: DidConnectRuntime | null): void {
178
+ injectedRuntime = runtime;
179
+ }
180
+
181
+ /** Inject only the DID-Connect token storage (storage slot; runtime keeps its default authenticator/handlers). */
182
+ export function setDidConnectTokenStorage(storage: DidConnectTokenStorage | null): void {
183
+ injectedTokenStorage = storage;
184
+ }
185
+
186
+ // The blocklet-server runtime — the ONLY runtime that uses the @blocklet/sdk
187
+ // wallet wrapper (autoConnect / notification relay / memberAppInfo / federated
188
+ // login / BlockletService). This is NOT "the node default": arc-node embedded and
189
+ // CF use the AUTH_SERVICE + @arcblock/did-connect-js runtime instead. The
190
+ // blocklet-server bootstrap injects this explicitly; it is also the legacy
191
+ // fallback when no runtime is injected (tests / a bare blocklet-server start).
192
+ //
193
+ // The signing wallet is derived from BLOCKLET_APP_PK (permanent app pk → address =
194
+ // appId) to fix the migrated-blocklet rotating-session-key cert mismatch
195
+ // ("Agent did does not match with certificate issuer", @blocklet/sdk PR #12810).
196
+ export function createBlockletServerDidConnectRuntime(): DidConnectRuntime {
197
+ return {
198
+ createAuthenticator() {
199
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
200
+ const { WalletAuthenticator } = require('@blocklet/sdk/lib/wallet-authenticator');
201
+ // @blocklet/sdk bundles @arcblock/did-connect-js@4.0.2, which made `txEncoder`
202
+ // mandatory for encoding *Tx prepareTx claims (scan-to-pay → TransferV3Tx) and
203
+ // dropped the old internal @ocap/client fallback (now only a devDependency; the
204
+ // shipped dist never requires it). The SDK's WalletAuthenticator wrapper does not
205
+ // inject one, so we pass the same CBOR encoder the CF/arc runtime uses
206
+ // (`createTxEncoder` from @ocap/client/encode). It passes through the wrapper's
207
+ // `...getAuthenticatorProps(options)` spread untouched.
208
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
209
+ const { createTxEncoder } = require('@ocap/client/encode');
210
+ return new WalletAuthenticator({ wallet: makeWallet(undefined, '', 'sk'), txEncoder: createTxEncoder() });
211
+ },
212
+ createHandlers({ authenticator: auth, tokenStorage }) {
213
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
214
+ const { WalletHandlers } = require('@blocklet/sdk/lib/wallet-handler');
215
+ return new WalletHandlers({ authenticator: auth, tokenStorage });
216
+ },
217
+ };
218
+ }
219
+
220
+ // Consume the injected runtime. When none is injected we fall back to the
221
+ // blocklet-server (SDK) runtime as a LEGACY compat for a bare blocklet-server /
222
+ // test start — NOT as a generic node default. CF and arc-node embedded hosts MUST
223
+ // inject their AUTH_SERVICE runtime; if a CF host ever reaches this fallback, the
224
+ // workerd @blocklet/sdk wallet-* shims are fail-fast (they throw on construct), so
225
+ // it can never silently register zero DID routes. (A spec also asserts the
226
+ // AUTH_SERVICE runtime never imports the @blocklet/sdk wallet modules.)
227
+ function activeRuntime(): DidConnectRuntime {
228
+ return injectedRuntime ?? createBlockletServerDidConnectRuntime();
229
+ }
230
+
231
+ function buildTokenStorage(): DidConnectTokenStorage {
232
+ // priority: runtime-provided store → explicit storage slot → blocklet-server
233
+ // nedb default (file-backed; the node blocklet-server host only).
234
+ if (injectedRuntime?.tokenStorage) return injectedRuntime.tokenStorage;
235
+ if (injectedTokenStorage) return injectedTokenStorage;
236
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
237
+ const AuthStorage = require('@arcblock/did-connect-storage-nedb');
238
+ return new AuthStorage({
239
+ // `env.dataDir` (the blocklet runtime data dir) is undefined in a bare
240
+ // embedded host like arc — `store/sequelize.ts` already guards its own
241
+ // use, but this DID-Connect token store was the one unguarded path and
242
+ // crashed `buildConnectRoutesHono` with `path.join(undefined, …)`. Fall
243
+ // back to the OS temp dir (DID-Connect session tokens are ephemeral; the
244
+ // blocklet server still gets its real dataDir natively).
245
+ dbPath: path.join(env.dataDir || os.tmpdir(), 'auth.db'),
70
246
  // @ts-ignore
71
247
  onload: console.warn,
72
- }),
248
+ });
249
+ }
250
+
251
+ export const authenticator: any = lazyProxy(() => activeRuntime().createAuthenticator());
252
+
253
+ export const handlers: any = lazyProxy(() => {
254
+ const tokenStorage = buildTokenStorage();
255
+ return activeRuntime().createHandlers({ authenticator, tokenStorage });
73
256
  });
74
257
 
75
- export const blocklet = new BlockletService();
258
+ // The user directory (`blocklet.getUser` etc.). SAME single seam as the wallet:
259
+ // the active IdentityDriver's `directory()` if it provides one, else the real
260
+ // @blocklet/sdk BlockletService. arc-node injects a DID-echo directory (the real
261
+ // BlockletService can't construct without BLOCKLET_APP_ID); CF resolves the real
262
+ // BlockletService to its build-alias shim; blocklet-server gets the real one. No
263
+ // `isCfWorker`/runtime branch — only "does the driver provide a directory".
264
+ export const blocklet: any = lazyProxy(() => {
265
+ const driver = getIdentityDriver();
266
+ if (typeof driver.directory === 'function') {
267
+ return driver.directory();
268
+ }
269
+ ensureNotificationPatch();
270
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
271
+ const { BlockletService } = require('@blocklet/sdk/service/auth');
272
+ return new BlockletService();
273
+ });
76
274
 
77
275
  export async function getVaultAddress() {
78
276
  try {
@@ -88,7 +286,9 @@ export async function getVaultAddress() {
88
286
  }
89
287
 
90
288
  export type CallbackArgs = {
91
- request: Request & { context: Record<string, any> };
289
+ // did-connect-js passes its framework-adapted request to handler callbacks
290
+ // (createHonoRequest under attachHono); only `.context` is read here.
291
+ request: { context: Record<string, any>; [key: string]: any };
92
292
  userDid: string;
93
293
  userPk: string;
94
294
  didwallet: {
@@ -1,13 +1,29 @@
1
1
  import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
2
2
 
3
+ import {
4
+ TENANT_CONTEXT_MISSING,
5
+ TenantError,
6
+ assertValidInstanceDid,
7
+ getDefaultInstanceDid,
8
+ getTenantMode,
9
+ } from './tenant';
10
+
11
+ export * from './tenant';
12
+
3
13
  interface RequestContext {
4
14
  requestedBy?: string;
5
15
  requestId?: string;
16
+ instanceDid?: string;
6
17
  }
7
18
 
8
19
  class RequestContextManager {
9
20
  private storage = new AsyncLocalStorage<RequestContext>();
10
21
  private contexts = new Map<string, RequestContext>();
22
+ // System-operation flag: when set, TenantModel bypasses tenant scoping so a
23
+ // legitimate cross-tenant read (queue dispatch, IAP bundle->tenant reverse
24
+ // lookup, event fan-out) can load rows across tenants. Entered ONLY via the
25
+ // system* helpers — a normal route context can never read across tenants.
26
+ private systemStorage = new AsyncLocalStorage<boolean>();
11
27
 
12
28
  getContext(requestId?: string): RequestContext {
13
29
  if (requestId && this.contexts.has(requestId)) {
@@ -20,16 +36,73 @@ class RequestContextManager {
20
36
  return this.getContext(requestId).requestedBy;
21
37
  }
22
38
 
39
+ /**
40
+ * Run fn with the given tenant. Nested calls shadow the outer tenant and restore it on exit.
41
+ * Other context fields (requestId, requestedBy) are inherited from the enclosing scope.
42
+ */
43
+ withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
44
+ try {
45
+ assertValidInstanceDid(instanceDid);
46
+ } catch (err) {
47
+ return Promise.reject(err);
48
+ }
49
+ const parent = this.storage.getStore();
50
+ // frozen so fn cannot mutate the stored context and affect sibling calls
51
+ const next = Object.freeze({ ...parent, instanceDid });
52
+ return this.storage.run(next, () => Promise.resolve(fn()));
53
+ }
54
+
55
+ /**
56
+ * Current tenant. Single mode falls back to the deployment app DID;
57
+ * multi mode fails closed with TENANT_CONTEXT_MISSING.
58
+ */
59
+ getInstanceDid(): string {
60
+ const instanceDid = this.storage.getStore()?.instanceDid;
61
+ if (instanceDid) return instanceDid;
62
+ if (getTenantMode() === 'single') return getDefaultInstanceDid();
63
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'tenant context is missing in multi-tenant mode');
64
+ }
65
+
66
+ /**
67
+ * Non-throwing peek at the established tenant context — returns the stored
68
+ * instanceDid or undefined when no context is set (regardless of tenant mode).
69
+ * Used by the DID-Connect tenant-context middleware to decide whether the
70
+ * request is already scoped (e.g. the CF worker wrapped /api/* in withTenant) or
71
+ * needs its own Host→tenant resolution.
72
+ */
73
+ peekInstanceDid(): string | undefined {
74
+ return this.storage.getStore()?.instanceDid;
75
+ }
76
+
77
+ /**
78
+ * Run fn as a system operation: TenantModel scoping is bypassed for the span
79
+ * of fn so legitimate cross-tenant reads can load rows regardless of tenant.
80
+ * The scope ends when fn settles — callers must enforce the row's tenant
81
+ * themselves (e.g. assertJobObjectTenant). Explicit by construction: only the
82
+ * system* helpers enter this, so a normal route can never read across tenants.
83
+ */
84
+ runAsSystem<T>(fn: () => Promise<T> | T): Promise<T> {
85
+ return this.systemStorage.run(true, () => Promise.resolve(fn()));
86
+ }
87
+
88
+ /** True inside a runAsSystem span — TenantModel checks this to skip scoping. */
89
+ isSystem(): boolean {
90
+ return this.systemStorage.getStore() === true;
91
+ }
92
+
23
93
  run<T>(context: RequestContext, fn: () => Promise<T> | T): Promise<T> {
24
94
  const requestId = context.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
95
+ // inherit tenant from the enclosing scope unless explicitly provided
96
+ const instanceDid = context.instanceDid ?? this.storage.getStore()?.instanceDid;
25
97
 
26
98
  this.contexts.set(requestId, {
27
99
  ...context,
100
+ instanceDid,
28
101
  requestId,
29
102
  });
30
103
 
31
104
  return new Promise((resolve, reject) => {
32
- this.storage.run({ ...context, requestId }, async () => {
105
+ this.storage.run({ ...context, instanceDid, requestId }, async () => {
33
106
  const resource = new AsyncResource('RequestContext');
34
107
  try {
35
108
  const result = await resource.runInAsyncScope(fn);
@@ -46,3 +119,18 @@ class RequestContextManager {
46
119
  }
47
120
 
48
121
  export const context = new RequestContextManager();
122
+
123
+ /** Run fn with the given tenant — also the test injection helper for jest. */
124
+ export function withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
125
+ return context.withTenant(instanceDid, fn);
126
+ }
127
+
128
+ /** Current tenant; see RequestContextManager#getInstanceDid for mode semantics. */
129
+ export function getInstanceDid(): string {
130
+ return context.getInstanceDid();
131
+ }
132
+
133
+ /** True inside a runAsSystem span; TenantModel uses it to bypass scoping. */
134
+ export function isSystemContext(): boolean {
135
+ return context.isSystem();
136
+ }
@@ -1,8 +1,8 @@
1
1
  import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
- import { getUrl } from '@blocklet/sdk';
2
+ import { getUrl } from '@blocklet/sdk/lib/component';
3
3
  import { PaymentCurrency, Price, Product, RechargeConfig } from '../store/models';
4
4
  import { trimDecimals } from './math-utils';
5
- import { createPaymentLink } from '../routes/payment-links';
5
+ import { createPaymentLink } from '../routes/hono/payment-links';
6
6
  import logger from './logger';
7
7
 
8
8
  export async function formatCurrencyToken(amount: string, currencyId: string) {
@@ -5,8 +5,14 @@ import relativeTime from 'dayjs/plugin/relativeTime';
5
5
  import timezone from 'dayjs/plugin/timezone'; // dependent on utc plugin
6
6
  import utc from 'dayjs/plugin/utc';
7
7
 
8
- import('dayjs/locale/en');
9
- import('dayjs/locale/zh');
8
+ // Use explicit `.js` so the dynamic import resolves under Node ESM (strict
9
+ // resolution: dayjs ships no `exports` map, so the extensionless subpath
10
+ // `dayjs/locale/en` fails when payment-core is embedded in a Node ESM host
11
+ // like arc). Webpack/blocklet-server tolerate the extension too.
12
+ // eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
13
+ import('dayjs/locale/en.js');
14
+ // eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
15
+ import('dayjs/locale/zh.js');
10
16
 
11
17
  dayjs.extend(relativeTime);
12
18
  dayjs.extend(localizedFormat);