payment-kit 1.29.0 → 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 (312) 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/docs/2026-06-10-bundle-size-analysis.md +288 -0
  236. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  237. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  238. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  239. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  240. package/cloudflare/queue-runtime-mode.ts +13 -0
  241. package/cloudflare/run-build.js +31 -56
  242. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  243. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  244. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  245. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  246. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  247. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  248. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  249. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  250. package/cloudflare/shims/cron.ts +38 -158
  251. package/cloudflare/shims/events.ts +124 -0
  252. package/cloudflare/shims/fastq.ts +15 -1
  253. package/cloudflare/shims/nedb-storage.ts +16 -8
  254. package/cloudflare/shims/node-fetch.ts +35 -0
  255. package/cloudflare/shims/xss.ts +8 -0
  256. package/cloudflare/tenant-middleware.ts +36 -0
  257. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  258. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  259. package/cloudflare/worker.ts +204 -433
  260. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  261. package/jest.config.js +3 -1
  262. package/package.json +33 -38
  263. package/scripts/core-env-whitelist.json +1 -0
  264. package/scripts/e2e-12b-runtime.ts +149 -0
  265. package/scripts/e2e-core-config.ts +125 -0
  266. package/scripts/e2e-d1-tenancy.ts +116 -0
  267. package/scripts/e2e-d2-cron-queue.ts +139 -0
  268. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  269. package/scripts/e2e-hono-s2.ts +125 -0
  270. package/scripts/e2e-hono-s3e.ts +135 -0
  271. package/scripts/e2e-hono-s4.ts +114 -0
  272. package/scripts/e2e-migration-contract.ts +100 -0
  273. package/scripts/e2e-s0.ts +61 -0
  274. package/scripts/e2e-s1.ts +107 -0
  275. package/scripts/e2e-s2.ts +178 -0
  276. package/scripts/e2e-s3.ts +110 -0
  277. package/scripts/e2e-s4.ts +191 -0
  278. package/scripts/e2e-s5.ts +139 -0
  279. package/scripts/e2e-s6.ts +127 -0
  280. package/scripts/e2e-tenant-model.ts +119 -0
  281. package/scripts/e2e-tenant-worker.ts +199 -0
  282. package/scripts/gen-sql-migrations.js +46 -0
  283. package/scripts/phase8-codemod.js +219 -0
  284. package/scripts/phase9a-env-getters-codemod.js +82 -0
  285. package/scripts/scan-core-env.js +109 -0
  286. package/scripts/scan-tenant-queries.js +235 -0
  287. package/scripts/schema-drift-guard.ts +210 -0
  288. package/scripts/tenant-scan-whitelist.json +1 -0
  289. package/src/env.d.ts +13 -1
  290. package/tsconfig.json +1 -1
  291. package/api/src/libs/did-space.ts +0 -235
  292. package/api/src/libs/middleware.ts +0 -50
  293. package/api/src/libs/security.ts +0 -192
  294. package/api/src/queues/space.ts +0 -662
  295. package/api/src/routes/credit-tokens.ts +0 -38
  296. package/api/src/routes/exchange-rates.ts +0 -87
  297. package/api/src/routes/index.ts +0 -142
  298. package/api/src/routes/integrations/stripe.ts +0 -61
  299. package/api/src/routes/meters.ts +0 -274
  300. package/api/src/routes/passports.ts +0 -68
  301. package/api/src/routes/redirect.ts +0 -20
  302. package/api/src/routes/tool.ts +0 -65
  303. package/api/src/routes/webhook-endpoints.ts +0 -126
  304. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  305. package/cloudflare/shims/did-space-js.ts +0 -17
  306. package/cloudflare/shims/did-space.ts +0 -11
  307. package/cloudflare/shims/express-compat/index.ts +0 -80
  308. package/cloudflare/shims/express-compat/types.ts +0 -41
  309. package/cloudflare/shims/lock.ts +0 -115
  310. package/cloudflare/shims/queue.ts +0 -611
  311. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  312. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,194 @@
