payment-kit 1.29.1 → 1.29.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,245 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { Sequelize } from 'sequelize';
5
+ import { SequelizeStorage, Umzug } from 'umzug';
6
+
7
+ import { withTenant } from '../../src/libs/context';
8
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
9
+ import { systemFindByPk } from '../../src/store/scoped';
10
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
11
+
12
+ jest.mock('../../src/libs/logger', () => ({
13
+ __esModule: true,
14
+ default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
15
+ }));
16
+
17
+ const STORE_DIR = path.join(__dirname, '../../src/store');
18
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-matrix-a-'));
19
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
20
+ const umzug = new Umzug({
21
+ migrations: {
22
+ glob: ['migrations/*.ts', { cwd: STORE_DIR }],
23
+ resolve: ({ name, path: p, context }) => {
24
+ // eslint-disable-next-line import/no-dynamic-require, global-require
25
+ const migration = require(p!);
26
+ return {
27
+ name: name.replace(/\.ts$/, '.js'),
28
+ up: () => migration.up({ context }),
29
+ down: () => migration.down({ context }),
30
+ };
31
+ },
32
+ },
33
+ context: sequelize.getQueryInterface(),
34
+ storage: new SequelizeStorage({ sequelize }),
35
+ logger: undefined,
36
+ });
37
+
38
+ let models: any;
39
+ let createQueue: any;
40
+ let assertJobObjectTenant: any;
41
+
42
+ beforeAll(async () => {
43
+ await umzug.up();
44
+ // eslint-disable-next-line global-require
45
+ models = require('../../src/store/models');
46
+ models.initialize(sequelize);
47
+ // eslint-disable-next-line global-require
48
+ const queueModule = require('../../src/libs/queue');
49
+ createQueue = queueModule.default;
50
+ assertJobObjectTenant = queueModule.assertJobObjectTenant;
51
+ }, 120000);
52
+
53
+ afterAll(async () => {
54
+ await sequelize.close();
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ });
57
+
58
+ const waitFor = (emitter: any, events: string[]): Promise<{ event: string; data: any }> =>
59
+ new Promise((resolve) => {
60
+ for (const event of events) {
61
+ emitter.on(event, (data: any) => resolve({ event, data }));
62
+ }
63
+ });
64
+
65
+ describe('queue tenant layer — first batch (phase 5)', () => {
66
+ beforeEach(async () => {
67
+ await sequelize.query('DELETE FROM customers');
68
+ await sequelize.query('DELETE FROM jobs');
69
+ });
70
+
71
+ describe('happy path', () => {
72
+ it('push stamps the active tenant into the payload, job row included, handler runs in that tenant', async () => {
73
+ const seen: string[] = [];
74
+ const queue = createQueue({
75
+ name: `tm-happy-${Date.now()}`,
76
+ onJob: async (job: any) => {
77
+ // eslint-disable-next-line global-require
78
+ const { getInstanceDid } = require('../../src/libs/context');
79
+ seen.push(job.instance_did, getInstanceDid());
80
+ },
81
+ });
82
+
83
+ const handle = await withTenant(TENANT_A, async () => queue.push({ job: { businessId: 'x1' }, persist: true }));
84
+ await waitFor(handle, ['finished', 'failed']);
85
+ expect(seen).toEqual([TENANT_A, TENANT_A]);
86
+ });
87
+
88
+ it('handler object guard passes for matching tenants', async () => {
89
+ const customer = await withTenant(TENANT_A, () =>
90
+ models.Customer.create({ livemode: false, did: 'z-m-a', delinquent: false, instance_did: TENANT_A })
91
+ );
92
+ await withTenant(TENANT_A, async () => {
93
+ expect(() => assertJobObjectTenant(customer)).not.toThrow();
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('bad input', () => {
99
+ it('multi mode push without tenant context is rejected at the gate', async () => {
100
+ process.env.PAYMENT_TENANT_MODE = 'multi';
101
+ try {
102
+ const queue = createQueue({ name: `tm-bad-${Date.now()}`, onJob: async () => {} });
103
+ expect(() => queue.push({ job: { businessId: 'x' } })).toThrow(
104
+ expect.objectContaining({ code: TENANT_CONTEXT_MISSING })
105
+ );
106
+ } finally {
107
+ delete process.env.PAYMENT_TENANT_MODE;
108
+ }
109
+ });
110
+
111
+ it('illegal tenant DID in the payload is refused', () => {
112
+ const queue = createQueue({ name: `tm-bad2-${Date.now()}`, onJob: async () => {} });
113
+ expect(() => queue.push({ job: { businessId: 'x', instance_did: 'a b' } })).toThrow(
114
+ expect.objectContaining({ code: TENANT_CONTEXT_MISSING })
115
+ );
116
+ });
117
+ });
118
+
119
+ describe('security: forged payload tenant vs object tenant', () => {
120
+ it('handler refuses to act on another tenant object; the object is untouched', async () => {
121
+ const victim = await withTenant(TENANT_B, () =>
122
+ models.Customer.create({ livemode: false, did: 'z-victim', delinquent: false, instance_did: TENANT_B })
123
+ );
124
+
125
+ let observedError: any;
126
+ const queue = createQueue({
127
+ name: `tm-forged-${Date.now()}`,
128
+ onJob: async (job: any) => {
129
+ // handlers load the tenant-stamped object cross-tenant (system) then
130
+ // enforce its tenant — mirrors the real queue handlers so a forged
131
+ // payload surfaces an observable TENANT_MISMATCH rather than folding
132
+ // into a scoped null (tenant-design §10).
133
+ const row: any = await systemFindByPk(models.Customer, job.customerId);
134
+ assertJobObjectTenant(row); // throws TENANT_MISMATCH
135
+ await row.update({ name: 'pwned' });
136
+ },
137
+ });
138
+ // forge: payload claims TENANT_A but targets B's customer
139
+ const handle = await withTenant(TENANT_A, async () =>
140
+ queue.push({ job: { customerId: victim.id }, persist: true })
141
+ );
142
+ const outcome = await waitFor(handle, ['failed', 'finished']);
143
+ observedError = (outcome.data as any).error;
144
+
145
+ expect(outcome.event).toBe('failed');
146
+ expect(observedError?.code).toBe(TENANT_MISMATCH);
147
+ const reloaded: any = await systemFindByPk(models.Customer, victim.id);
148
+ expect(reloaded.name).toBeNull(); // not 'pwned'
149
+ expect(reloaded.instance_did).toBe(TENANT_B);
150
+ });
151
+ });
152
+
153
+ describe('data loss: refused jobs surface as failures, not silence', () => {
154
+ it('the failed event fires with the tenant error attached', async () => {
155
+ const queue = createQueue({
156
+ name: `tm-fail-${Date.now()}`,
157
+ onJob: async () => {
158
+ const err: any = new Error('refused');
159
+ err.code = TENANT_MISMATCH;
160
+ err.nonRetryable = true;
161
+ throw err;
162
+ },
163
+ });
164
+ const handle = await withTenant(TENANT_A, async () => queue.push({ job: { businessId: 'x' }, persist: true }));
165
+ const outcome = await waitFor(handle, ['failed', 'finished']);
166
+ expect(outcome.event).toBe('failed');
167
+ expect((outcome.data as any).error?.code).toBe(TENANT_MISMATCH);
168
+ });
169
+ });
170
+
171
+ describe('data damage: retries keep the original tenant', () => {
172
+ it('a retried job re-executes under the same tenant, never a different one', async () => {
173
+ const tenantsSeen: string[] = [];
174
+ const queue = createQueue({
175
+ name: `tm-retry-${Date.now()}`,
176
+ onJob: async (job: any) => {
177
+ tenantsSeen.push(job.instance_did);
178
+ if (tenantsSeen.length < 2) throw new Error('transient');
179
+ },
180
+ options: { maxRetries: 2, retryDelay: 1 },
181
+ });
182
+ const handle = await withTenant(TENANT_B, async () => queue.push({ job: { businessId: 'r1' }, persist: true }));
183
+ await waitFor(handle, ['finished', 'failed']);
184
+ expect(tenantsSeen.length).toBeGreaterThanOrEqual(2);
185
+ expect(new Set(tenantsSeen)).toEqual(new Set([TENANT_B]));
186
+ });
187
+ });
188
+
189
+ // NOTE: spec's "channel-identifier job id prefix" row converts to a no-op
190
+ // for Phase 5 queues — all job ids are internal globally-unique business
191
+ // ids (see queue-matrix.md); channel-identifier dedup moves to Phase 6
192
+ // integrations as (instance_did, identifier) composite keys.
193
+ describe('data leak: legacy job strategy', () => {
194
+ it('a pre-tenant job (no instance_did) executes under the default tenant in single mode', async () => {
195
+ const seen: string[] = [];
196
+ const queue = createQueue({
197
+ name: `tm-legacy-${Date.now()}`,
198
+ onJob: async () => {
199
+ // eslint-disable-next-line global-require
200
+ const { getInstanceDid } = require('../../src/libs/context');
201
+ seen.push(getInstanceDid());
202
+ },
203
+ });
204
+ // simulate a legacy row: bypass push's injection by writing the store directly
205
+ await queue.store.addJob('legacy-1', { businessId: 'legacy' }, {});
206
+ const row = await queue.get('legacy-1');
207
+ expect(row.instance_did).toBeUndefined();
208
+ // re-deliver it the way startup recovery does (push with persist=false keeps payload as-is? no — push injects)
209
+ // execution path: runJobWithTenant falls back to the default tenant
210
+ const handle = queue.push({
211
+ job: row,
212
+ id: 'legacy-1',
213
+ persist: false,
214
+ skipDuplicateCheck: true,
215
+ fromStore: true,
216
+ });
217
+ await waitFor(handle, ['finished', 'failed']);
218
+ // eslint-disable-next-line global-require
219
+ const { getDefaultInstanceDid } = require('../../src/libs/tenant');
220
+ expect(seen).toEqual([getDefaultInstanceDid()]);
221
+ });
222
+
223
+ it('a legacy job is refused permanently in multi mode (structured, non-retryable)', async () => {
224
+ const queue = createQueue({
225
+ name: `tm-legacy-multi-${Date.now()}`,
226
+ onJob: async () => 'should-not-run',
227
+ });
228
+ process.env.PAYMENT_TENANT_MODE = 'multi';
229
+ try {
230
+ const handle = queue.push({
231
+ job: { businessId: 'legacy-multi' },
232
+ id: 'legacy-multi-1',
233
+ persist: false,
234
+ fromStore: true, // store re-delivery path: gate skipped, execution-side refuses
235
+ });
236
+ const outcome = await waitFor(handle, ['failed', 'finished']);
237
+ expect(outcome.event).toBe('failed');
238
+ expect((outcome.data as any).error?.code).toBe(TENANT_CONTEXT_MISSING);
239
+ expect((outcome.data as any).error?.nonRetryable).toBe(true);
240
+ } finally {
241
+ delete process.env.PAYMENT_TENANT_MODE;
242
+ }
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,168 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { Sequelize } from 'sequelize';
5
+ import { SequelizeStorage, Umzug } from 'umzug';
6
+
7
+ import { withTenant } from '../../src/libs/context';
8
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
9
+ import { systemFindByPk } from '../../src/store/scoped';
10
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
11
+
12
+ jest.mock('../../src/libs/logger', () => ({
13
+ __esModule: true,
14
+ default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
15
+ }));
16
+
17
+ const STORE_DIR = path.join(__dirname, '../../src/store');
18
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-matrix-b-'));
19
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
20
+ const umzug = new Umzug({
21
+ migrations: {
22
+ glob: ['migrations/*.ts', { cwd: STORE_DIR }],
23
+ resolve: ({ name, path: p, context }) => {
24
+ // eslint-disable-next-line import/no-dynamic-require, global-require
25
+ const migration = require(p!);
26
+ return {
27
+ name: name.replace(/\.ts$/, '.js'),
28
+ up: () => migration.up({ context }),
29
+ down: () => migration.down({ context }),
30
+ };
31
+ },
32
+ },
33
+ context: sequelize.getQueryInterface(),
34
+ storage: new SequelizeStorage({ sequelize }),
35
+ logger: undefined,
36
+ });
37
+
38
+ let models: any;
39
+ let createQueue: any;
40
+ let assertJobObjectTenant: any;
41
+
42
+ beforeAll(async () => {
43
+ await umzug.up();
44
+ // eslint-disable-next-line global-require
45
+ models = require('../../src/store/models');
46
+ models.initialize(sequelize);
47
+ // eslint-disable-next-line global-require
48
+ const queueModule = require('../../src/libs/queue');
49
+ createQueue = queueModule.default;
50
+ assertJobObjectTenant = queueModule.assertJobObjectTenant;
51
+ }, 120000);
52
+
53
+ afterAll(async () => {
54
+ await sequelize.close();
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ });
57
+
58
+ const waitFor = (emitter: any, events: string[]): Promise<{ event: string; data: any }> =>
59
+ new Promise((resolve) => {
60
+ for (const event of events) {
61
+ emitter.on(event, (data: any) => resolve({ event, data }));
62
+ }
63
+ });
64
+
65
+ describe('queue tenant layer — remaining queues (phase 6)', () => {
66
+ beforeEach(async () => {
67
+ await sequelize.query('DELETE FROM coupons');
68
+ await sequelize.query('DELETE FROM jobs');
69
+ });
70
+
71
+ describe('happy path + data damage: phase 6 object-bound handler pattern', () => {
72
+ it('a coupon-bound job (discount-status shape) executes only for its own tenant', async () => {
73
+ const coupon = await withTenant(TENANT_B, () =>
74
+ models.Coupon.create({
75
+ livemode: false,
76
+ instance_did: TENANT_B,
77
+ name: 'b-coupon',
78
+ duration: 'once',
79
+ valid: true,
80
+ created_via: 'api',
81
+ percent_off: 10,
82
+ })
83
+ );
84
+
85
+ const executed: string[] = [];
86
+ const queue = createQueue({
87
+ name: `tmb-coupon-${Date.now()}`,
88
+ onJob: async (job: any) => {
89
+ // load cross-tenant (system) then enforce — mirrors real handlers
90
+ const row: any = await systemFindByPk(models.Coupon, job.id);
91
+ assertJobObjectTenant(row);
92
+ executed.push(row.id);
93
+ },
94
+ });
95
+
96
+ // same tenant: executes
97
+ const ok = await withTenant(TENANT_B, async () => queue.push({ job: { id: coupon.id }, persist: true }));
98
+ const okOutcome = await waitFor(ok, ['finished', 'failed']);
99
+ expect(okOutcome.event).toBe('finished');
100
+ expect(executed).toEqual([coupon.id]);
101
+
102
+ // forged tenant: refused
103
+ const forged = await withTenant(TENANT_A, async () =>
104
+ queue.push({ job: { id: coupon.id }, id: `forged-${coupon.id}`, persist: true })
105
+ );
106
+ const forgedOutcome = await waitFor(forged, ['finished', 'failed']);
107
+ expect(forgedOutcome.event).toBe('failed');
108
+ expect((forgedOutcome.data as any).error?.code).toBe(TENANT_MISMATCH);
109
+ expect(executed).toHaveLength(1);
110
+ });
111
+ });
112
+
113
+ describe('data loss: in-flight legacy jobs across an upgrade window', () => {
114
+ it('single mode consumes a pre-tenant job normally (default tenant)', async () => {
115
+ const seen: string[] = [];
116
+ const queue = createQueue({
117
+ name: `tmb-legacy-${Date.now()}`,
118
+ onJob: async () => {
119
+ // eslint-disable-next-line global-require
120
+ const { getInstanceDid } = require('../../src/libs/context');
121
+ seen.push(getInstanceDid());
122
+ },
123
+ });
124
+ await queue.store.addJob('legacy-b-1', { anything: true }, {});
125
+ const row = await queue.get('legacy-b-1');
126
+ const handle = queue.push({
127
+ job: row,
128
+ id: 'legacy-b-1',
129
+ persist: false,
130
+ skipDuplicateCheck: true,
131
+ fromStore: true,
132
+ });
133
+ const outcome = await waitFor(handle, ['finished', 'failed']);
134
+ expect(outcome.event).toBe('finished');
135
+ // eslint-disable-next-line global-require
136
+ const { getDefaultInstanceDid } = require('../../src/libs/tenant');
137
+ expect(seen).toEqual([getDefaultInstanceDid()]);
138
+ });
139
+ });
140
+
141
+ describe('data leak: multi mode never falls back to a default tenant', () => {
142
+ it('a legacy job is refused with a structured non-retryable error and never executes', async () => {
143
+ const executed: any[] = [];
144
+ const queue = createQueue({
145
+ name: `tmb-legacy-multi-${Date.now()}`,
146
+ onJob: async (job: any) => {
147
+ executed.push(job);
148
+ },
149
+ });
150
+ process.env.PAYMENT_TENANT_MODE = 'multi';
151
+ try {
152
+ const handle = queue.push({
153
+ job: { anything: true },
154
+ id: 'legacy-multi-b',
155
+ persist: false,
156
+ fromStore: true,
157
+ });
158
+ const outcome = await waitFor(handle, ['finished', 'failed']);
159
+ expect(outcome.event).toBe('failed');
160
+ expect((outcome.data as any).error?.code).toBe(TENANT_CONTEXT_MISSING);
161
+ expect((outcome.data as any).error?.nonRetryable).toBe(true);
162
+ expect(executed).toHaveLength(0);
163
+ } finally {
164
+ delete process.env.PAYMENT_TENANT_MODE;
165
+ }
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,107 @@
1
+ // Phase 2 (express→hono) — DID-Connect surface migration. Proves the SAME
2
+ // @blocklet/sdk WalletHandlers attach to a hono app via did-connect-js v4 native
3
+ // attachHono (isHonoApp dispatch, design §3.3): all routes register and a request
4
+ // is dispatched to the generateSession handler over app.fetch. The 14 handlers
5
+ // are unchanged (framework-agnostic CallbackArgs); only the app type changes.
6
+ //
7
+ // NOTE: the session-DEPENDENT spec-table categories (Security ensureSignedJson,
8
+ // Data-loss deep-link, Data-damage appInfo, Data-leak token isolation) and the
9
+ // happy-path session SHAPE are validated over a REAL socket in
10
+ // scripts/e2e-hono-s2.ts (10 self-validating checks, logs/s2-e2e.log). They
11
+ // cannot run in jest: generateSession needs a valid appInfo.icon URI, which jest's
12
+ // bare blocklet env does not provide (the SDK authenticator rejects it). The E2E
13
+ // uses the spike's testSetup env (parsed blocklet.yml → valid appInfo).
14
+ import { buildConnectRoutesHono } from '../../../src/service';
15
+
16
+ const PREFIX = '/api/did';
17
+ // a representative subset of the 14 actions
18
+ const ACTIONS = ['collect', 'payment', 'subscription'];
19
+ const ALL_ACTIONS = [
20
+ 'collect',
21
+ 'collect-batch',
22
+ 'payment',
23
+ 'setup',
24
+ 'subscription',
25
+ 'change-payment',
26
+ 'change-plan',
27
+ 'recharge',
28
+ 'recharge-account',
29
+ 'delegation',
30
+ 'overdraft-protection',
31
+ 're-stake',
32
+ 'auto-recharge-auth',
33
+ 'change-payer',
34
+ ];
35
+
36
+ let connectApp: ReturnType<typeof buildConnectRoutesHono>;
37
+
38
+ beforeAll(() => {
39
+ connectApp = buildConnectRoutesHono();
40
+ });
41
+
42
+ const registeredPaths = (): Set<string> => new Set(((connectApp as any).routes || []).map((r: any) => r.path));
43
+
44
+ describe('Phase 2 — attachHono route registration (all 14 handlers)', () => {
45
+ it('registers token/status/timeout/auth/auth-submit for each action', () => {
46
+ const paths = registeredPaths();
47
+ for (const action of ACTIONS) {
48
+ for (const sub of ['token', 'status', 'timeout', 'auth', 'auth/submit']) {
49
+ expect(paths.has(`${PREFIX}/${action}/${sub}`)).toBe(true);
50
+ }
51
+ }
52
+ });
53
+
54
+ it('registers the full route set (token/status/timeout/auth/auth-submit) for ALL 14 actions', () => {
55
+ const paths = registeredPaths();
56
+ expect(ALL_ACTIONS.length).toBe(14);
57
+ for (const action of ALL_ACTIONS) {
58
+ for (const sub of ['token', 'status', 'timeout', 'auth', 'auth/submit']) {
59
+ expect(paths.has(`${PREFIX}/${action}/${sub}`)).toBe(true);
60
+ }
61
+ }
62
+ });
63
+
64
+ it('a POST to /auth is registered (the signed authInfo endpoint) and dispatched to the handler', async () => {
65
+ const paths = registeredPaths();
66
+ expect(paths.has(`${PREFIX}/payment/auth`)).toBe(true); // POST exists too
67
+ // an unsigned POST with a forged token reaches the handler (500/4xx, never a
68
+ // hono 404) — proving attachHono wired the POST body adapter; the actual
69
+ // ensureSignedJson rejection on a VALID session is in the E2E.
70
+ const res = await connectApp.fetch(
71
+ new Request(`http://app.local${PREFIX}/payment/auth?_t_=deadbeef`, {
72
+ method: 'POST',
73
+ headers: { 'content-type': 'application/json' },
74
+ body: JSON.stringify({ userDid: 'did:abt:zForged', claims: [] }),
75
+ })
76
+ );
77
+ expect(res.status).not.toBe(404);
78
+ const body = await res.text();
79
+ expect(body).not.toContain('"status":"created"'); // never resolves a session for a forged token
80
+ });
81
+ });
82
+
83
+ describe('Phase 2 — attachHono dispatches GET .../token to the handler (over app.fetch)', () => {
84
+ it('reaches the generateSession handler (200 JSON), not a hono 404', async () => {
85
+ // The full successful session (valid appInfo) is validated over a real socket
86
+ // in scripts/e2e-hono-s2.ts (a valid appInfo.icon URI is an env detail jest's
87
+ // bare blocklet env lacks). Here we prove attachHono ROUTED the request to the
88
+ // did-connect handler: a 200 JSON response, never hono's 404 Not Found.
89
+ const res = await connectApp.fetch(new Request(`http://app.local${PREFIX}/collect/token`));
90
+ expect(res.status).toBe(200);
91
+ const body = await res.json(); // handler output is JSON (session OR a did-connect error)
92
+ expect(typeof body).toBe('object');
93
+ });
94
+ });
95
+
96
+ describe('Phase 2 — bad input: an invalid/forged session token does not resolve', () => {
97
+ it('GET .../payment/auth with a non-existent _t_ does not return a valid authInfo/session', async () => {
98
+ const res = await connectApp.fetch(
99
+ new Request(`http://app.local${PREFIX}/payment/auth?_t_=deadbeef-not-a-session`)
100
+ );
101
+ // attachHono surfaces the did-connect error path (4xx or an error-status body),
102
+ // never a created session for a forged token.
103
+ const body = await res.json().catch(() => ({}));
104
+ const looksLikeValidSession = res.status === 200 && (body.status === 'created' || body.authInfo);
105
+ expect(looksLikeValidSession).toBe(false);
106
+ });
107
+ });
@@ -0,0 +1,96 @@
1
+ // Phase 4 (express→hono) — adapter collapse.
2
+ //
3
+ // The loopback http.Server + express app shell are gone: svc.http.fetch is now a
4
+ // thin base-strip wrapper over honoApp.fetch, and the full node app is the hono
5
+ // app itself. This spec locks the two things the collapse must preserve byte-for-
6
+ // byte (the arc consumer contract): the base-strip semantics and raw-body
7
+ // fidelity through the strip, plus the hono app's healthz + onError.
8
+ import { Hono } from 'hono';
9
+ import crypto from 'crypto';
10
+ import { createFetchHandler } from '../../src/libs/http-fetch-adapter';
11
+ import { buildHonoApp } from '../../src/service';
12
+
13
+ // A stand-in "core" hono app: echoes the internal path + the raw body bytes so we
14
+ // can assert exactly what the adapter forwarded after base-stripping.
15
+ function buildEchoApp(): Hono {
16
+ const app = new Hono();
17
+ app.get('/api/echo', (c) => c.json({ path: new URL(c.req.url).pathname }));
18
+ app.post('/api/raw', async (c) => {
19
+ const buf = Buffer.from(await c.req.arrayBuffer());
20
+ return c.json({ len: buf.length, sig: crypto.createHmac('sha256', 'k').update(buf).digest('hex') });
21
+ });
22
+ return app;
23
+ }
24
+
25
+ describe('Phase 4 collapse — svc.http.fetch base-strip over app.fetch', () => {
26
+ const handler = createFetchHandler(buildEchoApp());
27
+
28
+ it('happy: strips the host mount prefix to reach the internal /api/*', async () => {
29
+ const res = await handler(new Request('http://app.local/.well-known/payment/api/echo'), {
30
+ basePath: '/.well-known/payment',
31
+ });
32
+ expect(res.status).toBe(200);
33
+ expect((await res.json()).path).toBe('/api/echo');
34
+ });
35
+
36
+ it('happy: no basePath → request passes through unchanged', async () => {
37
+ const res = await handler(new Request('http://app.local/api/echo'));
38
+ expect((await res.json()).path).toBe('/api/echo');
39
+ });
40
+
41
+ it('bad input: exact segment-boundary strip — basePath itself maps to "/"', async () => {
42
+ // /.well-known/payment (=== basePath) → "/", which the echo app 404s; the
43
+ // point is the strip is applied (not left as the prefix).
44
+ const res = await handler(new Request('http://app.local/.well-known/payment'), {
45
+ basePath: '/.well-known/payment',
46
+ });
47
+ expect(res.status).toBe(404); // "/" has no route — strip happened, didn't match
48
+ });
49
+
50
+ it('bad input: segment-boundary negative — "/mntbeta" is NOT stripped by basePath "/mnt"', async () => {
51
+ // a naive startsWith would wrongly strip "/mnt" from "/mntbeta/..."; the precise
52
+ // === / "+ /" check must leave it intact (→ /mntbeta/api/echo, 404).
53
+ const res = await handler(new Request('http://app.local/mntbeta/api/echo'), { basePath: '/mnt' });
54
+ expect(res.status).toBe(404);
55
+ // the correctly-prefixed twin strips to /api/echo and resolves
56
+ const ok = await handler(new Request('http://app.local/mnt/api/echo'), { basePath: '/mnt' });
57
+ expect(ok.status).toBe(200);
58
+ expect((await ok.json()).path).toBe('/api/echo');
59
+ });
60
+
61
+ it('security/data loss: raw body bytes survive the strip intact (Stripe webhook fidelity)', async () => {
62
+ const payload = JSON.stringify({ evt: 'x', nested: { a: 1, b: '<b>' } });
63
+ const res = await handler(
64
+ new Request('http://app.local/.well-known/payment/api/raw', {
65
+ method: 'POST',
66
+ headers: { 'content-type': 'application/json' },
67
+ body: payload,
68
+ }),
69
+ { basePath: '/.well-known/payment' }
70
+ );
71
+ const body = await res.json();
72
+ expect(body.len).toBe(Buffer.byteLength(payload)); // every byte forwarded
73
+ expect(body.sig).toBe(crypto.createHmac('sha256', 'k').update(payload).digest('hex'));
74
+ });
75
+ });
76
+
77
+ describe('Phase 4 collapse — buildHonoApp surface', () => {
78
+ const app = buildHonoApp();
79
+
80
+ it('serves /api/healthz natively (was on the express resource router)', async () => {
81
+ const res = await app.fetch(new Request('http://app.local/api/healthz'));
82
+ expect(res.status).toBe(200);
83
+ expect(await res.json()).toEqual({ ok: true });
84
+ });
85
+
86
+ it('onError maps a thrown error to 500 JSON (express-async-errors equivalent)', async () => {
87
+ const a = buildHonoApp((native) => {
88
+ native.get('/api/boom', () => {
89
+ throw new Error('kaboom');
90
+ });
91
+ });
92
+ const res = await a.fetch(new Request('http://app.local/api/boom'));
93
+ expect(res.status).toBe(500);
94
+ expect((await res.json()).error).toBe('kaboom');
95
+ });
96
+ });
@@ -0,0 +1,60 @@
1
+ // S3-CF Phase 1 inversion ③ — DID-Connect token storage slot.
2
+ //
3
+ // The DID-Connect token handshake store is now a host-injected slot (same
4
+ // reversal as db/queue/cron). The CF worker injects a D1-backed store so the
5
+ // token state lands in PAYMENT_DB; node hosts omit it and keep the file-backed
6
+ // nedb default. This spec locks the factory→libs/auth wiring and the default.
7
+ import fs from 'fs';
8
+ import os from 'os';
9
+ import path from 'path';
10
+
11
+ import { Sequelize } from 'sequelize';
12
+
13
+ import * as auth from '../../src/libs/auth';
14
+ import { createEmbeddedPaymentService } from '../../src/service';
15
+
16
+ function freshSequelize(): Sequelize {
17
+ const dbFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'pay-storage-slot-')), 'test.db');
18
+ return new Sequelize({ dialect: 'sqlite', storage: dbFile, logging: false });
19
+ }
20
+
21
+ const baseConfig = { BLOCKLET_APP_PID: 'zMOCK_STORAGE_SLOT' };
22
+
23
+ describe('Phase 1 (S3-CF) ③ — DID-Connect token storage slot', () => {
24
+ afterEach(() => {
25
+ // reset the module-level injection so specs don't leak into each other
26
+ auth.setDidConnectTokenStorage(null);
27
+ jest.restoreAllMocks();
28
+ });
29
+
30
+ it('wires a host-provided storage slot into libs/auth (setDidConnectTokenStorage)', () => {
31
+ const spy = jest.spyOn(auth, 'setDidConnectTokenStorage');
32
+ const sentinel: auth.DidConnectTokenStorage = {
33
+ read: async () => null,
34
+ create: async () => undefined,
35
+ update: async () => undefined,
36
+ delete: async () => undefined,
37
+ };
38
+
39
+ createEmbeddedPaymentService({
40
+ config: baseConfig,
41
+ db: { sequelize: freshSequelize() },
42
+ tenancy: { mode: 'single', instanceDid: 'zMOCK_STORAGE_SLOT' },
43
+ storage: sentinel,
44
+ });
45
+
46
+ expect(spy).toHaveBeenCalledWith(sentinel);
47
+ });
48
+
49
+ it('omitting the slot leaves the node nedb default (setter never called)', () => {
50
+ const spy = jest.spyOn(auth, 'setDidConnectTokenStorage');
51
+
52
+ createEmbeddedPaymentService({
53
+ config: baseConfig,
54
+ db: { sequelize: freshSequelize() },
55
+ tenancy: { mode: 'single', instanceDid: 'zMOCK_STORAGE_SLOT' },
56
+ });
57
+
58
+ expect(spy).not.toHaveBeenCalled();
59
+ });
60
+ });