payment-kit 1.29.0 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +36 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +27 -24
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  236. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  237. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  238. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  239. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  240. package/cloudflare/queue-runtime-mode.ts +13 -0
  241. package/cloudflare/run-build.js +31 -56
  242. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  243. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  244. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  245. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  246. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  247. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  248. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  249. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  250. package/cloudflare/shims/cron.ts +38 -158
  251. package/cloudflare/shims/events.ts +124 -0
  252. package/cloudflare/shims/fastq.ts +15 -1
  253. package/cloudflare/shims/nedb-storage.ts +16 -8
  254. package/cloudflare/shims/node-fetch.ts +35 -0
  255. package/cloudflare/shims/xss.ts +8 -0
  256. package/cloudflare/tenant-middleware.ts +36 -0
  257. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  258. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  259. package/cloudflare/worker.ts +204 -433
  260. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  261. package/jest.config.js +3 -1
  262. package/package.json +33 -38
  263. package/scripts/core-env-whitelist.json +1 -0
  264. package/scripts/e2e-12b-runtime.ts +149 -0
  265. package/scripts/e2e-core-config.ts +125 -0
  266. package/scripts/e2e-d1-tenancy.ts +116 -0
  267. package/scripts/e2e-d2-cron-queue.ts +139 -0
  268. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  269. package/scripts/e2e-hono-s2.ts +125 -0
  270. package/scripts/e2e-hono-s3e.ts +135 -0
  271. package/scripts/e2e-hono-s4.ts +114 -0
  272. package/scripts/e2e-migration-contract.ts +100 -0
  273. package/scripts/e2e-s0.ts +61 -0
  274. package/scripts/e2e-s1.ts +107 -0
  275. package/scripts/e2e-s2.ts +178 -0
  276. package/scripts/e2e-s3.ts +110 -0
  277. package/scripts/e2e-s4.ts +191 -0
  278. package/scripts/e2e-s5.ts +139 -0
  279. package/scripts/e2e-s6.ts +127 -0
  280. package/scripts/e2e-tenant-model.ts +119 -0
  281. package/scripts/e2e-tenant-worker.ts +199 -0
  282. package/scripts/gen-sql-migrations.js +46 -0
  283. package/scripts/phase8-codemod.js +219 -0
  284. package/scripts/phase9a-env-getters-codemod.js +82 -0
  285. package/scripts/scan-core-env.js +109 -0
  286. package/scripts/scan-tenant-queries.js +235 -0
  287. package/scripts/schema-drift-guard.ts +210 -0
  288. package/scripts/tenant-scan-whitelist.json +1 -0
  289. package/src/env.d.ts +13 -1
  290. package/tsconfig.json +1 -1
  291. package/api/src/libs/did-space.ts +0 -235
  292. package/api/src/libs/middleware.ts +0 -50
  293. package/api/src/libs/security.ts +0 -192
  294. package/api/src/queues/space.ts +0 -662
  295. package/api/src/routes/credit-tokens.ts +0 -38
  296. package/api/src/routes/exchange-rates.ts +0 -87
  297. package/api/src/routes/index.ts +0 -142
  298. package/api/src/routes/integrations/stripe.ts +0 -61
  299. package/api/src/routes/meters.ts +0 -274
  300. package/api/src/routes/passports.ts +0 -68
  301. package/api/src/routes/redirect.ts +0 -20
  302. package/api/src/routes/tool.ts +0 -65
  303. package/api/src/routes/webhook-endpoints.ts +0 -126
  304. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  305. package/cloudflare/shims/did-space-js.ts +0 -17
  306. package/cloudflare/shims/did-space.ts +0 -11
  307. package/cloudflare/shims/express-compat/index.ts +0 -80
  308. package/cloudflare/shims/express-compat/types.ts +0 -41
  309. package/cloudflare/shims/lock.ts +0 -115
  310. package/cloudflare/shims/queue.ts +0 -611
  311. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  312. 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
+ });