1
+ // Phase 11 (W2-3): secrets slot driver contract — per-tenant keyring.
2
+ //
3
+ // Replaces the process-level single key (cloudflare/shims/blocklet-sdk/security
4
+ // `_password`, initialized once from APP_PID's EK). Each tenant gets its own
5
+ // encryption key derived from its own EK, so one tenant's key can never decrypt
6
+ // another's ciphertext.
7
+ //
8
+ // Two surfaces, because the payment hot path (PaymentMethod.encrypt/decrypt
9
+ // Settings, getStripeClient, ~50 sync call sites) cannot become async without a
10
+ // large, risky ripple:
11
+ // - encrypt/decrypt (async): the full contract — lazy EK fetch + cache + TTL,
12
+ // and decrypt-failure forces one EK re-fetch (tolerates identity-side EK
13
+ // rotation). Hosts use this.
14
+ // - encryptSync/decryptSync: the hot path — uses an already-resolved key
15
+ // (default driver: the process key; keyring driver: the cached password,
16
+ // warmed via warmup()). Throws if the keyring key is cold (fail-closed).
17
+ // - warmup(instanceDid): async pre-resolve so the sync path is a cache hit
18
+ // (no-op for the single-tenant default driver).
19
+ //
20
+ // single mode (Blocklet Server) uses the default driver = the existing process
21
+ // `@blocklet/sdk/lib/security`, so existing ciphertext stays decryptable and
22
+ // behavior is unchanged.
23
+
24
+ /* eslint-disable max-classes-per-file */
25
+ import crypto from 'crypto';
26
+
27
+ import type { IdentityDriver } from './identity';
28
+
29
+ // crypto-js ships no type declarations; required untyped (same AES chain as the
30
+ // prior process security so ciphertext is interchangeable)
31
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
32
+ const CryptoJS: any = require('crypto-js');
33
+
34
+ export interface SecretsDriver {
35
+ /** async per-tenant encrypt — lazy-resolves the tenant key on first use */
36
+ encrypt(instanceDid: string, value: string): Promise<string>;
37
+ /** async per-tenant decrypt — re-fetches the EK once on failure (EK rotation tolerance) */
38
+ decrypt(instanceDid: string, value: string): Promise<string>;
39
+ /** sync hot path — requires the tenant key already resolved (warmup first for the keyring) */
40
+ encryptSync(instanceDid: string, value: string): string;
41
+ decryptSync(instanceDid: string, value: string): string;
42
+ /** pre-resolve the tenant key so the sync path is a cache hit (no-op for the default driver) */
43
+ warmup(instanceDid: string): Promise<void>;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // default driver — single-tenant, delegates to the process @blocklet/sdk security
48
+ // ---------------------------------------------------------------------------
49
+
50
+ class DefaultSecretsDriver implements SecretsDriver {
51
+ // lazily required so importing this module stays side-effect-free
52
+ private security() {
53
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
54
+ return require('@blocklet/sdk/lib/security').default ?? require('@blocklet/sdk/lib/security');
55
+ }
56
+
57
+ encryptSync(_instanceDid: string, value: string): string {
58
+ return this.security().encrypt(value);
59
+ }
60
+
61
+ decryptSync(_instanceDid: string, value: string): string {
62
+ return this.security().decrypt(value);
63
+ }
64
+
65
+ // eslint-disable-next-line require-await -- async contract; the single key is sync
66
+ async encrypt(instanceDid: string, value: string): Promise<string> {
67
+ return this.encryptSync(instanceDid, value);
68
+ }
69
+
70
+ // eslint-disable-next-line require-await -- async contract; the single key is sync
71
+ async decrypt(instanceDid: string, value: string): Promise<string> {
72
+ return this.decryptSync(instanceDid, value);
73
+ }
74
+
75
+ async warmup(): Promise<void> {
76
+ /* single key is always available — nothing to warm */
77
+ }
78
+ }
79
+
80
+ export function createDefaultSecretsDriver(): SecretsDriver {
81
+ return new DefaultSecretsDriver();
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // keyring driver — per-tenant key derived from each tenant's EK
86
+ // ---------------------------------------------------------------------------
87
+
88
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min
89
+
90
+ type CacheEntry = { password: string; expiresAt: number };
91
+
92
+ // AES decrypt with a wrong key can throw "Malformed UTF-8" or return empty,
93
+ // depending on the ciphertext bytes. Normalize both to '' so a wrong key is a
94
+ // deterministic failure (never the plaintext, never a crash) — this is what
95
+ // drives the decrypt-retry and what keeps cross-tenant decryption safe.
96
+ function safeUtf8(decrypted: any): string {
97
+ try {
98
+ return decrypted.toString(CryptoJS.enc.Utf8);
99
+ } catch {
100
+ return '';
101
+ }
102
+ }
103
+
104
+ function derivePassword(appEk: string, instanceDid: string): string {
105
+ // identical chain to the prior process security: PBKDF2(EK, salt) -> AES key.
106
+ // salt = the tenant DID (== blockletDid in single mode), so single-mode
107
+ // ciphertext written by the old path stays decryptable when the same EK is
108
+ // returned by identity.getAppEk(instanceDid).
109
+ return crypto.pbkdf2Sync(appEk, instanceDid, 256, 32, 'sha512').toString('hex');
110
+ }
111
+
112
+ class KeyringSecretsDriver implements SecretsDriver {
113
+ private identity: IdentityDriver;
114
+ private ttlMs: number;
115
+ private cache = new Map<string, CacheEntry>();
116
+
117
+ constructor(identity: IdentityDriver, opts?: { ttlMs?: number }) {
118
+ if (typeof identity.getAppEk !== 'function') {
119
+ throw new Error('createKeyringSecretsDriver: identity driver must provide getAppEk(instanceDid)');
120
+ }
121
+ this.identity = identity;
122
+ this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
123
+ }
124
+
125
+ private fresh(instanceDid: string): string | null {
126
+ const entry = this.cache.get(instanceDid);
127
+ if (entry && entry.expiresAt > Date.now()) return entry.password;
128
+ return null;
129
+ }
130
+
131
+ private async resolvePassword(instanceDid: string, force = false): Promise<string> {
132
+ if (!force) {
133
+ const cached = this.fresh(instanceDid);
134
+ if (cached) return cached;
135
+ }
136
+ const appEk = await this.identity.getAppEk!(instanceDid);
137
+ if (!appEk) {
138
+ throw new Error(`secrets: no EK for tenant ${instanceDid}`);
139
+ }
140
+ const password = derivePassword(appEk, instanceDid);
141
+ this.cache.set(instanceDid, { password, expiresAt: Date.now() + this.ttlMs });
142
+ return password;
143
+ }
144
+
145
+ async warmup(instanceDid: string): Promise<void> {
146
+ await this.resolvePassword(instanceDid);
147
+ }
148
+
149
+ encryptSync(instanceDid: string, value: string): string {
150
+ const password = this.fresh(instanceDid);
151
+ if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
152
+ return CryptoJS.AES.encrypt(value, password).toString();
153
+ }
154
+
155
+ decryptSync(instanceDid: string, value: string): string {
156
+ const password = this.fresh(instanceDid);
157
+ if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
158
+ return safeUtf8(CryptoJS.AES.decrypt(value, password));
159
+ }
160
+
161
+ async encrypt(instanceDid: string, value: string): Promise<string> {
162
+ const password = await this.resolvePassword(instanceDid);
163
+ return CryptoJS.AES.encrypt(value, password).toString();
164
+ }
165
+
166
+ async decrypt(instanceDid: string, value: string): Promise<string> {
167
+ let password = await this.resolvePassword(instanceDid);
168
+ let plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
169
+ if (!plain) {
170
+ // empty result == wrong key (likely rotated EK) -> force one re-fetch + retry
171
+ password = await this.resolvePassword(instanceDid, true);
172
+ plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
173
+ }
174
+ return plain;
175
+ }
176
+ }
177
+
178
+ export function createKeyringSecretsDriver(identity: IdentityDriver, opts?: { ttlMs?: number }): SecretsDriver {
179
+ return new KeyringSecretsDriver(identity, opts);
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // active driver + tenant-aware facade (resolves tenant from TenantContext)
184
+ // ---------------------------------------------------------------------------
185
+
186
+ let activeSecretsDriver: SecretsDriver = createDefaultSecretsDriver();
187
+
188
+ export function setSecretsDriver(driver: SecretsDriver): void {
189
+ activeSecretsDriver = driver;
190
+ }
191
+
192
+ export function getSecretsDriver(): SecretsDriver {
193
+ return activeSecretsDriver;
194
+ }
@@ -1,68 +1,184 @@
1
1
  import { env } from '@blocklet/sdk/lib/env';
2
2
 
3
- export const paymentStatCronTime: string = '0 1 0 * * *'; // 默认每天一次,计算前一天的
4
- export const subscriptionCronTime: string = process.env.SUBSCRIPTION_CRON_TIME || '0 */30 * * * *'; // 默认每 30 min 行一次
5
- export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
6
- export const expiredSessionCleanupCronTime: string = process.env.EXPIRED_SESSION_CLEANUP_CRON_TIME || '0 1 * * * *'; // 默认每小时执行一次
7
- export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
8
- export const stripeInvoiceCronTime: string = process.env.STRIPE_INVOICE_CRON_TIME || '0 */30 * * * *'; // 默认每 30min 执行一次
9
- export const stripePaymentCronTime: string = process.env.STRIPE_PAYMENT_CRON_TIME || '0 */20 * * * *'; // 默认每 20min 执行一次
10
- export const stripeSubscriptionCronTime: string = process.env.STRIPE_SUBSCRIPTION_CRON_TIME || '0 10 */8 * * *'; // 默认每 8小时 执行一次
11
- export const revokeStakeCronTime: string = process.env.REVOKE_STAKE_CRON_TIME || '0 */5 * * * *'; // 默认每 5 min 执行一次
12
- export const daysUntilCancel: string | undefined = process.env.DAYS_UNTIL_CANCEL;
13
- export const meteringSubscriptionDetectionCronTime: string =
14
- process.env.METERING_SUBSCRIPTION_DETECTION_CRON_TIME || '0 0 10 * * *'; // 默认每天 10:00 执行
15
- export const overdueDetectionCronTime: string = process.env.OVERDUE_DETECTION_CRON_TIME || '0 0 10 * * *'; // 默认每天 10:00 执行
16
- export const overdueThreshold: number = process.env.OVERDUE_THRESHOLD ? +process.env.OVERDUE_THRESHOLD : 5; // 默认超额阈值为 5
17
- export const depositVaultCronTime: string = process.env.DEPOSIT_VAULT_CRON_TIME || '0 */5 * * * *'; // 默认每 5 min 执行一次
18
- export const creditConsumptionCronTime: string = process.env.CREDIT_CONSUMPTION_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
19
- export const vendorStatusCheckCronTime: string = process.env.VENDOR_STATUS_CHECK_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
20
- export const vendorReturnScanCronTime: string = process.env.VENDOR_RETURN_SCAN_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
21
- export const iapReconcileCronTime: string = process.env.IAP_RECONCILE_CRON_TIME || '0 */5 * * * *'; // 默认每 5 min 执行一次:webhook 兜底,拉 App Store / Google Play 订阅最新状态
22
- export const eventRetryCronTime: string = process.env.EVENT_RETRY_CRON_TIME || '30 */5 * * * *'; // 默认每 5 min 执行一次(错开整点避开 iap-reconcile):扫 pending_webhooks>0 的事件兜底投递
23
- export const quoteCleanupCronTime: string = process.env.QUOTE_CLEANUP_CRON_TIME || '0 0 2 * * *'; // 默认每天凌晨 2 点执行一次
24
- export const vendorTimeoutMinutes: number = process.env.VENDOR_TIMEOUT_MINUTES
25
- ? +process.env.VENDOR_TIMEOUT_MINUTES
26
- : 10; // 默认 10 分钟超时
27
- export const webhookAlertWindowMinutes: number = process.env.WEBHOOK_ALERT_WINDOW_MINUTES
28
- ? +process.env.WEBHOOK_ALERT_WINDOW_MINUTES
29
- : 10; // webhook 连续失败告警时间窗口,默认 10 分钟
30
- export const webhookAlertMinFailures: number = process.env.WEBHOOK_ALERT_MIN_FAILURES
31
- ? +process.env.WEBHOOK_ALERT_MIN_FAILURES
32
- : 3; // webhook 触发告警的最小失败次数,默认 3
33
-
34
- export const shortUrlApiKey: string = process.env.SHORT_URL_API_KEY || '';
35
- export const shortUrlDomain: string = process.env.SHORT_URL_DOMAIN || 's.abtnet.io';
3
+ // ──────────────────────────────────────────────────────────────────────────
4
+ // Phase 8 (W2′): the injected-config slot, made authoritative here.
5
+ //
6
+ // libs/env.ts is the ONE module the core-env scanner exempts the single
7
+ // allowed reader of process.env / the CF env mirror. Phase 8 converges every
8
+ // scattered `process.env.X` read in api/src INTO the accessors below so the
9
+ // whitelist goes to zero.
10
+ //
11
+ // The factory (createEmbeddedPaymentService) calls setCoreConfig(config) so the
12
+ // injected config object becomes the source of truth. Reads fall back to
13
+ // process.env when a key is absent from the injected config — which keeps every
14
+ // host working unchanged: the blocklet server populates process.env natively,
15
+ // and the CF worker mirrors CF env into process.env per request (that mirror is
16
+ // deleted in Phase 12, once the worker routes through the factory config slot).
17
+ // ──────────────────────────────────────────────────────────────────────────
18
+ let activeConfig: Record<string, any> | undefined;
19
+
20
+ /** Wire the injected config object (factory only). Pass undefined to clear (tests). */
21
+ export function setCoreConfig(config: Record<string, any> | undefined): void {
22
+ activeConfig = config;
23
+ }
24
+
25
+ /** The injected config object, or undefined before the factory has run. */
26
+ export function getCoreConfig(): Record<string, any> | undefined {
27
+ return activeConfig;
28
+ }
29
+
30
+ /**
31
+ * Read a config key: the injected config wins; otherwise the process.env
32
+ * fallback (blocklet server native / worker mirror). Returns a string or
33
+ * undefined — callers parse/typecheck. This is the single boundary read.
34
+ */
35
+ export function readConfig(key: string): string | undefined {
36
+ const injected = activeConfig?.[key];
37
+ if (injected !== undefined && injected !== null) return String(injected);
38
+ const fromEnv = process.env[key];
39
+ return fromEnv === undefined ? undefined : fromEnv;
40
+ }
41
+
42
+ /** True iff the key is present (non-empty) in injected config or process.env. */
43
+ export function hasConfig(key: string): boolean {
44
+ const v = readConfig(key);
45
+ return v !== undefined && v !== '';
46
+ }
47
+
48
+ // ──────────────────────────────────────────────────────────────────────────
49
+ // P1 (9a review fix): these were import-time `process.env` consts — frozen
50
+ // before the factory could setCoreConfig, so injected config could never
51
+ // override them (now that 9a bundles the canonical core into the published
52
+ // package, that mattered for arc). They are LAZY getters now, reading via the
53
+ // readConfig boundary, honored at call time. Consumers call them at use time.
54
+ // ──────────────────────────────────────────────────────────────────────────
55
+ const numConfig = (key: string, fallback: number): number => {
56
+ const v = readConfig(key);
57
+ return v ? +v : fallback;
58
+ };
59
+
60
+ export const paymentStatCronTime = (): string => '0 1 0 * * *'; // 默认每天一次,计算前一天的
61
+ export const subscriptionCronTime = (): string => readConfig('SUBSCRIPTION_CRON_TIME') || '0 */30 * * * *';
62
+ export const notificationCronTime = (): string => readConfig('NOTIFICATION_CRON_TIME') || '0 5 */6 * * *';
63
+ export const expiredSessionCleanupCronTime = (): string =>
64
+ readConfig('EXPIRED_SESSION_CLEANUP_CRON_TIME') || '0 1 * * * *';
65
+ export const notificationCronConcurrency = (): number => Number(readConfig('NOTIFICATION_CRON_CONCURRENCY')) || 8;
66
+ export const stripeInvoiceCronTime = (): string => readConfig('STRIPE_INVOICE_CRON_TIME') || '0 */30 * * * *';
67
+ export const stripePaymentCronTime = (): string => readConfig('STRIPE_PAYMENT_CRON_TIME') || '0 */20 * * * *';
68
+ export const stripeSubscriptionCronTime = (): string => readConfig('STRIPE_SUBSCRIPTION_CRON_TIME') || '0 10 */8 * * *';
69
+ export const revokeStakeCronTime = (): string => readConfig('REVOKE_STAKE_CRON_TIME') || '0 */5 * * * *';
70
+ export const daysUntilCancel = (): string | undefined => readConfig('DAYS_UNTIL_CANCEL');
71
+ export const meteringSubscriptionDetectionCronTime = (): string =>
72
+ readConfig('METERING_SUBSCRIPTION_DETECTION_CRON_TIME') || '0 0 10 * * *';
73
+ export const overdueDetectionCronTime = (): string => readConfig('OVERDUE_DETECTION_CRON_TIME') || '0 0 10 * * *';
74
+ export const overdueThreshold = (): number => numConfig('OVERDUE_THRESHOLD', 5);
75
+ export const depositVaultCronTime = (): string => readConfig('DEPOSIT_VAULT_CRON_TIME') || '0 */5 * * * *';
76
+ export const creditConsumptionCronTime = (): string => readConfig('CREDIT_CONSUMPTION_CRON_TIME') || '0 */10 * * * *';
77
+ export const vendorStatusCheckCronTime = (): string => readConfig('VENDOR_STATUS_CHECK_CRON_TIME') || '0 */10 * * * *';
78
+ export const vendorReturnScanCronTime = (): string => readConfig('VENDOR_RETURN_SCAN_CRON_TIME') || '0 */10 * * * *';
79
+ export const iapReconcileCronTime = (): string => readConfig('IAP_RECONCILE_CRON_TIME') || '0 */5 * * * *';
80
+ export const eventRetryCronTime = (): string => readConfig('EVENT_RETRY_CRON_TIME') || '30 */5 * * * *';
81
+ export const quoteCleanupCronTime = (): string => readConfig('QUOTE_CLEANUP_CRON_TIME') || '0 0 2 * * *';
82
+ export const vendorTimeoutMinutes = (): number => numConfig('VENDOR_TIMEOUT_MINUTES', 10);
83
+ export const webhookAlertWindowMinutes = (): number => numConfig('WEBHOOK_ALERT_WINDOW_MINUTES', 10);
84
+ export const webhookAlertMinFailures = (): number => numConfig('WEBHOOK_ALERT_MIN_FAILURES', 3);
85
+
86
+ export const shortUrlApiKey = (): string => readConfig('SHORT_URL_API_KEY') || '';
87
+ export const shortUrlDomain = (): string => readConfig('SHORT_URL_DOMAIN') || 's.abtnet.io';
36
88
 
37
89
  // sequelize 配置相关
38
- export const sequelizeOptionsPoolMin: number = process.env.SEQUELIZE_OPTIONS_POOL_MIN
39
- ? +process.env.SEQUELIZE_OPTIONS_POOL_MIN
40
- : 0;
41
- export const sequelizeOptionsPoolMax: number = process.env.SEQUELIZE_OPTIONS_POOL_MAX
42
- ? +process.env.SEQUELIZE_OPTIONS_POOL_MAX
43
- : 5;
44
- export const sequelizeOptionsPoolIdle: number = process.env.SEQUELIZE_OPTIONS_POOL_IDLE
45
- ? +process.env.SEQUELIZE_OPTIONS_POOL_IDLE
46
- : 10 * 1000;
47
-
48
- export const updateDataConcurrency: number = process.env.UPDATE_DATA_CONCURRENCY
49
- ? +process.env.UPDATE_DATA_CONCURRENCY
50
- : 5; // 默认并发数为 5
90
+ export const sequelizeOptionsPoolMin = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_MIN', 0);
91
+ export const sequelizeOptionsPoolMax = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_MAX', 5);
92
+ export const sequelizeOptionsPoolIdle = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_IDLE', 10 * 1000);
93
+
94
+ export const updateDataConcurrency = (): number => numConfig('UPDATE_DATA_CONCURRENCY', 5);
51
95
 
52
96
  // When set to 'true' or '1', the system stops accepting new orders.
53
97
  // Existing checkout sessions can still be viewed but new submissions will be rejected.
54
- export const stopAcceptingOrders: boolean =
55
- process.env.PAYMENT_KIT_STOP_ACCEPTING_ORDERS === 'true' || process.env.PAYMENT_KIT_STOP_ACCEPTING_ORDERS === '1';
98
+ export const stopAcceptingOrders = (): boolean =>
99
+ readConfig('PAYMENT_KIT_STOP_ACCEPTING_ORDERS') === 'true' || readConfig('PAYMENT_KIT_STOP_ACCEPTING_ORDERS') === '1';
56
100
 
57
- export const exchangeRateCacheTTLSeconds: number = process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS
58
- ? +process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS
59
- : 10 * 60;
101
+ export const exchangeRateCacheTTLSeconds = (): number => numConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS', 10 * 60);
60
102
 
61
103
  // System-level maximum pending amount limit (in token format, e.g., "10")
62
104
  // Default is 0 (disabled). Set PAYMENT_KIT_MAX_PENDING_AMOUNT to enable this limit.
63
- export const systemMaxPendingAmount: number = process.env.PAYMENT_KIT_MAX_PENDING_AMOUNT
64
- ? +process.env.PAYMENT_KIT_MAX_PENDING_AMOUNT
65
- : 5;
105
+ export const systemMaxPendingAmount = (): number => numConfig('PAYMENT_KIT_MAX_PENDING_AMOUNT', 5);
106
+
107
+ // Whether a locked price may still be edited. Lazy (reads injected config via
108
+ // the boundary at call time) like every other Phase 8 accessor.
109
+ export const allowChangeLockedPrice = (): boolean => readConfig('PAYMENT_CHANGE_LOCKED_PRICE') === '1';
110
+
111
+ // ──────────────────────────────────────────────────────────────────────────
112
+ // Phase 8 (W2′): converged accessors. Every read below used to live inline in
113
+ // api/src as a direct process.env / __CF_ENV__ access (the 57-entry whitelist).
114
+ // They are LAZY (functions, not import-time consts) so the injected config —
115
+ // wired by the factory AFTER module import — is honored at call time, and so
116
+ // tests that flip process.env at runtime keep working.
117
+ // ──────────────────────────────────────────────────────────────────────────
118
+
119
+ // -- runtime mode / environment --
120
+ export const blockletMode = (): string | undefined => readConfig('BLOCKLET_MODE');
121
+ export const isProduction = (): boolean => blockletMode() === 'production';
122
+ export const nodeEnv = (): string | undefined => readConfig('NODE_ENV');
123
+ export const isTestEnv = (): boolean => nodeEnv() === 'test';
124
+ export const isDevelopmentEnv = (): boolean => nodeEnv() === 'development';
125
+ export const enableDevFakeAuth = (): boolean => readConfig('ENABLE_DEV_FAKE_AUTH') === '1';
126
+
127
+ // -- tenant mode-source (getTenantMode / getDefaultInstanceDid read these) --
128
+ export const tenantModeRaw = (): string | undefined => readConfig('PAYMENT_TENANT_MODE');
129
+ export const blockletAppPid = (): string | undefined => readConfig('BLOCKLET_APP_PID');
130
+
131
+ // -- app identity / urls --
132
+ export const blockletAppId = (): string | undefined => readConfig('BLOCKLET_APP_ID');
133
+ export const blockletAppName = (): string | undefined => readConfig('BLOCKLET_APP_NAME');
134
+ export const blockletAppUrl = (): string | undefined => readConfig('BLOCKLET_APP_URL');
135
+ export const blockletAppHost = (): string | undefined => readConfig('BLOCKLET_APP_HOST');
136
+ export const blockletAppDir = (): string | undefined => readConfig('BLOCKLET_APP_DIR');
137
+ export const blockletPort = (): string | undefined => readConfig('BLOCKLET_PORT');
138
+ export const blockletMountPoints = (): string | undefined => readConfig('BLOCKLET_MOUNT_POINTS');
139
+
140
+ // -- integrations --
141
+ export const appStoreWriteEnabled = (): boolean => readConfig('APP_STORE_WRITE_ENABLED') === 'true';
142
+ export const appStoreSkipSignatureVerify = (): boolean => readConfig('APP_STORE_SKIP_SIGNATURE_VERIFY') === 'true';
143
+ export const googlePubsubSkipSignatureVerify = (): boolean =>
144
+ readConfig('GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY') === 'true';
145
+ export const googlePubsubPushServiceAccount = (): string | undefined =>
146
+ readConfig('GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT');
147
+ export const googlePubsubAllowUnverifiedSender = (): boolean =>
148
+ readConfig('GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER') === 'true';
149
+ export const googlePlayWebhookUrl = (): string | undefined => readConfig('GOOGLE_PLAY_WEBHOOK_URL');
150
+ export const stripeWebhookSecret = (): string | undefined => readConfig('STRIPE_WEBHOOK_SECRET');
151
+ export const iapReconcileBatchSize = (): number => Number(readConfig('IAP_RECONCILE_BATCH_SIZE') ?? '100');
152
+
153
+ // -- payment params --
154
+ export const paymentBillingThreshold = (): number => +(readConfig('PAYMENT_BILLING_THRESHOLD') as string);
155
+ export const paymentMinStakeAmount = (): number => +(readConfig('PAYMENT_MIN_STAKE_AMOUNT') as string);
156
+ export const paymentDaysUntilDue = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_DUE');
157
+ export const paymentDaysUntilCancel = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_CANCEL');
158
+ export const paymentReloadSubscriptionJobs = (): boolean => readConfig('PAYMENT_RELOAD_SUBSCRIPTION_JOBS') === '1';
159
+ export const paymentRateVolatilityThreshold = (): string | undefined => readConfig('PAYMENT_RATE_VOLATILITY_THRESHOLD');
160
+ export const paymentLivemode = (): boolean => readConfig('PAYMENT_LIVEMODE') !== 'false';
161
+
162
+ // -- credit queue --
163
+ export const creditLowBalanceThresholdPercentage = (): number =>
164
+ parseInt(readConfig('CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE') || '10', 10);
165
+ export const creditBatchSize = (): number => Math.max(1, parseInt(readConfig('CREDIT_BATCH_SIZE') || '50', 10));
166
+ export const creditBatchWindowMs = (): number =>
167
+ Math.max(10, parseInt(readConfig('CREDIT_BATCH_WINDOW_MS') || '3000', 10));
168
+ export const creditQueueConcurrency = (): number =>
169
+ Math.max(1, Math.min(20, parseInt(readConfig('CREDIT_QUEUE_CONCURRENCY') || '5', 10) || 5));
170
+
171
+ // -- exchange rate cache TTL source (the value itself is exchangeRateCacheTTLSeconds above) --
172
+ export const exchangeRateCacheTTLFromEnv = (): boolean => hasConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS');
173
+
174
+ // -- store / sequelize logging --
175
+ export const sqlLog = (): boolean => readConfig('SQL_LOG') === '1';
176
+ export const sqlBenchmark = (): boolean => readConfig('SQL_BENCHMARK') === '1';
177
+
178
+ // -- CF worker runtime detection + env mirror (set by cloudflare/worker.ts on
179
+ // globalThis; the boundary so core never reads the global directly) --
180
+ export const cfEnv = (): any => (globalThis as any).__CF_ENV__;
181
+ export const isCfWorker = (): boolean => !!cfEnv();
66
182
 
67
183
  export default {
68
184
  ...env,
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-await-in-loop */
2
2
  import BigNumber from 'bignumber.js';
3
+ import { exchangeRateCacheTTLFromEnv, exchangeRateCacheTTLSeconds } from '../env';
3
4
  import { ExchangeRateProvider } from '../../store/models/exchange-rate-provider';
4
5
  import logger from '../logger';
5
6
  import { events } from '../event';
@@ -8,15 +9,15 @@ import { SymbolNotSupportedError } from './types';
8
9
  import { TokenDataProvider } from './token-data-provider';
9
10
  import { CoinGeckoProvider } from './coingecko-provider';
10
11
  import { CoinMarketCapProvider } from './coinmarketcap-provider';
11
- import { exchangeRateCacheTTLSeconds } from '../env';
12
12
 
13
13
  interface CacheEntry {
14
14
  data: ExchangeRateResult;
15
15
  timestamp: number;
16
16
  }
17
17
 
18
- // Cache TTL from environment variable, default 10 minutes
19
- const CACHE_TTL_MS = (exchangeRateCacheTTLSeconds || 10 * 60) * 1000;
18
+ // Cache TTL from config, default 10 minutes. Lazy so injected config (set after
19
+ // module import) is honored not frozen in a module-level const.
20
+ const cacheTtlMs = (): number => (exchangeRateCacheTTLSeconds() || 10 * 60) * 1000;
20
21
  const MAX_RATE_AGE_MS = 5 * 60 * 1000; // 5 minutes
21
22
  const MAX_DEVIATION_PERCENT = 5; // 5%
22
23
  const HISTORY_WINDOW_SIZE = 10;
@@ -185,7 +186,7 @@ export class ExchangeRateService {
185
186
 
186
187
  // Check cache first
187
188
  const cached = this.cache.get(cacheKey);
188
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
189
+ if (cached && Date.now() - cached.timestamp < cacheTtlMs()) {
189
190
  logger.debug('Exchange rate cache hit', { symbol, age: Date.now() - cached.timestamp });
190
191
  return cached.data;
191
192
  }
@@ -575,8 +576,8 @@ export function getExchangeRateService(): ExchangeRateService {
575
576
  if (!serviceInstance) {
576
577
  serviceInstance = new ExchangeRateService();
577
578
  logger.info('Exchange rate service initialized', {
578
- cache_ttl_seconds: CACHE_TTL_MS / 1000,
579
- cache_ttl_source: process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS ? 'env' : 'default',
579
+ cache_ttl_seconds: cacheTtlMs() / 1000,
580
+ cache_ttl_source: exchangeRateCacheTTLFromEnv() ? 'env' : 'default',
580
581
  });
581
582
  }
582
583
  return serviceInstance;
@@ -0,0 +1,50 @@
1
+ // D5 — the Web-Fetch entry for hosts that own their own app shell (arc-node's
2
+ // registerPrefixHandler): `svc.http.fetch(req, {basePath})` with no express bridge.
3
+ //
4
+ // Phase 4 (express→hono): the loopback http.Server + express app are gone, so this
5
+ // is just a base-strip wrapper over `honoApp.fetch`. The outward signature +
6
+ // strip semantics are unchanged (arc consumes this contract); raw-body fidelity
7
+ // (Stripe webhook signature) holds because the stripped path reuses the exact
8
+ // request bytes/headers, and status/Set-Cookie/redirect come from the hono Response.
9
+
10
+ import type { Hono } from 'hono';
11
+
12
+ /** Options for svc.http.fetch — basePath is stripped to reach the internal /api/*. */
13
+ export interface PaymentFetchOptions {
14
+ /** the host's mount prefix (e.g. "/.well-known/payment"); stripped, no alias special-case */
15
+ basePath?: string;
16
+ }
17
+
18
+ export interface FetchHandler {
19
+ (request: Request, opts?: PaymentFetchOptions): Promise<Response>;
20
+ /** teardown hook (host lifecycle). No-op now that there is no loopback server. */
21
+ close(): Promise<void>;
22
+ }
23
+
24
+ export function createFetchHandler(app: Hono): FetchHandler {
25
+ const handler = (async (request: Request, opts?: PaymentFetchOptions): Promise<Response> => {
26
+ const url = new URL(request.url);
27
+
28
+ // ① base-strip: remove basePath → internal /api/* path (no alias special-case).
29
+ // Precise segment-boundary check (=== basePath || startsWith(basePath + '/'))
30
+ // so "/foo" never matches "/foobar" — byte-identical to the old loopback strip.
31
+ const basePath = opts?.basePath ?? '';
32
+ if (basePath && (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`))) {
33
+ url.pathname = url.pathname.slice(basePath.length) || '/';
34
+ // ② rebuild the request at the stripped URL. The body is read ONCE into a
35
+ // fixed buffer (raw bytes preserved for Stripe webhook signature) and the
36
+ // original headers/method are carried verbatim (Host preserved).
37
+ const method = request.method.toUpperCase();
38
+ const hasBody = method !== 'GET' && method !== 'HEAD';
39
+ const body = hasBody ? await request.arrayBuffer() : undefined;
40
+ return app.fetch(new Request(url.toString(), { method, headers: request.headers, body }));
41
+ }
42
+
43
+ // no strip needed — hand the request straight to hono (it owns body parsing).
44
+ return app.fetch(request);
45
+ }) as FetchHandler;
46
+
47
+ handler.close = (): Promise<void> => Promise.resolve();
48
+
49
+ return handler;
50
+ }
@@ -1,4 +1,4 @@
1
- import { component } from '@blocklet/sdk';
1
+ import component from '@blocklet/sdk/lib/component';
2
2
  import type { LiteralUnion } from 'type-fest';
3
3
  import { withQuery } from 'ufo';
4
4