payment-kit 1.29.1 → 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 (310) 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/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,153 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { Sequelize } from 'sequelize';
5
+ import { SequelizeStorage, Umzug } from 'umzug';
6
+
7
+ import { withTenant } from '../../src/libs/context';
8
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
9
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
10
+
11
+ jest.mock('../../src/libs/logger', () => ({
12
+ __esModule: true,
13
+ default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
14
+ }));
15
+
16
+ const STORE_DIR = path.join(__dirname, '../../src/store');
17
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-tenant-'));
18
+ const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
19
+ const umzug = new Umzug({
20
+ migrations: {
21
+ glob: ['migrations/*.ts', { cwd: STORE_DIR }],
22
+ resolve: ({ name, path: p, context }) => {
23
+ // eslint-disable-next-line import/no-dynamic-require, global-require
24
+ const migration = require(p!);
25
+ return {
26
+ name: name.replace(/\.ts$/, '.js'),
27
+ up: () => migration.up({ context }),
28
+ down: () => migration.down({ context }),
29
+ };
30
+ },
31
+ },
32
+ context: sequelize.getQueryInterface(),
33
+ storage: new SequelizeStorage({ sequelize }),
34
+ logger: undefined,
35
+ });
36
+
37
+ let audit: typeof import('../../src/libs/audit');
38
+ let logger: any;
39
+
40
+ beforeAll(async () => {
41
+ await umzug.up();
42
+ // eslint-disable-next-line global-require
43
+ const models = require('../../src/store/models');
44
+ models.initialize(sequelize);
45
+ // eslint-disable-next-line global-require
46
+ audit = require('../../src/libs/audit');
47
+ // eslint-disable-next-line global-require
48
+ logger = require('../../src/libs/logger').default;
49
+ }, 120000);
50
+
51
+ afterAll(async () => {
52
+ await sequelize.close();
53
+ fs.rmSync(dir, { recursive: true, force: true });
54
+ });
55
+
56
+ beforeEach(async () => {
57
+ await sequelize.query('DELETE FROM events');
58
+ });
59
+
60
+ const fakeModel = (tenant: string | null, extra: Record<string, any> = {}) => ({
61
+ id: 'obj_1',
62
+ livemode: false,
63
+ instance_did: tenant,
64
+ dataValues: { id: 'obj_1', instance_did: tenant, ...extra },
65
+ _previousDataValues: {},
66
+ });
67
+
68
+ describe('audit tenant resolution (phase 4)', () => {
69
+ describe('happy path', () => {
70
+ it('takes the tenant from the model row', async () => {
71
+ await audit.createEvent('Customer', 'customer.created', fakeModel(TENANT_A));
72
+ const [rows] = await sequelize.query('SELECT type, instance_did FROM events');
73
+ expect(rows).toEqual([{ type: 'customer.created', instance_did: TENANT_A }]);
74
+ });
75
+
76
+ it('falls back to the tenant context when the model has none', async () => {
77
+ await withTenant(TENANT_B, () => audit.createEvent('Customer', 'customer.created', fakeModel(null)));
78
+ const [rows] = await sequelize.query('SELECT instance_did FROM events');
79
+ expect(rows).toEqual([{ instance_did: TENANT_B }]);
80
+ });
81
+
82
+ it('createFlexibleEvent (system source) uses the tenant context', async () => {
83
+ const event: any = await withTenant(TENANT_A, () =>
84
+ audit.createFlexibleEvent('payout.created', 'payout', 'po_1', { ok: true })
85
+ );
86
+ expect(event.instance_did).toBe(TENANT_A);
87
+ });
88
+ });
89
+
90
+ describe('bad input: both sources missing', () => {
91
+ it('rejects in multi mode and writes no event row', async () => {
92
+ process.env.PAYMENT_TENANT_MODE = 'multi';
93
+ try {
94
+ await expect(audit.createEvent('Customer', 'customer.created', fakeModel(null))).rejects.toMatchObject({
95
+ code: TENANT_CONTEXT_MISSING,
96
+ });
97
+ } finally {
98
+ delete process.env.PAYMENT_TENANT_MODE;
99
+ }
100
+ const [rows] = await sequelize.query('SELECT COUNT(*) AS n FROM events');
101
+ expect((rows as any[])[0].n).toBe(0);
102
+ });
103
+ });
104
+
105
+ describe('security: contradictory sources', () => {
106
+ it('rejects when model tenant and context tenant disagree', async () => {
107
+ await withTenant(TENANT_A, async () => {
108
+ await expect(audit.createEvent('Customer', 'customer.created', fakeModel(TENANT_B))).rejects.toMatchObject({
109
+ code: TENANT_MISMATCH,
110
+ });
111
+ });
112
+ const [rows] = await sequelize.query('SELECT COUNT(*) AS n FROM events');
113
+ expect((rows as any[])[0].n).toBe(0);
114
+ });
115
+ });
116
+
117
+ describe('data loss: rejection never blocks the business write', () => {
118
+ it('reportAuditFailure is fire-and-forget with a dedicated code', () => {
119
+ const err = Object.assign(new Error('conflict'), { code: TENANT_MISMATCH });
120
+ expect(() => audit.reportAuditFailure(err)).not.toThrow();
121
+ expect(logger.error).toHaveBeenCalledWith(
122
+ '[audit] event creation failed',
123
+ expect.objectContaining({ code: TENANT_MISMATCH })
124
+ );
125
+
126
+ audit.reportAuditFailure(new Error('boom'));
127
+ expect(logger.error).toHaveBeenCalledWith(
128
+ '[audit] event creation failed',
129
+ expect.objectContaining({ code: 'EVENT_CREATE_FAILED' })
130
+ );
131
+ });
132
+ });
133
+
134
+ describe('data leak: status/custom event paths carry the tenant too', () => {
135
+ it('createStatusEvent stamps the model tenant', async () => {
136
+ const model = fakeModel(TENANT_A, { status: 'active' });
137
+ await audit.createStatusEvent('Subscription', 'customer.subscription', { active: 'activated' }, model, {
138
+ fields: ['status'],
139
+ });
140
+ const [rows] = await sequelize.query('SELECT instance_did FROM events');
141
+ expect(rows).toEqual([{ instance_did: TENANT_A }]);
142
+ });
143
+
144
+ it('createCustomEvent stamps the model tenant', async () => {
145
+ const model = fakeModel(TENANT_B, { status: 'canceled' });
146
+ await audit.createCustomEvent('Subscription', 'customer.subscription', () => 'deleted', model, {
147
+ fields: ['status'],
148
+ });
149
+ const [rows] = await sequelize.query('SELECT type, instance_did FROM events');
150
+ expect(rows).toEqual([{ type: 'customer.subscription.deleted', instance_did: TENANT_B }]);
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,204 @@
1
+ import {
2
+ TENANT_CONTEXT_MISSING,
3
+ TenantError,
4
+ context,
5
+ getDefaultInstanceDid,
6
+ getInstanceDid,
7
+ getTenantMode,
8
+ withTenant,
9
+ } from '../../src/libs/context';
10
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
11
+
12
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
13
+
14
+ describe('libs/context tenant infrastructure', () => {
15
+ const originalMode = process.env.PAYMENT_TENANT_MODE;
16
+
17
+ afterEach(() => {
18
+ if (originalMode === undefined) {
19
+ delete process.env.PAYMENT_TENANT_MODE;
20
+ } else {
21
+ process.env.PAYMENT_TENANT_MODE = originalMode;
22
+ }
23
+ });
24
+
25
+ const asMulti = () => {
26
+ process.env.PAYMENT_TENANT_MODE = 'multi';
27
+ };
28
+
29
+ describe('tenant mode', () => {
30
+ it('defaults to single mode', () => {
31
+ delete process.env.PAYMENT_TENANT_MODE;
32
+ expect(getTenantMode()).toBe('single');
33
+ });
34
+
35
+ it('reads multi mode from env', () => {
36
+ asMulti();
37
+ expect(getTenantMode()).toBe('multi');
38
+ });
39
+ });
40
+
41
+ describe('happy path', () => {
42
+ it('returns the tenant inside withTenant and across await boundaries', async () => {
43
+ await withTenant(TENANT_A, async () => {
44
+ expect(getInstanceDid()).toBe(TENANT_A);
45
+ await sleep(5);
46
+ expect(getInstanceDid()).toBe(TENANT_A);
47
+ await Promise.resolve().then(() => {
48
+ expect(getInstanceDid()).toBe(TENANT_A);
49
+ });
50
+ });
51
+ });
52
+
53
+ it('exposes instanceDid via context.getContext()', async () => {
54
+ await withTenant(TENANT_A, async () => {
55
+ expect(context.getContext().instanceDid).toBe(TENANT_A);
56
+ });
57
+ });
58
+
59
+ it('returns the value produced by fn', async () => {
60
+ const result = await withTenant(TENANT_A, async () => 42);
61
+ expect(result).toBe(42);
62
+ });
63
+
64
+ it('keeps tenant when context.run is nested inside withTenant', async () => {
65
+ await withTenant(TENANT_A, () =>
66
+ context.run({ requestedBy: 'user-x' }, async () => {
67
+ expect(getInstanceDid()).toBe(TENANT_A);
68
+ expect(context.getRequestedBy()).toBe('user-x');
69
+ })
70
+ );
71
+ });
72
+
73
+ it('keeps tenant when withTenant is nested inside context.run', async () => {
74
+ await context.run({ requestedBy: 'user-x' }, () =>
75
+ withTenant(TENANT_A, async () => {
76
+ expect(getInstanceDid()).toBe(TENANT_A);
77
+ expect(context.getRequestedBy()).toBe('user-x');
78
+ })
79
+ );
80
+ });
81
+ });
82
+
83
+ describe('bad input', () => {
84
+ it.each([['' as any], [' ' as any], [null as any], [undefined as any], [123 as any], ['a b' as any]])(
85
+ 'rejects invalid instanceDid %p',
86
+ async (bad) => {
87
+ await expect(withTenant(bad, async () => 'never')).rejects.toMatchObject({
88
+ code: TENANT_CONTEXT_MISSING,
89
+ });
90
+ }
91
+ );
92
+
93
+ it('throws TENANT_CONTEXT_MISSING in multi mode without context', () => {
94
+ asMulti();
95
+ try {
96
+ getInstanceDid();
97
+ throw new Error('expected getInstanceDid to throw');
98
+ } catch (err: any) {
99
+ expect(err).toBeInstanceOf(TenantError);
100
+ expect(err.code).toBe(TENANT_CONTEXT_MISSING);
101
+ }
102
+ });
103
+ });
104
+
105
+ describe('security: concurrent isolation', () => {
106
+ it('does not leak tenant between interleaved concurrent withTenant scopes', async () => {
107
+ const observed: Record<string, string[]> = { a: [], b: [] };
108
+ await Promise.all([
109
+ withTenant(TENANT_A, async () => {
110
+ observed.a!.push(getInstanceDid());
111
+ await sleep(10);
112
+ observed.a!.push(getInstanceDid());
113
+ await sleep(20);
114
+ observed.a!.push(getInstanceDid());
115
+ }),
116
+ withTenant(TENANT_B, async () => {
117
+ observed.b!.push(getInstanceDid());
118
+ await sleep(15);
119
+ observed.b!.push(getInstanceDid());
120
+ await sleep(5);
121
+ observed.b!.push(getInstanceDid());
122
+ }),
123
+ ]);
124
+ expect(observed.a).toEqual([TENANT_A, TENANT_A, TENANT_A]);
125
+ expect(observed.b).toEqual([TENANT_B, TENANT_B, TENANT_B]);
126
+ });
127
+ });
128
+
129
+ describe('data loss: context survives async scheduling primitives', () => {
130
+ it('keeps tenant across setTimeout', async () => {
131
+ await withTenant(TENANT_A, async () => {
132
+ const seen = await new Promise((resolve) => {
133
+ setTimeout(() => resolve(getInstanceDid()), 5);
134
+ });
135
+ expect(seen).toBe(TENANT_A);
136
+ });
137
+ });
138
+
139
+ it('keeps tenant across queueMicrotask', async () => {
140
+ await withTenant(TENANT_A, async () => {
141
+ const seen = await new Promise((resolve) => {
142
+ queueMicrotask(() => resolve(getInstanceDid()));
143
+ });
144
+ expect(seen).toBe(TENANT_A);
145
+ });
146
+ });
147
+
148
+ it('keeps tenant inside event emitter callbacks registered within the scope', async () => {
149
+ // eslint-disable-next-line global-require
150
+ const { EventEmitter } = require('events');
151
+ const emitter = new EventEmitter();
152
+ await withTenant(TENANT_A, async () => {
153
+ const seen = await new Promise((resolve) => {
154
+ emitter.on('ping', () => resolve(getInstanceDid()));
155
+ emitter.emit('ping');
156
+ });
157
+ expect(seen).toBe(TENANT_A);
158
+ });
159
+ });
160
+ });
161
+
162
+ describe('data damage: nesting and immutability', () => {
163
+ it('restores outer tenant after a nested withTenant exits', async () => {
164
+ await withTenant(TENANT_A, async () => {
165
+ expect(getInstanceDid()).toBe(TENANT_A);
166
+ await withTenant(TENANT_B, async () => {
167
+ expect(getInstanceDid()).toBe(TENANT_B);
168
+ });
169
+ expect(getInstanceDid()).toBe(TENANT_A);
170
+ });
171
+ });
172
+
173
+ it('prevents fn from mutating the stored context to affect sibling calls', async () => {
174
+ await withTenant(TENANT_A, async () => {
175
+ const ctx = context.getContext() as any;
176
+ try {
177
+ ctx.instanceDid = TENANT_B;
178
+ } catch {
179
+ // frozen object throws in strict mode — either way the mutation must not stick
180
+ }
181
+ expect(getInstanceDid()).toBe(TENANT_A);
182
+ });
183
+ });
184
+ });
185
+
186
+ describe('data leak: single mode default fill', () => {
187
+ it('falls back to the configured app DID in single mode', () => {
188
+ delete process.env.PAYMENT_TENANT_MODE;
189
+ // jest globalSetup configures BLOCKLET_APP_PID with the test wallet address
190
+ expect(getInstanceDid()).toBe(getDefaultInstanceDid());
191
+ expect(getDefaultInstanceDid()).toBe(process.env.BLOCKLET_APP_PID);
192
+ expect(getDefaultInstanceDid()).not.toBe(TENANT_A);
193
+ expect(getDefaultInstanceDid()).not.toBe(TENANT_B);
194
+ });
195
+
196
+ it('never leaks a tenant injected by another scope into the default', async () => {
197
+ await withTenant(TENANT_A, async () => {
198
+ expect(getInstanceDid()).toBe(TENANT_A);
199
+ });
200
+ // outside any scope, single mode returns the app DID again
201
+ expect(getInstanceDid()).toBe(getDefaultInstanceDid());
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,115 @@
1
+ // Phase 8 (W2′): the injected-config slot is authoritative. libs/env.ts is the
2
+ // single boundary; the factory wires setCoreConfig(config) and core reads prefer
3
+ // the injected config over process.env (the worker mirror / blocklet server
4
+ // native fallback). getTenantMode's mode-source reads the injected config too.
5
+
6
+ import { setCoreConfig, readConfig, hasConfig } from '../../src/libs/env';
7
+ import { getTenantMode, getDefaultInstanceDid, setDefaultInstanceDid } from '../../src/libs/tenant';
8
+ import { createDefaultSecretsDriver } from '../../src/libs/drivers';
9
+ import { createEmbeddedPaymentService, MissingConfigError } from '../../src/service';
10
+
11
+ const ORIG_MODE = process.env.PAYMENT_TENANT_MODE;
12
+ const ORIG_PID = process.env.BLOCKLET_APP_PID;
13
+
14
+ afterEach(() => {
15
+ setCoreConfig(undefined); // clear the injected config singleton between tests
16
+ setDefaultInstanceDid(undefined); // clear any tenancy-slot override
17
+ if (ORIG_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
18
+ else process.env.PAYMENT_TENANT_MODE = ORIG_MODE;
19
+ if (ORIG_PID === undefined) delete process.env.BLOCKLET_APP_PID;
20
+ else process.env.BLOCKLET_APP_PID = ORIG_PID;
21
+ });
22
+
23
+ describe('Phase 8 — injected config is authoritative (happy path)', () => {
24
+ it('readConfig prefers injected config over process.env', () => {
25
+ process.env.PHASE8_FOO = 'from-env';
26
+ setCoreConfig({ PHASE8_FOO: 'from-config' });
27
+ expect(readConfig('PHASE8_FOO')).toBe('from-config');
28
+ delete process.env.PHASE8_FOO;
29
+ });
30
+
31
+ it('readConfig falls back to process.env when the key is absent from injected config', () => {
32
+ process.env.PHASE8_BAR = 'env-only';
33
+ setCoreConfig({ OTHER: 'x' });
34
+ expect(readConfig('PHASE8_BAR')).toBe('env-only');
35
+ delete process.env.PHASE8_BAR;
36
+ });
37
+
38
+ it('hasConfig is true for injected and env keys, false for absent', () => {
39
+ setCoreConfig({ PHASE8_PRESENT: 'v' });
40
+ expect(hasConfig('PHASE8_PRESENT')).toBe(true);
41
+ expect(hasConfig('PHASE8_DEFINITELY_ABSENT_KEY')).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('Phase 8 — getTenantMode mode-source from injected config (data isolation)', () => {
46
+ it('reads tenant mode from the injected config', () => {
47
+ delete process.env.PAYMENT_TENANT_MODE; // no env fallback — prove config is the source
48
+ setCoreConfig({ PAYMENT_TENANT_MODE: 'multi' });
49
+ expect(getTenantMode()).toBe('multi');
50
+ setCoreConfig({ PAYMENT_TENANT_MODE: 'single' });
51
+ expect(getTenantMode()).toBe('single');
52
+ });
53
+
54
+ it('injected config wins over a conflicting process.env (single source of truth)', () => {
55
+ process.env.PAYMENT_TENANT_MODE = 'single';
56
+ setCoreConfig({ PAYMENT_TENANT_MODE: 'multi' });
57
+ expect(getTenantMode()).toBe('multi'); // config wins, not the env mirror
58
+ });
59
+
60
+ it('SECURITY: getTenantMode takes no request input — only config/env decide the mode', () => {
61
+ setCoreConfig({ PAYMENT_TENANT_MODE: 'single' });
62
+ // getTenantMode() has no parameters; there is no request-level surface to tamper.
63
+ expect(getTenantMode.length).toBe(0);
64
+ expect(getTenantMode()).toBe('single');
65
+ });
66
+
67
+ it('getDefaultInstanceDid reads the app DID from injected config', () => {
68
+ delete process.env.BLOCKLET_APP_PID;
69
+ process.env.PAYMENT_TENANT_MODE = 'single';
70
+ setCoreConfig({ BLOCKLET_APP_PID: 'did:abt:zCONFIGAPP' });
71
+ expect(getDefaultInstanceDid()).toBe('did:abt:zCONFIGAPP');
72
+ });
73
+ });
74
+
75
+ describe('Phase 8 — fail-fast on a missing required config field (bad input)', () => {
76
+ it('single-mode factory with no BLOCKLET_APP_PID (config + env both absent) throws MissingConfigError', () => {
77
+ delete process.env.BLOCKLET_APP_PID;
78
+ expect(() =>
79
+ createEmbeddedPaymentService({
80
+ config: {}, // no BLOCKLET_APP_PID
81
+ db: { sequelize: {} as any },
82
+ // no tenancy slot -> defaults to single mode
83
+ })
84
+ ).toThrow(MissingConfigError);
85
+ });
86
+
87
+ it('the thrown error names the missing field (not a silent default)', () => {
88
+ delete process.env.BLOCKLET_APP_PID;
89
+ try {
90
+ createEmbeddedPaymentService({ config: {}, db: { sequelize: {} as any } });
91
+ throw new Error('expected MissingConfigError');
92
+ } catch (err: any) {
93
+ expect(err).toBeInstanceOf(MissingConfigError);
94
+ expect(err.code).toBe('MISSING_CONFIG_FIELD');
95
+ expect(err.field).toBe('BLOCKLET_APP_PID');
96
+ }
97
+ });
98
+
99
+ it('multi mode does NOT require BLOCKLET_APP_PID (tenant resolved per request)', () => {
100
+ delete process.env.BLOCKLET_APP_PID;
101
+ // multi mode passes the config fail-fast; it fails later on the db slot only
102
+ // if absent — here we give a truthy sequelize so the config check is what we
103
+ // assert. D1: multi also requires identity + secrets slots (else it would
104
+ // silently degrade to single), so provide minimal stubs.
105
+ expect(() =>
106
+ createEmbeddedPaymentService({
107
+ config: { PAYMENT_TENANT_MODE: 'multi' },
108
+ db: { sequelize: {} as any },
109
+ tenancy: { mode: 'multi' },
110
+ identity: { resolveInstanceDidForHost: () => null } as any,
111
+ secrets: createDefaultSecretsDriver(),
112
+ })
113
+ ).not.toThrow(MissingConfigError);
114
+ });
115
+ });