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,100 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 11 (W2′) E2E: the migration-driver contract provisions the embedded D1
3
+ // schema through the db driver — NO wrangler in the library path. Applies the D1
4
+ // SQL migrations via applySqlMigrations over an in-process SQLite db driver,
5
+ // proves idempotency AND per-migration atomicity (a mid-migration failure rolls
6
+ // back fully and a re-run recovers cleanly).
7
+ //
8
+ // npx tsx scripts/e2e-migration-contract.ts
9
+
10
+ import fs from 'fs';
11
+ import os from 'os';
12
+ import path from 'path';
13
+ import { Sequelize } from 'sequelize';
14
+
15
+ import { createNodeDbDriver } from '../api/src/libs/drivers/db';
16
+ import { applySqlMigrations, type SqlMigration } from '../api/src/libs/drivers/migrate-runner';
17
+ import { paymentCoreSqlMigrations } from '../api/src/store/sql-migrations';
18
+
19
+ // A throwaway FILE-backed SQLite (not :memory:) so pooled connections share one
20
+ // database — matching D1 / arc-node file semantics.
21
+ function freshDriver() {
22
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'mig-')), 'd1.sqlite');
23
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: file, logging: false, pool: { max: 5 } });
24
+ return { driver: createNodeDbDriver(sequelize), sequelize };
25
+ }
26
+
27
+ async function main() {
28
+ const migrations = paymentCoreSqlMigrations;
29
+ const checks: Array<[string, boolean]> = [];
30
+
31
+ // S11.2 fresh apply via the driver (no wrangler)
32
+ const a = freshDriver();
33
+ const firstRun = await applySqlMigrations(a.driver, migrations);
34
+ console.log(`=== S11.2 apply ${migrations.length} D1 SQL migrations via the db driver (no wrangler) ===`);
35
+ console.log(JSON.stringify({ applied: firstRun.length, names: firstRun }));
36
+ checks.push(['all migrations applied', firstRun.length === migrations.length]);
37
+
38
+ // S11.3 idempotent re-run
39
+ const secondRun = await applySqlMigrations(a.driver, migrations);
40
+ console.log('\n=== S11.3 idempotent re-run ===');
41
+ console.log(JSON.stringify({ applied_second_run: secondRun.length, idempotent: secondRun.length === 0 }));
42
+ checks.push(['idempotent re-run', secondRun.length === 0]);
43
+
44
+ // S11.4/5 schema + events parity (0008)
45
+ const tables = (
46
+ await a.driver.all<{ name: string }>(
47
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT IN ('_sql_migrations')"
48
+ )
49
+ ).map((r) => r.name);
50
+ const eventsCols = (await a.driver.all<{ name: string }>('PRAGMA table_info("events")')).map((c) => c.name);
51
+ console.log('\n=== S11.4/5 provisioned schema + events parity (0008) ===');
52
+ console.log(JSON.stringify({ table_count: tables.length, has_api_version: eventsCols.includes('api_version'), has_metadata: eventsCols.includes('metadata') }));
53
+ checks.push(['events parity (api_version+metadata)', eventsCols.includes('api_version') && eventsCols.includes('metadata')]);
54
+ await a.sequelize.close();
55
+
56
+ // S11.7 ATOMICITY: a migration whose 2nd statement fails must roll back fully
57
+ // (no partial column, no tracker row) and a fixed re-run must recover.
58
+ const b = freshDriver();
59
+ await applySqlMigrations(b.driver, migrations); // baseline schema
60
+ const broken: SqlMigration = {
61
+ name: '9999_atomicity_probe.sql',
62
+ sql: 'ALTER TABLE `events` ADD COLUMN `probe_col` TEXT;\nINSERT INTO `no_such_table` (x) VALUES (1);',
63
+ };
64
+ let threw = false;
65
+ try {
66
+ await applySqlMigrations(b.driver, [...migrations, broken]);
67
+ } catch {
68
+ threw = true;
69
+ }
70
+ const colsAfterFail = (await b.driver.all<{ name: string }>('PRAGMA table_info("events")')).map((c) => c.name);
71
+ const trackerAfterFail = (await b.driver.all<{ name: string }>("SELECT name FROM _sql_migrations WHERE name='9999_atomicity_probe.sql'")).length;
72
+ console.log('\n=== S11.7 atomicity: broken migration rolls back fully ===');
73
+ console.log(JSON.stringify({ threw, probe_col_rolled_back: !colsAfterFail.includes('probe_col'), tracker_not_written: trackerAfterFail === 0 }));
74
+ checks.push(['broken migration threw', threw]);
75
+ checks.push(['partial DDL rolled back (no probe_col)', !colsAfterFail.includes('probe_col')]);
76
+ checks.push(['tracker not written for failed migration', trackerAfterFail === 0]);
77
+
78
+ // re-run with the migration FIXED recovers cleanly (no "duplicate column")
79
+ const fixed: SqlMigration = { name: '9999_atomicity_probe.sql', sql: 'ALTER TABLE `events` ADD COLUMN `probe_col` TEXT;' };
80
+ const recovery = await applySqlMigrations(b.driver, [...migrations, fixed]);
81
+ const colsAfterFix = (await b.driver.all<{ name: string }>('PRAGMA table_info("events")')).map((c) => c.name);
82
+ console.log('\n=== S11.8 fixed re-run recovers (no replay of half-applied DDL) ===');
83
+ console.log(JSON.stringify({ applied: recovery, probe_col_present: colsAfterFix.includes('probe_col') }));
84
+ checks.push(['recovery applies the fixed migration', recovery.includes('9999_atomicity_probe.sql') && colsAfterFix.includes('probe_col')]);
85
+ await b.sequelize.close();
86
+
87
+ console.log('\n=== ASSERTIONS ===');
88
+ let ok = true;
89
+ for (const [name, pass] of checks) {
90
+ if (!pass) ok = false;
91
+ console.log(JSON.stringify({ check: name, pass }));
92
+ }
93
+ console.log(JSON.stringify({ success: ok, no_wrangler_in_path: true, migrations_from_package: true }));
94
+ if (!ok) process.exit(1);
95
+ }
96
+
97
+ main().catch((err) => {
98
+ console.error(JSON.stringify({ success: false, error: String(err?.stack || err) }));
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,61 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 0 E2E — run real context module calls and print raw JSON evidence.
3
+ import { TENANT_CONTEXT_MISSING, context, getInstanceDid, withTenant } from '../api/src/libs/context';
4
+
5
+ async function main() {
6
+ // E1: withTenant fills instanceDid; requestId comes from context.run
7
+ const e1 = await context.run({ requestedBy: 'e2e' }, () =>
8
+ withTenant('did:abt:zA', async () => context.getContext())
9
+ );
10
+ console.log('=== E1 withTenant + getContext ===');
11
+ console.log(JSON.stringify(e1));
12
+
13
+ // E2 (negative): multi mode without withTenant -> TENANT_CONTEXT_MISSING
14
+ process.env.PAYMENT_TENANT_MODE = 'multi';
15
+ console.log('=== E2 multi mode without context (negative) ===');
16
+ try {
17
+ getInstanceDid();
18
+ console.log(JSON.stringify({ unexpected: 'no error thrown' }));
19
+ process.exit(1);
20
+ } catch (err: any) {
21
+ console.log(JSON.stringify({ name: err.name, code: err.code, message: err.message }));
22
+ if (err.code !== TENANT_CONTEXT_MISSING) process.exit(1);
23
+ }
24
+
25
+ // Adversarial: invalid instanceDid inputs must be rejected
26
+ console.log('=== E2b adversarial inputs (negative) ===');
27
+ for (const bad of ['', ' ', 'a b', null, undefined, 123, { toString: () => 'did:abt:zX' }]) {
28
+ try {
29
+ // eslint-disable-next-line no-await-in-loop
30
+ await withTenant(bad as any, async () => 'never');
31
+ console.log(JSON.stringify({ input: String(bad), unexpected: 'accepted' }));
32
+ process.exit(1);
33
+ } catch (err: any) {
34
+ console.log(JSON.stringify({ input: String(bad), code: err.code }));
35
+ }
36
+ }
37
+
38
+ // Adversarial: context does not leak out of scope in multi mode (fail-closed after exit)
39
+ console.log('=== E2c context does not survive scope exit in multi mode ===');
40
+ await withTenant('did:abt:zA', async () => {});
41
+ try {
42
+ getInstanceDid();
43
+ console.log(JSON.stringify({ unexpected: 'leaked tenant after scope exit' }));
44
+ process.exit(1);
45
+ } catch (err: any) {
46
+ console.log(JSON.stringify({ afterScopeExit: err.code }));
47
+ }
48
+
49
+ // single mode default fill
50
+ delete process.env.PAYMENT_TENANT_MODE;
51
+ process.env.BLOCKLET_APP_PID = process.env.BLOCKLET_APP_PID || 'zE2EDefaultAppPid';
52
+ console.log('=== E2d single mode default fill ===');
53
+ console.log(JSON.stringify({ singleModeDefault: getInstanceDid() }));
54
+
55
+ console.log(JSON.stringify({ success: true }));
56
+ }
57
+
58
+ main().catch((err) => {
59
+ console.error(err);
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,107 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 1 E2E — run the full Umzug chain on a throwaway sqlite file, prove the
3
+ // instance_did column exists on all 38 tables, then prove down removes it
4
+ // without losing rows. Raw JSON only.
5
+ import fs from 'fs';
6
+ import os from 'os';
7
+ import path from 'path';
8
+
9
+ async function main() {
10
+ // blocklet env must exist before importing any app module
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-s1-'));
15
+ // mirror tools/jest-setup.js so import-time env checks pass
16
+ process.env.ABT_NODE_DID = wallet.address;
17
+ process.env.ABT_NODE_PK = wallet.publicKey;
18
+ process.env.ABT_NODE_PORT = '8089';
19
+ process.env.ABT_NODE_SERVICE_PORT = '40406';
20
+ process.env.BLOCKLET_MODE = 'test';
21
+ process.env.BLOCKLET_DID = wallet.address;
22
+ process.env.BLOCKLET_COMPONENT_DID = wallet.address;
23
+ process.env.BLOCKLET_LOG_DIR = tmp;
24
+ process.env.BLOCKLET_DATA_DIR = tmp;
25
+ process.env.BLOCKLET_APP_PK = wallet.publicKey;
26
+ process.env.BLOCKLET_APP_SK = wallet.secretKey;
27
+ process.env.BLOCKLET_APP_PSK = wallet.secretKey;
28
+ process.env.BLOCKLET_APP_EK = wallet.secretKey;
29
+ process.env.BLOCKLET_APP_PID = wallet.address;
30
+ process.env.BLOCKLET_APP_ID = wallet.address;
31
+ process.env.BLOCKLET_APP_IDS = wallet.address;
32
+ process.env.BLOCKLET_APP_NAME = 'payment-kit-e2e';
33
+ process.env.BLOCKLET_APP_DESCRIPTION = 'payment-kit-e2e';
34
+ process.env.BLOCKLET_APP_URL = 'http://127.0.0.1:3030';
35
+ process.env.BLOCKLET_MOUNT_POINTS = JSON.stringify([
36
+ { title: 'e2e', did: wallet.address, name: 'e2e', version: '0.0.1', mountPoint: '/', status: 6, port: 8181, resources: [] },
37
+ ]);
38
+
39
+ const { Sequelize } = await import('sequelize');
40
+ const { SequelizeStorage, Umzug } = await import('umzug');
41
+ const { TENANT_TABLES } = await import('../api/src/store/tenant-tables');
42
+
43
+ const dbPath = path.join(tmp, 'e2e.db');
44
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: dbPath, logging: false });
45
+ const storeDir = path.resolve(__dirname, '../api/src/store');
46
+ const umzug = new Umzug({
47
+ migrations: {
48
+ glob: ['migrations/*.ts', { cwd: storeDir }],
49
+ resolve: ({ name, path: p, context }) => {
50
+ // eslint-disable-next-line import/no-dynamic-require, global-require
51
+ const migration = require(p!);
52
+ return {
53
+ name: name.replace(/\.ts$/, '.js'),
54
+ up: () => migration.up({ context }),
55
+ down: () => migration.down({ context }),
56
+ };
57
+ },
58
+ },
59
+ context: sequelize.getQueryInterface(),
60
+ storage: new SequelizeStorage({ sequelize }),
61
+ logger: undefined,
62
+ });
63
+
64
+ await umzug.up();
65
+
66
+ // E1: every tenant table has the nullable column
67
+ console.log('=== E1 umzug up: instance_did present on all 38 tables ===');
68
+ const e1: { table: string; name: string; notnull: number }[] = [];
69
+ for (const table of TENANT_TABLES) {
70
+ // eslint-disable-next-line no-await-in-loop
71
+ const [rows] = await sequelize.query(
72
+ `SELECT name, "notnull" FROM pragma_table_info('${table}') WHERE name = 'instance_did'`
73
+ );
74
+ const row = (rows as any[])[0];
75
+ e1.push({ table, name: row?.name, notnull: row?.notnull });
76
+ }
77
+ console.log(JSON.stringify(e1));
78
+ const missing = e1.filter((r) => r.name !== 'instance_did');
79
+ console.log(JSON.stringify({ tables: e1.length, missing: missing.length }));
80
+ if (e1.length !== 38 || missing.length > 0) process.exit(1);
81
+
82
+ // E3 (negative): down removes the column, business rows survive
83
+ const [before] = await sequelize.query('SELECT COUNT(*) AS n FROM payment_currencies');
84
+ await umzug.down();
85
+ const [cols] = await sequelize.query(
86
+ "SELECT name FROM pragma_table_info('customers') WHERE name = 'instance_did'"
87
+ );
88
+ const [after] = await sequelize.query('SELECT COUNT(*) AS n FROM payment_currencies');
89
+ console.log('=== E3 umzug down: column gone, rows preserved (negative) ===');
90
+ console.log(
91
+ JSON.stringify({
92
+ instanceDidColumnsAfterDown: (cols as any[]).length,
93
+ paymentCurrenciesBefore: (before as any[])[0].n,
94
+ paymentCurrenciesAfter: (after as any[])[0].n,
95
+ })
96
+ );
97
+ if ((cols as any[]).length !== 0) process.exit(1);
98
+ if ((before as any[])[0].n !== (after as any[])[0].n) process.exit(1);
99
+
100
+ console.log(JSON.stringify({ success: true }));
101
+ await sequelize.close();
102
+ }
103
+
104
+ main().catch((err) => {
105
+ console.error(err);
106
+ process.exit(1);
107
+ });
@@ -0,0 +1,178 @@
1
+ /* eslint-disable no-console, no-await-in-loop */
2
+ // Phase 2 E2E — backfill + unique key revisions on a throwaway sqlite DB.
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-s2-'));
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
+ BLOCKLET_DID: wallet.address,
19
+ BLOCKLET_COMPONENT_DID: wallet.address,
20
+ BLOCKLET_LOG_DIR: tmp,
21
+ BLOCKLET_DATA_DIR: tmp,
22
+ BLOCKLET_APP_PK: wallet.publicKey,
23
+ BLOCKLET_APP_SK: wallet.secretKey,
24
+ BLOCKLET_APP_PSK: wallet.secretKey,
25
+ BLOCKLET_APP_EK: wallet.secretKey,
26
+ BLOCKLET_APP_PID: wallet.address,
27
+ BLOCKLET_APP_ID: wallet.address,
28
+ BLOCKLET_APP_IDS: wallet.address,
29
+ BLOCKLET_APP_NAME: 'payment-kit-e2e',
30
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit-e2e',
31
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
32
+ BLOCKLET_MOUNT_POINTS: JSON.stringify([
33
+ {
34
+ title: 'e2e',
35
+ did: wallet.address,
36
+ name: 'e2e',
37
+ version: '0.0.1',
38
+ mountPoint: '/',
39
+ status: 6,
40
+ port: 8181,
41
+ resources: [],
42
+ },
43
+ ]),
44
+ });
45
+
46
+ const { Sequelize } = await import('sequelize');
47
+ const { SequelizeStorage, Umzug } = await import('umzug');
48
+ const { TENANT_TABLES } = await import('../api/src/store/tenant-tables');
49
+ const { runTenantBackfill } = await import('../api/src/store/tenant-backfill');
50
+
51
+ const storeDir = path.resolve(__dirname, '../api/src/store');
52
+ const makeHarness = (file: string) => {
53
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(tmp, file), logging: false });
54
+ const umzug = new Umzug({
55
+ migrations: {
56
+ glob: ['migrations/*.ts', { cwd: storeDir }],
57
+ resolve: ({ name, path: p, context }) => {
58
+ // eslint-disable-next-line import/no-dynamic-require, global-require
59
+ const migration = require(p!);
60
+ return {
61
+ name: name.replace(/\.ts$/, '.js'),
62
+ up: () => migration.up({ context }),
63
+ down: () => migration.down({ context }),
64
+ };
65
+ },
66
+ },
67
+ context: sequelize.getQueryInterface(),
68
+ storage: new SequelizeStorage({ sequelize }),
69
+ logger: undefined,
70
+ });
71
+ return { sequelize, umzug };
72
+ };
73
+
74
+ // --- E1: seed NULL rows, run backfill, prove zero NULLs per table
75
+ const h1 = makeHarness('e1.db');
76
+ await h1.umzug.up();
77
+ await h1.umzug.down(); // back to phase 1 state (columns exist, no backfill)
78
+ const now = new Date().toISOString();
79
+ await h1.sequelize.query(
80
+ `INSERT INTO customers (id, livemode, did, delinquent, created_at, updated_at) VALUES ('cus_e1', 0, 'z-e1', 0, '${now}', '${now}')`
81
+ );
82
+ await h1.sequelize.query(
83
+ `INSERT INTO products (id, livemode, active, name, type, created_at, updated_at) VALUES ('prod_e1', 0, 1, 'e1', 'service', '${now}', '${now}')`
84
+ );
85
+ await h1.umzug.up(); // backfill migration
86
+
87
+ console.log('=== E1 backfill: zero NULLs across all 38 tables ===');
88
+ const e1: { table: string; nulls: number }[] = [];
89
+ for (const table of TENANT_TABLES) {
90
+ const [rows] = await h1.sequelize.query(`SELECT COUNT(*) AS n FROM ${table} WHERE instance_did IS NULL`);
91
+ e1.push({ table, nulls: (rows as any[])[0].n });
92
+ }
93
+ console.log(JSON.stringify(e1));
94
+ if (e1.some((r) => r.nulls !== 0)) process.exit(1);
95
+ const [filled] = await h1.sequelize.query("SELECT instance_did FROM customers WHERE id = 'cus_e1'");
96
+ console.log(JSON.stringify({ backfilledValue: (filled as any[])[0].instance_did, appPid: wallet.address }));
97
+
98
+ // --- E2: per-tenant unique key, cross-tenant duplicates allowed
99
+ console.log('=== E2 unique key: (A,CODE1) ok, (B,CODE1) ok, (A,CODE1) rejected ===');
100
+ const promo = (id: string, tenant: string) =>
101
+ h1.sequelize.query(
102
+ `INSERT INTO promotion_codes (id, livemode, active, code, coupon_id, instance_did, created_at, updated_at)
103
+ VALUES ('${id}', 0, 1, 'CODE1', 'coup_x', '${tenant}', '${now}', '${now}')`
104
+ );
105
+ await promo('promo_a', 'did:abt:zTenantA');
106
+ console.log(JSON.stringify({ insert: 'promo_a tenant A', ok: true }));
107
+ await promo('promo_b', 'did:abt:zTenantB');
108
+ console.log(JSON.stringify({ insert: 'promo_b tenant B (same code)', ok: true }));
109
+ try {
110
+ await promo('promo_a2', 'did:abt:zTenantA');
111
+ console.log(JSON.stringify({ unexpected: 'duplicate accepted' }));
112
+ process.exit(1);
113
+ } catch (err: any) {
114
+ console.log(
115
+ JSON.stringify({ insert: 'promo_a2 tenant A duplicate', error: err.original?.message || err.message })
116
+ );
117
+ }
118
+
119
+ // --- E3 (negative): backfill without phase 1 columns fails loudly
120
+ console.log('=== E3 backfill on a DB without phase 1 (negative) ===');
121
+ const h3 = makeHarness('e3.db');
122
+ await h3.umzug.up();
123
+ await h3.umzug.down(); // revert backfill
124
+ await h3.umzug.down(); // revert tenant columns -> pre-phase-1 schema
125
+ try {
126
+ await runTenantBackfill(h3.sequelize as any);
127
+ console.log(JSON.stringify({ unexpected: 'backfill succeeded without phase 1' }));
128
+ process.exit(1);
129
+ } catch (err: any) {
130
+ console.log(JSON.stringify({ error: err.message }));
131
+ }
132
+ const [pragma] = await h3.sequelize.query(
133
+ "SELECT COUNT(*) AS n FROM pragma_table_info('customers') WHERE name = 'instance_did'"
134
+ );
135
+ console.log(JSON.stringify({ instanceDidColumnsAfterFailedBackfill: (pragma as any[])[0].n }));
136
+
137
+ // --- L3 adversarial: suspicious app DID must never reach DDL. The app DID
138
+ // is snapshotted at SDK import, so the malicious value must be present from
139
+ // process start -> run the attempt in a child process.
140
+ console.log('=== L3 adversarial: app DID with a quote refused before DDL ===');
141
+ const h4 = makeHarness('e4.db');
142
+ await h4.umzug.up({ to: '20260610-tenant-columns.js' } as any);
143
+ const { execFileSync } = await import('child_process');
144
+ const childScript = `
145
+ const path = require('path');
146
+ (async () => {
147
+ const { Sequelize } = require('sequelize');
148
+ const { runTenantBackfill } = require(${JSON.stringify(path.join(storeDir, 'tenant-backfill.ts'))});
149
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: ${JSON.stringify(path.join(tmp, 'e4.db'))}, logging: false });
150
+ try {
151
+ await runTenantBackfill(sequelize);
152
+ console.log(JSON.stringify({ unexpected: 'suspicious DID accepted' }));
153
+ process.exit(2);
154
+ } catch (err) {
155
+ console.log(JSON.stringify({ error: String(err.message).slice(0, 140) }));
156
+ }
157
+ await sequelize.close();
158
+ })();
159
+ `;
160
+ const childOut = execFileSync('npx', ['tsx', '-e', childScript], {
161
+ env: { ...process.env, BLOCKLET_APP_PID: "z'); DROP TABLE customers; --" },
162
+ encoding: 'utf8',
163
+ });
164
+ console.log(childOut.trim().split('\n').pop());
165
+ const [still] = await h4.sequelize.query("SELECT COUNT(*) AS n FROM sqlite_master WHERE name = 'customers'");
166
+ console.log(JSON.stringify({ customersTableSurvives: (still as any[])[0].n === 1 }));
167
+ if ((still as any[])[0].n !== 1) process.exit(1);
168
+
169
+ console.log(JSON.stringify({ success: true }));
170
+ await h1.sequelize.close();
171
+ await h3.sequelize.close();
172
+ await h4.sequelize.close();
173
+ }
174
+
175
+ main().catch((err) => {
176
+ console.error(err);
177
+ process.exit(1);
178
+ });
@@ -0,0 +1,110 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 3 E2E — scoped helpers against a real dual-tenant sqlite DB.
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-s3-'));
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
+ BLOCKLET_DID: wallet.address,
19
+ BLOCKLET_COMPONENT_DID: wallet.address,
20
+ BLOCKLET_LOG_DIR: tmp,
21
+ BLOCKLET_DATA_DIR: tmp,
22
+ BLOCKLET_APP_PK: wallet.publicKey,
23
+ BLOCKLET_APP_SK: wallet.secretKey,
24
+ BLOCKLET_APP_PSK: wallet.secretKey,
25
+ BLOCKLET_APP_EK: wallet.secretKey,
26
+ BLOCKLET_APP_PID: wallet.address,
27
+ BLOCKLET_APP_ID: wallet.address,
28
+ BLOCKLET_APP_IDS: wallet.address,
29
+ BLOCKLET_APP_NAME: 'payment-kit-e2e',
30
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit-e2e',
31
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
32
+ BLOCKLET_MOUNT_POINTS: JSON.stringify([
33
+ {
34
+ title: 'e2e',
35
+ did: wallet.address,
36
+ name: 'e2e',
37
+ version: '0.0.1',
38
+ mountPoint: '/',
39
+ status: 6,
40
+ port: 8181,
41
+ resources: [],
42
+ },
43
+ ]),
44
+ });
45
+
46
+ const { Sequelize } = await import('sequelize');
47
+ const { SequelizeStorage, Umzug } = await import('umzug');
48
+
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
+
70
+ const models = await import('../api/src/store/models');
71
+ models.initialize(sequelize);
72
+ const { withTenant } = await import('../api/src/libs/context');
73
+ const { scopedCreate, scopedFindAll, scopedFindByPk } = await import('../api/src/store/scoped');
74
+
75
+ const TENANT_A = 'did:abt:zTenantAAAA';
76
+ const TENANT_B = 'did:abt:zTenantBBBB';
77
+
78
+ // E3: dual-tenant seed, scoped findAll sees only its own rows
79
+ await withTenant(TENANT_A, () =>
80
+ scopedCreate(models.Customer as any, { livemode: false, did: 'z-user-a', delinquent: false })
81
+ );
82
+ const bCustomer: any = await withTenant(TENANT_B, () =>
83
+ scopedCreate(models.Customer as any, { livemode: false, did: 'z-user-b', delinquent: false })
84
+ );
85
+
86
+ console.log('=== E3 dual-tenant scoped findAll ===');
87
+ const aView = await withTenant(TENANT_A, () => scopedFindAll(models.Customer as any));
88
+ console.log(JSON.stringify({ tenant: 'A', rows: aView.map((r: any) => ({ did: r.did, instance_did: r.instance_did })) }));
89
+ const bView = await withTenant(TENANT_B, () => scopedFindAll(models.Customer as any));
90
+ console.log(JSON.stringify({ tenant: 'B', rows: bView.map((r: any) => ({ did: r.did, instance_did: r.instance_did })) }));
91
+ if (aView.length !== 1 || bView.length !== 1) process.exit(1);
92
+
93
+ // adversarial: A loads B's pk -> TENANT_MISMATCH
94
+ console.log('=== E3b cross-tenant findByPk (negative) ===');
95
+ try {
96
+ await withTenant(TENANT_A, () => scopedFindByPk(models.Customer as any, bCustomer.id));
97
+ console.log(JSON.stringify({ unexpected: 'cross-tenant read succeeded' }));
98
+ process.exit(1);
99
+ } catch (err: any) {
100
+ console.log(JSON.stringify({ code: err.code, message: err.message }));
101
+ }
102
+
103
+ console.log(JSON.stringify({ success: true }));
104
+ await sequelize.close();
105
+ }
106
+
107
+ main().catch((err) => {
108
+ console.error(err);
109
+ process.exit(1);
110
+ });