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,66 @@
1
+ // Phase 8 (W2-1a): the libs/lock facade — tenant prefix + fail-closed.
2
+ //
3
+ // The driver-level semantics live in packages/payment-core/tests/drivers; this
4
+ // spec covers the facade behavior that the call sites depend on: tenant
5
+ // prefixing, the global-scope exemption, and multi-mode fail-closed.
6
+ //
7
+ // Dynamic require() is required: each case re-imports lock.ts after setting the
8
+ // tenant-mode env + jest.resetModules so the module-level driver is fresh.
9
+ /* eslint-disable global-require, import/no-dynamic-require */
10
+
11
+ describe('getLock facade — tenant prefix + fail-closed', () => {
12
+ const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
13
+
14
+ afterEach(() => {
15
+ if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
16
+ else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
17
+ jest.resetModules();
18
+ });
19
+
20
+ it('rejects an empty lock name', () => {
21
+ const { getLock } = require('../../src/libs/lock');
22
+ expect(() => getLock('')).toThrow(/non-empty lock name/);
23
+ });
24
+
25
+ it('multi mode: tenant-scoped getLock without context fails closed', () => {
26
+ process.env.PAYMENT_TENANT_MODE = 'multi';
27
+ jest.resetModules();
28
+ const { getLock } = require('../../src/libs/lock');
29
+ let code: string | undefined;
30
+ try {
31
+ getLock('payment-intent-1');
32
+ } catch (e: any) {
33
+ code = e.code;
34
+ }
35
+ expect(code).toBe('TENANT_CONTEXT_MISSING');
36
+ });
37
+
38
+ it('multi mode: global-scoped getLock needs no tenant', () => {
39
+ process.env.PAYMENT_TENANT_MODE = 'multi';
40
+ jest.resetModules();
41
+ const { getLock } = require('../../src/libs/lock');
42
+ const lock = getLock('startInvoiceQueue', { scope: 'global' });
43
+ expect(lock.name).toBe('startInvoiceQueue');
44
+ });
45
+
46
+ it('multi mode: tenant-scoped getLock inside withTenant carries the prefix', async () => {
47
+ process.env.PAYMENT_TENANT_MODE = 'multi';
48
+ jest.resetModules();
49
+ const { getLock } = require('../../src/libs/lock');
50
+ const { withTenant } = require('../../src/libs/context');
51
+ await withTenant('did:abt:TENANT_A', () => {
52
+ const lock = getLock('payment-intent-1');
53
+ expect(lock.name).toBe('tenant:did:abt:TENANT_A::payment-intent-1');
54
+ });
55
+ });
56
+
57
+ it('single mode: tenant-scoped getLock falls back to the deployment app DID prefix', () => {
58
+ process.env.PAYMENT_TENANT_MODE = 'single';
59
+ jest.resetModules();
60
+ const { getLock } = require('../../src/libs/lock');
61
+ const lock = getLock('payment-intent-1');
62
+ // single mode never fails closed — name is prefixed with the constant default tenant
63
+ expect(lock.name.startsWith('tenant:')).toBe(true);
64
+ expect(lock.name.endsWith('::payment-intent-1')).toBe(true);
65
+ });
66
+ });
@@ -0,0 +1,222 @@
1
+ import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { Sequelize } from 'sequelize';
6
+ import { SequelizeStorage, Umzug } from 'umzug';
7
+
8
+ import { withTenant } from '../../src/libs/context';
9
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
10
+ import {
11
+ scopedAggregate,
12
+ scopedCount,
13
+ scopedCreate,
14
+ scopedDestroy,
15
+ scopedFindAll,
16
+ scopedFindByPk,
17
+ scopedFindOne,
18
+ scopedQuery,
19
+ scopedSum,
20
+ scopedUpdate,
21
+ } from '../../src/store/scoped';
22
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
23
+
24
+ const STORE_DIR = path.join(__dirname, '../../src/store');
25
+
26
+ // real DB: full migration chain on a temp file, then bind the model
27
+ // singletons to it (order matters — initialize() after DDL, see
28
+ // tenant-columns-model.spec.ts for why)
29
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scoped-spec-'));
30
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
31
+ const umzug = new Umzug({
32
+ migrations: {
33
+ glob: ['migrations/*.ts', { cwd: STORE_DIR }],
34
+ resolve: ({ name, path: p, context }) => {
35
+ // eslint-disable-next-line import/no-dynamic-require, global-require
36
+ const migration = require(p!);
37
+ return {
38
+ name: name.replace(/\.ts$/, '.js'),
39
+ up: () => migration.up({ context }),
40
+ down: () => migration.down({ context }),
41
+ };
42
+ },
43
+ },
44
+ context: sequelize.getQueryInterface(),
45
+ storage: new SequelizeStorage({ sequelize }),
46
+ logger: undefined,
47
+ });
48
+
49
+ let Customer: any;
50
+
51
+ beforeAll(async () => {
52
+ await umzug.up();
53
+ // eslint-disable-next-line global-require
54
+ const models = require('../../src/store/models');
55
+ models.initialize(sequelize);
56
+ Customer = models.Customer;
57
+ }, 120000);
58
+
59
+ afterAll(async () => {
60
+ await sequelize.close();
61
+ fs.rmSync(dir, { recursive: true, force: true });
62
+ });
63
+
64
+ const seedCustomer = (tenant: string, did: string) =>
65
+ withTenant(tenant, () => scopedCreate(Customer, { livemode: false, did, delinquent: false }));
66
+
67
+ describe('scoped helpers (phase 3)', () => {
68
+ beforeEach(async () => {
69
+ await sequelize.query('DELETE FROM customers');
70
+ });
71
+
72
+ describe('happy path', () => {
73
+ it('scoped find returns only the active tenant rows; create stamps the tenant', async () => {
74
+ await seedCustomer(TENANT_A, 'z-user-a');
75
+ await seedCustomer(TENANT_B, 'z-user-b');
76
+
77
+ const aRows = await withTenant(TENANT_A, () => scopedFindAll(Customer));
78
+ expect(aRows.map((r: any) => r.did)).toEqual(['z-user-a']);
79
+ expect((aRows[0] as any).instance_did).toBe(TENANT_A);
80
+
81
+ const bRows = await withTenant(TENANT_B, () => scopedFindAll(Customer));
82
+ expect(bRows.map((r: any) => r.did)).toEqual(['z-user-b']);
83
+ });
84
+ });
85
+
86
+ describe('bad input', () => {
87
+ it('multi mode without context fails closed', async () => {
88
+ process.env.PAYMENT_TENANT_MODE = 'multi';
89
+ try {
90
+ await expect(scopedFindAll(Customer)).rejects.toMatchObject({ code: TENANT_CONTEXT_MISSING });
91
+ } finally {
92
+ delete process.env.PAYMENT_TENANT_MODE;
93
+ }
94
+ });
95
+
96
+ it('raw SQL without an instance_did binding is refused', async () => {
97
+ await withTenant(TENANT_A, async () => {
98
+ expect(() => scopedQuery(sequelize, 'SELECT * FROM customers')).toThrow(
99
+ expect.objectContaining({ code: TENANT_MISMATCH })
100
+ );
101
+ });
102
+ });
103
+
104
+ it('raw SQL with the binding is auto-supplied the active tenant', async () => {
105
+ await seedCustomer(TENANT_A, 'z-user-a');
106
+ await seedCustomer(TENANT_B, 'z-user-b');
107
+ const [rows] = await withTenant(TENANT_A, () =>
108
+ scopedQuery(sequelize, 'SELECT did FROM customers WHERE instance_did = $instance_did')
109
+ );
110
+ expect((rows as any[]).map((r) => r.did)).toEqual(['z-user-a']);
111
+ });
112
+ });
113
+
114
+ describe('security', () => {
115
+ it('explicit foreign where.instance_did is rejected, not silently overridden', async () => {
116
+ await withTenant(TENANT_A, async () => {
117
+ await expect(scopedFindAll(Customer, { where: { instance_did: TENANT_B } as any })).rejects.toMatchObject({
118
+ code: TENANT_MISMATCH,
119
+ });
120
+ });
121
+ });
122
+
123
+ it('explicit foreign instance_did on create is rejected', async () => {
124
+ await withTenant(TENANT_A, async () => {
125
+ await expect(
126
+ scopedCreate(Customer, { livemode: false, did: 'z-x', delinquent: false, instance_did: TENANT_B })
127
+ ).rejects.toMatchObject({ code: TENANT_MISMATCH });
128
+ });
129
+ });
130
+
131
+ it('scanner flags the deliberately-violating fixture file (raw query, no $instance_did)', () => {
132
+ const fixture = path.join(__dirname, '../fixtures/bare-query-violation.ts');
133
+ let failed = false;
134
+ let output = '';
135
+ try {
136
+ execFileSync('node', [path.join(__dirname, '../../../scripts/scan-tenant-queries.js'), '--json', fixture], {
137
+ encoding: 'utf8',
138
+ });
139
+ } catch (err: any) {
140
+ failed = true;
141
+ output = err.stdout || '';
142
+ }
143
+ expect(failed).toBe(true);
144
+ const result = JSON.parse(output);
145
+ expect(result.violations.length).toBeGreaterThan(0);
146
+ expect(result.violations[0].file).toContain('bare-query-violation.ts');
147
+ expect(result.violations[0].line).toBeGreaterThan(0);
148
+ });
149
+ });
150
+
151
+ describe('data loss', () => {
152
+ it('scoped destroy/update only touch the active tenant rows', async () => {
153
+ await seedCustomer(TENANT_A, 'z-user-a');
154
+ await seedCustomer(TENANT_B, 'z-user-b');
155
+
156
+ await withTenant(TENANT_A, () => scopedDestroy(Customer, { where: {} }));
157
+ const [allRows] = await sequelize.query('SELECT did, instance_did FROM customers');
158
+ expect(allRows).toHaveLength(1);
159
+ expect((allRows as any[])[0].instance_did).toBe(TENANT_B);
160
+
161
+ await withTenant(TENANT_B, () => scopedUpdate(Customer, { name: 'renamed' }, { where: {} }));
162
+ const [updated] = await sequelize.query('SELECT name FROM customers');
163
+ expect((updated as any[])[0].name).toBe('renamed');
164
+ });
165
+ });
166
+
167
+ describe('data damage', () => {
168
+ it('findByPk on another tenant row returns null (cross-tenant = not-found, §13.1)', async () => {
169
+ // Customer now extends TenantModel: scopedFindByPk delegates to the
170
+ // scoped override, so a cross-tenant pk resolves to null (not-found)
171
+ // rather than throwing — the unified §13.1 semantics (404, not 403).
172
+ const created: any = await seedCustomer(TENANT_B, 'z-user-b');
173
+ await withTenant(TENANT_A, async () => {
174
+ await expect(scopedFindByPk(Customer, created.id)).resolves.toBeNull();
175
+ });
176
+ // same pk under the right tenant works
177
+ const found: any = await withTenant(TENANT_B, () => scopedFindByPk(Customer, created.id));
178
+ expect(found.did).toBe('z-user-b');
179
+ });
180
+
181
+ it('scopedUpdate refuses to change instance_did (rows never change tenant)', async () => {
182
+ await seedCustomer(TENANT_A, 'z-user-a');
183
+ await withTenant(TENANT_A, async () => {
184
+ await expect(scopedUpdate(Customer, { instance_did: TENANT_B } as any, { where: {} })).rejects.toMatchObject({
185
+ code: TENANT_MISMATCH,
186
+ });
187
+ });
188
+ });
189
+ });
190
+
191
+ describe('data leak', () => {
192
+ it('findOne/count never see the other tenant even with identical data', async () => {
193
+ await seedCustomer(TENANT_A, 'z-same-shape');
194
+ await withTenant(TENANT_B, () =>
195
+ scopedCreate(Customer, { livemode: false, did: 'z-same-shape-b', delinquent: false, name: 'same' })
196
+ );
197
+
198
+ await withTenant(TENANT_A, async () => {
199
+ expect(await scopedCount(Customer)).toBe(1);
200
+ const one: any = await scopedFindOne(Customer, { where: { livemode: false } });
201
+ expect(one.did).toBe('z-same-shape');
202
+ expect(await scopedFindOne(Customer, { where: { did: 'z-same-shape-b' } })).toBeNull();
203
+ });
204
+ });
205
+
206
+ it('sum/aggregate are tenant-isolated too', async () => {
207
+ await withTenant(TENANT_A, () =>
208
+ scopedCreate(Customer, { livemode: false, did: 'z-sum-a', delinquent: false, next_invoice_sequence: 5 })
209
+ );
210
+ await withTenant(TENANT_B, () =>
211
+ scopedCreate(Customer, { livemode: false, did: 'z-sum-b', delinquent: false, next_invoice_sequence: 11 })
212
+ );
213
+ await withTenant(TENANT_A, async () => {
214
+ expect(await scopedSum(Customer, 'next_invoice_sequence' as any)).toBe(5);
215
+ expect(await scopedAggregate(Customer, 'next_invoice_sequence' as any, 'max')).toBe(5);
216
+ });
217
+ await withTenant(TENANT_B, async () => {
218
+ expect(await scopedSum(Customer, 'next_invoice_sequence' as any)).toBe(11);
219
+ });
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,52 @@
1
+ // Phase 11 (W2-3): the tenant-aware secrets facade resolves the tenant from
2
+ // TenantContext and fails closed in multi mode without context.
3
+ /* eslint-disable global-require */
4
+
5
+ describe('secrets facade — tenant resolution + fail-closed', () => {
6
+ const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
7
+
8
+ afterEach(() => {
9
+ if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
10
+ else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
11
+ jest.resetModules();
12
+ });
13
+
14
+ it('multi mode: encryptSecret without tenant context fails closed', () => {
15
+ process.env.PAYMENT_TENANT_MODE = 'multi';
16
+ jest.resetModules();
17
+ const { encryptSecret } = require('../../src/libs/secrets');
18
+ let code: string | undefined;
19
+ try {
20
+ encryptSecret('value');
21
+ } catch (e: any) {
22
+ code = e.code;
23
+ }
24
+ expect(code).toBe('TENANT_CONTEXT_MISSING');
25
+ });
26
+
27
+ it('single mode: encryptSecret/decryptSecret roundtrip via the default (process) key', () => {
28
+ process.env.PAYMENT_TENANT_MODE = 'single';
29
+ jest.resetModules();
30
+ const { encryptSecret, decryptSecret } = require('../../src/libs/secrets');
31
+ const ct = encryptSecret('stripe-secret');
32
+ expect(decryptSecret(ct)).toBe('stripe-secret');
33
+ });
34
+
35
+ it('multi mode: inside withTenant, the facade resolves and uses that tenant', async () => {
36
+ process.env.PAYMENT_TENANT_MODE = 'multi';
37
+ jest.resetModules();
38
+ const { withTenant } = require('../../src/libs/context');
39
+ const { setSecretsDriver, createKeyringSecretsDriver } = require('../../src/libs/drivers');
40
+ const { encryptSecret, decryptSecret, warmupSecrets } = require('../../src/libs/secrets');
41
+ const identity = {
42
+ resolveInstanceDidForHost: () => null,
43
+ getAppEk: (did: string) => `ek-${did}`,
44
+ };
45
+ setSecretsDriver(createKeyringSecretsDriver(identity));
46
+ await withTenant('did:abt:zFACADE_A', async () => {
47
+ await warmupSecrets(); // warm current tenant for the sync hot path
48
+ const ct = encryptSecret('per-tenant');
49
+ expect(decryptSecret(ct)).toBe('per-tenant');
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,37 @@
1
+ // P5 T5.1 / TM-8 — resolveServiceHost regression lock.
2
+ //
3
+ // The full P5 login E2E (DID Connect against arc /.well-known/service) is blocked
4
+ // on D-4 (arc endpoint) + P4 (arc wiring), so these are the in-workspace locks
5
+ // that DON'T depend on D-4: the blocklet-server fallback (TM-8) and the arc path.
6
+ import { resolveServiceHost } from '../../../src/libs/service-host';
7
+
8
+ describe('P5 T5.1 — resolveServiceHost', () => {
9
+ const realWindow = (global as any).window;
10
+ afterEach(() => {
11
+ (global as any).window = realWindow;
12
+ });
13
+
14
+ it('TM-8 (regression): blocklet-server form (no injected serviceHost) → prefix, unchanged', () => {
15
+ (global as any).window = { blocklet: { prefix: '/' } };
16
+ expect(resolveServiceHost('/')).toBe('/');
17
+ (global as any).window = { blocklet: { prefix: '/payment' } };
18
+ expect(resolveServiceHost('/payment')).toBe('/payment');
19
+ });
20
+
21
+ it('arc form: injected window.blocklet.serviceHost wins over prefix', () => {
22
+ (global as any).window = { blocklet: { serviceHost: '/.well-known/service' } };
23
+ expect(resolveServiceHost('/.well-known/payment')).toBe('/.well-known/service');
24
+ });
25
+
26
+ it('bad-input: empty/missing serviceHost falls back to prefix (not undefined/empty)', () => {
27
+ (global as any).window = { blocklet: { serviceHost: '' } };
28
+ expect(resolveServiceHost('/.well-known/payment')).toBe('/.well-known/payment');
29
+ (global as any).window = {};
30
+ expect(resolveServiceHost('/x')).toBe('/x');
31
+ });
32
+
33
+ it('no window at all (SSR/test) → prefix, no throw', () => {
34
+ delete (global as any).window;
35
+ expect(resolveServiceHost('/y')).toBe('/y');
36
+ });
37
+ });
@@ -0,0 +1,209 @@
1
+ // D1 (S3.0) — TenancySlot is the authoritative source for PAYMENT_TENANT_MODE.
2
+ //
3
+ // The factory derives effectiveConfig.PAYMENT_TENANT_MODE from slots.tenancy.mode
4
+ // (so getTenantMode(), which reads the libs/env config boundary, sees the slot
5
+ // mode without any env). A config PAYMENT_TENANT_MODE that disagrees with the
6
+ // slot is a fail-fast. Intending multi must never silently degrade to single:
7
+ // the default identity/secrets drivers ARE single-tenant behaviors, so a
8
+ // multi-mode host that omits them fails closed. The legacy single path (no
9
+ // tenancy slot + BLOCKLET_APP_PID) stays valid.
10
+
11
+ import { Sequelize } from 'sequelize';
12
+ import { setCoreConfig, getCoreConfig, readConfig } from '../../src/libs/env';
13
+ import { getTenantMode, setDefaultInstanceDid } from '../../src/libs/tenant';
14
+ import {
15
+ createDefaultIdentityDriver,
16
+ setIdentityDriver,
17
+ createDefaultSecretsDriver,
18
+ setSecretsDriver,
19
+ } from '../../src/libs/drivers';
20
+ import { createEmbeddedPaymentService, PaymentCoreSlotError, TenancySlotError } from '../../src/service';
21
+
22
+ const ORIG_MODE = process.env.PAYMENT_TENANT_MODE;
23
+ const ORIG_PID = process.env.BLOCKLET_APP_PID;
24
+
25
+ // minimal slot stubs — only presence matters for D1 (the factory wires them,
26
+ // it does not exercise their methods at construction time).
27
+ const identityStub = { resolveInstanceDidForHost: () => null } as any;
28
+ const secretsStub = createDefaultSecretsDriver();
29
+ // a real in-memory sqlite sequelize: the factory's initialize() defines models
30
+ // on it (no queries at construction), so it must be a genuine Sequelize, not a
31
+ // stub. The data-damage/bad-input cases throw BEFORE initialize() so they never
32
+ // touch it, but the happy/leak cases run the factory to completion.
33
+ const db = { sequelize: new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) as any };
34
+
35
+ afterEach(() => {
36
+ setCoreConfig(undefined);
37
+ setDefaultInstanceDid(undefined);
38
+ // restore module-level driver singletons to their defaults so the multi
39
+ // tests do not leak an injected identity/secrets driver into later specs.
40
+ setIdentityDriver(createDefaultIdentityDriver());
41
+ setSecretsDriver(createDefaultSecretsDriver());
42
+ if (ORIG_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
43
+ else process.env.PAYMENT_TENANT_MODE = ORIG_MODE;
44
+ if (ORIG_PID === undefined) delete process.env.BLOCKLET_APP_PID;
45
+ else process.env.BLOCKLET_APP_PID = ORIG_PID;
46
+ });
47
+
48
+ describe('D1 — happy path: tenancy slot drives getTenantMode without env', () => {
49
+ it('tenancy.mode=multi makes getTenantMode() === "multi" with no env', () => {
50
+ delete process.env.PAYMENT_TENANT_MODE;
51
+ createEmbeddedPaymentService({
52
+ config: {},
53
+ db,
54
+ tenancy: { mode: 'multi' },
55
+ identity: identityStub,
56
+ secrets: secretsStub,
57
+ });
58
+ expect(getTenantMode()).toBe('multi');
59
+ });
60
+
61
+ it('tenancy.mode=single makes getTenantMode() === "single" with no env', () => {
62
+ delete process.env.PAYMENT_TENANT_MODE;
63
+ createEmbeddedPaymentService({
64
+ config: {},
65
+ db,
66
+ tenancy: { mode: 'single', instanceDid: 'did:abt:zSINGLEAPP' },
67
+ });
68
+ expect(getTenantMode()).toBe('single');
69
+ });
70
+ });
71
+
72
+ describe('D1 — bad input: invalid tenancy.mode fails fast', () => {
73
+ it('an unknown tenancy.mode throws at construction with the legal enum in the message', () => {
74
+ delete process.env.PAYMENT_TENANT_MODE;
75
+ let thrown: any;
76
+ try {
77
+ createEmbeddedPaymentService({
78
+ config: {},
79
+ db,
80
+ tenancy: { mode: 'foo' } as any,
81
+ });
82
+ throw new Error('expected a TenancySlotError');
83
+ } catch (err: any) {
84
+ thrown = err;
85
+ }
86
+ expect(thrown).toBeInstanceOf(TenancySlotError);
87
+ expect(thrown.message).toMatch(/single/);
88
+ expect(thrown.message).toMatch(/multi/);
89
+ // fail-fast: it did not silently write a mode into the config boundary
90
+ expect(getCoreConfig()).toBeUndefined();
91
+ });
92
+ });
93
+
94
+ describe('D1 — security: intending multi never silently degrades to single', () => {
95
+ it('multi mode without an identity slot fails closed', () => {
96
+ delete process.env.PAYMENT_TENANT_MODE;
97
+ expect(() =>
98
+ createEmbeddedPaymentService({
99
+ config: {},
100
+ db,
101
+ tenancy: { mode: 'multi' },
102
+ secrets: secretsStub,
103
+ // identity omitted
104
+ })
105
+ ).toThrow(PaymentCoreSlotError);
106
+ });
107
+
108
+ it('multi mode without a secrets slot fails closed', () => {
109
+ delete process.env.PAYMENT_TENANT_MODE;
110
+ expect(() =>
111
+ createEmbeddedPaymentService({
112
+ config: {},
113
+ db,
114
+ tenancy: { mode: 'multi' },
115
+ identity: identityStub,
116
+ // secrets omitted
117
+ })
118
+ ).toThrow(PaymentCoreSlotError);
119
+ });
120
+
121
+ it('multi mode with both slots does NOT fall back to single', () => {
122
+ delete process.env.PAYMENT_TENANT_MODE;
123
+ createEmbeddedPaymentService({
124
+ config: {},
125
+ db,
126
+ tenancy: { mode: 'multi' },
127
+ identity: identityStub,
128
+ secrets: secretsStub,
129
+ });
130
+ expect(getTenantMode()).toBe('multi');
131
+ expect(getTenantMode()).not.toBe('single');
132
+ });
133
+ });
134
+
135
+ describe('D1 — data loss surrogate: legacy single compat preserved', () => {
136
+ it('no tenancy slot + BLOCKLET_APP_PID present resolves as a valid single deployment', () => {
137
+ delete process.env.PAYMENT_TENANT_MODE;
138
+ process.env.BLOCKLET_APP_PID = 'did:abt:zLEGACYAPP';
139
+ expect(() =>
140
+ createEmbeddedPaymentService({
141
+ config: { BLOCKLET_APP_PID: 'did:abt:zLEGACYAPP' },
142
+ db,
143
+ })
144
+ ).not.toThrow();
145
+ expect(getTenantMode()).toBe('single');
146
+ });
147
+ });
148
+
149
+ describe('D1 — data damage: config<->slot conflict fails fast with no half config', () => {
150
+ it('config.PAYMENT_TENANT_MODE that disagrees with tenancy.mode throws and writes nothing', () => {
151
+ delete process.env.PAYMENT_TENANT_MODE;
152
+ let thrown: any;
153
+ try {
154
+ createEmbeddedPaymentService({
155
+ config: { PAYMENT_TENANT_MODE: 'single' },
156
+ db,
157
+ tenancy: { mode: 'multi' },
158
+ identity: identityStub,
159
+ secrets: secretsStub,
160
+ });
161
+ throw new Error('expected a TenancySlotError');
162
+ } catch (err: any) {
163
+ thrown = err;
164
+ }
165
+ expect(thrown).toBeInstanceOf(TenancySlotError);
166
+ // no mode was written into the config boundary (setCoreConfig never ran)
167
+ expect(getCoreConfig()).toBeUndefined();
168
+ });
169
+
170
+ it('a config that AGREES with the slot is accepted (no false conflict)', () => {
171
+ delete process.env.PAYMENT_TENANT_MODE;
172
+ createEmbeddedPaymentService({
173
+ config: { PAYMENT_TENANT_MODE: 'multi' },
174
+ db,
175
+ tenancy: { mode: 'multi' },
176
+ identity: identityStub,
177
+ secrets: secretsStub,
178
+ });
179
+ expect(getTenantMode()).toBe('multi');
180
+ });
181
+ });
182
+
183
+ describe('D1 — data leak surrogate: mode has a single source (the config boundary)', () => {
184
+ it('setDefaultInstanceDid does not flip the resolved tenant mode', () => {
185
+ delete process.env.PAYMENT_TENANT_MODE;
186
+ createEmbeddedPaymentService({
187
+ config: {},
188
+ db,
189
+ tenancy: { mode: 'multi' },
190
+ identity: identityStub,
191
+ secrets: secretsStub,
192
+ });
193
+ expect(getTenantMode()).toBe('multi');
194
+ setDefaultInstanceDid('did:abt:zSOMEONE'); // the only other tenancy setter
195
+ expect(getTenantMode()).toBe('multi'); // mode is unaffected — config is the one source
196
+ });
197
+
198
+ it('effectiveConfig carries the slot mode through the readConfig boundary', () => {
199
+ delete process.env.PAYMENT_TENANT_MODE;
200
+ createEmbeddedPaymentService({
201
+ config: {},
202
+ db,
203
+ tenancy: { mode: 'multi' },
204
+ identity: identityStub,
205
+ secrets: secretsStub,
206
+ });
207
+ expect(readConfig('PAYMENT_TENANT_MODE')).toBe('multi');
208
+ });
209
+ });
@@ -0,0 +1,42 @@
1
+ // Phase 10 (W2-2): libs/tenant default-tenant + row-tenant resolution.
2
+ //
3
+ // The HTTP-driven tenant-middleware scenarios (single mode, multi-mode Host→tenant,
4
+ // fail-closed, X-Forwarded-Host security, malformed Host) moved to the hono
5
+ // middlewares/hono/context.spec.ts when the express contextMiddleware was deleted
6
+ // (express→hono Phase 4). What remains here is the framework-agnostic libs/tenant
7
+ // unit coverage that has no HTTP surface.
8
+ import { getDefaultInstanceDid, setDefaultInstanceDid, resolveRowTenant } from '../../src/libs/tenant';
9
+
10
+ const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
11
+
12
+ // Data damage — the single-mode tenancy slot value drives the default tenant, and
13
+ // pre-backfill (null instance_did) rows resolve to it (upgrade path intact).
14
+ describe('single-mode tenancy slot value (no "tenant swap" on upgrade)', () => {
15
+ const ORIG = (() => {
16
+ const saved = process.env.PAYMENT_TENANT_MODE;
17
+ process.env.PAYMENT_TENANT_MODE = 'single';
18
+ const v = getDefaultInstanceDid();
19
+ if (saved === undefined) delete process.env.PAYMENT_TENANT_MODE;
20
+ else process.env.PAYMENT_TENANT_MODE = saved;
21
+ return v;
22
+ })();
23
+
24
+ beforeEach(() => {
25
+ process.env.PAYMENT_TENANT_MODE = 'single';
26
+ });
27
+ afterEach(() => {
28
+ setDefaultInstanceDid(ORIG); // restore to the env-derived default
29
+ if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
30
+ else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
31
+ });
32
+
33
+ it('the configured instanceDid becomes the default tenant and old null rows map to it', () => {
34
+ setDefaultInstanceDid('did:abt:zCONFIGURED');
35
+ expect(getDefaultInstanceDid()).toBe('did:abt:zCONFIGURED');
36
+ // a pre-backfill row (instance_did = null) resolves to the configured default,
37
+ // not some other tenant — old data stays visible, no corruption
38
+ expect(resolveRowTenant({ instance_did: null })).toBe('did:abt:zCONFIGURED');
39
+ // a row that already carries a tenant is unaffected
40
+ expect(resolveRowTenant({ instance_did: 'did:abt:zEXISTING' })).toBe('did:abt:zEXISTING');
41
+ });
42
+ });