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
@@ -1,13 +1,29 @@
1
1
  import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
2
2
 
3
+ import {
4
+ TENANT_CONTEXT_MISSING,
5
+ TenantError,
6
+ assertValidInstanceDid,
7
+ getDefaultInstanceDid,
8
+ getTenantMode,
9
+ } from './tenant';
10
+
11
+ export * from './tenant';
12
+
3
13
  interface RequestContext {
4
14
  requestedBy?: string;
5
15
  requestId?: string;
16
+ instanceDid?: string;
6
17
  }
7
18
 
8
19
  class RequestContextManager {
9
20
  private storage = new AsyncLocalStorage<RequestContext>();
10
21
  private contexts = new Map<string, RequestContext>();
22
+ // System-operation flag: when set, TenantModel bypasses tenant scoping so a
23
+ // legitimate cross-tenant read (queue dispatch, IAP bundle->tenant reverse
24
+ // lookup, event fan-out) can load rows across tenants. Entered ONLY via the
25
+ // system* helpers — a normal route context can never read across tenants.
26
+ private systemStorage = new AsyncLocalStorage<boolean>();
11
27
 
12
28
  getContext(requestId?: string): RequestContext {
13
29
  if (requestId && this.contexts.has(requestId)) {
@@ -20,16 +36,62 @@ class RequestContextManager {
20
36
  return this.getContext(requestId).requestedBy;
21
37
  }
22
38
 
39
+ /**
40
+ * Run fn with the given tenant. Nested calls shadow the outer tenant and restore it on exit.
41
+ * Other context fields (requestId, requestedBy) are inherited from the enclosing scope.
42
+ */
43
+ withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
44
+ try {
45
+ assertValidInstanceDid(instanceDid);
46
+ } catch (err) {
47
+ return Promise.reject(err);
48
+ }
49
+ const parent = this.storage.getStore();
50
+ // frozen so fn cannot mutate the stored context and affect sibling calls
51
+ const next = Object.freeze({ ...parent, instanceDid });
52
+ return this.storage.run(next, () => Promise.resolve(fn()));
53
+ }
54
+
55
+ /**
56
+ * Current tenant. Single mode falls back to the deployment app DID;
57
+ * multi mode fails closed with TENANT_CONTEXT_MISSING.
58
+ */
59
+ getInstanceDid(): string {
60
+ const instanceDid = this.storage.getStore()?.instanceDid;
61
+ if (instanceDid) return instanceDid;
62
+ if (getTenantMode() === 'single') return getDefaultInstanceDid();
63
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'tenant context is missing in multi-tenant mode');
64
+ }
65
+
66
+ /**
67
+ * Run fn as a system operation: TenantModel scoping is bypassed for the span
68
+ * of fn so legitimate cross-tenant reads can load rows regardless of tenant.
69
+ * The scope ends when fn settles — callers must enforce the row's tenant
70
+ * themselves (e.g. assertJobObjectTenant). Explicit by construction: only the
71
+ * system* helpers enter this, so a normal route can never read across tenants.
72
+ */
73
+ runAsSystem<T>(fn: () => Promise<T> | T): Promise<T> {
74
+ return this.systemStorage.run(true, () => Promise.resolve(fn()));
75
+ }
76
+
77
+ /** True inside a runAsSystem span — TenantModel checks this to skip scoping. */
78
+ isSystem(): boolean {
79
+ return this.systemStorage.getStore() === true;
80
+ }
81
+
23
82
  run<T>(context: RequestContext, fn: () => Promise<T> | T): Promise<T> {
24
83
  const requestId = context.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
84
+ // inherit tenant from the enclosing scope unless explicitly provided
85
+ const instanceDid = context.instanceDid ?? this.storage.getStore()?.instanceDid;
25
86
 
26
87
  this.contexts.set(requestId, {
27
88
  ...context,
89
+ instanceDid,
28
90
  requestId,
29
91
  });
30
92
 
31
93
  return new Promise((resolve, reject) => {
32
- this.storage.run({ ...context, requestId }, async () => {
94
+ this.storage.run({ ...context, instanceDid, requestId }, async () => {
33
95
  const resource = new AsyncResource('RequestContext');
34
96
  try {
35
97
  const result = await resource.runInAsyncScope(fn);
@@ -46,3 +108,18 @@ class RequestContextManager {
46
108
  }
47
109
 
48
110
  export const context = new RequestContextManager();
111
+
112
+ /** Run fn with the given tenant — also the test injection helper for jest. */
113
+ export function withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
114
+ return context.withTenant(instanceDid, fn);
115
+ }
116
+
117
+ /** Current tenant; see RequestContextManager#getInstanceDid for mode semantics. */
118
+ export function getInstanceDid(): string {
119
+ return context.getInstanceDid();
120
+ }
121
+
122
+ /** True inside a runAsSystem span; TenantModel uses it to bypass scoping. */
123
+ export function isSystemContext(): boolean {
124
+ return context.isSystem();
125
+ }
@@ -1,8 +1,8 @@
1
1
  import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
- import { getUrl } from '@blocklet/sdk';
2
+ import { getUrl } from '@blocklet/sdk/lib/component';
3
3
  import { PaymentCurrency, Price, Product, RechargeConfig } from '../store/models';
4
4
  import { trimDecimals } from './math-utils';
5
- import { createPaymentLink } from '../routes/payment-links';
5
+ import { createPaymentLink } from '../routes/hono/payment-links';
6
6
  import logger from './logger';
7
7
 
8
8
  export async function formatCurrencyToken(amount: string, currencyId: string) {
@@ -5,8 +5,14 @@ import relativeTime from 'dayjs/plugin/relativeTime';
5
5
  import timezone from 'dayjs/plugin/timezone'; // dependent on utc plugin
6
6
  import utc from 'dayjs/plugin/utc';
7
7
 
8
- import('dayjs/locale/en');
9
- import('dayjs/locale/zh');
8
+ // Use explicit `.js` so the dynamic import resolves under Node ESM (strict
9
+ // resolution: dayjs ships no `exports` map, so the extensionless subpath
10
+ // `dayjs/locale/en` fails when payment-core is embedded in a Node ESM host
11
+ // like arc). Webpack/blocklet-server tolerate the extension too.
12
+ // eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
13
+ import('dayjs/locale/en.js');
14
+ // eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
15
+ import('dayjs/locale/zh.js');
10
16
 
11
17
  dayjs.extend(relativeTime);
12
18
  dayjs.extend(localizedFormat);
@@ -0,0 +1,118 @@
1
+ // Phase 8 (W2-1a): DID Connect token storage on the db driver.
2
+ //
3
+ // In Blocklet Server the real @arcblock/did-connect-storage-nedb (file-backed)
4
+ // is used unchanged. In the CF worker that package was aliased to a NO-OP stub
5
+ // (cloudflare/shims/nedb-storage.ts) — DID Connect state was silently dropped.
6
+ // This is the from-scratch persistent implementation on top of the db driver
7
+ // contract, so the same code runs on the node (sqlite) and d1 backends and is
8
+ // covered by the driver consistency suite.
9
+ //
10
+ // Records are flexible JSON documents keyed by `token` (the DID-Auth session
11
+ // token, globally unique — no instance_did column needed; isolation is by the
12
+ // unguessable token, matching the original file store's scope).
13
+
14
+ import type { DbDriver } from './db';
15
+
16
+ const { EventEmitter } = require('events');
17
+
18
+ const TABLE = 'did_auth_records';
19
+
20
+ export interface AuthRecord {
21
+ token: string;
22
+ [key: string]: any;
23
+ }
24
+
25
+ export class DbAuthStorage extends EventEmitter {
26
+ private driver: DbDriver;
27
+ private ready: Promise<void> | null = null;
28
+
29
+ constructor(driver: DbDriver) {
30
+ super();
31
+ if (!driver) throw new Error('DbAuthStorage requires a db driver');
32
+ this.driver = driver;
33
+ }
34
+
35
+ // idempotent lazy schema — the original file store self-managed its db file
36
+ // (it was never part of the Umzug/D1 migration chain), so we keep that here.
37
+ private ensureTable(): Promise<void> {
38
+ if (!this.ready) {
39
+ this.ready = this.driver
40
+ .exec(
41
+ `CREATE TABLE IF NOT EXISTS ${TABLE} (` +
42
+ 'token TEXT PRIMARY KEY, ' +
43
+ 'doc TEXT NOT NULL, ' +
44
+ 'created_at INTEGER NOT NULL, ' +
45
+ 'updated_at INTEGER NOT NULL)'
46
+ )
47
+ .then(() => undefined)
48
+ .catch((err) => {
49
+ // reset so a transient failure can be retried on next call
50
+ this.ready = null;
51
+ throw err;
52
+ });
53
+ }
54
+ return this.ready;
55
+ }
56
+
57
+ async read(token: string): Promise<AuthRecord | null> {
58
+ if (!token) throw new Error('token is required to read auth record');
59
+ await this.ensureTable();
60
+ const row = await this.driver.get<{ doc: string }>(`SELECT doc FROM ${TABLE} WHERE token = ?`, [token]);
61
+ if (!row) return null;
62
+ return JSON.parse(row.doc) as AuthRecord;
63
+ }
64
+
65
+ async create(token: string, status = 'created'): Promise<AuthRecord> {
66
+ if (!token) throw new Error('token is required to create auth record');
67
+ await this.ensureTable();
68
+ const now = Date.now();
69
+ const doc: AuthRecord = { token, status, createdAt: now, updatedAt: now };
70
+ await this.driver.exec(`INSERT INTO ${TABLE} (token, doc, created_at, updated_at) VALUES (?, ?, ?, ?)`, [
71
+ token,
72
+ JSON.stringify(doc),
73
+ now,
74
+ now,
75
+ ]);
76
+ this.emit('create', doc);
77
+ return doc;
78
+ }
79
+
80
+ async update(token: string, updates: Record<string, any> = {}): Promise<AuthRecord | null> {
81
+ if (!token) throw new Error('token is required to update auth record');
82
+ await this.ensureTable();
83
+ const current = await this.read(token);
84
+ if (!current) return null;
85
+ const now = Date.now();
86
+ const merged: AuthRecord = { ...current, ...updates, token, updatedAt: now };
87
+ await this.driver.exec(`UPDATE ${TABLE} SET doc = ?, updated_at = ? WHERE token = ?`, [
88
+ JSON.stringify(merged),
89
+ now,
90
+ token,
91
+ ]);
92
+ this.emit('update', merged);
93
+ return merged;
94
+ }
95
+
96
+ async delete(token: string): Promise<number> {
97
+ if (!token) throw new Error('token is required to delete auth record');
98
+ await this.ensureTable();
99
+ const res = await this.driver.exec(`DELETE FROM ${TABLE} WHERE token = ?`, [token]);
100
+ this.emit('destroy', token);
101
+ return res.changes;
102
+ }
103
+
104
+ async exist(token: string, did: any): Promise<boolean> {
105
+ if (!token) throw new Error('token is required to check auth record');
106
+ const doc = await this.read(token);
107
+ return !!doc && doc.did === did;
108
+ }
109
+
110
+ async clear(): Promise<void> {
111
+ await this.ensureTable();
112
+ await this.driver.exec(`DELETE FROM ${TABLE}`);
113
+ }
114
+ }
115
+
116
+ export function createAuthStorage(driver: DbDriver): DbAuthStorage {
117
+ return new DbAuthStorage(driver);
118
+ }
@@ -0,0 +1,264 @@
1
+ // Phase 9 (W2-1b): cron slot driver contract.
2
+ //
3
+ // The cron contract is: register jobs + a due-poll dispatch entry. The HOST
4
+ // provides the trigger — CF `scheduled()` calls runDue() every minute; the Node
5
+ // host uses @abtnode/cron's own scheduler. Both share ONE cron-expression
6
+ // matcher (previously duplicated in cloudflare/shims/cron.ts), so embedded and
7
+ // worker agree on exactly when a job is due.
8
+
9
+ export interface CronJob {
10
+ name: string;
11
+ time: string;
12
+ fn: () => Promise<any> | any;
13
+ /**
14
+ * runOnInit: honored by the node host scheduler (@abtnode/cron) which runs
15
+ * the job once at registration. The shared registry's runDue (used by the
16
+ * cf-cron host) does NOT auto-run it — CF deliberately skips runOnInit and
17
+ * lets the next matching trigger fire it (running at module-init would block
18
+ * the request / risk CPU limits). It is a host-trigger concern, not a matcher
19
+ * concern.
20
+ */
21
+ options?: { runOnInit?: boolean };
22
+ }
23
+
24
+ export interface CronDriver {
25
+ kind: 'node-cron' | 'cf-cron';
26
+ /** register jobs (idempotent: clears + re-adds) */
27
+ register(jobs: CronJob[], onError?: (err: Error, name: string) => void): void;
28
+ /** add a single job after init */
29
+ addJob(name: string, time: string, fn: CronJob['fn'], options?: CronJob['options']): void;
30
+ /** due-poll dispatch entry — runs every job whose schedule matches `now` */
31
+ runDue(now?: Date): Promise<{ ran: string[]; skipped: string[] }>;
32
+ /** run one named job regardless of schedule (manual trigger) */
33
+ runJob(name: string): Promise<void>;
34
+ /** registered job descriptions (name + schedule) */
35
+ getJobNames(): string[];
36
+ /**
37
+ * D2 teardown surface. Stop all live timers (the node-cron driver tears down
38
+ * its @abtnode/cron scheduler so the process has no dangling cron timer); the
39
+ * registry is preserved so a later start() can re-register. No-op for the
40
+ * passive cf-cron driver (it never owns a timer — the host drives runDue).
41
+ */
42
+ stop(): void;
43
+ /** stop + clear the registry (a full reset; start() re-registers from scratch). */
44
+ dispose(): void;
45
+ }
46
+
47
+ // @abtnode/cron's CronScheduler shape we drive (lib/scheduler.js): addJob wires
48
+ // a self-scheduling `cron` CronJob (start=true) and exposes the live jobs map.
49
+ interface AbtnodeCronScheduler {
50
+ jobs: Record<string, { start(): void; stop(): void }>;
51
+ addJob(name: string, time: string, fn: (...args: any[]) => any, options?: Record<string, any>): unknown;
52
+ }
53
+
54
+ // --- shared cron-expression matcher ---
55
+ // 6-field: second minute hour dayOfMonth month dayOfWeek (seconds ignored —
56
+ // CF triggers are minute-level). Supports numbers, *, */N, ranges, lists.
57
+
58
+ function parseField(field: string, min: number, max: number): number[] | null {
59
+ // null means "match all"
60
+ if (field === '*') return null;
61
+
62
+ const values = new Set<number>();
63
+
64
+ for (const part of field.split(',')) {
65
+ const stepMatch = part.match(/^\*\/(\d+)$/);
66
+ const rangeMatch = part.match(/^(\d+)-(\d+)$/);
67
+ if (stepMatch) {
68
+ const step = parseInt(stepMatch[1]!, 10);
69
+ for (let i = min; i <= max; i += step) values.add(i);
70
+ } else if (rangeMatch) {
71
+ const from = parseInt(rangeMatch[1]!, 10);
72
+ const to = parseInt(rangeMatch[2]!, 10);
73
+ for (let i = from; i <= to; i += 1) values.add(i);
74
+ } else {
75
+ const num = parseInt(part, 10);
76
+ if (!Number.isNaN(num)) values.add(num);
77
+ }
78
+ }
79
+
80
+ return values.size > 0 ? Array.from(values) : null;
81
+ }
82
+
83
+ /**
84
+ * Whether `date` matches a 5- or 6-field cron expression (minute granularity).
85
+ */
86
+ export function matchesCron(cronExpr: string, date: Date): boolean {
87
+ const fields = cronExpr.trim().split(/\s+/);
88
+ if (fields.length < 5) return true; // can't parse — run it
89
+
90
+ const offset = fields.length >= 6 ? 1 : 0;
91
+ // length >= 5 guaranteed above, so offset..offset+4 are present
92
+ const minuteField = parseField(fields[offset]!, 0, 59);
93
+ const hourField = parseField(fields[offset + 1]!, 0, 23);
94
+ const domField = parseField(fields[offset + 2]!, 1, 31);
95
+ // cron months are 1-12 (matched against getUTCMonth()+1); a 0-11 range here
96
+ // made step patterns like */3 generate {0,3,6,9} and never match real months.
97
+ const monthField = parseField(fields[offset + 3]!, 1, 12);
98
+ const dowField = parseField(fields[offset + 4]!, 0, 6);
99
+
100
+ const m = date.getUTCMinutes();
101
+ const h = date.getUTCHours();
102
+ const dom = date.getUTCDate();
103
+ const month = date.getUTCMonth() + 1; // JS 0-based → cron 1-based
104
+ const dow = date.getUTCDay();
105
+
106
+ if (minuteField && !minuteField.includes(m)) return false;
107
+ if (hourField && !hourField.includes(h)) return false;
108
+ if (domField && !domField.includes(dom)) return false;
109
+ if (monthField && !monthField.includes(month)) return false;
110
+ if (dowField && !dowField.includes(dow)) return false;
111
+
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Whether a cron expression should fire at `date`. Minute-level match — the
117
+ * trigger source (CF scheduled / node cron) fires every minute, so each
118
+ * expression triggers at its designed frequency (see the 2026-04-17 incident
119
+ * note in the prior cloudflare/shims/cron.ts history).
120
+ */
121
+ export function shouldRunInWindow(cronExpr: string, date: Date): boolean {
122
+ return matchesCron(cronExpr, date);
123
+ }
124
+
125
+ /**
126
+ * A cron registry implementing the due-poll dispatch entry on top of the shared
127
+ * matcher. Both the node-cron and cf-cron drivers are built from this — the
128
+ * only difference is who calls runDue (node scheduler vs CF scheduled()).
129
+ */
130
+ export function createCronRegistry(kind: CronDriver['kind']): CronDriver {
131
+ const jobs: CronJob[] = [];
132
+ let onErrorHandler: ((err: Error, name: string) => void) | undefined;
133
+
134
+ // node-cron self-schedules through @abtnode/cron; cf-cron stays a passive
135
+ // matcher registry whose runDue() is driven by the host's scheduled().
136
+ const selfSchedule = kind === 'node-cron';
137
+ let scheduler: AbtnodeCronScheduler | null = null;
138
+
139
+ // Tear down every live @abtnode/cron timer (each scheduler.jobs[*] is a `cron`
140
+ // CronJob with its own setTimeout). Clears the scheduler so the process has no
141
+ // dangling cron handle and a later start() rebuilds from scratch.
142
+ const stopScheduler = (): void => {
143
+ if (!scheduler) return;
144
+ for (const job of Object.values(scheduler.jobs)) {
145
+ try {
146
+ job.stop();
147
+ } catch {
148
+ /* a job already stopped — ignore */
149
+ }
150
+ }
151
+ scheduler = null;
152
+ };
153
+
154
+ // (Re)build the live scheduler from the current jobs array. Always stops the
155
+ // previous scheduler first so register()/start() is idempotent: no double
156
+ // timers, no double registration. Lazy-requires @abtnode/cron so the cf-cron
157
+ // path and pure imports never load the Node scheduler.
158
+ const startScheduler = (): void => {
159
+ stopScheduler();
160
+ if (!selfSchedule || jobs.length === 0) return;
161
+ // eslint-disable-next-line global-require
162
+ const mod = require('@abtnode/cron');
163
+ // real package (CJS): module.exports = { init }; the CF shim (ESM default)
164
+ // resolves to { default: { init } } under the bundler interop.
165
+ type AbtnodeInit = (params: {
166
+ context: any;
167
+ jobs: any[];
168
+ onError: (e: Error, n: string) => void;
169
+ }) => AbtnodeCronScheduler;
170
+ const init: AbtnodeInit = mod.init ?? mod.default?.init;
171
+ // D2/multi: skip runOnInit in multi-tenant mode — same as the CF host
172
+ // (cloudflare/shims/cron.ts). A runOnInit job fires immediately at register,
173
+ // before any request, so it has NO tenant context; in multi mode that makes
174
+ // a tenant-requiring startup push (e.g. deposit.vault / credit.consumption)
175
+ // throw TENANT_CONTEXT_MISSING and abort lifecycle.start(). The job still
176
+ // runs on its next scheduled tick (where the handler resolves its own
177
+ // tenant). Single mode keeps runOnInit (the default tenant is always present).
178
+ // eslint-disable-next-line global-require
179
+ const { getTenantMode } = require('../tenant');
180
+ const isMulti = getTenantMode() === 'multi';
181
+ scheduler = init({
182
+ context: {},
183
+ jobs: jobs.map((j) => ({
184
+ name: j.name,
185
+ time: j.time,
186
+ fn: j.fn,
187
+ options: isMulti ? { ...(j.options || {}), runOnInit: false } : j.options || {},
188
+ })),
189
+ onError: onErrorHandler ?? (() => {}),
190
+ });
191
+ };
192
+
193
+ return {
194
+ kind,
195
+ register(next, onError) {
196
+ jobs.length = 0;
197
+ onErrorHandler = onError;
198
+ for (const job of next || []) {
199
+ if (job.name && job.time && typeof job.fn === 'function') jobs.push(job);
200
+ }
201
+ startScheduler();
202
+ },
203
+ addJob(name, time, fn, options) {
204
+ jobs.push({ name, time, fn, options });
205
+ // keep the live scheduler in sync when one is already running
206
+ if (selfSchedule && scheduler) {
207
+ try {
208
+ scheduler.addJob(name, time, fn, options || {});
209
+ } catch (err: any) {
210
+ onErrorHandler?.(err, name);
211
+ }
212
+ }
213
+ },
214
+ stop() {
215
+ stopScheduler();
216
+ },
217
+ dispose() {
218
+ stopScheduler();
219
+ jobs.length = 0;
220
+ },
221
+ async runDue(now = new Date()) {
222
+ const ran: string[] = [];
223
+ const skipped: string[] = [];
224
+ for (const job of jobs) {
225
+ if (shouldRunInWindow(job.time, now)) {
226
+ ran.push(job.name);
227
+ try {
228
+ // eslint-disable-next-line no-await-in-loop -- jobs run sequentially within a tick
229
+ await job.fn();
230
+ } catch (err: any) {
231
+ onErrorHandler?.(err, job.name);
232
+ }
233
+ } else {
234
+ skipped.push(job.name);
235
+ }
236
+ }
237
+ return { ran, skipped };
238
+ },
239
+ async runJob(name) {
240
+ const job = jobs.find((j) => j.name === name);
241
+ if (!job) return;
242
+ try {
243
+ await job.fn();
244
+ } catch (err: any) {
245
+ onErrorHandler?.(err, name);
246
+ }
247
+ },
248
+ getJobNames() {
249
+ return jobs.map((j) => `${j.name} (${j.time})`);
250
+ },
251
+ };
252
+ }
253
+
254
+ // Active cron driver — injectable by the factory's `cron` slot; defaults to a
255
+ // node-cron registry. The worker shell injects the cf-cron driver.
256
+ let activeCronDriver: CronDriver = createCronRegistry('node-cron');
257
+
258
+ export function setCronDriver(driver: CronDriver): void {
259
+ activeCronDriver = driver;
260
+ }
261
+
262
+ export function getCronDriver(): CronDriver {
263
+ return activeCronDriver;
264
+ }
@@ -0,0 +1,170 @@
1
+ // Phase 8 (W2-1a): db slot driver contract.
2
+ //
3
+ // The db slot carries the SQL layer. Two implementations conform to the same
4
+ // contract:
5
+ // - node driver: wraps the existing Sequelize instance (raw helpers go
6
+ // through `sequelize.query`); `sequelize` is exposed so the factory can
7
+ // bind models to it (unchanged from Phase 7).
8
+ // - d1 driver: wraps a Cloudflare D1 binding (`prepare().bind().run()/all()/
9
+ // first()`), the worker-side implementation that `cloudflare/shims/
10
+ // sequelize-d1` is the model layer for. This phase formalizes the
11
+ // D1Binding contract beneath that model layer; physically relocating the
12
+ // sequelize-d1 files out of cloudflare/shims is part of the Phase 12 §3.1
13
+ // shim cleanup (the small, build-orphan shims — lock.ts, nedb-storage — are
14
+ // handled here since they carried no build-alias risk).
15
+ //
16
+ // The minimal raw surface (`exec/all/get`) is what tenant-agnostic
17
+ // infrastructure (e.g. the did-connect AuthStorage) builds on so a single
18
+ // implementation runs identically on both backends — see auth-storage.ts and
19
+ // the driver consistency suite.
20
+
21
+ export type DbDriverKind = 'node' | 'd1';
22
+
23
+ export interface DbExecResult {
24
+ /** rows affected by an INSERT/UPDATE/DELETE */
25
+ changes: number;
26
+ }
27
+
28
+ /** a single statement in a batch (the shared transactional primitive) */
29
+ export interface DbBatchOp {
30
+ sql: string;
31
+ params?: any[];
32
+ }
33
+
34
+ export interface DbDriver {
35
+ kind: DbDriverKind;
36
+ /** run a write statement; returns affected row count */
37
+ exec(sql: string, params?: any[]): Promise<DbExecResult>;
38
+ /** run a SELECT; returns all rows */
39
+ all<T = any>(sql: string, params?: any[]): Promise<T[]>;
40
+ /** run a SELECT; returns the first row or null */
41
+ get<T = any>(sql: string, params?: any[]): Promise<T | null>;
42
+ /**
43
+ * Run statements atomically. This is the contract's transactional primitive:
44
+ * D1 offers no interactive transactions, only batch-level atomicity, so the
45
+ * shared surface is "all-or-nothing batch" rather than an interactive
46
+ * transaction(fn). Any statement failing rolls the whole batch back. The node
47
+ * driver wraps a real Sequelize transaction; the d1 driver uses binding.batch.
48
+ */
49
+ batch(ops: DbBatchOp[]): Promise<any[]>;
50
+ /**
51
+ * The ORM instance, when the backend is Sequelize-based. Present on the node
52
+ * driver (model binding target); absent on the raw d1 binding driver.
53
+ */
54
+ sequelize?: any;
55
+ }
56
+
57
+ /** D1 binding shape (subset actually used). */
58
+ export interface D1Binding {
59
+ prepare(sql: string): {
60
+ bind(...params: any[]): {
61
+ run(): Promise<{ success?: boolean; meta?: { changes?: number; rows_written?: number } }>;
62
+ all(): Promise<{ results?: any[] }>;
63
+ first(): Promise<any | null>;
64
+ };
65
+ };
66
+ batch(stmts: any[]): Promise<any[]>;
67
+ }
68
+
69
+ /**
70
+ * Node driver — wraps an existing Sequelize instance. Raw helpers route through
71
+ * `sequelize.query` with bind replacements (`$1, $2, ...` style); no string
72
+ * interpolation of values, matching the d1 driver's parameter binding.
73
+ */
74
+ export function createNodeDbDriver(sequelize: any): DbDriver {
75
+ if (!sequelize) throw new Error('createNodeDbDriver: sequelize instance is required');
76
+ // Sequelize positional bind uses `$1`-style markers; callers pass `?` which we
77
+ // translate so the same SQL string works on both drivers.
78
+ const toBind = (sql: string) => {
79
+ let i = 0;
80
+ return sql.replace(/\?/g, () => {
81
+ i += 1;
82
+ return `$${i}`;
83
+ });
84
+ };
85
+ return {
86
+ kind: 'node',
87
+ sequelize,
88
+ async exec(sql, params = []) {
89
+ const [, meta] = await sequelize.query(toBind(sql), { bind: params });
90
+ // sqlite returns affected rows on meta.changes; fall back to 0
91
+ const changes = (meta && (meta.changes ?? meta.rowCount)) ?? 0;
92
+ return { changes };
93
+ },
94
+ async all(sql, params = []) {
95
+ const rows = await sequelize.query(toBind(sql), {
96
+ bind: params,
97
+ type: sequelize.QueryTypes ? sequelize.QueryTypes.SELECT : 'SELECT',
98
+ });
99
+ return rows as any[];
100
+ },
101
+ async get(sql, params = []) {
102
+ const rows = await sequelize.query(toBind(sql), {
103
+ bind: params,
104
+ type: sequelize.QueryTypes ? sequelize.QueryTypes.SELECT : 'SELECT',
105
+ });
106
+ return (rows as any[])[0] ?? null;
107
+ },
108
+ // eslint-disable-next-line require-await -- async contract; returns the transaction promise
109
+ async batch(ops) {
110
+ // real Sequelize transaction — any statement throwing rolls everything back
111
+ return sequelize.transaction(async (t: any) => {
112
+ const results: any[] = [];
113
+ for (const op of ops) {
114
+ // eslint-disable-next-line no-await-in-loop -- ordered within one transaction
115
+ const rows = await sequelize.query(toBind(op.sql), { bind: op.params ?? [], transaction: t });
116
+ results.push(rows);
117
+ }
118
+ return results;
119
+ });
120
+ },
121
+ };
122
+ }
123
+
124
+ /**
125
+ * D1 driver — wraps a Cloudflare D1 binding. This is the worker-side db driver;
126
+ * the sequelize-d1 model layer is built on the same binding. Accepts the
127
+ * binding directly or a lazy getter (the worker resolves the binding per
128
+ * request, so it may be absent at construction time).
129
+ */
130
+ export function createD1DbDriver(binding: D1Binding | (() => D1Binding)): DbDriver {
131
+ if (!binding) throw new Error('createD1DbDriver: D1 binding (or getter) is required');
132
+ const resolve = (): D1Binding => {
133
+ const b = typeof binding === 'function' ? (binding as () => D1Binding)() : binding;
134
+ if (!b) throw new Error('createD1DbDriver: D1 binding is not available');
135
+ return b;
136
+ };
137
+ return {
138
+ kind: 'd1',
139
+ async exec(sql, params = []) {
140
+ const res = await resolve()
141
+ .prepare(sql)
142
+ .bind(...params)
143
+ .run();
144
+ const changes = res?.meta?.changes ?? res?.meta?.rows_written ?? 0;
145
+ return { changes };
146
+ },
147
+ async all(sql, params = []) {
148
+ const res = await resolve()
149
+ .prepare(sql)
150
+ .bind(...params)
151
+ .all();
152
+ return (res?.results ?? []) as any[];
153
+ },
154
+ async get(sql, params = []) {
155
+ const row = await resolve()
156
+ .prepare(sql)
157
+ .bind(...params)
158
+ .first();
159
+ return (row ?? null) as any;
160
+ },
161
+ // eslint-disable-next-line require-await -- async contract; resolve() throws surface as rejection
162
+ async batch(ops) {
163
+ const b = resolve();
164
+ // D1 batch is atomic (implicitly transactional) — a failing statement
165
+ // rolls the batch back, matching the node driver's transaction semantics
166
+ const stmts = ops.map((op) => b.prepare(op.sql).bind(...(op.params ?? [])));
167
+ return b.batch(stmts);
168
+ },
169
+ };
170
+ }