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,72 @@
1
+ // Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/csrf.js.
2
+ //
3
+ // The crypto core (sign / verify / getCsrfSecret) is framework-agnostic and is
4
+ // REUSED VERBATIM from the SDK — tokens are byte-identical and interchangeable
5
+ // across the express and hono engines (proven in spikes/csrf-parity.mjs, §3.2).
6
+ // Only the express plumbing (req.cookies / res.cookie / res.status().send) is
7
+ // replaced with hono/cookie helpers + c.text(403). The shouldGenerateToken
8
+ // (GET) / shouldVerifyToken (mutating + cookie present + non-/mcp + non-didwallet)
9
+ // semantics are preserved faithfully.
10
+ import type { MiddlewareHandler } from 'hono';
11
+ import { getCookie, setCookie } from 'hono/cookie';
12
+ // eslint-disable-next-line import/no-extraneous-dependencies
13
+ import { sign, verify, getCsrfSecret } from '@blocklet/sdk/lib/util/csrf';
14
+ // eslint-disable-next-line import/no-extraneous-dependencies
15
+ import { isDidWalletConnect } from '@blocklet/sdk/lib/util/wallet';
16
+
17
+ const isEmpty = (v: unknown): boolean => v === undefined || v === null || v === '';
18
+
19
+ // Express SDK: shouldGenerateToken === GET; shouldVerifyToken === mutating
20
+ // method AND an x-csrf-token cookie already exists AND path is not /mcp AND the
21
+ // caller is not a DID Wallet connect request.
22
+ const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
23
+
24
+ /**
25
+ * hono csrf middleware. Faithful to the SDK express version:
26
+ * - GET: if a login_token cookie exists, (re)issue x-csrf-token = sign(secret,
27
+ * login_token) when it differs from the current cookie. {sameSite:'Strict',
28
+ * secure:true} matches the express res.cookie attributes.
29
+ * - mutating: only ENFORCED when a login_token AND an x-csrf-token cookie are
30
+ * both present and the path is not /mcp and the caller is not a DID Wallet
31
+ * (parity with the SDK — absent cookie => skip, never reject). The header
32
+ * must equal the cookie and verify() against the login_token, else 403.
33
+ */
34
+ export function csrf(): MiddlewareHandler {
35
+ // async (no await): the SDK crypto core is synchronous, but the handler mixes a
36
+ // sync Response (c.text 403) with next()'s promise — async unifies the return
37
+ // type to the MiddlewareHandler contract.
38
+ // eslint-disable-next-line require-await
39
+ return async (c, next) => {
40
+ const method = c.req.method.toUpperCase();
41
+ const loginToken = getCookie(c, 'login_token');
42
+ const existingCsrf = getCookie(c, 'x-csrf-token');
43
+
44
+ if (method === 'GET') {
45
+ if (loginToken) {
46
+ const newCsrf = sign(getCsrfSecret(), loginToken);
47
+ if (newCsrf !== existingCsrf) {
48
+ setCookie(c, 'x-csrf-token', newCsrf, { sameSite: 'Strict', secure: true });
49
+ }
50
+ }
51
+ return next();
52
+ }
53
+
54
+ if (MUTATING.includes(method)) {
55
+ // shouldVerifyToken parity: skip (do NOT reject) when the SDK would skip.
56
+ if (c.req.path.includes('/mcp')) return next();
57
+ if (isEmpty(loginToken)) return next();
58
+ if (isEmpty(existingCsrf)) return next();
59
+ if (isDidWalletConnect(c.req.header())) return next();
60
+
61
+ const headerCsrf = c.req.header('x-csrf-token');
62
+ if (existingCsrf === headerCsrf && verify(getCsrfSecret(), existingCsrf as string, loginToken as string)) {
63
+ return next();
64
+ }
65
+ return c.text('Invalid request: csrf token mismatch, please refresh the page try again', 403);
66
+ }
67
+
68
+ return next();
69
+ };
70
+ }
71
+
72
+ export default csrf;
@@ -0,0 +1,194 @@
1
+ // Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/fallback.js.
2
+ //
3
+ // SPA HTML fallback: serve index.html for html GET/HEAD requests with OG meta +
4
+ // theme styles/script + blocklet.js injection. The injection logic is ported
5
+ // VERBATIM from the SDK (same helpers: getBlockletSettings / getBlockletJs / env,
6
+ // @blocklet/theme buildThemeStyles+buildThemeScript, lodash escape) — only the
7
+ // express req/res plumbing becomes hono (c.req / c.html). The express-only test
8
+ // hook (`next(source)` in NODE_ENV=test) is dropped. did-pay calls this with no
9
+ // getPageData, so pageData defaults to {} (title/description from env). Inert
10
+ // until SPA serving moves off the bridge (Phase 4); forked + unit-tested now.
11
+ import fs from 'fs';
12
+ import { join } from 'path';
13
+ import crypto from 'crypto';
14
+ import type { MiddlewareHandler, Context } from 'hono';
15
+ // eslint-disable-next-line import/no-extraneous-dependencies
16
+ import { joinURL } from 'ufo';
17
+ import escape from 'lodash/escape';
18
+ // eslint-disable-next-line import/no-extraneous-dependencies
19
+ import { RESOURCE_PATTERN } from '@blocklet/constant';
20
+ // eslint-disable-next-line import/no-extraneous-dependencies
21
+ import { buildThemeStyles, buildThemeScript } from '@blocklet/theme';
22
+ // eslint-disable-next-line import/no-extraneous-dependencies
23
+ import { env, getBlockletSettings, getBlockletJs } from '@blocklet/sdk/lib/config';
24
+ // eslint-disable-next-line import/no-extraneous-dependencies
25
+ import { SERVICE_PREFIX } from '@blocklet/sdk/lib/util/constants';
26
+ import { readConfig, isTestEnv } from '../../libs/env';
27
+
28
+ interface PageData {
29
+ title?: string;
30
+ description?: string;
31
+ ogImage?: string;
32
+ embed?: string;
33
+ }
34
+
35
+ interface FallbackOptions {
36
+ root?: string;
37
+ getPageData?: (c: Context) => Promise<PageData> | PageData;
38
+ maxLength?: number;
39
+ timeout?: number;
40
+ cacheTtl?: number;
41
+ injectBlockletJs?: boolean;
42
+ }
43
+
44
+ const DEFAULT_CACHE_TTL = 60 * 1000;
45
+ const cache = new Map<
46
+ string,
47
+ { html: string; timestamp: number; etag: string; pageGroup: string; pathPrefix: string }
48
+ >();
49
+ const cacheEnabled = readConfig('FALLBACK_CACHE_ENABLED') === 'true' || isTestEnv();
50
+
51
+ const TITLE_TAG_REGEX = /<title>(.+)<\/title>/;
52
+ const HEAD_END_TAG = '</head>';
53
+
54
+ const buildOpenGraph = (
55
+ pageData: Required<Pick<PageData, 'title' | 'description' | 'ogImage'>>,
56
+ appUrl: string
57
+ ): string =>
58
+ [
59
+ `<meta property="og:title" content="${pageData.title}" data-react-helmet="true" />`,
60
+ `<meta property="og:description" content="${pageData.description}" data-react-helmet="true" />`,
61
+ '<meta property="og:type" content="website" data-react-helmet="true" />',
62
+ `<meta property="og:url" content="${appUrl}" data-react-helmet="true" />`,
63
+ `<meta property="og:image" content="${pageData.ogImage}" data-react-helmet="true" />`,
64
+ '<meta name="twitter:card" content="summary_large_image" data-react-helmet="true" />',
65
+ ].join('\n');
66
+
67
+ const validatePageData = (data: PageData, maxLength: number): void => {
68
+ if (data.title && data.title.length > maxLength) throw new Error('Title too long');
69
+ if (data.description && data.description.length > maxLength) throw new Error('Description too long');
70
+ };
71
+
72
+ const generateETag = (content: string): string => `W/"${crypto.createHash('sha1').update(content).digest('base64')}"`;
73
+
74
+ const getCacheKey = (pathname: string, filePath: string, pageGroup: string, pathPrefix: string): string =>
75
+ crypto.createHash('sha1').update(`${pathname}:${filePath}:${pageGroup}:${pathPrefix}`).digest('base64');
76
+
77
+ const tryWithTimeout = <T>(asyncFn: () => Promise<T> | T, timeout: number): Promise<T> =>
78
+ new Promise<T>((resolve, reject) => {
79
+ const timer = setTimeout(() => reject(new Error(`Operation timed out after ${timeout} ms`)), timeout);
80
+ Promise.resolve()
81
+ .then(asyncFn)
82
+ .then((result) => resolve(result))
83
+ .catch((err) => reject(err))
84
+ .finally(() => clearTimeout(timer));
85
+ });
86
+
87
+ function acceptsHtml(accept: string): boolean {
88
+ if (!accept) return true;
89
+ return accept.includes('text/html') || accept.includes('application/xhtml+xml') || accept.includes('*/*');
90
+ }
91
+
92
+ export function fallback(file: string, options: FallbackOptions = {}): MiddlewareHandler {
93
+ const filePath = options.root ? join(options.root, file) : file;
94
+ if (!fs.existsSync(filePath)) {
95
+ throw new Error(`Fallback file not found at: ${filePath}`);
96
+ }
97
+
98
+ return async (c, next) => {
99
+ const method = c.req.method.toUpperCase();
100
+ if (
101
+ (method !== 'GET' && method !== 'HEAD') ||
102
+ !acceptsHtml(c.req.header('accept') || '') ||
103
+ RESOURCE_PATTERN.test(c.req.path)
104
+ ) {
105
+ return next();
106
+ }
107
+
108
+ const pageGroup = c.req.header('x-page-group') || '';
109
+ const pathPrefix = c.req.header('x-path-prefix') || '';
110
+ const cacheKey = getCacheKey(c.req.path, filePath, pageGroup, pathPrefix);
111
+ const { theme } = getBlockletSettings();
112
+
113
+ if (cacheEnabled) {
114
+ const cached = cache.get(cacheKey);
115
+ const cacheTtl = options.cacheTtl || DEFAULT_CACHE_TTL;
116
+ if (
117
+ cached &&
118
+ Date.now() - cached.timestamp < cacheTtl &&
119
+ cached.pageGroup === pageGroup &&
120
+ cached.pathPrefix === pathPrefix
121
+ ) {
122
+ c.header('X-Cache', 'HIT');
123
+ c.header('ETag', cached.etag);
124
+ return c.html(cached.html);
125
+ }
126
+ }
127
+
128
+ const pageData: PageData = await tryWithTimeout(
129
+ options.getPageData ? () => options.getPageData!(c) : () => Promise.resolve({}),
130
+ options.timeout || 5000
131
+ );
132
+ validatePageData(pageData, options.maxLength || 1000);
133
+ const title = escape(pageData.title || (env as any).appName);
134
+ const description = escape(pageData.description || (env as any).appDescription);
135
+ const ogImage = pageData.ogImage || joinURL((env as any).appUrl || '/', SERVICE_PREFIX, '/blocklet/og.png');
136
+
137
+ let source = await fs.promises.readFile(filePath, 'utf8');
138
+ if (title) {
139
+ source = source.includes('<title>')
140
+ ? source.replace(TITLE_TAG_REGEX, `<title>${title}</title>`)
141
+ : source.replace(HEAD_END_TAG, `<title>${title}</title>${HEAD_END_TAG}`);
142
+ }
143
+ if (description && !source.includes('<meta name="description"')) {
144
+ source = source.replace(
145
+ HEAD_END_TAG,
146
+ `<meta name="description" content="${description}" data-react-helmet="true" />${HEAD_END_TAG}`
147
+ );
148
+ }
149
+ if (!source.includes('meta property="og:image"')) {
150
+ source = source.replace(
151
+ HEAD_END_TAG,
152
+ `${buildOpenGraph({ title, description, ogImage }, (env as any).appUrl || '/')}\n${HEAD_END_TAG}`
153
+ );
154
+ }
155
+ if (pageData.embed) {
156
+ source = source.replace(
157
+ HEAD_END_TAG,
158
+ `<link rel="blocklet-open-embed" type="application/json" href="${pageData.embed}" />${HEAD_END_TAG}`
159
+ );
160
+ }
161
+
162
+ const blockletJs = getBlockletJs(pageGroup, pathPrefix);
163
+ if (blockletJs && options.injectBlockletJs !== false) {
164
+ source = source
165
+ .replace('<script src="__blocklet__.js"></script>', `<script>${blockletJs}</script>`)
166
+ .replace('<script src="__meta__.js"></script>', `<script>${blockletJs}</script>`);
167
+ }
168
+
169
+ const themeStyles = buildThemeStyles(theme);
170
+ const themeScript = buildThemeScript(theme);
171
+ if (!source.includes('<style id="blocklet-theme">')) {
172
+ source = source.replace(HEAD_END_TAG, `${themeStyles}${HEAD_END_TAG}`);
173
+ }
174
+ if (!source.includes('<script id="blocklet-theme-script">')) {
175
+ source = source.replace(HEAD_END_TAG, `${themeScript}${HEAD_END_TAG}`);
176
+ }
177
+
178
+ const etag = generateETag(source);
179
+ cache.set(cacheKey, { html: source, timestamp: Date.now(), etag, pageGroup, pathPrefix });
180
+
181
+ if (options.cacheTtl) {
182
+ c.header('Cache-Control', `public, max-age=${options.cacheTtl}`);
183
+ } else {
184
+ c.header('Surrogate-Control', 'no-store');
185
+ c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
186
+ c.header('Expires', '0');
187
+ }
188
+ c.header('X-Cache', 'MISS');
189
+ c.header('ETag', etag);
190
+ return c.html(source);
191
+ };
192
+ }
193
+
194
+ export default fallback;
@@ -0,0 +1,73 @@
1
+ // Phase 1 (express→hono) — the native route group middleware pipeline.
2
+ //
3
+ // Assembled in the SAME order as the legacy express app shell
4
+ // (service.ts buildNodeHandler: cookie→json→urlencoded→cors→xss→csrf→ensureI18n
5
+ // →cdn→context). hono parses cookies (hono/cookie) and the body (xss is the
6
+ // single body read-point) on demand, so there is no separate cookie/json/
7
+ // urlencoded middleware.
8
+ //
9
+ // IMPORTANT — scoping (design §2 + §7 risk "回环桥 catch-all 与 native 路由遮蔽"):
10
+ // the pipeline is applied SCOPED to each migrated route prefix
11
+ // (`native.use('/api/<domain>/*', mw)`), NOT globally (`use('*')`). A global
12
+ // `use('*')` would run for EVERY request — including those that fall through to
13
+ // the catch-all bridge — double-sourcing cors, consuming the raw body (breaking
14
+ // the Stripe webhook), and resolving the tenant twice. Scoping keeps the bridge
15
+ // path entirely free of native middleware (verified empirically + by the Phase 0
16
+ // single-cors / raw-body invariants). As Phase 3 migrates a domain, it calls
17
+ // mountNativeGroup(native, '/api/<domain>', register) so the SAME pipeline covers
18
+ // that domain's routes and nothing else.
19
+ import type { Hono, MiddlewareHandler } from 'hono';
20
+ // eslint-disable-next-line import/no-extraneous-dependencies
21
+ import { cors } from 'hono/cors';
22
+ import { xss } from './xss';
23
+ import { csrf } from './csrf';
24
+ import { cdn } from './cdn';
25
+ import { ensureI18n, contextMiddleware } from './context';
26
+ import { isTestEnv } from '../../libs/env';
27
+
28
+ // The full app-shell pipeline (single instances, reused across prefixes). This
29
+ // module — and therefore csrf/cdn/context (which import @blocklet/sdk/lib/util/*
30
+ // the CF worker shim does not map) — is only reachable from the NODE host
31
+ // (service.ts getHonoApp); the CF worker mounts resources via the import-light
32
+ // resource-mount.ts, so it never pulls these in.
33
+ const sharedPipeline: MiddlewareHandler[] = [cors(), xss(), csrf(), ensureI18n(), cdn(), contextMiddleware()];
34
+
35
+ /** The full app-shell middleware array — the node host's appShell for mountResourceGroup. */
36
+ export function fullPipeline(): MiddlewareHandler[] {
37
+ return sharedPipeline;
38
+ }
39
+
40
+ /**
41
+ * Apply the native app-shell pipeline scoped to a route prefix, then register
42
+ * that group's routes. `prefix` is a path like '/api/customers' (no trailing
43
+ * slash); the pipeline is bound to `${prefix}/*`, which hono matches for both
44
+ * the bare prefix and its sub-paths but for nothing outside it.
45
+ */
46
+ export function mountNativeGroup(native: Hono, prefix: string, register: (native: Hono) => void): void {
47
+ for (const mw of sharedPipeline) {
48
+ native.use(`${prefix}/*`, mw);
49
+ }
50
+ register(native);
51
+ }
52
+
53
+ export function configureNativePipeline(native: Hono): void {
54
+ // Phase 1 production: the native group has no business routes yet (DID-Connect
55
+ // in Phase 2, resource routes in Phase 3); every request still falls through
56
+ // to the express bridge. The only native route is a test-only diagnostic that
57
+ // exercises the full chain (cors→xss→csrf→ensureI18n→cdn→context) end-to-end.
58
+ if (isTestEnv()) {
59
+ mountNativeGroup(native, '/api/__e2e', (n) => {
60
+ const echo = (c: any) =>
61
+ c.json({
62
+ body: c.get('sanitizedBody') ?? null,
63
+ query: c.req.query('q') ?? null,
64
+ user: c.get('user') ?? null,
65
+ locale: c.get('locale') ?? null,
66
+ });
67
+ n.get('/api/__e2e/echo', echo);
68
+ n.post('/api/__e2e/echo', echo);
69
+ });
70
+ }
71
+ }
72
+
73
+ export default configureNativePipeline;
@@ -0,0 +1,42 @@
1
+ // Phase 4 (express→hono) — resource-domain mounting, split out of pipeline.ts so
2
+ // it stays import-light for the CF worker bundle.
3
+ //
4
+ // The CF worker reaches this module via service.http.resourceRoutes →
5
+ // routes/hono → mountResourceGroup. It must NOT statically import the full
6
+ // app-shell middleware (csrf/cdn/ensureI18n/contextMiddleware), because those pull
7
+ // @blocklet/sdk/lib/util/* subpaths the worker's blocklet-sdk shim does not map —
8
+ // and the worker runs a LITE app-shell anyway (it owns cors + tenant). So this
9
+ // module imports ONLY xss (the routes depend on it to populate sanitizedBody) plus
10
+ // the resource-level livemode/baseCurrency middleware. The node host injects its
11
+ // full app-shell array via the `appShell` option (built in pipeline.ts).
12
+ import type { Hono, MiddlewareHandler } from 'hono';
13
+ import { xss } from './xss';
14
+
15
+ // LITE app-shell — the worker default. Only xss (self-skips the Stripe raw-body
16
+ // path via RAW_BODY_PREFIXES); cors/csrf/cdn/i18n/context are provided by the
17
+ // worker (cors + tenant) or intentionally absent (the worker never ran csrf/cdn).
18
+ const litePipeline: MiddlewareHandler[] = [xss()];
19
+
20
+ /**
21
+ * Mount a RESOURCE domain — the app-shell middleware (default LITE; the node host
22
+ * passes its full pipeline via `opts.appShell`) + the resource-level livemode
23
+ * middleware (routes/index.ts:49 parity) + (for the 5 domains that need it)
24
+ * loadBaseCurrency, all scoped to `${prefix}/*`, then the routes. Middleware order
25
+ * mirrors express: app-shell → livemode → [baseCurrency] → route.
26
+ */
27
+ export function mountResourceGroup(
28
+ native: Hono,
29
+ prefix: string,
30
+ subApp: Hono,
31
+ opts: { baseCurrency?: boolean; appShell?: MiddlewareHandler[] } = {}
32
+ ): void {
33
+ // eslint-disable-next-line global-require
34
+ const { livemode, loadBaseCurrency } = require('./resource');
35
+ const appShell = opts.appShell ?? litePipeline;
36
+ for (const mw of appShell) native.use(`${prefix}/*`, mw);
37
+ native.use(`${prefix}/*`, livemode());
38
+ if (opts.baseCurrency) native.use(`${prefix}/*`, loadBaseCurrency());
39
+ native.route(prefix, subApp); // sub-app routes are relative to the prefix
40
+ }
41
+
42
+ export default mountResourceGroup;
@@ -0,0 +1,63 @@
1
+ // Phase 3 (express→hono) — resource-route prerequisite middleware, the hono fork
2
+ // of the two router-level middlewares in routes/index.ts:
3
+ // ① livemode (GLOBAL): routes/index.ts:49 — req.livemode = … for every resource
4
+ // route. Honors ?livemode, then a CF-prefilled value, then PAYMENT_LIVEMODE.
5
+ // ② loadBaseCurrency (SELECTIVE): routes/index.ts:86 — only 5 domains
6
+ // (checkout-sessions / donations / payment-links / prices / products).
7
+ // The baseCurrencyCache + lazy PaymentCurrency.addHook registration are
8
+ // framework-agnostic and preserved verbatim.
9
+ import type { MiddlewareHandler } from 'hono';
10
+ import { paymentLivemode } from '../../libs/env';
11
+ import { PaymentCurrency } from '../../store/models/payment-currency';
12
+
13
+ /** ① livemode — global across all native resource routes (parity with routes/index.ts:49). */
14
+ export function livemode(): MiddlewareHandler {
15
+ return (c, next) => {
16
+ const q = c.req.query('livemode');
17
+ if (q !== undefined && q !== '') {
18
+ try {
19
+ c.set('livemode', !!JSON.parse(String(q)));
20
+ } catch {
21
+ c.set('livemode', true);
22
+ }
23
+ } else {
24
+ // No upstream prefill on hono (unlike CF createExpressReq); fall back to the
25
+ // env var, defaulting to livemode=true unless PAYMENT_LIVEMODE=false.
26
+ c.set('livemode', paymentLivemode());
27
+ }
28
+ return next();
29
+ };
30
+ }
31
+
32
+ // Lazy base-currency cache with TTL (invalidated on PaymentCurrency update/destroy).
33
+ const baseCurrencyCache = new Map<string, { data: any; expires: number }>();
34
+ const BASE_CURRENCY_TTL = 5 * 60_000;
35
+ let baseCurrencyHooksRegistered = false;
36
+ function ensureBaseCurrencyHooks() {
37
+ if (baseCurrencyHooksRegistered) return;
38
+ baseCurrencyHooksRegistered = true;
39
+ PaymentCurrency.addHook('afterUpdate', 'invalidateBaseCurrencyCache', () => baseCurrencyCache.clear());
40
+ PaymentCurrency.addHook('afterDestroy', 'invalidateBaseCurrencyCacheOnDelete', () => baseCurrencyCache.clear());
41
+ }
42
+
43
+ /** ② loadBaseCurrency — only for the 5 domains that read c.get('baseCurrency'). */
44
+ export function loadBaseCurrency(): MiddlewareHandler {
45
+ return async (c, next) => {
46
+ ensureBaseCurrencyHooks();
47
+ const livemodeVal = c.get('livemode');
48
+ const key = `base_${livemodeVal}`;
49
+ const cached = baseCurrencyCache.get(key);
50
+ if (cached && cached.expires > Date.now()) {
51
+ c.set('baseCurrency', cached.data);
52
+ } else {
53
+ const baseCurrency = await PaymentCurrency.findOne({
54
+ where: { is_base_currency: true, livemode: livemodeVal },
55
+ });
56
+ c.set('baseCurrency', baseCurrency);
57
+ if (baseCurrency) {
58
+ baseCurrencyCache.set(key, { data: baseCurrency, expires: Date.now() + BASE_CURRENCY_TTL });
59
+ }
60
+ }
61
+ return next();
62
+ };
63
+ }
@@ -0,0 +1,214 @@
1
+ // Phase 1 (express→hono) — hono fork of api/src/libs/security.ts authenticate().
2
+ //
3
+ // Behavior is identical to the express version; the plumbing changes:
4
+ // - req.user= / req.customer= / req.doc= → c.set('user'|'customer'|'doc', ...)
5
+ // - mine mode: req.query.customer_id = id → c.set('customer_id', id) (hono query
6
+ // is immutable; routes read c.get('customer_id') ?? c.req.query('customer_id')
7
+ // so a forged ?customer_id cannot bypass the verified injection — design §7).
8
+ // - the express Bearer branch MUTATED req.headers['x-user-did'] then re-read it;
9
+ // hono headers are immutable, so the resolved identity is carried in LOCALS
10
+ // (userDid/userRole/...) instead — same cascade, no header mutation.
11
+ // - component-sig verify needs an express-req shape for getVerifyData(); a thin
12
+ // shim is built from the context (body = the sanitized body, xss ran first).
13
+ import type { MiddlewareHandler } from 'hono';
14
+ import type { Model } from 'sequelize';
15
+ // eslint-disable-next-line import/no-extraneous-dependencies
16
+ import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
17
+ // eslint-disable-next-line import/no-extraneous-dependencies
18
+ import { verifyLoginToken } from '@blocklet/sdk/lib/util/verify-session';
19
+ // eslint-disable-next-line import/no-extraneous-dependencies
20
+ import { getWallet } from '@blocklet/sdk/lib/wallet';
21
+ import { isDevelopmentEnv, enableDevFakeAuth } from '../../libs/env';
22
+ import { Customer } from '../../store/models/customer';
23
+
24
+ // Phase 13b parity: lazy wallet — getWallet() needs BLOCKLET_APP_PK which a bare
25
+ // host lacks; only the embed branch uses it.
26
+ let cachedWallet: ReturnType<typeof getWallet> | undefined;
27
+ const wallet = () => {
28
+ cachedWallet ??= getWallet();
29
+ return cachedWallet;
30
+ };
31
+
32
+ type PermissionSpec<T extends Model> = {
33
+ component?: boolean;
34
+ roles?: string[];
35
+ record?: {
36
+ model: T;
37
+ field: string;
38
+ findById?: (id: string) => Promise<T | null>;
39
+ };
40
+ mine?: boolean;
41
+ embed?: boolean;
42
+ ensureLogin?: boolean;
43
+ };
44
+
45
+ export function authenticate<T extends Model>({
46
+ component,
47
+ roles,
48
+ record,
49
+ mine,
50
+ embed,
51
+ ensureLogin,
52
+ }: PermissionSpec<T>): MiddlewareHandler {
53
+ return async (c, next) => {
54
+ // Dev-only bypass (NODE_ENV=development AND ENABLE_DEV_FAKE_AUTH=1 AND the
55
+ // x-dev-fake-did header). Production never sets ENABLE_DEV_FAKE_AUTH.
56
+ if (isDevelopmentEnv() && enableDevFakeAuth()) {
57
+ const devDid = c.req.header('x-dev-fake-did');
58
+ if (devDid) {
59
+ c.set('user', {
60
+ did: devDid,
61
+ role: 'owner',
62
+ provider: 'dev',
63
+ fullName: 'dev-fake-user',
64
+ walletOS: '',
65
+ via: 'dev',
66
+ });
67
+ return next();
68
+ }
69
+ }
70
+
71
+ // Identity carried in LOCALS (hono headers are immutable). Seed from the
72
+ // BS-injected x-user-* headers; the Bearer branch may override.
73
+ let userDid = c.req.header('x-user-did');
74
+ let userRole = c.req.header('x-user-role');
75
+ let userProvider = c.req.header('x-user-provider');
76
+ let userFullname = c.req.header('x-user-fullname');
77
+ let userWalletOs = c.req.header('x-user-wallet-os');
78
+
79
+ // Authenticate by Authorization: Bearer <login-token> (local JWT verify, no
80
+ // HTTP callback) when BS did not inject x-user-did (tunnel bypass).
81
+ const authHeader = c.req.header('authorization');
82
+ if (authHeader && /^Bearer\s+/i.test(authHeader) && !userDid) {
83
+ const token = authHeader.replace(/^Bearer\s+/i, '').trim();
84
+ if (token) {
85
+ const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
86
+ if (session?.did) {
87
+ const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
88
+ userDid = canonicalDid;
89
+ userRole = `blocklet-${session.role || 'user'}`;
90
+ userProvider = session.provider || 'wallet';
91
+ userFullname = encodeURIComponent(session.fullName || '');
92
+ userWalletOs = session.walletOS || '';
93
+ }
94
+ }
95
+ }
96
+
97
+ // authenticate by component call
98
+ const sig = c.req.header('x-component-sig');
99
+ if (component && sig) {
100
+ const url = new URL(c.req.url);
101
+ const shimReq = {
102
+ get: (h: string) => c.req.header(h),
103
+ body: c.get('sanitizedBody') ?? {},
104
+ method: c.req.method,
105
+ originalUrl: url.pathname + url.search,
106
+ query: c.req.query(),
107
+ };
108
+ const { data } = getVerifyData(shimReq as any, 'component');
109
+ const verified = await verify(data, sig);
110
+ if (!verified) {
111
+ return c.json({ error: 'Invalid signature for component call' }, 401);
112
+ }
113
+ const componentDid = c.req.header('x-component-did') as string;
114
+ c.set('user', {
115
+ did: componentDid,
116
+ role: 'owner',
117
+ provider: 'wallet',
118
+ fullName: componentDid,
119
+ walletOS: '',
120
+ via: 'api',
121
+ });
122
+ return next();
123
+ }
124
+
125
+ // authenticate by authToken for embed
126
+ const embedToken = c.req.query('authToken') || '';
127
+ const embedId = c.req.param('id') || c.req.query('subscription_id') || '';
128
+ if (embed && embedToken && embedId) {
129
+ // next() is intentionally OUTSIDE the try: the try only guards the
130
+ // signature verification, never the downstream route (awaiting next here
131
+ // would wrongly report route errors as embed-auth failures — parity with
132
+ // the express version, which does not await next()).
133
+ let embedOk = false;
134
+ try {
135
+ const w = wallet();
136
+ const verified = await w.verify(embedId, embedToken);
137
+ if (!verified) {
138
+ return c.json({ error: `Invalid signature for embed: ${embedId}` }, 401);
139
+ }
140
+ c.set('user', {
141
+ did: w.address,
142
+ role: 'owner',
143
+ provider: 'wallet',
144
+ fullName: 'embed',
145
+ walletOS: '',
146
+ via: 'embed',
147
+ });
148
+ embedOk = true;
149
+ } catch (err: any) {
150
+ return c.json({ error: `Invalid signature for embed: ${embedId}: ${err.message}` }, 401);
151
+ }
152
+ if (embedOk) return next();
153
+ }
154
+
155
+ if (userDid) {
156
+ const role = (userRole || '').replace('blocklet-', '') || 'guest';
157
+ const user = {
158
+ did: userDid,
159
+ role,
160
+ provider: userProvider as string,
161
+ fullName: decodeURIComponent(userFullname || ''),
162
+ walletOS: userWalletOs as string,
163
+ via: 'dashboard',
164
+ };
165
+ c.set('user', user);
166
+
167
+ // authenticate by session user role
168
+ if (roles) {
169
+ if (roles.includes(user.role)) {
170
+ return next();
171
+ }
172
+ }
173
+
174
+ if (ensureLogin) {
175
+ user.via = 'api';
176
+ c.set('user', user);
177
+ return next();
178
+ }
179
+
180
+ if (mine) {
181
+ const customer = await Customer.findOne({ where: { did: user.did } });
182
+ if (customer) {
183
+ c.set('customer', customer);
184
+ // hono query is immutable — inject the VERIFIED id into context so a
185
+ // forged ?customer_id=<other> cannot bypass the mine check.
186
+ c.set('customer_id', customer.id);
187
+ return next();
188
+ }
189
+ }
190
+
191
+ // authenticate by record owner
192
+ if (record) {
193
+ const { model, field = 'customer_id', findById } = record;
194
+ const id = c.req.param('id') as string;
195
+ const doc: T | null =
196
+ findById && typeof findById === 'function' ? await findById(id) : await (model as any).findByPk(id);
197
+ if (doc && doc[field as keyof T]) {
198
+ const customer = await Customer.findOne({ where: { did: user.did } });
199
+ c.set('doc', doc);
200
+ c.set('customer', customer);
201
+ if (customer && customer.id === doc[field as keyof T]) {
202
+ user.via = 'portal';
203
+ c.set('user', user);
204
+ return next();
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ return c.json({ error: 'Not authorized to perform this action' }, 403);
211
+ };
212
+ }
213
+
214
+ export default authenticate;