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,175 @@
1
+ // Phase 12b (W2′): the core queue RUNTIME surface.
2
+ //
3
+ // Decision (2026-06-12, build-phases 12b, option A): the node engine
4
+ // (api/src/libs/queue) is the ONE canonical queue semantics for every runtime.
5
+ // A host swaps only the TRIGGER (CF scheduled() vs the in-process poll loop),
6
+ // the EXECUTOR (fastq shim vs real fastq) and FLUSH (drain-before-response vs
7
+ // no-op). It NEVER forks the engine — the old cloudflare/shims/queue.ts
8
+ // duplicate engine is dead under the canonical build.ts (nothing populates its
9
+ // registry there) and is removed in 12c.
10
+ //
11
+ // This module is the seam the worker drives instead of reaching into
12
+ // shims/queue internals:
13
+ // - a queue REGISTRY (each createQueue registers its handle + due-dispatch)
14
+ // - dispatchDueJobs() — host-driven scheduled re-dispatch (workerd trigger =
15
+ // CF scheduled(); on node the per-queue loop() does the same on a timer)
16
+ // - flushQueueWork() — drain in-flight push/execution work before the
17
+ // isolate freezes (no-op on a long-lived node process)
18
+ //
19
+ // Runtime modes:
20
+ // 'node' — long-lived process: each scheduled queue's loop() polls D1.
21
+ // 'workerd' — frozen isolate: loop() is disabled; the host calls
22
+ // dispatchDueJobs() from scheduled() and flushQueueWork() before
23
+ // returning the response.
24
+
25
+ export type QueueRuntimeMode = 'node' | 'workerd';
26
+
27
+ let mode: QueueRuntimeMode = 'node';
28
+
29
+ export function setQueueRuntimeMode(next: QueueRuntimeMode): void {
30
+ mode = next;
31
+ }
32
+
33
+ export function getQueueRuntimeMode(): QueueRuntimeMode {
34
+ return mode;
35
+ }
36
+
37
+ export interface RegisteredQueue {
38
+ name: string;
39
+ /** whether this queue accepts delayed/scheduled jobs (gates due-dispatch) */
40
+ enableScheduledJob: boolean;
41
+ /** the public queue handle (push / pushAndWait / cancel / get / ...) */
42
+ handle: any;
43
+ /**
44
+ * Re-deliver this queue's due delayed rows once — the body of the node
45
+ * loop(). The host (CF scheduled()) calls this through dispatchDueJobs();
46
+ * the node loop() calls it on its own timer. Same code path either way.
47
+ */
48
+ redispatchDue: () => Promise<{ dispatched: number; failed: number }>;
49
+ /**
50
+ * D2 teardown: stop this queue's node poll loop (clears its sleep timer). The
51
+ * host (arc/Node) calls stopAllQueues() on lifecycle.stop() so no poll timer
52
+ * survives a stop / ARC_PAYMENT toggle. No-op on workerd (no loop runs).
53
+ */
54
+ stop?: () => void;
55
+ }
56
+
57
+ const registry = new Map<string, RegisteredQueue>();
58
+
59
+ export function registerQueue(entry: RegisteredQueue): void {
60
+ registry.set(entry.name, entry);
61
+ }
62
+
63
+ /** the queue handle the worker's queue() consumer looks up by name (no more shim registry) */
64
+ export function getQueueHandler(name: string): any | undefined {
65
+ return registry.get(name)?.handle;
66
+ }
67
+
68
+ export function getAllQueueNames(): string[] {
69
+ return Array.from(registry.keys());
70
+ }
71
+
72
+ /**
73
+ * Host-driven scheduled dispatch (the workerd trigger is CF scheduled()). Runs
74
+ * every registered scheduled queue's due-row re-dispatch once. On a node host
75
+ * the per-queue loop() does exactly this on a timer; a workerd host calls this
76
+ * explicitly because it cannot run a background timer in a frozen isolate.
77
+ */
78
+ export async function dispatchDueJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
79
+ let dispatched = 0;
80
+ let failed = 0;
81
+ const queues: string[] = [];
82
+ for (const entry of registry.values()) {
83
+ // eslint-disable-next-line no-continue -- skip non-scheduled queues in the dispatch loop
84
+ if (!entry.enableScheduledJob) continue;
85
+ try {
86
+ // eslint-disable-next-line no-await-in-loop -- queues dispatch sequentially within a tick
87
+ const r = await entry.redispatchDue();
88
+ dispatched += r.dispatched;
89
+ failed += r.failed;
90
+ if (r.dispatched > 0 || r.failed > 0) queues.push(entry.name);
91
+ } catch {
92
+ failed += 1;
93
+ }
94
+ }
95
+ return { dispatched, failed, queues };
96
+ }
97
+
98
+ /**
99
+ * D2 teardown: stop every registered queue's node poll loop. The Node host
100
+ * (arc) calls this from lifecycle.stop() so no background poll timer survives a
101
+ * stop / ARC_PAYMENT toggle. The registry entries stay (a later start() rebuilds
102
+ * the loops by recreating the queues). Idempotent — safe to call when stopped.
103
+ */
104
+ export function stopAllQueues(): void {
105
+ for (const entry of registry.values()) {
106
+ try {
107
+ entry.stop?.();
108
+ } catch {
109
+ /* a queue already stopped — ignore */
110
+ }
111
+ }
112
+ }
113
+
114
+ /** stop every queue loop AND clear the registry (full reset). */
115
+ export function disposeQueues(): void {
116
+ stopAllQueues();
117
+ registry.clear();
118
+ }
119
+
120
+ // --- in-flight push tracking (workerd flush) ---
121
+ // On node the process stays alive and fastq drains naturally; nothing is
122
+ // tracked (zero overhead, byte-identical behavior). On workerd the isolate is
123
+ // torn down after the response, so the host must await in-flight executions
124
+ // before it returns — otherwise an immediate push is silently dropped.
125
+ const pending = new Set<Promise<any>>();
126
+
127
+ export function trackPending(p: Promise<any>): void {
128
+ if (mode !== 'workerd') return;
129
+ const wrapped = Promise.resolve(p).catch(() => undefined);
130
+ pending.add(wrapped);
131
+ wrapped.then(() => {
132
+ pending.delete(wrapped);
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Drain in-flight push/execution work. The CF worker calls this before
138
+ * returning the HTTP response and at the end of scheduled()/queue() so no
139
+ * immediate push is lost when the isolate freezes. No-op on a node host.
140
+ */
141
+ export async function flushQueueWork(): Promise<void> {
142
+ if (mode !== 'workerd') return;
143
+ const MAX_ITERATIONS = 10;
144
+ // Wall-clock guard: a misbehaving handler whose fastq callback never fires
145
+ // must not block the isolate's response forever. The D1 jobs row is the
146
+ // source of truth, so anything still pending past the budget is re-dispatched
147
+ // on the next scheduled() tick rather than lost.
148
+ const deadline = Date.now() + 5000;
149
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
150
+ if (pending.size === 0) break;
151
+ const remaining = deadline - Date.now();
152
+ if (remaining <= 0) break;
153
+ const batch = Array.from(pending);
154
+ // eslint-disable-next-line no-await-in-loop
155
+ await Promise.race([
156
+ Promise.allSettled(batch),
157
+ new Promise<void>((resolve) => {
158
+ setTimeout(resolve, remaining);
159
+ }),
160
+ ]);
161
+ }
162
+ }
163
+
164
+ // Exported for unit tests only — not part of the host-facing surface.
165
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- test-only export sentinel
166
+ export const __test__ = {
167
+ reset() {
168
+ registry.clear();
169
+ pending.clear();
170
+ mode = 'node';
171
+ },
172
+ registrySize() {
173
+ return registry.size;
174
+ },
175
+ };
@@ -10,9 +10,9 @@ import { fromTokenToUnit } from '@ocap/util';
10
10
 
11
11
  import { updatePassportExtra } from '../integrations/blocklet/passport';
12
12
  import { replace } from '../locales';
13
- import { createPaymentLink } from '../routes/payment-links';
14
- import { createPrice } from '../routes/prices';
15
- import { createProductAndPrices } from '../routes/products';
13
+ import { createPaymentLink } from '../routes/hono/payment-links';
14
+ import { createPrice } from '../routes/hono/prices';
15
+ import { createProductAndPrices } from '../routes/hono/products';
16
16
  import { PaymentCurrency, PaymentLink, Price, Product, nextPriceId } from '../store/models';
17
17
 
18
18
  export async function getPackResource(type: string) {
@@ -0,0 +1,38 @@
1
+ // Phase 11 (W2-3): tenant-aware secrets facade.
2
+ //
3
+ // Replaces direct `security.encrypt/decrypt` calls in the payment hot path. The
4
+ // tenant is resolved from the TenantContext (single point), so call sites do
5
+ // not change signature. single mode -> the default secrets driver (process
6
+ // key), unchanged; multi mode -> the keyring driver keyed per tenant.
7
+ //
8
+ // Sync surface: the payment loop (PaymentMethod.encrypt/decrypt Settings,
9
+ // getStripeClient, ~50 sync callers) stays synchronous. The keyring driver's
10
+ // sync path requires the tenant key to be warmed (the worker shell warms it at
11
+ // request entry); the default driver's process key is always available.
12
+
13
+ import { getInstanceDid } from './context';
14
+ import { getSecretsDriver } from './drivers/secrets';
15
+
16
+ /** Encrypt a value under the current tenant's key (sync hot path). */
17
+ export function encryptSecret(value: string): string {
18
+ return getSecretsDriver().encryptSync(getInstanceDid(), value);
19
+ }
20
+
21
+ /** Decrypt a value under the current tenant's key (sync hot path). */
22
+ export function decryptSecret(value: string): string {
23
+ return getSecretsDriver().decryptSync(getInstanceDid(), value);
24
+ }
25
+
26
+ /** Async per-tenant variants (lazy EK fetch + retry) for non-hot-path callers. */
27
+ export function encryptSecretAsync(value: string): Promise<string> {
28
+ return getSecretsDriver().encrypt(getInstanceDid(), value);
29
+ }
30
+
31
+ export function decryptSecretAsync(value: string): Promise<string> {
32
+ return getSecretsDriver().decrypt(getInstanceDid(), value);
33
+ }
34
+
35
+ /** Pre-resolve the current tenant's key so the sync path is a cache hit. */
36
+ export function warmupSecrets(instanceDid?: string): Promise<void> {
37
+ return getSecretsDriver().warmup(instanceDid ?? getInstanceDid());
38
+ }
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
6
6
  import isEqual from 'lodash/isEqual';
7
7
  import pAll from 'p-all';
8
8
  import omit from 'lodash/omit';
9
+ import { paymentBillingThreshold, paymentMinStakeAmount } from './env';
9
10
  import dayjs from './dayjs';
10
11
  import { validCoupon } from './discount/coupon';
11
12
  import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
@@ -425,7 +426,7 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
425
426
  }
426
427
  }
427
428
 
428
- const threshold = +(process.env.PAYMENT_BILLING_THRESHOLD as string);
429
+ const threshold = paymentBillingThreshold();
429
430
  if (threshold > 0) {
430
431
  return threshold;
431
432
  }
@@ -441,7 +442,7 @@ export function getMinStakeAmount(config: Record<string, any> = {}) {
441
442
  }
442
443
  }
