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,191 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 4 E2E — real dual-tenant webhook delivery through the actual
3
+ // fanout (handleEvent) + delivery (handleWebhook) pipeline, with two live
4
+ // HTTP receivers. Raw JSON evidence only.
5
+ import fs from 'fs';
6
+ import http from 'http';
7
+ import os from 'os';
8
+ import path from 'path';
9
+
10
+ async function main() {
11
+ const { fromRandom } = await import('@ocap/wallet');
12
+ const { types } = await import('@ocap/mcrypto');
13
+ const wallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
14
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-s4-'));
15
+ Object.assign(process.env, {
16
+ ABT_NODE_DID: wallet.address,
17
+ ABT_NODE_PK: wallet.publicKey,
18
+ ABT_NODE_PORT: '8089',
19
+ ABT_NODE_SERVICE_PORT: '40406',
20
+ BLOCKLET_MODE: 'test',
21
+ BLOCKLET_DID: wallet.address,
22
+ BLOCKLET_COMPONENT_DID: wallet.address,
23
+ BLOCKLET_LOG_DIR: tmp,
24
+ BLOCKLET_DATA_DIR: tmp,
25
+ BLOCKLET_APP_PK: wallet.publicKey,
26
+ BLOCKLET_APP_SK: wallet.secretKey,
27
+ BLOCKLET_APP_PSK: wallet.secretKey,
28
+ BLOCKLET_APP_EK: wallet.secretKey,
29
+ BLOCKLET_APP_PID: wallet.address,
30
+ BLOCKLET_APP_ID: wallet.address,
31
+ BLOCKLET_APP_IDS: wallet.address,
32
+ BLOCKLET_APP_NAME: 'payment-kit-e2e',
33
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit-e2e',
34
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
35
+ BLOCKLET_MOUNT_POINTS: JSON.stringify([
36
+ {
37
+ title: 'e2e',
38
+ did: wallet.address,
39
+ name: 'e2e',
40
+ version: '0.0.1',
41
+ mountPoint: '/',
42
+ status: 6,
43
+ port: 8181,
44
+ resources: [],
45
+ },
46
+ ]),
47
+ });
48
+
49
+ const { Sequelize } = await import('sequelize');
50
+ const { SequelizeStorage, Umzug } = await import('umzug');
51
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(tmp, 'e2e.db'), logging: false });
52
+ const storeDir = path.resolve(__dirname, '../api/src/store');
53
+ const umzug = new Umzug({
54
+ migrations: {
55
+ glob: ['migrations/*.ts', { cwd: storeDir }],
56
+ resolve: ({ name, path: p, context }) => {
57
+ // eslint-disable-next-line import/no-dynamic-require, global-require
58
+ const migration = require(p!);
59
+ return {
60
+ name: name.replace(/\.ts$/, '.js'),
61
+ up: () => migration.up({ context }),
62
+ down: () => migration.down({ context }),
63
+ };
64
+ },
65
+ },
66
+ context: sequelize.getQueryInterface(),
67
+ storage: new SequelizeStorage({ sequelize }),
68
+ logger: undefined,
69
+ });
70
+ await umzug.up();
71
+
72
+ const models = await import('../api/src/store/models');
73
+ models.initialize(sequelize);
74
+ const { withTenant } = await import('../api/src/libs/context');
75
+ const { handleEvent } = await import('../api/src/queues/event');
76
+ const { handleWebhook } = await import('../api/src/queues/webhook');
77
+ const { assertEventTenantAccessible } = await import('../api/src/routes/hono/events');
78
+
79
+ const TENANT_A = 'did:abt:zTenantAAAA';
80
+ const TENANT_B = 'did:abt:zTenantBBBB';
81
+
82
+ // two live receivers
83
+ const received: Record<string, any[]> = { A: [], B: [] };
84
+ const makeReceiver = (key: 'A' | 'B') =>
85
+ new Promise<{ server: http.Server; url: string }>((resolve) => {
86
+ const server = http.createServer((req, res) => {
87
+ let body = '';
88
+ req.on('data', (chunk) => {
89
+ body += chunk;
90
+ });
91
+ req.on('end', () => {
92
+ received[key]!.push(JSON.parse(body));
93
+ res.writeHead(200, { 'content-type': 'application/json' });
94
+ res.end('{"ok":true}');
95
+ });
96
+ });
97
+ server.listen(0, '127.0.0.1', () => {
98
+ const { port } = server.address() as any;
99
+ resolve({ server, url: `http://127.0.0.1:${port}/hook` });
100
+ });
101
+ });
102
+ const receiverA = await makeReceiver('A');
103
+ const receiverB = await makeReceiver('B');
104
+
105
+ const seedEndpoint = (tenant: string, url: string) =>
106
+ withTenant(tenant, () =>
107
+ (models.WebhookEndpoint as any).create({
108
+ instance_did: tenant,
109
+ livemode: false,
110
+ url,
111
+ description: 'e2e',
112
+ status: 'enabled',
113
+ enabled_events: ['customer.updated'],
114
+ secret: 'whsec_e2e',
115
+ api_version: 'e2e',
116
+ })
117
+ );
118
+ const endpointA = await seedEndpoint(TENANT_A, receiverA.url);
119
+ await seedEndpoint(TENANT_B, receiverB.url);
120
+
121
+ const event: any = await withTenant(TENANT_A, () =>
122
+ (models.Event as any).create({
123
+ type: 'customer.updated',
124
+ instance_did: TENANT_A,
125
+ api_version: 'e2e',
126
+ livemode: false,
127
+ object_id: 'cus_e2e',
128
+ object_type: 'customer',
129
+ data: { object: { id: 'cus_e2e', name: 'after-update' } },
130
+ request: { id: '', idempotency_key: '', requested_by: 'e2e' },
131
+ metadata: {},
132
+ pending_webhooks: 99,
133
+ })
134
+ );
135
+
136
+ // E1: fanout + deliver — only tenant A's receiver gets the event. The real
137
+ // webhookQueue (scheduled by addWebhookJob inside handleEvent) performs the
138
+ // delivery asynchronously; endpointA is referenced again in E2b below.
139
+ void endpointA;
140
+ await handleEvent({ eventId: event.id });
141
+ await new Promise((resolve) => {
142
+ setTimeout(resolve, 2500);
143
+ });
144
+
145
+ console.log('=== E1 dual receivers: A delivered, B silent ===');
146
+ console.log(
147
+ JSON.stringify({
148
+ receiverA: received.A!.map((e) => ({ type: e.type, id: e.id })),
149
+ receiverB: received.B,
150
+ })
151
+ );
152
+ if (received.A!.length !== 1 || received.B!.length !== 0) process.exit(1);
153
+
154
+ const [attempts] = await sequelize.query('SELECT status, instance_did FROM webhook_attempts');
155
+ console.log(JSON.stringify({ attempts }));
156
+
157
+ // E2 (negative): A-tenant caller retries B's event -> TENANT_MISMATCH (the
158
+ // HTTP route maps this to 403; full curl-level proof needs Phase 10's
159
+ // Host -> tenant wiring plus admin credentials, so the guard is driven
160
+ // directly here)
161
+ console.log('=== E2 cross-tenant manual retry refused (negative) ===');
162
+ try {
163
+ await withTenant(TENANT_A, async () => assertEventTenantAccessible({ instance_did: TENANT_B }));
164
+ console.log(JSON.stringify({ unexpected: 'guard passed' }));
165
+ process.exit(1);
166
+ } catch (err: any) {
167
+ console.log(JSON.stringify({ code: err.code, httpStatus: 403, message: err.message }));
168
+ }
169
+
170
+ // adversarial: forged pair (A event -> B endpoint) refused, zero attempts written
171
+ console.log('=== E2b forged event/endpoint pair refused (adversarial) ===');
172
+ await sequelize.query('DELETE FROM webhook_attempts');
173
+ const endpointBRow: any = await (models.WebhookEndpoint as any).findOne({
174
+ where: { instance_did: TENANT_B },
175
+ });
176
+ await handleWebhook({ eventId: event.id, webhookId: endpointBRow.id });
177
+ const [forged] = await sequelize.query('SELECT COUNT(*) AS n FROM webhook_attempts');
178
+ console.log(JSON.stringify({ attemptsAfterForgedPair: (forged as any[])[0].n, receiverB: received.B!.length }));
179
+ if ((forged as any[])[0].n !== 0 || received.B!.length !== 0) process.exit(1);
180
+
181
+ console.log(JSON.stringify({ success: true }));
182
+ receiverA.server.close();
183
+ receiverB.server.close();
184
+ await sequelize.close();
185
+ process.exit(0);
186
+ }
187
+
188
+ main().catch((err) => {
189
+ console.error(err);
190
+ process.exit(1);
191
+ });
@@ -0,0 +1,139 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 5 E2E — queue tenant layer against the real jobs table.
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+
7
+ async function main() {
8
+ const { fromRandom } = await import('@ocap/wallet');
9
+ const { types } = await import('@ocap/mcrypto');
10
+ const wallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
11
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-s5-'));
12
+ Object.assign(process.env, {
13
+ ABT_NODE_DID: wallet.address,
14
+ ABT_NODE_PK: wallet.publicKey,
15
+ ABT_NODE_PORT: '8089',
16
+ ABT_NODE_SERVICE_PORT: '40406',
17
+ BLOCKLET_MODE: 'test',
18
+ NODE_ENV: 'test',
19
+ BLOCKLET_DID: wallet.address,
20
+ BLOCKLET_COMPONENT_DID: wallet.address,
21
+ BLOCKLET_LOG_DIR: tmp,
22
+ BLOCKLET_DATA_DIR: tmp,
23
+ BLOCKLET_APP_PK: wallet.publicKey,
24
+ BLOCKLET_APP_SK: wallet.secretKey,
25
+ BLOCKLET_APP_PSK: wallet.secretKey,
26
+ BLOCKLET_APP_EK: wallet.secretKey,
27
+ BLOCKLET_APP_PID: wallet.address,
28
+ BLOCKLET_APP_ID: wallet.address,
29
+ BLOCKLET_APP_IDS: wallet.address,
30
+ BLOCKLET_APP_NAME: 'payment-kit-e2e',
31
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit-e2e',
32
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
33
+ BLOCKLET_MOUNT_POINTS: JSON.stringify([
34
+ {
35
+ title: 'e2e',
36
+ did: wallet.address,
37
+ name: 'e2e',
38
+ version: '0.0.1',
39
+ mountPoint: '/',
40
+ status: 6,
41
+ port: 8181,
42
+ resources: [],
43
+ },
44
+ ]),
45
+ });
46
+
47
+ const { Sequelize } = await import('sequelize');
48
+ const { SequelizeStorage, Umzug } = await import('umzug');
49
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(tmp, 'e2e.db'), logging: false });
50
+ const storeDir = path.resolve(__dirname, '../api/src/store');
51
+ const umzug = new Umzug({
52
+ migrations: {
53
+ glob: ['migrations/*.ts', { cwd: storeDir }],
54
+ resolve: ({ name, path: p, context }) => {
55
+ // eslint-disable-next-line import/no-dynamic-require, global-require
56
+ const migration = require(p!);
57
+ return {
58
+ name: name.replace(/\.ts$/, '.js'),
59
+ up: () => migration.up({ context }),
60
+ down: () => migration.down({ context }),
61
+ };
62
+ },
63
+ },
64
+ context: sequelize.getQueryInterface(),
65
+ storage: new SequelizeStorage({ sequelize }),
66
+ logger: undefined,
67
+ });
68
+ await umzug.up();
69
+ const models = await import('../api/src/store/models');
70
+ models.initialize(sequelize);
71
+
72
+ const { withTenant } = await import('../api/src/libs/context');
73
+ const queueModule = await import('../api/src/libs/queue');
74
+ const createQueue = queueModule.default;
75
+ const { assertJobObjectTenant } = queueModule;
76
+
77
+ const TENANT_A = 'did:abt:zTenantAAAA';
78
+ const TENANT_B = 'did:abt:zTenantBBBB';
79
+
80
+ // E1: enqueue a delayed job under tenant A, read the raw jobs-table row
81
+ const delayedQueue = createQueue({
82
+ name: 'e2e-cycle',
83
+ onJob: async () => 'ok',
84
+ options: { enableScheduledJob: true },
85
+ });
86
+ await withTenant(TENANT_A, async () =>
87
+ delayedQueue.push({ job: { subscriptionId: 'sub_e2e_1' }, id: 'sub_e2e_1', delay: 3600 })
88
+ );
89
+ await new Promise((resolve) => {
90
+ setTimeout(resolve, 300);
91
+ });
92
+ const [jobRows] = await sequelize.query(
93
+ "SELECT id, queue, job FROM jobs WHERE queue = 'e2e-cycle' AND id = 'sub_e2e_1'"
94
+ );
95
+ console.log('=== E1 delayed subscription-cycle style job row (raw jobs table) ===');
96
+ console.log(JSON.stringify(jobRows));
97
+ const payload = JSON.parse((jobRows as any[])[0].job);
98
+ console.log(JSON.stringify({ payloadInstanceDid: payload.instance_did, expected: TENANT_A, idShape: 'business id, no tenant prefix (see queue-matrix.md)' }));
99
+ if (payload.instance_did !== TENANT_A) process.exit(1);
100
+
101
+ // E2 (negative): forged payload tenant A, object belongs to B
102
+ const victim: any = await withTenant(TENANT_B, () =>
103
+ (models.Customer as any).create({ livemode: false, did: 'z-victim-e2e', delinquent: false, instance_did: TENANT_B })
104
+ );
105
+ const beforeName = victim.name ?? null;
106
+ const forgedQueue = createQueue({
107
+ name: 'e2e-forged',
108
+ onJob: async (job: any) => {
109
+ const row = await (models.Customer as any).findByPk(job.customerId);
110
+ assertJobObjectTenant(row);
111
+ await row.update({ name: 'pwned' });
112
+ },
113
+ });
114
+ console.log('=== E2 forged payload (tenant A, object B) refused; B object unchanged (negative) ===');
115
+ const outcome: any = await new Promise((resolve) => {
116
+ withTenant(TENANT_A, async () => {
117
+ const handle = forgedQueue.push({ job: { customerId: victim.id }, persist: true });
118
+ handle.on('failed', (data: any) => resolve({ event: 'failed', error: { code: data.error?.code, message: data.error?.message } }));
119
+ handle.on('finished', () => resolve({ event: 'finished' }));
120
+ });
121
+ });
122
+ console.log(JSON.stringify(outcome));
123
+ const reloaded: any = await (models.Customer as any).findByPk(victim.id);
124
+ console.log(
125
+ JSON.stringify({ victimNameAfter: reloaded.name ?? null, beforeName, victimTenant: reloaded.instance_did })
126
+ );
127
+ if (outcome.event !== 'failed' || outcome.error.code !== 'TENANT_MISMATCH' || reloaded.name !== beforeName) {
128
+ process.exit(1);
129
+ }
130
+
131
+ console.log(JSON.stringify({ success: true }));
132
+ await sequelize.close();
133
+ process.exit(0);
134
+ }
135
+
136
+ main().catch((err) => {
137
+ console.error(err);
138
+ process.exit(1);
139
+ });
@@ -0,0 +1,127 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 6 E2E (E3) — a pre-tenant legacy job driven in single and multi mode.
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+
7
+ async function main() {
8
+ const { fromRandom } = await import('@ocap/wallet');
9
+ const { types } = await import('@ocap/mcrypto');
10
+ const wallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
11
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-s6-'));
12
+ Object.assign(process.env, {
13
+ ABT_NODE_DID: wallet.address,
14
+ ABT_NODE_PK: wallet.publicKey,
15
+ ABT_NODE_PORT: '8089',
16
+ ABT_NODE_SERVICE_PORT: '40406',
17
+ BLOCKLET_MODE: 'test',
18
+ NODE_ENV: 'test',
19
+ BLOCKLET_DID: wallet.address,
20
+ BLOCKLET_COMPONENT_DID: wallet.address,
21
+ BLOCKLET_LOG_DIR: tmp,
22
+ BLOCKLET_DATA_DIR: tmp,
23
+ BLOCKLET_APP_PK: wallet.publicKey,
24
+ BLOCKLET_APP_SK: wallet.secretKey,
25
+ BLOCKLET_APP_PSK: wallet.secretKey,
26
+ BLOCKLET_APP_EK: wallet.secretKey,
27
+ BLOCKLET_APP_PID: wallet.address,
28
+ BLOCKLET_APP_ID: wallet.address,
29
+ BLOCKLET_APP_IDS: wallet.address,
30
+ BLOCKLET_APP_NAME: 'payment-kit-e2e',
31
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit-e2e',
32
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
33
+ BLOCKLET_MOUNT_POINTS: JSON.stringify([
34
+ {
35
+ title: 'e2e',
36
+ did: wallet.address,
37
+ name: 'e2e',
38
+ version: '0.0.1',
39
+ mountPoint: '/',
40
+ status: 6,
41
+ port: 8181,
42
+ resources: [],
43
+ },
44
+ ]),
45
+ });
46
+
47
+ const { Sequelize } = await import('sequelize');
48
+ const { SequelizeStorage, Umzug } = await import('umzug');
49
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(tmp, 'e2e.db'), logging: false });
50
+ const storeDir = path.resolve(__dirname, '../api/src/store');
51
+ const umzug = new Umzug({
52
+ migrations: {
53
+ glob: ['migrations/*.ts', { cwd: storeDir }],
54
+ resolve: ({ name, path: p, context }) => {
55
+ // eslint-disable-next-line import/no-dynamic-require, global-require
56
+ const migration = require(p!);
57
+ return {
58
+ name: name.replace(/\.ts$/, '.js'),
59
+ up: () => migration.up({ context }),
60
+ down: () => migration.down({ context }),
61
+ };
62
+ },
63
+ },
64
+ context: sequelize.getQueryInterface(),
65
+ storage: new SequelizeStorage({ sequelize }),
66
+ logger: undefined,
67
+ });
68
+ await umzug.up();
69
+ const models = await import('../api/src/store/models');
70
+ models.initialize(sequelize);
71
+
72
+ const queueModule = await import('../api/src/libs/queue');
73
+ const createQueue = queueModule.default;
74
+ const { getDefaultInstanceDid } = await import('../api/src/libs/tenant');
75
+
76
+ const runLegacy = (mode: 'single' | 'multi') =>
77
+ new Promise<any>((resolve) => {
78
+ if (mode === 'multi') process.env.PAYMENT_TENANT_MODE = 'multi';
79
+ else delete process.env.PAYMENT_TENANT_MODE;
80
+ const seen: string[] = [];
81
+ const queue = createQueue({
82
+ name: `e2e-legacy-${mode}`,
83
+ onJob: async () => {
84
+ // eslint-disable-next-line global-require
85
+ const { getInstanceDid } = await import('../api/src/libs/context').then((m) => m);
86
+ seen.push(getInstanceDid());
87
+ return 'ok';
88
+ },
89
+ });
90
+ const handle = queue.push({
91
+ job: { legacy: true }, // NO instance_did — simulates a pre-upgrade row
92
+ id: `legacy-${mode}`,
93
+ persist: false,
94
+ fromStore: true,
95
+ } as any);
96
+ handle.on('finished', () => resolve({ mode, outcome: 'executed', tenants: seen }));
97
+ handle.on('failed', (data: any) =>
98
+ resolve({
99
+ mode,
100
+ outcome: 'refused',
101
+ error: { code: data.error?.code, nonRetryable: data.error?.nonRetryable },
102
+ })
103
+ );
104
+ });
105
+
106
+ console.log('=== E3 legacy (pre-tenant) job: single vs multi mode ===');
107
+ const single = await runLegacy('single');
108
+ console.log(JSON.stringify({ ...single, defaultTenant: getDefaultInstanceDid() }));
109
+ const multi = await runLegacy('multi');
110
+ console.log(JSON.stringify(multi));
111
+ delete process.env.PAYMENT_TENANT_MODE;
112
+
113
+ const ok =
114
+ single.outcome === 'executed' &&
115
+ single.tenants[0] === getDefaultInstanceDid() &&
116
+ multi.outcome === 'refused' &&
117
+ multi.error.code === 'TENANT_CONTEXT_MISSING' &&
118
+ multi.error.nonRetryable === true;
119
+ console.log(JSON.stringify({ success: ok }));
120
+ await sequelize.close();
121
+ process.exit(ok ? 0 : 1);
122
+ }
123
+
124
+ main().catch((err) => {
125
+ console.error(err);
126
+ process.exit(1);
127
+ });
@@ -0,0 +1,119 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 3 (W1′) Layer-2 E2E: drive the REAL models through TenantModel under
3
+ // two tenants against a real (in-memory) sqlite DB and emit JSON-shaped results.
4
+ // Self-contained: sets the blocklet env from a generated wallet BEFORE importing
5
+ // the models (mirrors tools/jest-setup), so it runs without a blocklet server.
6
+ // The live Host->tenant curl E2E depends on Phase 7/10 wiring; this proves the
7
+ // production scoping path deterministically.
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import { types } from '@ocap/mcrypto';
12
+ import { fromRandom } from '@ocap/wallet';
13
+
14
+ const wallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
15
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'p3-e2e-'));
16
+ process.env.BLOCKLET_MODE = 'test';
17
+ process.env.NODE_ENV = 'test';
18
+ process.env.BLOCKLET_LOG_DIR = tmpDir;
19
+ process.env.BLOCKLET_DATA_DIR = tmpDir;
20
+ process.env.BLOCKLET_APP_SK = wallet.secretKey;
21
+ process.env.BLOCKLET_APP_PSK = wallet.secretKey;
22
+ process.env.BLOCKLET_APP_PK = wallet.publicKey;
23
+ process.env.BLOCKLET_APP_EK = wallet.secretKey;
24
+ process.env.BLOCKLET_APP_ID = wallet.address;
25
+ process.env.BLOCKLET_APP_PID = wallet.address;
26
+ process.env.BLOCKLET_APP_IDS = wallet.address;
27
+ process.env.ABT_NODE_DID = wallet.address;
28
+ process.env.ABT_NODE_PK = wallet.publicKey;
29
+ process.env.ABT_NODE_PORT = '8089';
30
+ process.env.ABT_NODE_SERVICE_PORT = '40406';
31
+ process.env.BLOCKLET_DID = wallet.address;
32
+ process.env.BLOCKLET_COMPONENT_DID = wallet.address;
33
+ process.env.BLOCKLET_APP_NAME = 'did-pay-e2e';
34
+ process.env.BLOCKLET_APP_DESCRIPTION = 'phase3 e2e';
35
+ process.env.BLOCKLET_APP_URL = 'http://127.0.0.1:3030';
36
+ process.env.BLOCKLET_MOUNT_POINTS = JSON.stringify([
37
+ {
38
+ title: 'did-pay-e2e',
39
+ did: wallet.address,
40
+ name: 'did-pay',
41
+ version: '0.0.0',
42
+ mountPoint: '/',
43
+ status: 6,
44
+ port: 8181,
45
+ resources: [],
46
+ },
47
+ ]);
48
+
49
+ async function main() {
50
+ // import AFTER env is set so module-level wallet/logger init succeeds
51
+ /* eslint-disable global-require, @typescript-eslint/no-var-requires */
52
+ const { Sequelize } = require('sequelize');
53
+ const { withTenant } = require('../api/src/libs/context');
54
+ const { Coupon, initialize } = require('../api/src/store/models');
55
+ /* eslint-enable global-require, @typescript-eslint/no-var-requires */
56
+
57
+ const TENANT_A = 'did:abt:zE2ETenantAAAAAAAAAAAAAAAAAAAAA';
58
+ const TENANT_B = 'did:abt:zE2ETenantBBBBBBBBBBBBBBBBBBBBB';
59
+
60
+ const sequelize = new Sequelize('sqlite::memory:', { logging: false });
61
+ initialize(sequelize);
62
+ await sequelize.sync({ force: true });
63
+
64
+ const mk = (name: string) => ({ livemode: false, duration: 'once', name, created_via: 'api' });
65
+ const a1: any = await withTenant(TENANT_A, () => Coupon.create(mk('a-coupon')));
66
+ const b1: any = await withTenant(TENANT_B, () => Coupon.create(mk('b-coupon')));
67
+
68
+ const out: Record<string, any> = {};
69
+
70
+ const aList: any = await withTenant(TENANT_A, () => Coupon.findAll());
71
+ out['S3.1 list under tenant A — only A rows'] = {
72
+ count: aList.length,
73
+ rows: aList.map((r: any) => ({ id: r.id, name: r.name, instance_did: r.instance_did })),
74
+ };
75
+
76
+ const leak = await withTenant(TENANT_A, () => Coupon.findByPk(b1.id));
77
+ out['S3.2 cross-tenant findByPk (A reads B id)'] = { result: leak, expect: 'null = not-found (§13.1)' };
78
+
79
+ let negCreate: any;
80
+ try {
81
+ await withTenant(TENANT_A, () => Coupon.create({ ...mk('forged'), instance_did: TENANT_B }));
82
+ negCreate = { rejected: false };
83
+ } catch (err: any) {
84
+ negCreate = { rejected: true, code: err.code };
85
+ }
86
+ out['S3.3 NEGATIVE create with foreign instance_did'] = negCreate;
87
+
88
+ let negFind: any;
89
+ try {
90
+ await withTenant(TENANT_A, () => Coupon.findOne({ where: { instance_did: TENANT_B } }));
91
+ negFind = { rejected: false };
92
+ } catch (err: any) {
93
+ negFind = { rejected: true, code: err.code };
94
+ }
95
+ out['S3.4 NEGATIVE find with foreign where.instance_did'] = negFind;
96
+
97
+ const aCount = await withTenant(TENANT_A, () => Coupon.count());
98
+ const bCount = await withTenant(TENANT_B, () => Coupon.count());
99
+ out['S3.5 count isolation'] = { aCount, bCount, a1_stamp: a1.instance_did, b1_stamp: b1.instance_did };
100
+
101
+ // aggregate isolation: A adds a 30% coupon, B adds 99% — A's sum/max exclude B
102
+ await withTenant(TENANT_A, () => Coupon.create({ ...mk('a-30'), percent_off: 30 }));
103
+ await withTenant(TENANT_B, () => Coupon.create({ ...mk('b-99'), percent_off: 99 }));
104
+ const aSum = await withTenant(TENANT_A, () => Coupon.sum('percent_off'));
105
+ const aMax = await withTenant(TENANT_A, () => Coupon.max('percent_off'));
106
+ out['S3.6 sum/max aggregate isolation (A excludes B 99)'] = { aSum, aMax, expect: 'sum=30 max=30, not 99' };
107
+
108
+ for (const [k, v] of Object.entries(out)) {
109
+ console.log(`=== ${k} ===`);
110
+ console.log(JSON.stringify(v));
111
+ }
112
+
113
+ await sequelize.close();
114
+ }
115
+
116
+ main().catch((err) => {
117
+ console.error(err);
118
+ process.exit(1);
119
+ });