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,81 @@
1
+ // Phase 10 (W2-2): identity slot driver contract.
2
+ //
3
+ // The identity slot resolves a request Host to a tenant (instanceDid) and, from
4
+ // Phase 11, supplies per-tenant encryption keys. It mirrors the relevant slice
5
+ // of BlockletServiceRPCInterface (resolveInstanceDidForHost). The CF identity
6
+ // shims (blocklet-sdk/{auth-service,session,verify-session,verify-sign}) sit
7
+ // behind this contract.
8
+ //
9
+ // Resolution is single-point: ONLY the tenant-resolving middleware calls
10
+ // resolveInstanceDidForHost (the scanner enforces that no route/queue/cron
11
+ // reads Host directly). In single-tenant mode the default driver ignores the
12
+ // host and returns the deployment app DID, so Blocklet Server behavior is
13
+ // unchanged; multi-tenant hosts inject a driver that maps Host -> instanceDid
14
+ // and returns null for unknown hosts (the middleware then fails closed 4xx).
15
+
16
+ import { getDefaultInstanceDid, getTenantMode, TenantError, TENANT_HOST_UNRESOLVED } from '../tenant';
17
+
18
+ export interface IdentityDriver {
19
+ /**
20
+ * Resolve a request Host to its tenant instanceDid, or null/undefined when
21
+ * the host is unknown (multi-tenant fail-closed). The host value is the raw
22
+ * Host header — never a proxy header.
23
+ */
24
+ resolveInstanceDidForHost(host: string | undefined): Promise<string | null> | string | null;
25
+ /**
26
+ * Phase 11: the tenant's encryption key (EK). Required by the keyring secrets
27
+ * driver; optional on the contract because the single-tenant default secrets
28
+ * driver uses the process key and never calls it.
29
+ */
30
+ getAppEk?(instanceDid: string): Promise<string> | string;
31
+ }
32
+
33
+ /**
34
+ * Single-tenant default: every host maps to the deployment app DID. Used by
35
+ * Blocklet Server and the standalone worker's single-mode transition.
36
+ */
37
+ export function createDefaultIdentityDriver(): IdentityDriver {
38
+ return {
39
+ resolveInstanceDidForHost() {
40
+ return getDefaultInstanceDid();
41
+ },
42
+ };
43
+ }
44
+
45
+ let activeIdentityDriver: IdentityDriver = createDefaultIdentityDriver();
46
+
47
+ /** Inject the identity driver (multi-tenant hosts wire a Host->tenant map here). */
48
+ export function setIdentityDriver(driver: IdentityDriver): void {
49
+ activeIdentityDriver = driver;
50
+ }
51
+
52
+ export function getIdentityDriver(): IdentityDriver {
53
+ return activeIdentityDriver;
54
+ }
55
+
56
+ /**
57
+ * Resolve a request's raw Host header to a tenant instanceDid. SINGLE POINT of
58
+ * Host -> tenant resolution — both host adapters funnel through here (the
59
+ * Express `contextMiddleware` and the CF worker `tenantMiddleware`), so neither
60
+ * reinvents Host parsing (the tenant-scan rule forbids any route/queue/cron
61
+ * from reading Host directly).
62
+ *
63
+ * single mode: always the deployment app DID (Blocklet Server / standalone
64
+ * worker unchanged). multi mode: the identity driver maps Host -> instanceDid;
65
+ * an unknown/missing host throws TENANT_HOST_UNRESOLVED so the caller fails
66
+ * closed 4xx with no default-tenant fallback.
67
+ *
68
+ * The host MUST be the raw Host header — never a proxy header
69
+ * (X-Forwarded-Host / X-Real-IP). The runtime host (CF route / blocklet server /
70
+ * reverse proxy) is responsible for ensuring it cannot be forged by the client.
71
+ */
72
+ export async function resolveTenantForHost(host: string | undefined): Promise<string> {
73
+ if (getTenantMode() === 'single') {
74
+ return getDefaultInstanceDid();
75
+ }
76
+ const resolved = await activeIdentityDriver.resolveInstanceDidForHost(host);
77
+ if (!resolved) {
78
+ throw new TenantError(TENANT_HOST_UNRESOLVED, `no tenant resolved for host "${host ?? ''}"`);
79
+ }
80
+ return resolved;
81
+ }
@@ -0,0 +1,40 @@
1
+ // Phase 8 (W2-1a): db / locks driver contracts barrel.
2
+ export type { DbDriver, DbDriverKind, DbExecResult, D1Binding } from './db';
3
+ export { createNodeDbDriver, createD1DbDriver } from './db';
4
+
5
+ export type { LockHandle, LocksDriver, LocksDriverKind, LockScope } from './locks';
6
+ export { createMemoryLocksDriver, createD1LocksDriver, scopedLockName, MemoryLock } from './locks';
7
+
8
+ export type { AuthRecord } from './auth-storage';
9
+ export { DbAuthStorage, createAuthStorage } from './auth-storage';
10
+
11
+ export type { QueueOptions, PushParams, JobEvents, QueueHandle, QueueFactory, QueueHostHooks } from './queue';
12
+ export { nodeQueueHostHooks, setQueueHostHooks, getQueueHostHooks } from './queue';
13
+
14
+ export type { CronJob, CronDriver } from './cron';
15
+ export { matchesCron, shouldRunInWindow, createCronRegistry, setCronDriver, getCronDriver } from './cron';
16
+
17
+ export type { SqlMigration } from './migrate-runner';
18
+ export { applySqlMigrations, splitStatements } from './migrate-runner';
19
+
20
+ // The embedded D1 SQL lineage, inlined (store/sql-migrations is generated from
21
+ // cloudflare/migrations/*.sql) so it ships INSIDE the @arcblock/payment-service
22
+ // bundle. A host provisions the embedded schema with applyPaymentCoreMigrations
23
+ // alone — no repo paths, no wrangler. See migrate-runner.ts.
24
+ // eslint-disable-next-line import/first
25
+ import type { DbDriver as DbDriverForMigrations } from './db';
26
+ // eslint-disable-next-line import/first
27
+ import { applySqlMigrations as applySql } from './migrate-runner';
28
+ // eslint-disable-next-line import/first
29
+ import { paymentCoreSqlMigrations } from '../../store/sql-migrations';
30
+
31
+ export { paymentCoreSqlMigrations };
32
+ export function applyPaymentCoreMigrations(driver: DbDriverForMigrations): Promise<string[]> {
33
+ return applySql(driver, paymentCoreSqlMigrations);
34
+ }
35
+
36
+ export type { IdentityDriver } from './identity';
37
+ export { createDefaultIdentityDriver, setIdentityDriver, getIdentityDriver } from './identity';
38
+
39
+ export type { SecretsDriver } from './secrets';
40
+ export { createDefaultSecretsDriver, createKeyringSecretsDriver, setSecretsDriver, getSecretsDriver } from './secrets';
@@ -0,0 +1,226 @@
1
+ // Phase 8 (W2-1a): locks slot driver contract.
2
+ //
3
+ // W1 §2.2 exempts the `locks` table from an instance_did column: tenant
4
+ // isolation for locks is achieved by prefixing the lock NAME, not by a column.
5
+ // `scopedLockName` is the single place that prefix is applied, so both drivers
6
+ // (and the libs/lock facade) stay consistent.
7
+ //
8
+ // Two implementations conform to the contract:
9
+ // - memory driver: the original in-process EventEmitter lock (Blocklet Server
10
+ // single-process Node.js). Semantics unchanged — Phase 8 wraps, never
11
+ // rewrites.
12
+ // - d1 driver: the Cloudflare D1-backed lock (atomic INSERT OR IGNORE + TTL
13
+ // expiry + owner token), relocated here from the previously-orphaned
14
+ // cloudflare/shims/lock.ts so the worker wires it through the locks slot.
15
+
16
+ // the two lock classes are the two driver implementations — cohesive in one module
17
+ /* eslint-disable max-classes-per-file */
18
+
19
+ import type { D1Binding } from './db';
20
+
21
+ export interface LockHandle {
22
+ name: string;
23
+ /** whether this handle currently holds the lock — used by singleton start guards */
24
+ locked: boolean;
25
+ /**
26
+ * Acquire the lock. `maxWaitMs` is a bounded-wait hint: the d1 driver enforces
27
+ * it as a hard timeout (and rejects on expiry) because a holder lives in a
28
+ * different isolate and may crash; the in-process memory driver waits until
29
+ * release and does not time out — single process means there is no
30
+ * crashed-holder / cross-isolate-wait scenario. The shared parity cases
31
+ * (acquire-when-free, block-until-release) hold on both; TTL-expiry and
32
+ * timeout are d1-only capabilities (see drivers/locks.spec.ts).
33
+ */
34
+ acquire(maxWaitMs?: number): Promise<true>;
35
+ release(): void;
36
+ }
37
+
38
+ export type LocksDriverKind = 'memory' | 'd1';
39
+
40
+ export interface LocksDriver {
41
+ kind: LocksDriverKind;
42
+ getLock(name: string, options?: { ttl?: number }): LockHandle;
43
+ }
44
+
45
+ export type LockScope = 'tenant' | 'global';
46
+
47
+ /**
48
+ * Apply the tenant prefix to a lock name. Tenant-scoped locks (the default for
49
+ * per-resource locks) are isolated per deployment/tenant; global-scoped locks
50
+ * (process-level singleton guards like queue start guards) are intentionally
51
+ * shared and keep their bare name. In single-tenant mode the prefix is the
52
+ * constant deployment app DID, so lock identity is unchanged in practice.
53
+ */
54
+ export function scopedLockName(name: string, instanceDid: string | null, scope: LockScope): string {
55
+ if (scope === 'global') return name;
56
+ if (!instanceDid) {
57
+ // tenant scope requires a tenant — callers resolve it fail-closed before
58
+ // reaching here (see libs/lock.ts). Guard anyway so a programming error is
59
+ // loud rather than silently producing a cross-tenant-shared lock.
60
+ throw new Error('scopedLockName: tenant-scoped lock requires a non-empty instanceDid');
61
+ }
62
+ return `tenant:${instanceDid}::${name}`;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // memory driver — original in-process lock semantics
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const { EventEmitter } = require('events');
70
+
71
+ export class MemoryLock implements LockHandle {
72
+ name: string;
73
+ locked: boolean;
74
+ private events: any;
75
+
76
+ constructor(name: string) {
77
+ this.name = name;
78
+ this.locked = false;
79
+ this.events = new EventEmitter();
80
+ }
81
+
82
+ acquire(): Promise<true> {
83
+ return new Promise((resolve) => {
84
+ if (this.locked) {
85
+ const tryAcquire = () => {
86
+ if (!this.locked) {
87
+ this.locked = true;
88
+ this.events.removeListener('release', tryAcquire);
89
+ resolve(true);
90
+ }
91
+ };
92
+ this.events.on('release', tryAcquire);
93
+ } else {
94
+ this.locked = true;
95
+ resolve(true);
96
+ }
97
+ });
98
+ }
99
+
100
+ release(): void {
101
+ this.locked = false;
102
+ setImmediate(() => this.events.emit('release'));
103
+ }
104
+ }
105
+
106
+ export function createMemoryLocksDriver(): LocksDriver {
107
+ const locks = new Map<string, MemoryLock>();
108
+ return {
109
+ kind: 'memory',
110
+ getLock(name: string): LockHandle {
111
+ const exist = locks.get(name);
112
+ if (exist instanceof MemoryLock) return exist;
113
+ const lock = new MemoryLock(name);
114
+ locks.set(name, lock);
115
+ return lock;
116
+ },
117
+ };
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // d1 driver — D1-backed cross-isolate lock (relocated from shims/lock.ts)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ let nanoidCounter = 0;
125
+ function simpleId(): string {
126
+ nanoidCounter += 1;
127
+ return `${Date.now().toString(36)}-${nanoidCounter.toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
128
+ }
129
+
130
+ class D1Lock implements LockHandle {
131
+ name: string;
132
+ owner: string;
133
+ locked: boolean;
134
+ ttl: number;
135
+ private getBinding: () => D1Binding;
136
+
137
+ constructor(getBinding: () => D1Binding, name: string, options?: { ttl?: number }) {
138
+ this.getBinding = getBinding;
139
+ this.name = name;
140
+ this.owner = simpleId();
141
+ this.locked = false;
142
+ this.ttl = options?.ttl || 5000;
143
+ }
144
+
145
+ async acquire(maxWaitMs = 10000): Promise<true> {
146
+ const db: any = this.getBinding();
147
+ const deadline = Date.now() + maxWaitMs;
148
+ let delay = 30;
149
+
150
+ while (Date.now() < deadline) {
151
+ const now = Date.now();
152
+ try {
153
+ // eslint-disable-next-line no-await-in-loop -- polling retry until acquired or timed out
154
+ const batchResult = await db.batch([
155
+ db.prepare('DELETE FROM _locks WHERE name = ? AND expires_at < ?').bind(this.name, now),
156
+ db
157
+ .prepare('INSERT OR IGNORE INTO _locks (name, owner, expires_at) VALUES (?, ?, ?)')
158
+ .bind(this.name, this.owner, now + this.ttl),
159
+ db.prepare('SELECT owner FROM _locks WHERE name = ?').bind(this.name),
160
+ ]);
161
+ const row = batchResult[2]?.results?.[0] as { owner: string } | undefined;
162
+ if (row?.owner === this.owner) {
163
+ this.locked = true;
164
+ return true;
165
+ }
166
+ } catch (err: any) {
167
+ // eslint-disable-next-line no-console
168
+ console.error(`[D1Lock] acquire error for "${this.name}":`, err?.message || err);
169
+ }
170
+ // backoff with jitter, capped — captured into a const so the closure does
171
+ // not reference the mutated `delay`
172
+ const waitMs = delay + Math.random() * delay * 0.3;
173
+ // eslint-disable-next-line no-await-in-loop, no-promise-executor-return
174
+ await new Promise((r) => setTimeout(r, waitMs));
175
+ delay = Math.min(delay * 2, 500);
176
+ }
177
+ throw new Error(`[D1Lock] Failed to acquire lock "${this.name}" within ${maxWaitMs}ms`);
178
+ }
179
+
180
+ release(): void {
181
+ if (!this.locked) return;
182
+ this.locked = false;
183
+ try {
184
+ const db: any = this.getBinding();
185
+ const promise = db
186
+ .prepare('DELETE FROM _locks WHERE name = ? AND owner = ?')
187
+ .bind(this.name, this.owner)
188
+ .run()
189
+ // eslint-disable-next-line no-console
190
+ .catch((err: any) => console.error(`[D1Lock] release error for "${this.name}":`, err?.message || err));
191
+
192
+ // release is fire-and-forget; the worker shell flushes the pending delete
193
+ // before responding. This depends on ambient worker globals
194
+ // (__cfHttpContext__ / __cfWaitUntil__ / __cfPendingJobs__) set by the CF
195
+ // request handler — a host injecting the d1 locks driver must provide the
196
+ // same flush hooks (Phase 9 formalizes the flush contract). Outside a
197
+ // worker (e.g. the consistency suite) the delete still runs; it is simply
198
+ // not registered for flushing.
199
+ const isHttp = (globalThis as any).__cfHttpContext__;
200
+ if (isHttp) {
201
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
202
+ if (typeof waitUntil === 'function') waitUntil(promise);
203
+ } else {
204
+ const pending = (globalThis as any).__cfPendingJobs__;
205
+ if (Array.isArray(pending)) pending.push(promise);
206
+ }
207
+ } catch (err: any) {
208
+ // eslint-disable-next-line no-console
209
+ console.error(`[D1Lock] release setup error for "${this.name}":`, err?.message || err);
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * D1-backed locks driver. `getBinding` is a getter (the binding is resolved
216
+ * lazily per request in the worker). Each call returns a fresh lock — isolates
217
+ * never share instances, matching the original shim's behavior.
218
+ */
219
+ export function createD1LocksDriver(getBinding: () => D1Binding): LocksDriver {
220
+ return {
221
+ kind: 'd1',
222
+ getLock(name: string, options?: { ttl?: number }): LockHandle {
223
+ return new D1Lock(getBinding, name, options);
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,70 @@
1
+ // Phase 11 (W2′): runtime-neutral SQL migration runner — the migration-driver
2
+ // boundary. The library provisions the embedded D1 schema by applying the D1 SQL
3
+ // migrations through the host's db driver (its `exec`), NEVER by shelling out to
4
+ // the wrangler CLI. `wrangler d1 migrations apply` stays the CF-native local-dev
5
+ // / deploy provisioning mechanism for the SAME .sql files; this runner is the
6
+ // in-process path arc-node (and any non-CF host) uses via lifecycle/host wiring.
7
+ //
8
+ // Idempotent: applied migrations are tracked in `_sql_migrations`, so re-running
9
+ // is a no-op (the .sql contain non-IF-NOT-EXISTS DDL like ALTER ... ADD COLUMN).
10
+ //
11
+ // NOTE: this tracker (`_sql_migrations`) is independent of wrangler's own
12
+ // `d1_migrations` table. That is safe because the two provisioning paths target
13
+ // DIFFERENT databases — arc-node's embedded SQLite (this runner) vs the CF
14
+ // worker's bound D1 (wrangler) — and never share one. Do NOT run both paths
15
+ // against the SAME database: the runner is blind to `d1_migrations`, so it would
16
+ // re-apply everything and the non-idempotent ALTER ... ADD COLUMN would fail.
17
+
18
+ import type { DbDriver, DbBatchOp } from './db';
19
+
20
+ export interface SqlMigration {
21
+ /** stable id (the migration filename) — the idempotency key */
22
+ name: string;
23
+ /** the migration body; may contain multiple `;`-separated statements */
24
+ sql: string;
25
+ }
26
+
27
+ /** Strip line comments and split a migration body into individual statements. */
28
+ export function splitStatements(sql: string): string[] {
29
+ return sql
30
+ .split('\n')
31
+ .filter((line) => !line.trim().startsWith('--'))
32
+ .join('\n')
33
+ .split(';')
34
+ .map((s) => s.trim())
35
+ .filter((s) => s.length > 0);
36
+ }
37
+
38
+ /**
39
+ * Apply pending SQL migrations through the db driver, in order. Returns the
40
+ * names actually applied this run (empty on a fully-migrated db). No wrangler.
41
+ *
42
+ * Each migration is applied ATOMICALLY: all of its statements PLUS the tracker
43
+ * insert run in a single `driver.batch(...)` (the contract's all-or-nothing
44
+ * transactional primitive). So a mid-migration failure rolls the whole migration
45
+ * back AND leaves the tracker unwritten — a re-run retries that migration from a
46
+ * clean slate, never replaying half-applied (non-IF-NOT-EXISTS) DDL like
47
+ * `ALTER ... ADD COLUMN`. This satisfies "中断后重跑不丢已迁移状态(幂等 + 事务边界)".
48
+ */
49
+ export async function applySqlMigrations(driver: DbDriver, migrations: SqlMigration[]): Promise<string[]> {
50
+ await driver.exec('CREATE TABLE IF NOT EXISTS _sql_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)');
51
+ const rows = await driver.all<{ name: string }>('SELECT name FROM _sql_migrations');
52
+ const applied = new Set(rows.map((r) => r.name));
53
+
54
+ const ran: string[] = [];
55
+ const pending = migrations.filter((m) => !applied.has(m.name));
56
+ /* eslint-disable no-await-in-loop -- migrations are ordered + must apply sequentially */
57
+ for (const m of pending) {
58
+ const ops: DbBatchOp[] = splitStatements(m.sql).map((sql) => ({ sql }));
59
+ // tracker insert is the LAST op in the same batch — committed iff every
60
+ // schema statement of this migration committed (atomic boundary).
61
+ ops.push({
62
+ sql: 'INSERT INTO _sql_migrations (name, applied_at) VALUES (?, ?)',
63
+ params: [m.name, new Date().toISOString()],
64
+ });
65
+ await driver.batch(ops);
66
+ ran.push(m.name);
67
+ }
68
+ /* eslint-enable no-await-in-loop */
69
+ return ran;
70
+ }
@@ -0,0 +1,104 @@
1
+ // Phase 9 (W2-1b): queue slot driver contract.
2
+ //
3
+ // The queue contract is the WHOLE semantics the two engines share, not just
4
+ // enqueue:
5
+ // - jobs table (createQueueStore) = persistent scheduler / source of truth
6
+ // - immediate vs delayed dispatch (delayed rows wait for the due-poll)
7
+ // - consumer ack + failure re-delivery (retry_count up to maxRetries, then
8
+ // failed; nonRetryable errors fail immediately)
9
+ // - pushAndWait inline execution (caller awaits the result)
10
+ // - host flush hook (CF flushes pending push/timer work before responding;
11
+ // the Node long-lived process is a no-op)
12
+ // - tenant is carried in the payload (Phase 5/6); the contract layer passes
13
+ // it through and NEVER resolves Host on the background path
14
+ //
15
+ // ONE engine (api/src/libs/queue) serves every runtime (Phase 12b, option A):
16
+ // the host swaps only the EXECUTOR (real fastq on node, cloudflare/shims/fastq
17
+ // in the worker) and the TRIGGER (in-process poll loop on node, CF scheduled()
18
+ // calling the engine's dispatchDueJobs() in the worker — see
19
+ // api/src/libs/queue/runtime.ts). Persistence, retry policy and tenant handling
20
+ // are shared because there is no second engine. The old cloudflare/shims/queue.ts
21
+ // duplicate engine was removed in Phase 12c.
22
+
23
+ import type EventEmitter from 'events';
24
+
25
+ export interface QueueOptions<T> {
26
+ id?: (job: T) => string;
27
+ concurrency?: number;
28
+ maxRetries?: number;
29
+ maxTimeout?: number;
30
+ retryDelay?: number;
31
+ enableScheduledJob?: boolean;
32
+ }
33
+
34
+ export interface PushParams<T> {
35
+ job: T;
36
+ id?: string;
37
+ persist?: boolean;
38
+ /** seconds */
39
+ delay?: number;
40
+ /** unix timestamp in seconds */
41
+ runAt?: number;
42
+ skipDuplicateCheck?: boolean;
43
+ /**
44
+ * Internal re-delivery flag for rows already persisted (startup / scheduled
45
+ * recovery). Skips the enqueue tenant gate so the execution-side legacy
46
+ * strategy applies. NEVER set from application code.
47
+ */
48
+ fromStore?: boolean;
49
+ }
50
+
51
+ /** the per-job event channel returned by push (emits queued/finished/failed/retry/cancelled) */
52
+ export type JobEvents = EventEmitter & { id?: string };
53
+
54
+ export interface QueueHandle<T = any> {
55
+ push(params: PushParams<T>): JobEvents;
56
+ pushAndWait(params: PushParams<T>): Promise<{ id: string; job: T; result: any }>;
57
+ get(id: string): Promise<T | null>;
58
+ delete(id: string, knownExists?: boolean): Promise<boolean>;
59
+ cancel(id: string): Promise<T | null>;
60
+ update(id: string, updates: any): Promise<any>;
61
+ /**
62
+ * D2 teardown: stop this queue's node poll loop (clears its sleep timer).
63
+ * No-op when the queue is not scheduled or on a workerd host (no loop runs).
64
+ * The Node host tears every queue down via lifecycle.stop() → stopAllQueues().
65
+ */
66
+ stop?(): void;
67
+ options: Required<Omit<QueueOptions<T>, 'id'>>;
68
+ }
69
+
70
+ /** factory shape both engines export as default */
71
+ export type QueueFactory = <T = any>(params: {
72
+ name: string;
73
+ onJob: (job: T) => Promise<any>;
74
+ options?: QueueOptions<T>;
75
+ }) => QueueHandle<T> & EventEmitter;
76
+
77
+ /**
78
+ * Host flush hook. CF must flush pending push/timer work before returning the
79
+ * response (the isolate is torn down after); a long-lived Node process never
80
+ * needs to and uses the no-op below.
81
+ */
82
+ export interface QueueHostHooks {
83
+ flush(): Promise<void>;
84
+ }
85
+
86
+ /** Node host: long-lived process, nothing to flush before a response. */
87
+ export const nodeQueueHostHooks: QueueHostHooks = {
88
+ async flush() {
89
+ /* no-op — the process stays alive, in-flight jobs continue */
90
+ },
91
+ };
92
+
93
+ // Active host hooks — injectable by the factory's `queue` slot; defaults to the
94
+ // Node no-op. The worker shell injects hooks that flush pending push/timer work
95
+ // before responding.
96
+ let activeQueueHostHooks: QueueHostHooks = nodeQueueHostHooks;
97
+
98
+ export function setQueueHostHooks(hooks: QueueHostHooks): void {
99
+ activeQueueHostHooks = hooks;
100
+ }
101
+
102
+ export function getQueueHostHooks(): QueueHostHooks {
103
+ return activeQueueHostHooks;
104
+ }