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,177 @@
1
+ // SPIKE: validate the TenantModel base-class mechanism against BOTH query
2
+ // engines in one run — real Sequelize (Node) and the sequelize-d1 shim
3
+ // (worker). Go/No-Go for `TENANT-ISOLATION-DESIGN.md` §12.
4
+ //
5
+ // Proves: (a) static override + super works on both bases; (b) `this`
6
+ // resolves to the concrete model through the extra inheritance layer (shim);
7
+ // (c) scopeWhere idempotency absorbs the findByPk->findOne->findAll double
8
+ // injection; (d) cross-tenant read/write/aggregate are isolated; (e) the
9
+ // worker reads the tenant via ALS (withTenant).
10
+ import { DatabaseSync } from 'node:sqlite';
11
+ import { DataTypes, Model as RealModel, Sequelize } from 'sequelize';
12
+
13
+ import { withTenant } from '../../src/libs/context';
14
+ import { makeTenantModel } from '../../src/store/tenant-model';
15
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
16
+
17
+ // 'coupons' is a real tenant table (in TENANT_TABLES) -> _isTenantTable() true.
18
+ // We give it a minimal schema we fully control, on a throwaway DB.
19
+ const TABLE = 'coupons';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Base 1: real Sequelize (Node runtime)
23
+ // ---------------------------------------------------------------------------
24
+ describe('TenantModel over real Sequelize', () => {
25
+ let sequelize: Sequelize;
26
+ let Widget: any;
27
+
28
+ beforeAll(async () => {
29
+ sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false });
30
+ Widget = class extends makeTenantModel(RealModel) {};
31
+ Widget.init(
32
+ {
33
+ id: { type: DataTypes.STRING, primaryKey: true },
34
+ instance_did: { type: DataTypes.STRING },
35
+ name: { type: DataTypes.STRING },
36
+ qty: { type: DataTypes.INTEGER },
37
+ },
38
+ { sequelize, modelName: 'Widget', tableName: TABLE, timestamps: false }
39
+ );
40
+ await sequelize.sync();
41
+ });
42
+
43
+ afterAll(async () => {
44
+ await sequelize.close();
45
+ });
46
+
47
+ beforeEach(async () => {
48
+ await sequelize.query(`DELETE FROM ${TABLE}`);
49
+ await withTenant(TENANT_A, () => Widget.create({ id: 'a1', name: 'a-one', qty: 10 }));
50
+ await withTenant(TENANT_A, () => Widget.create({ id: 'a2', name: 'a-two', qty: 5 }));
51
+ await withTenant(TENANT_B, () => Widget.create({ id: 'b1', name: 'b-one', qty: 100 }));
52
+ });
53
+
54
+ it('findAll returns only the active tenant', async () => {
55
+ const a = await withTenant(TENANT_A, () => Widget.findAll());
56
+ expect(a.map((r: any) => r.id).sort()).toEqual(['a1', 'a2']);
57
+ const b = await withTenant(TENANT_B, () => Widget.findAll());
58
+ expect(b.map((r: any) => r.id)).toEqual(['b1']);
59
+ });
60
+
61
+ it('findByPk across tenants returns null (delegation + idempotent double-scope)', async () => {
62
+ expect(await withTenant(TENANT_A, () => Widget.findByPk('a1'))).not.toBeNull();
63
+ expect(await withTenant(TENANT_A, () => Widget.findByPk('b1'))).toBeNull();
64
+ });
65
+
66
+ it('create stamps the active tenant', async () => {
67
+ const row = await withTenant(TENANT_A, () => Widget.findByPk('a1'));
68
+ expect(row.instance_did).toBe(TENANT_A);
69
+ });
70
+
71
+ it('sum aggregates only the active tenant', async () => {
72
+ expect(await withTenant(TENANT_A, () => Widget.sum('qty'))).toBe(15);
73
+ expect(await withTenant(TENANT_B, () => Widget.sum('qty'))).toBe(100);
74
+ });
75
+
76
+ it('update never crosses tenants', async () => {
77
+ await withTenant(TENANT_A, () => Widget.update({ name: 'a-upd' }, { where: {} }));
78
+ const b1 = await withTenant(TENANT_B, () => Widget.findByPk('b1'));
79
+ expect(b1.name).toBe('b-one');
80
+ });
81
+
82
+ it('explicit matching instance_did is idempotent; conflicting fails closed', async () => {
83
+ await expect(withTenant(TENANT_A, () => Widget.findAll({ where: { instance_did: TENANT_A } }))).resolves.toHaveLength(2);
84
+ await expect(withTenant(TENANT_A, () => Widget.findAll({ where: { instance_did: TENANT_B } }))).rejects.toThrow(/conflicts with the active tenant/);
85
+ });
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Base 2: sequelize-d1 shim (worker runtime), backed by node:sqlite as a
90
+ // minimal D1-compatible fake.
91
+ // ---------------------------------------------------------------------------
92
+ function makeFakeD1(db: DatabaseSync) {
93
+ const prepare = (sql: string) => {
94
+ let bound: any[] = [];
95
+ const stmt: any = {
96
+ __sql: sql,
97
+ bind: (...vals: any[]) => {
98
+ bound = vals;
99
+ return stmt;
100
+ },
101
+ all: () => ({ results: db.prepare(sql).all(...bound), meta: {} }),
102
+ run: () => {
103
+ const r = db.prepare(sql).run(...bound);
104
+ return { meta: { changes: Number(r.changes), last_row_id: Number(r.lastInsertRowid) } };
105
+ },
106
+ first: () => db.prepare(sql).get(...bound) ?? null,
107
+ };
108
+ return stmt;
109
+ };
110
+ return {
111
+ prepare,
112
+ batch: async (stmts: any[]) =>
113
+ stmts.map((s) => {
114
+ if (/^\s*SELECT/i.test(s.__sql)) return s.all();
115
+ s.run();
116
+ return { results: [], meta: {} };
117
+ }),
118
+ };
119
+ }
120
+
121
+ describe('TenantModel over sequelize-d1 shim', () => {
122
+ let ShimModel: any;
123
+ let setDB: any;
124
+ let Widget: any;
125
+
126
+ beforeAll(() => {
127
+ // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
128
+ const shim = require('../../../cloudflare/shims/sequelize-d1');
129
+ ShimModel = shim.Model;
130
+ setDB = shim.setDB;
131
+
132
+ const db = new DatabaseSync(':memory:');
133
+ db.exec(`CREATE TABLE ${TABLE} (id TEXT PRIMARY KEY, instance_did TEXT, name TEXT, qty INTEGER)`);
134
+ setDB(makeFakeD1(db));
135
+
136
+ Widget = class extends makeTenantModel(ShimModel) {};
137
+ Widget.init(
138
+ { id: {}, instance_did: {}, name: {}, qty: {} },
139
+ { sequelize: { models: {} }, modelName: 'Widget', tableName: TABLE }
140
+ );
141
+ });
142
+
143
+ beforeEach(async () => {
144
+ await withTenant(TENANT_A, () => Widget.destroy({ where: {} }));
145
+ await withTenant(TENANT_B, () => Widget.destroy({ where: {} }));
146
+ await withTenant(TENANT_A, () => Widget.create({ id: 'a1', name: 'a-one', qty: 10 }));
147
+ await withTenant(TENANT_A, () => Widget.create({ id: 'a2', name: 'a-two', qty: 5 }));
148
+ await withTenant(TENANT_B, () => Widget.create({ id: 'b1', name: 'b-one', qty: 100 }));
149
+ });
150
+
151
+ it('findAll returns only the active tenant', async () => {
152
+ const a = await withTenant(TENANT_A, () => Widget.findAll());
153
+ expect(a.map((r: any) => r.id).sort()).toEqual(['a1', 'a2']);
154
+ const b = await withTenant(TENANT_B, () => Widget.findAll());
155
+ expect(b.map((r: any) => r.id)).toEqual(['b1']);
156
+ });
157
+
158
+ it('findByPk across tenants returns null (this-resolution + delegation through extra layer)', async () => {
159
+ expect(await withTenant(TENANT_A, () => Widget.findByPk('a1'))).not.toBeNull();
160
+ expect(await withTenant(TENANT_A, () => Widget.findByPk('b1'))).toBeNull();
161
+ });
162
+
163
+ it('create stamps the active tenant', async () => {
164
+ const row = await withTenant(TENANT_A, () => Widget.findByPk('a1'));
165
+ expect(row.instance_did).toBe(TENANT_A);
166
+ });
167
+
168
+ it('update never crosses tenants', async () => {
169
+ await withTenant(TENANT_A, () => Widget.update({ name: 'a-upd' }, { where: {} }));
170
+ const b1 = await withTenant(TENANT_B, () => Widget.findByPk('b1'));
171
+ expect(b1.name).toBe('b-one');
172
+ });
173
+
174
+ it('conflicting explicit instance_did fails closed', async () => {
175
+ await expect(withTenant(TENANT_A, () => Widget.findAll({ where: { instance_did: TENANT_B } }))).rejects.toThrow(/conflicts with the active tenant/);
176
+ });
177
+ });
@@ -0,0 +1,162 @@
1
+ // Phase 3 (W1′) — TenantModel landed on the REAL models.
2
+ //
3
+ // The spike (tenant-model-spike.spec.ts) proved the mechanism on controlled
4
+ // same-named tables against both engines. This proves the PRODUCTION wiring:
5
+ // the real Coupon class (with its init/associations/hooks) extends TenantModel
6
+ // and is transparently tenant-scoped across the full 6-class matrix.
7
+ import { Sequelize } from 'sequelize';
8
+
9
+ import { withTenant } from '../../src/libs/context';
10
+ import { TENANT_MISMATCH } from '../../src/libs/tenant';
11
+ import { Coupon, initialize } from '../../src/store/models';
12
+ import { isTenantTable, scopeWhere, stampTenant } from '../../src/store/scoped-core';
13
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
14
+
15
+ const sequelize = new Sequelize('sqlite::memory:', { logging: false });
16
+ initialize(sequelize);
17
+
18
+ beforeAll(async () => {
19
+ await sequelize.sync({ force: true });
20
+ });
21
+ afterAll(() => sequelize.close());
22
+
23
+ beforeEach(async () => {
24
+ // truncate between tests so counts are deterministic
25
+ await sequelize.query('DELETE FROM coupons');
26
+ });
27
+
28
+ function makeCoupon(overrides: Record<string, any> = {}) {
29
+ return {
30
+ livemode: false,
31
+ duration: 'once',
32
+ name: 'spring-sale',
33
+ created_via: 'api',
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ describe('TenantModel on the real Coupon model', () => {
39
+ describe('Happy path', () => {
40
+ it('findAll under tenant A returns only A rows; create stamps instance_did', async () => {
41
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon({ name: 'a-1' }) as any));
42
+ await withTenant(TENANT_B, () => Coupon.create(makeCoupon({ name: 'b-1' }) as any));
43
+
44
+ const aRows = await withTenant(TENANT_A, () => Coupon.findAll());
45
+ expect(aRows.map((r: any) => r.name)).toEqual(['a-1']);
46
+ expect(aRows.every((r: any) => r.instance_did === TENANT_A)).toBe(true);
47
+ });
48
+ });
49
+
50
+ describe('Bad input', () => {
51
+ it('explicit conflicting where.instance_did fails closed (reject, not resolve)', async () => {
52
+ await expect(
53
+ withTenant(TENANT_A, () => Coupon.findOne({ where: { instance_did: TENANT_B } as any }))
54
+ ).rejects.toMatchObject({ code: TENANT_MISMATCH });
55
+ });
56
+
57
+ it('undefined / empty where does not crash', async () => {
58
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon() as any));
59
+ await expect(withTenant(TENANT_A, () => Coupon.findAll())).resolves.toBeDefined();
60
+ await expect(withTenant(TENANT_A, () => Coupon.findOne({}))).resolves.toBeDefined();
61
+ });
62
+ });
63
+
64
+ describe('Security', () => {
65
+ it('prototype-pollution keys in a where payload do not poison scope injection', async () => {
66
+ // scopeWhere spreads the caller where into a fresh object; a malicious
67
+ // __proto__ own-key must not leak onto Object.prototype.
68
+ const malicious = JSON.parse('{"__proto__": {"polluted": true}, "name": "x"}');
69
+ const scoped = await withTenant(TENANT_A, async () => scopeWhere(malicious));
70
+ expect(({} as any).polluted).toBeUndefined();
71
+ expect(scoped.instance_did).toBe(TENANT_A);
72
+ expect(scoped.name).toBe('x');
73
+ });
74
+
75
+ it('create cannot stamp a foreign tenant via explicit instance_did', async () => {
76
+ await expect(
77
+ withTenant(TENANT_A, () => Coupon.create(makeCoupon({ instance_did: TENANT_B }) as any))
78
+ ).rejects.toMatchObject({ code: TENANT_MISMATCH });
79
+ });
80
+ });
81
+
82
+ describe('Data loss', () => {
83
+ it('update/destroy never cross tenants', async () => {
84
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon({ name: 'keep' }) as any));
85
+ await withTenant(TENANT_B, () => Coupon.create(makeCoupon({ name: 'victim' }) as any));
86
+
87
+ // A updates "everything" — must not touch B's row
88
+ await withTenant(TENANT_A, () => Coupon.update({ name: 'renamed' }, { where: {} }));
89
+ const bRow = await withTenant(TENANT_B, () => Coupon.findOne());
90
+ expect(bRow!.name).toBe('victim');
91
+
92
+ // A destroys "everything" — B's row survives
93
+ await withTenant(TENANT_A, () => Coupon.destroy({ where: {} }));
94
+ const bCount = await withTenant(TENANT_B, () => Coupon.count());
95
+ const aCount = await withTenant(TENANT_A, () => Coupon.count());
96
+ expect({ aCount, bCount }).toEqual({ aCount: 0, bCount: 1 });
97
+ });
98
+ });
99
+
100
+ describe('Data damage', () => {
101
+ it('roundtrip create -> findByPk preserves unicode name + metadata', async () => {
102
+ const created: any = await withTenant(TENANT_A, () =>
103
+ Coupon.create(makeCoupon({ name: '春季促销 🎉', metadata: { k: 'välue', n: 1 } }) as any)
104
+ );
105
+ const fetched: any = await withTenant(TENANT_A, () => Coupon.findByPk(created.id));
106
+ expect(fetched.name).toBe('春季促销 🎉');
107
+ expect(fetched.metadata).toEqual({ k: 'välue', n: 1 });
108
+ });
109
+ });
110
+
111
+ describe('Data leak', () => {
112
+ it('cross-tenant findByPk returns null; aggregates do not cross', async () => {
113
+ const bRow: any = await withTenant(TENANT_B, () => Coupon.create(makeCoupon({ name: 'b-only' }) as any));
114
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon({ name: 'a-only' }) as any));
115
+
116
+ // A cannot fetch B's row by its (globally unique) id
117
+ const leaked = await withTenant(TENANT_A, () => Coupon.findByPk(bRow.id));
118
+ expect(leaked).toBeNull();
119
+
120
+ // findAndCountAll count excludes the other tenant
121
+ const { count } = await withTenant(TENANT_A, () => Coupon.findAndCountAll());
122
+ expect(count).toBe(1);
123
+
124
+ const aCount = await withTenant(TENANT_A, () => Coupon.count());
125
+ expect(aCount).toBe(1);
126
+ });
127
+
128
+ it('sum/max aggregates never cross tenants', async () => {
129
+ // A: two coupons (10 + 30 = 40, max 30); B: one coupon (99)
130
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon({ name: 'a-10', percent_off: 10 }) as any));
131
+ await withTenant(TENANT_A, () => Coupon.create(makeCoupon({ name: 'a-30', percent_off: 30 }) as any));
132
+ await withTenant(TENANT_B, () => Coupon.create(makeCoupon({ name: 'b-99', percent_off: 99 }) as any));
133
+
134
+ const aSum = await withTenant(TENANT_A, () => Coupon.sum('percent_off'));
135
+ const aMax = await withTenant(TENANT_A, () => Coupon.max('percent_off'));
136
+ const bSum = await withTenant(TENANT_B, () => Coupon.sum('percent_off'));
137
+
138
+ // A's aggregates exclude B's 99 entirely
139
+ expect(aSum).toBe(40);
140
+ expect(aMax).toBe(30);
141
+ expect(bSum).toBe(99);
142
+ });
143
+ });
144
+ });
145
+
146
+ describe('scoped-core is engine-agnostic (Phase 3 cross-engine import assertion)', () => {
147
+ // The same scopeWhere/stampTenant/isTenantTable the worker shim imports.
148
+ // No Node-only dependency is required to inject the tenant.
149
+ it('scopeWhere injects instance_did for tenant tables under withTenant', async () => {
150
+ await withTenant(TENANT_A, async () => {
151
+ expect(isTenantTable('coupons')).toBe(true);
152
+ expect(scopeWhere({ name: 'x' })).toEqual({ name: 'x', instance_did: TENANT_A });
153
+ expect(stampTenant({ name: 'x' })).toEqual({ name: 'x', instance_did: TENANT_A });
154
+ });
155
+ });
156
+
157
+ it('scopeWhere fails closed on a conflicting explicit tenant', async () => {
158
+ await expect(withTenant(TENANT_A, async () => scopeWhere({ instance_did: TENANT_B }))).rejects.toMatchObject({
159
+ code: TENANT_MISMATCH,
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,196 @@
1
+ // Phase 4 (W1′) — residual-hole closure on top of Phase 3's TenantModel.
2
+ //
3
+ // Covers the three Phase 4 deliverables that close the escapes the base class
4
+ // can't reach on its own:
5
+ // - 洞 G: raw sequelize.query on tenant tables is instance_did-guarded.
6
+ // - 洞 H observability: a forged cross-tenant job emits a structured
7
+ // TENANT_VIOLATION alert (not a silent scoped null).
8
+ // - system* security: the cross-tenant bypass is explicit-only — a normal
9
+ // route/tenant context can never read across tenants.
10
+ import { Sequelize } from 'sequelize';
11
+
12
+ import { context, isSystemContext, withTenant } from '../../src/libs/context';
13
+ import { assertJobObjectTenant } from '../../src/libs/queue';
14
+ import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
15
+ import logger from '../../src/libs/logger';
16
+ import { Coupon, MeterEvent, PromotionCode, initialize } from '../../src/store/models';
17
+ import { systemFindByPk } from '../../src/store/scoped';
18
+ import { TENANT_A, TENANT_B } from '../fixtures/tenants';
19
+
20
+ jest.mock('../../src/libs/logger');
21
+
22
+ const sequelize = new Sequelize('sqlite::memory:', { logging: false });
23
+ initialize(sequelize);
24
+
25
+ beforeAll(async () => {
26
+ await sequelize.sync({ force: true });
27
+ });
28
+ afterAll(() => sequelize.close());
29
+
30
+ beforeEach(async () => {
31
+ jest.clearAllMocks();
32
+ await sequelize.query('DELETE FROM meter_events');
33
+ await sequelize.query('DELETE FROM coupons');
34
+ await sequelize.query('DELETE FROM promotion_codes');
35
+ });
36
+
37
+ describe('洞 G — raw query instance_did guard (MeterEvent.getEventStats)', () => {
38
+ let seq = 0;
39
+ const seedEvent = (tenant: string, value: number) => {
40
+ seq += 1;
41
+ return withTenant(tenant, () =>
42
+ MeterEvent.create({
43
+ event_name: 'api.calls',
44
+ identifier: `evt-${tenant}-${seq}`,
45
+ timestamp: 1700000000 + seq,
46
+ created_via: 'api',
47
+ livemode: true,
48
+ status: 'completed',
49
+ payload: { value: String(value), customer_id: 'c-1' },
50
+ } as any)
51
+ );
52
+ };
53
+
54
+ it('the raw SUM aggregate never crosses tenants', async () => {
55
+ await seedEvent(TENANT_A, 10);
56
+ await seedEvent(TENANT_A, 30);
57
+ await seedEvent(TENANT_B, 999);
58
+
59
+ const aStats = await withTenant(TENANT_A, () => MeterEvent.getEventStats('api.calls'));
60
+ const bStats = await withTenant(TENANT_B, () => MeterEvent.getEventStats('api.calls'));
61
+
62
+ // A's total_value (raw SUM) excludes B's 999 entirely
63
+ expect(Number(aStats.total_value)).toBe(40);
64
+ expect(aStats.total_events).toBe(2);
65
+ expect(Number(bStats.total_value)).toBe(999);
66
+ });
67
+
68
+ it('Bad input: a guarded read fails closed with no tenant context (multi mode) — never full-scans', async () => {
69
+ // The raw guards bind getInstanceDid() into the WHERE; that same getter is
70
+ // the fail-closed foundation. In multi mode with no withTenant context it
71
+ // throws TENANT_CONTEXT_MISSING, so the guarded query is REFUSED rather than
72
+ // run unbound (which would scan every tenant's rows). Proven here on a single
73
+ // scoped op (no Promise.all fan-out -> no orphaned rejections).
74
+ const saved = process.env.PAYMENT_TENANT_MODE;
75
+ process.env.PAYMENT_TENANT_MODE = 'multi';
76
+ try {
77
+ await expect(Coupon.count()).rejects.toMatchObject({ code: TENANT_CONTEXT_MISSING });
78
+ } finally {
79
+ if (saved === undefined) delete process.env.PAYMENT_TENANT_MODE;
80
+ else process.env.PAYMENT_TENANT_MODE = saved;
81
+ }
82
+ });
83
+ });
84
+
85
+ describe('洞 H observability — assertJobObjectTenant emits TENANT_VIOLATION', () => {
86
+ it('a forged cross-tenant job logs a structured violation AND throws', async () => {
87
+ await withTenant(TENANT_A, () => {
88
+ // row belongs to B, job is running under A -> forged
89
+ let thrown: any;
90
+ try {
91
+ assertJobObjectTenant({ instance_did: TENANT_B });
92
+ } catch (err) {
93
+ thrown = err;
94
+ }
95
+ expect(thrown?.code).toBe(TENANT_MISMATCH);
96
+ expect((thrown as any)?.nonRetryable).toBe(true);
97
+ expect(logger.error).toHaveBeenCalledWith(
98
+ 'TENANT_VIOLATION',
99
+ expect.objectContaining({ code: TENANT_MISMATCH, rowTenant: TENANT_B, jobTenant: TENANT_A })
100
+ );
101
+ });
102
+ });
103
+
104
+ it('a same-tenant job neither logs nor throws', async () => {
105
+ await withTenant(TENANT_A, () => {
106
+ expect(() => assertJobObjectTenant({ instance_did: TENANT_A })).not.toThrow();
107
+ expect(logger.error).not.toHaveBeenCalled();
108
+ });
109
+ });
110
+
111
+ it('an absent object is the handler no-op path (no violation)', async () => {
112
+ await withTenant(TENANT_A, () => {
113
+ expect(() => assertJobObjectTenant(null)).not.toThrow();
114
+ expect(logger.error).not.toHaveBeenCalled();
115
+ });
116
+ });
117
+ });
118
+
119
+ describe('system* security — the bypass is explicit-only', () => {
120
+ it('a normal tenant context is NOT a system context', async () => {
121
+ await withTenant(TENANT_A, () => {
122
+ expect(isSystemContext()).toBe(false);
123
+ });
124
+ });
125
+
126
+ it('a normal context query stays scoped (cannot read cross-tenant)', async () => {
127
+ const bRow: any = await withTenant(TENANT_B, () =>
128
+ Coupon.create({ livemode: false, duration: 'once', name: 'b', created_via: 'api' } as any)
129
+ );
130
+ // bare findByPk under A -> scoped -> null (no leak)
131
+ const leaked = await withTenant(TENANT_A, () => Coupon.findByPk(bRow.id));
132
+ expect(leaked).toBeNull();
133
+ });
134
+
135
+ it('only the system* helper enters runAsSystem; it restores afterwards', async () => {
136
+ const bRow: any = await withTenant(TENANT_B, () =>
137
+ Coupon.create({ livemode: false, duration: 'once', name: 'b2', created_via: 'api' } as any)
138
+ );
139
+ await withTenant(TENANT_A, async () => {
140
+ // inside systemFindByPk the bypass is active -> cross-tenant row loads
141
+ const viaSystem = await systemFindByPk(Coupon, bRow.id);
142
+ expect(viaSystem).not.toBeNull();
143
+ // and the flag is restored the moment the helper settles
144
+ expect(isSystemContext()).toBe(false);
145
+ // so a subsequent bare read is scoped again
146
+ const leaked = await Coupon.findByPk(bRow.id);
147
+ expect(leaked).toBeNull();
148
+ });
149
+ });
150
+
151
+ it('runAsSystem is reentrant-safe and does not leak across withTenant siblings', async () => {
152
+ let insideFlag = false;
153
+ await context.runAsSystem(async () => {
154
+ insideFlag = isSystemContext();
155
+ });
156
+ expect(insideFlag).toBe(true);
157
+ // outside the span, flag is gone
158
+ expect(isSystemContext()).toBe(false);
159
+ });
160
+ });
161
+
162
+ describe('洞 F — eager include transitive safety (scoped root => same-tenant children)', () => {
163
+ // Base-class override does not fire for the JOIN itself, but the ROOT query is
164
+ // scoped, and associations join by globally-unique FK ids that belong to one
165
+ // tenant, so children follow the (scoped) root — no cross-tenant child rows.
166
+ const seedPromo = async (tenant: string, name: string) =>
167
+ withTenant(tenant, async () => {
168
+ const coupon: any = await Coupon.create({
169
+ livemode: false,
170
+ duration: 'once',
171
+ name,
172
+ created_via: 'api',
173
+ } as any);
174
+ return PromotionCode.create({
175
+ livemode: false,
176
+ active: true,
177
+ code: `CODE-${name}`,
178
+ coupon_id: coupon.id,
179
+ } as any);
180
+ });
181
+
182
+ it('findAll with include returns only the active tenant rows + their own coupon', async () => {
183
+ await seedPromo(TENANT_A, 'a');
184
+ await seedPromo(TENANT_B, 'b');
185
+
186
+ const aRows: any = await withTenant(TENANT_A, () =>
187
+ PromotionCode.findAll({ include: [{ model: Coupon, as: 'coupon' }] })
188
+ );
189
+ expect(aRows).toHaveLength(1);
190
+ expect(aRows[0].code).toBe('CODE-a');
191
+ expect(aRows[0].instance_did).toBe(TENANT_A);
192
+ // the eager-loaded child belongs to the same tenant (FK-consistent)
193
+ expect(aRows[0].coupon.instance_did).toBe(TENANT_A);
194
+ expect(aRows[0].coupon.name).toBe('a');
195
+ });
196
+ });
package/api/third.d.ts CHANGED
@@ -14,6 +14,10 @@ declare module 'flat';
14
14
 
15
15
  declare module '@abtnode/cron';
16
16
 
17
+ declare module '@abtnode/constant';
18
+
19
+ declare module '@abtnode/util/lib/get-token-from-req';
20
+
17
21
  declare module 'sql-where-parser';
18
22
 
19
23
  declare module 'cls-hooked';
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.29.0
17
+ version: 1.29.2
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -8,7 +8,7 @@
8
8
  Payment Kit Worker
9
9
  ├── D1 Database # 业务数据(customers/invoices/subscriptions ...)
10
10
  ├── KV: DID_CONNECT_KV # DID Connect session 临时存储
11
- ├── CF Queue # 任务队列(fastq CF Queue 适配)
11
+ ├── CF Queue # 可选 executor 绑定;队列引擎是 api/src/libs/queue(node 引擎)
12
12
  ├── Hyperdrive # (可选)连接 Postgres,仅在启用时使用
13
13
  ├── Service Binding
14
14
  │ ├── AUTH_SERVICE → blocklet-service (BlockletServiceRPC)
@@ -20,9 +20,29 @@ Payment Kit Worker
20
20
 
21
21
  - **Hono** 替代 Express(路由 + 中间件适配层)
22
22
  - **Sequelize-D1 shim** 替代 SQLite,表结构自动 sync
23
- - **CF Queue** 替代 fastq,加 D1-native cron fallback
24
23
  - **wrapAsyncListener** 追踪 `EventEmitter` async listener 的 Promise,防止 CF runtime 提前回收
25
24
 
25
+ ### worker 是 host adapter(Phase 12,2026-06)
26
+
27
+ worker **不再自带**一套业务引擎。它只是一个 host adapter:
28
+
29
+ - **业务 `/api` 入口统一经 `createEmbeddedPaymentService({ config, db, slots })`**,worker 只挂载
30
+ `svc.http.resourceRoutes`,DID-Connect 走 worker 自己的 Hono shell。worker **绝不**访问 `svc.handler`
31
+ (那是 node-only Express app 壳)——`ensurePaymentService` 用 Proxy 把 `.handler` 访问 trap 成 hard error。
32
+ - **队列只有一套引擎** = `api/src/libs/queue`(node 引擎)。worker 不再用已删除的 `shims/queue.ts`。host 只替换
33
+ trigger / executor / flush:executor = `shims/fastq`,trigger = `scheduled()` 调 `dispatchDueJobs()`,
34
+ flush = `flushQueueWork()`,全部经 `api/src/libs/queue/runtime.ts` 这个 host-facing surface 驱动。
35
+ workerd 模式下 node 引擎的后台 `loop()` 被禁用(冻结的 isolate 不能跑后台 timer)。
36
+ - **config 经 config slot 授权**:`envToPaymentCoreConfig(env)` → `setCoreConfig`,HTTP 与 scheduled/queue 两条
37
+ 路径都不再有 `process.env` mirror。
38
+ - **构建**:**`run-build.js` 是 standalone 的 CF deploy build**(`node run-build.js` → `wrangler deploy`),
39
+ 带全部 bundle 优化(stripe-cf、axios-lite、native fetch、cbor-only、ethers wordlists drop、noop packages、
40
+ node builtin shims;见 `docs/2026-06-10-bundle-size-analysis.md`,gzip ~1 MiB,实测部署 + 支付路由验证过)。
41
+ Phase 12b/12c 只从中移除了两个已死的 plugin:`queue-shim`(option A:队列引擎统一为 `api/src/libs/queue`,
42
+ `shims/queue.ts` 已删)和 `lock-shim`(lock 已改成 Phase 8 driver,`shims/lock.ts` 已删)。
43
+ **`build.ts` 是 diagnostic/backup 构建**(`npx tsx build.ts`,arc-integration 诊断用)——它**不**带上述优化、
44
+ 用不同的 node-builtin 策略,**不是** deploy build,部署一律用 `run-build.js`。
45
+
26
46
  ---
27
47
 
28
48
  ## ⚠️ 必须先部署的前置服务
@@ -465,8 +485,9 @@ wrangler d1 execute payment-kit-prod --remote \
465
485
  ```
466
486
  blocklets/core/cloudflare/
467
487
  ├── worker.ts # Worker 入口(Hono 路由 + Express 适配)
468
- ├── run-build.js # esbuild 构建脚本
469
- ├── build.ts # 构建配置 + shim 别名映射
488
+ ├── run-build.js # standalone CF deploy build(esbuild + 全部 bundle 优化)← 部署用这个
489
+ ├── build.ts # diagnostic/backup 构建(更少优化;不是 deploy build)
490
+ ├── queue-runtime-mode.ts # 把 core 队列引擎 pin 成 workerd(在业务队列加载前)
470
491
  ├── vite.config.ts # 前端 Vite 构建配置
471
492
  ├── did-connect-auth.ts # DID Connect 登录路由(挂载需要 APP_SK + DID_CONNECT_KV)
472
493
  ├── wrangler.jsonc # 生产配置模板
@@ -480,8 +501,7 @@ blocklets/core/cloudflare/
480
501
  │ │ ├── verify-sign.ts # 组件签名验证(委托 AUTH_SERVICE)
481
502
  │ │ └── ...
482
503
  │ ├── sequelize-d1/ # Sequelize → D1 适配
483
- │ ├── queue.ts # fastq CF Queue 适配
484
- │ ├── lock.ts # 分布式锁(D1 实现)
504
+ │ ├── fastq.ts # fastq executor 替身(队列引擎仍是 api/src/libs/queue)
485
505
  │ ├── cron.ts # 定时任务
486
506
  │ └── ...
487
507
  └── test-aigne-hub/ # Gateway 集成参考实现