443
444
 
444
- const threshold = +(process.env.PAYMENT_MIN_STAKE_AMOUNT as string);
445
+ const threshold = paymentMinStakeAmount();
445
446
  if (threshold > 0) {
446
447
  return threshold;
447
448
  }
@@ -8,6 +8,7 @@ import type { LiteralUnion } from 'type-fest';
8
8
  import { withQuery } from 'ufo';
9
9
 
10
10
  import { Op } from 'sequelize';
11
+ import env, { paymentDaysUntilDue, paymentDaysUntilCancel } from './env';
11
12
  import {
12
13
  ChainType,
13
14
  Customer,
@@ -27,9 +28,8 @@ import {
27
28
  TLineItemExpanded,
28
29
  UsageRecord,
29
30
  } from '../store/models';
30
- import { createEvent } from './audit';
31
+ import { createEvent, reportAuditFailure } from './audit';
31
32
  import dayjs from './dayjs';
32
- import env from './env';
33
33
  import logger from './logger';
34
34
  import { getExchangeRateService } from './exchange-rate';
35
35
  import { getExchangeRateSymbol } from './exchange-rate/token-address-mapping';
@@ -107,11 +107,11 @@ export function parseIntegerConfig(alternatives: any[], defaultValue: number) {
107
107
  }
108
108
 
109
109
  export function getDaysUntilDue(query: Record<string, any> = {}) {
110
- return parseIntegerConfig([query.days_until_due, process.env.PAYMENT_DAYS_UNTIL_DUE], 6);
110
+ return parseIntegerConfig([query.days_until_due, paymentDaysUntilDue()], 6);
111
111
  }
112
112
 
113
113
  export function getDaysUntilCancel(query: Record<string, any> = {}) {
114
- return parseIntegerConfig([query.days_until_cancel, process.env.PAYMENT_DAYS_UNTIL_CANCEL], 0);
114
+ return parseIntegerConfig([query.days_until_cancel, paymentDaysUntilCancel()], 0);
115
115
  }
116
116
 
117
117
  export const getDueUnit = (interval: string) => {
@@ -821,7 +821,7 @@ export async function finalizeStripeSubscriptionUpdate({
821
821
  await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
822
822
  logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
823
823
 
824
- createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
824
+ createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(reportAuditFailure);
825
825
  }
826
826
 
827
827
  logger.info('subscription update finalized', { subscription: subscription.id, updates, items });
@@ -0,0 +1,92 @@
1
+ import { tenantModeRaw, blockletAppPid } from './env';
2
+
3
+ export type TenantMode = 'single' | 'multi';
4
+
5
+ export const TENANT_CONTEXT_MISSING = 'TENANT_CONTEXT_MISSING';
6
+ export const TENANT_MISMATCH = 'TENANT_MISMATCH';
7
+ // programmer error: a scoped helper was pointed at a non-tenant model —
8
+ // distinct from TENANT_MISMATCH so monitoring can tell data races from bugs
9
+ export const INVALID_TENANT_TABLE = 'INVALID_TENANT_TABLE';
10
+ // multi-tenant fail-closed: a request Host did not resolve to any tenant
11
+ // (Phase 10) — the request is refused 4xx with no default-tenant fallback
12
+ export const TENANT_HOST_UNRESOLVED = 'TENANT_HOST_UNRESOLVED';
13
+
14
+ export type TenantErrorCode =
15
+ | typeof TENANT_CONTEXT_MISSING
16
+ | typeof TENANT_MISMATCH
17
+ | typeof INVALID_TENANT_TABLE
18
+ | typeof TENANT_HOST_UNRESOLVED;
19
+
20
+ export class TenantError extends Error {
21
+ code: TenantErrorCode;
22
+
23
+ constructor(code: TenantErrorCode, message: string) {
24
+ super(message);
25
+ this.name = 'TenantError';
26
+ this.code = code;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Tenant mode of this deployment:
32
+ * - `single` (default, blocklet server legacy): tenant context falls back to the deployment's own app DID
33
+ * - `multi`: no fallback — missing tenant context is a fail-closed error
34
+ *
35
+ * Source of the mode is the existing env mechanism for now; Phase 12 converges it into the config slot.
36
+ */
37
+ export function getTenantMode(): TenantMode {
38
+ // Phase 8: mode-source reads the injected config (libs/env boundary), falling
39
+ // back to process.env. Not request-tamperable — config is set once at factory
40
+ // init, never from request input.
41
+ return tenantModeRaw() === 'multi' ? 'multi' : 'single';
42
+ }
43
+
44
+ /**
45
+ * Single source of truth for "this deployment's app DID" (single-tenant default tenant).
46
+ * Phase 2 backfill values and Phase 10 `tenancy.instanceDid` must reuse this getter.
47
+ */
48
+ // Phase 10: the single-mode tenancy slot value, when the host supplies one via
49
+ // createEmbeddedPaymentService({ tenancy: { mode:'single', instanceDid } }).
50
+ // Preferred over the env snapshot so a host can declare its single-tenant
51
+ // identity explicitly (and so the slot is not silently ignored).
52
+ let overrideDefaultInstanceDid: string | undefined;
53
+
54
+ /** Wire the single-mode tenancy slot value (factory only). Pass undefined to clear. */
55
+ export function setDefaultInstanceDid(did: string | undefined): void {
56
+ if (did !== undefined) assertValidInstanceDid(did);
57
+ overrideDefaultInstanceDid = did;
58
+ }
59
+
60
+ export function getDefaultInstanceDid(): string {
61
+ // explicit tenancy slot value wins; otherwise the app DID from the injected
62
+ // config (libs/env boundary), which itself falls back to process.env's
63
+ // BLOCKLET_APP_PID (the value @blocklet/sdk's env.appPid wrapped before Phase 8).
64
+ const did = overrideDefaultInstanceDid || blockletAppPid() || '';
65
+ if (!did) {
66
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'app DID is not configured for this deployment');
67
+ }
68
+ return did;
69
+ }
70
+
71
+ // Deliberately loose: deployments use both bare addresses (z8iZ...) and
72
+ // did:abt: URIs, so we only reject values that cannot be a DID at all
73
+ // (non-strings, empty/whitespace, embedded whitespace). Strict format
74
+ // enforcement belongs to the identity slot (Phase 10), which resolves
75
+ // Host -> instanceDid from a trusted source.
76
+ /**
77
+ * Tenant of a loaded row, fail-closed: rows written since Phase 2 always
78
+ * carry instance_did; a NULL means pre-backfill data, which is only legal in
79
+ * single mode (where it can only belong to the default tenant).
80
+ */
81
+ export function resolveRowTenant(row: { instance_did?: string | null } | null | undefined): string {
82
+ const fromRow = row?.instance_did;
83
+ if (fromRow) return fromRow;
84
+ if (getTenantMode() === 'single') return getDefaultInstanceDid();
85
+ throw new TenantError(TENANT_CONTEXT_MISSING, 'row has no tenant and deployment is in multi mode');
86
+ }
87
+
88
+ export function assertValidInstanceDid(instanceDid: unknown): asserts instanceDid is string {
89
+ if (typeof instanceDid !== 'string' || !/^\S{3,}$/.test(instanceDid)) {
90
+ throw new TenantError(TENANT_CONTEXT_MISSING, `invalid instanceDid: ${JSON.stringify(instanceDid)}`);
91
+ }
92
+ }
@@ -30,7 +30,7 @@ export async function formatToShortUrl({
30
30
  validUntil?: string;
31
31
  maxVisits?: number;
32
32
  }): Promise<string> {
33
- const apiKey = shortUrlApiKey;
33
+ const apiKey = shortUrlApiKey();
34
34
 
35
35
  if (!apiKey) {
36
36
  return url;
@@ -43,14 +43,14 @@ export async function formatToShortUrl({
43
43
  maxVisits,
44
44
  tags: [],
45
45
  shortCodeLength: 8,
46
- domain: shortUrlDomain,
46
+ domain: shortUrlDomain(),
47
47
  findIfExists: true,
48
48
  validateUrl: true,
49
49
  forwardQuery: true,
50
50
  crawlable: true,
51
51
  };
52
52
 
53
- const response = await fetch(`https://${shortUrlDomain}/rest/v3/short-urls`, {
53
+ const response = await fetch(`https://${shortUrlDomain()}/rest/v3/short-urls`, {
54
54
  method: 'POST',
55
55
  headers: {
56
56
  'Content-Type': 'application/json',
@@ -10,10 +10,10 @@ import type { LiteralUnion } from 'type-fest';
10
10
  import { joinURL, withQuery, withTrailingSlash } from 'ufo';
11
11
 
12
12
  import axios from 'axios';
13
- import { ethers } from 'ethers';
14
13
  import { fromUnitToToken } from '@ocap/util';
15
14
  import get from 'lodash/get';
16
15
  import trimEnd from 'lodash/trimEnd';
16
+ import { googlePlayWebhookUrl, blockletAppUrl, blockletMountPoints, blockletAppId, blockletAppName } from './env';
17
17
  import dayjs from './dayjs';
18
18
  import { blocklet, wallet } from './auth';
19
19
  import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
@@ -37,7 +37,7 @@ export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook'
37
37
  // Lazy-eval (function not constant) because dotenv loads env AFTER this module
38
38
  // is imported — a constant captured at module-load would only see BLOCKLET_APP_URL.
39
39
  export const googlePlayEndpoint = (): string =>
40
- process.env.GOOGLE_PLAY_WEBHOOK_URL || getUrl('/api/integrations/google-play/webhook');
40
+ googlePlayWebhookUrl() || getUrl('/api/integrations/google-play/webhook');
41
41
 
42
42
  // Back-compat constant for any caller that captures it at module-load.
43
43
  // Prefer googlePlayEndpoint() going forward.
@@ -262,7 +262,7 @@ const cachedBlockletJsonResult = new Map<string, { data: any; expiry: number }>(
262
262
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour
263
263
 
264
264
  export async function getBlockletJson(url?: string) {
265
- const blockletKey = url || process.env.BLOCKLET_APP_URL || 'default';
265
+ const blockletKey = url || blockletAppUrl() || 'default';
266
266
  const now = Date.now();
267
267
 
268
268
  if (cachedBlockletJsonResult.has(blockletKey)) {
@@ -271,7 +271,7 @@ export async function getBlockletJson(url?: string) {
271
271
  return cached.data;
272
272
  }
273
273
  }
274
- const baseUrl = url || process.env.BLOCKLET_APP_URL;
274
+ const baseUrl = url || blockletAppUrl();
275
275
  if (!baseUrl) {
276
276
  return null;
277
277
  }
@@ -282,14 +282,14 @@ export async function getBlockletJson(url?: string) {
282
282
  return blockletMeta;
283
283
  } catch (err) {
284
284
  logger.error(`getBlockletJson error for ${scriptUrl}`, err);
285
- if (process.env.BLOCKLET_MOUNT_POINTS) {
286
- const BLOCKLET_MOUNT_POINTS = safeJsonParse(process.env.BLOCKLET_MOUNT_POINTS, []);
285
+ if (blockletMountPoints()) {
286
+ const BLOCKLET_MOUNT_POINTS = safeJsonParse(blockletMountPoints(), []);
287
287
  return {
288
288
  componentMountPoints: BLOCKLET_MOUNT_POINTS,
289
- appId: process.env.BLOCKLET_APP_ID,
290
- appName: process.env.BLOCKLET_APP_NAME,
289
+ appId: blockletAppId(),
290
+ appName: blockletAppName(),
291
291
  appLogo: '/.well-known/service/blocklet/logo',
292
- appUrl: process.env.BLOCKLET_APP_URL,
292
+ appUrl: blockletAppUrl(),
293
293
  };
294
294
  }
295
295
  return null;
@@ -313,9 +313,9 @@ export async function getUserOrAppInfo(
313
313
  if (appInfo) {
314
314
  return {
315
315
  name: appInfo.name,
316
- avatar: joinURL(process.env.BLOCKLET_APP_URL!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
316
+ avatar: joinURL(blockletAppUrl()!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
317
317
  type: 'dapp',
318
- url: joinURL(process.env.BLOCKLET_APP_URL!, appInfo.mountPoint),
318
+ url: joinURL(blockletAppUrl()!, appInfo.mountPoint),
319
319
  };
320
320
  }
321
321
  }
@@ -324,7 +324,7 @@ export async function getUserOrAppInfo(
324
324
  const locale = get(user, 'locale', 'en');
325
325
  return {
326
326
  name: user?.fullName,
327
- avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
327
+ avatar: joinURL(blockletAppUrl()!, user?.avatar),
328
328
  type: 'user',
329
329
  url: getCustomerProfileUrl({ userDid: address, locale }),
330
330
  };
@@ -608,7 +608,15 @@ export async function isUserInBlocklist(did: string, paymentMethod: PaymentMetho
608
608
  }
609
609
 
610
610
  export function resolveAddressChainTypes(address: string): LiteralUnion<'ethereum' | 'base' | 'arcblock', string>[] {
611
- if (ethers.isAddress(address)) {
611
+ // Phase 13b2: lazy ethers. An eager top-level `import 'ethers'` loaded ethers
612
+ // during createEmbeddedPaymentService assembly (libs/util is pulled in at factory
613
+ // time), so a host that force-resolves an incompatible @noble/hashes (e.g. arc's
614
+ // `@noble/hashes:^2.2.0` override vs ethers@6.16's declared 1.3.2) crashed ethers
615
+ // at require. Deferring to call time keeps the factory + rpc.entitlements.check
616
+ // ethers-free; this fn runs only on EVM address resolution.
617
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
618
+ const { isAddress } = require('ethers');
619
+ if (isAddress(address)) {
612
620
  return ['ethereum', 'base', 'arcblock'];
613
621
  }
614
622
  return ['arcblock'];
@@ -0,0 +1,63 @@
1
+ // Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/cdn.js.
2
+ //
3
+ // The express version monkeypatches res.send to rewrite asset URLs to the CDN
4
+ // host on outgoing HTML. hono responses are built differently, so this fork
5
+ // rewrites in the RESPONSE phase: run the handler, then if the response is HTML
6
+ // (production GET/HEAD, non-resource, html-accepting), read it once and rebuild
7
+ // the Response with rewritten URLs. The transform core (AssetHostTransformer) is
8
+ // REUSED VERBATIM from the SDK — byte-identical rewriting. Inert for JSON /api
9
+ // routes (content-type is not text/html), which is the only surface in Phases
10
+ // 1-3; it becomes load-bearing when SPA HTML serving moves off the bridge.
11
+ import type { MiddlewareHandler } from 'hono';
12
+ // eslint-disable-next-line import/no-extraneous-dependencies
13
+ import { AssetHostTransformer } from '@blocklet/sdk/lib/util/asset-host-transformer';
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
15
+ import { env } from '@blocklet/sdk/lib/config';
16
+ // eslint-disable-next-line import/no-extraneous-dependencies
17
+ import { BLOCKLET_PROXY_PATH_PREFIX } from '@abtnode/constant';
18
+ // eslint-disable-next-line import/no-extraneous-dependencies
19
+ import { RESOURCE_PATTERN } from '@blocklet/constant';
20
+ import { nodeEnv, readConfig } from '../../libs/env';
21
+
22
+ function isProductionRuntime(): boolean {
23
+ return nodeEnv() === 'production' || readConfig('ABT_NODE_SERVICE_ENV') === 'production';
24
+ }
25
+
26
+ // Parity with express req.accepts(['html', ...]) for the html family.
27
+ function acceptsHtml(accept: string): boolean {
28
+ if (!accept) return true; // express treats a missing Accept as accept-all
29
+ return accept.includes('text/html') || accept.includes('application/xhtml+xml') || accept.includes('*/*');
30
+ }
31
+
32
+ function shouldProcess(method: string, path: string, accept: string): boolean {
33
+ if (!isProductionRuntime()) return false;
34
+ if (method !== 'GET' && method !== 'HEAD') return false;
35
+ if (path.includes('/.well-known/service/')) return false;
36
+ if (RESOURCE_PATTERN.test(path)) return false;
37
+ return acceptsHtml(accept);
38
+ }
39
+
40
+ export function cdn(): MiddlewareHandler {
41
+ // Lazy + skip-if-absent: a bare/embedded host without componentDid never
42
+ // rewrites (the SDK throws "did is required" eagerly; the fork stays inert).
43
+ let transformer: AssetHostTransformer | undefined;
44
+ return async (c, next) => {
45
+ await next();
46
+ const assetHost = (env as any).assetCdnHost;
47
+ const did = (env as any).componentDid;
48
+ if (!assetHost || !did) return;
49
+ if (!shouldProcess(c.req.method.toUpperCase(), c.req.path, c.req.header('accept') || '')) return;
50
+
51
+ const contentType = c.res.headers.get('content-type') || '';
52
+ if (!contentType.includes('text/html')) return;
53
+
54
+ transformer ??= new AssetHostTransformer(`${BLOCKLET_PROXY_PATH_PREFIX}/${did}/`);
55
+ const html = await c.res.text();
56
+ const transformed = transformer.transform(html, assetHost);
57
+ const rebuilt = new Response(transformed, c.res);
58
+ rebuilt.headers.delete('content-length'); // body length changed; let the server derive it
59
+ c.res = rebuilt;
60
+ };
61
+ }
62
+
63
+ export default cdn;
@@ -0,0 +1,73 @@
1
+ // Phase 1 (express→hono) — hono fork of api/src/libs/middleware.ts
2
+ // (ensureI18n + contextMiddleware). Behavior is identical to the express
3
+ // version; only the req/res plumbing changes:
4
+ // - req.query.locale / req.t= → c.req.query('locale') / c.set('t', ...)
5
+ // - req.get('x-component-sig') → c.req.header('x-component-sig')
6
+ // - req.body (component sig verify) → c.get('sanitizedBody') (xss is the single
7
+ // body read-point, already ran upstream)
8
+ // - req.headers.host (tenant) → c.req.header('host') (raw Host only, never
9
+ // a proxy header — single tenant resolution)
10
+ // - res.status(400).json(...) → c.json(..., 400) (fail-closed on unknown
11
+ // host in multi mode)
12
+ // - context.run(..., next) → context.run(..., () => next()) (same ALS)
13
+ import type { MiddlewareHandler } from 'hono';
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
15
+ import { verify } from '@blocklet/sdk/lib/util/verify-sign';
16
+ import { translate } from '../../locales';
17
+ import { context } from '../../libs/context';
18
+ import { TenantError, TENANT_HOST_UNRESOLVED } from '../../libs/tenant';
19
+ import { resolveTenantForHost } from '../../libs/drivers/identity';
20
+
21
+ export function ensureI18n(): MiddlewareHandler {
22
+ return (c, next) => {
23
+ c.set('locale', String(c.req.query('locale') || 'en'));
24
+ c.set('t', translate);
25
+ return next();
26
+ };
27
+ }
28
+
29
+ export function contextMiddleware(): MiddlewareHandler {
30
+ return async (c, next) => {
31
+ const requestId = c.req.header('x-request-id') || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
32
+ let requestedBy = 'system';
33
+
34
+ // component signature — verify against the SANITIZED body (xss ran first)
35
+ const sig = c.req.header('x-component-sig');
36
+ const componentDid = c.req.header('x-component-did');
37
+ if (sig && componentDid) {
38
+ const data = c.get('sanitizedBody') ?? {};
39
+ const verified = await verify(data, sig);
40
+ if (verified) {
41
+ requestedBy = componentDid;
42
+ }
43
+ }
44
+
45
+ // user DID from headers
46
+ const userDid = c.req.header('x-user-did');
47
+ if (userDid) {
48
+ requestedBy = userDid;
49
+ }
50
+
51
+ // authenticated user (security middleware set this upstream)
52
+ const user = c.get('user');
53
+ if (user?.did) {
54
+ requestedBy = user.did;
55
+ }
56
+
57
+ // Resolve tenant from the raw Host (single point). Multi-mode unknown host →
58
+ // 4xx fail-closed, no default-tenant fallback.
59
+ let instanceDid: string;
60
+ try {
61
+ instanceDid = await resolveTenantForHost(c.req.header('host'));
62
+ } catch (err) {
63
+ if (err instanceof TenantError && err.code === TENANT_HOST_UNRESOLVED) {
64
+ return c.json({ error: { code: err.code, message: err.message } }, 400);
65
+ }
66
+ throw err;
67
+ }
68
+
69
+ return context.run({ requestId, requestedBy, instanceDid }, async () => {
70
+ await next();
71
+ });
72
+ };
73
+ }