payment-kit 1.29.1 → 1.29.3

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 (343) 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 +47 -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 +41 -37
  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/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,4 +1,4 @@
1
- // App Store integration routes.
1
+ // Phase 3 (express→hono) — hono fork of routes/integrations/app-store.ts.
2
2
  //
3
3
  // /verify — client-initiated verify. Payload schema mirrors aistro
4
4
  // (`{ receipt?, signedTransaction?, ... }`, `.or('receipt','signedTransaction')`)
@@ -6,18 +6,24 @@
6
6
  // JWS path takes priority; falls back to legacy receipt verifyReceipt.
7
7
  // /webhook — App Store Server Notifications V2. Stubbed; full state machine
8
8
  // lands in A1-followup.
9
+ //
10
+ // NOT raw-body: all three POST routes read the body via c.get('sanitizedBody') ?? {}.
11
+ // (Contrast with stripe.ts which does c.req.arrayBuffer() — that's Stripe only.)
9
12
 
10
- import { Request, Response, Router } from 'express';
13
+ import { Hono } from 'hono';
11
14
  import Joi from 'joi';
12
15
 
13
- import handleAppStoreNotification from '../../integrations/app-store/handlers';
14
- import { ingestVerifiedAppStorePurchase } from '../../integrations/app-store/handlers/subscription';
15
- import { peekNotificationRouting } from '../../integrations/app-store/notification-routing';
16
- import logger from '../../libs/logger';
17
- import { authenticate } from '../../libs/security';
18
- import { Customer, PaymentMethod } from '../../store/models';
16
+ import handleAppStoreNotification from '../../../integrations/app-store/handlers';
17
+ import { ingestVerifiedAppStorePurchase } from '../../../integrations/app-store/handlers/subscription';
18
+ import { peekNotificationRouting } from '../../../integrations/app-store/notification-routing';
19
+ import logger from '../../../libs/logger';
20
+ import { withTenant } from '../../../libs/context';
21
+ import { TENANT_MISMATCH, resolveRowTenant } from '../../../libs/tenant';
22
+ import { authenticate } from '../../../middlewares/hono/security';
23
+ import { Customer, PaymentMethod } from '../../../store/models';
24
+ import { systemFindAll } from '../../../store/scoped';
19
25
 
20
- const router = Router();
26
+ const app = new Hono();
21
27
  const userAuth = authenticate<Customer>({ component: false, ensureLogin: true });
22
28
 
23
29
  const verifyBodySchema = Joi.object<{
@@ -35,21 +41,20 @@ const verifyBodySchema = Joi.object<{
35
41
  language: Joi.string().empty(['', null]),
36
42
  }).or('receipt', 'signedTransaction');
37
43
 
38
- router.post('/verify', userAuth, async (req: Request, res: Response) => {
44
+ app.post('/verify', userAuth, async (c) => {
39
45
  try {
40
- const did = (req as any).user?.did;
46
+ const did = c.get('user')?.did;
41
47
  if (!did) {
42
- res.status(401).json({ error: 'unauthenticated' });
43
- return;
48
+ return c.json({ error: 'unauthenticated' }, 401);
44
49
  }
45
- const input = await verifyBodySchema.validateAsync(req.body, { stripUnknown: true });
50
+ const body = c.get('sanitizedBody') ?? {};
51
+ const input = await verifyBodySchema.validateAsync(body, { stripUnknown: true });
46
52
 
47
53
  const method = await PaymentMethod.findOne({
48
- where: { type: 'app_store', active: true, livemode: !!req.livemode },
54
+ where: { type: 'app_store', active: true, livemode: !!c.get('livemode') },
49
55
  });
50
56
  if (!method) {
51
- res.status(503).json({ error: 'app_store PaymentMethod not configured' });
52
- return;
57
+ return c.json({ error: 'app_store PaymentMethod not configured' }, 503);
53
58
  }
54
59
  const client = method.getAppStoreClient();
55
60
 
@@ -61,7 +66,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
61
66
  receipt: input.receipt,
62
67
  });
63
68
 
64
- res.json({
69
+ return c.json({
65
70
  success: true,
66
71
  subscription_id: result.subscription.id,
67
72
  isFirstSubscribe: result.isFirstSubscribe,
@@ -77,7 +82,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
77
82
  } catch (err: any) {
78
83
  const message = err?.message || (typeof err === 'string' ? err : null) || 'verify failed';
79
84
  logger.error('app_store verify failed', { message, stack: err?.stack });
80
- res.status(400).json({ success: false, error: { message, code: err?.code } });
85
+ return c.json({ success: false, error: { message, code: err?.code } }, 400);
81
86
  }
82
87
  });
83
88
 
@@ -112,21 +117,20 @@ const restoreBodySchema = Joi.object<{
112
117
  * or create one. Partial success is allowed — per-item failures land in the
113
118
  * `errors` array; per-item successes land in `restored`.
114
119
  */
115
- router.post('/restore', userAuth, async (req: Request, res: Response) => {
120
+ app.post('/restore', userAuth, async (c) => {
116
121
  try {
117
- const did = (req as any).user?.did;
122
+ const did = c.get('user')?.did;
118
123
  if (!did) {
119
- res.status(401).json({ error: 'unauthenticated' });
120
- return;
124
+ return c.json({ error: 'unauthenticated' }, 401);
121
125
  }
122
- const input = await restoreBodySchema.validateAsync(req.body, { stripUnknown: true });
126
+ const body = c.get('sanitizedBody') ?? {};
127
+ const input = await restoreBodySchema.validateAsync(body, { stripUnknown: true });
123
128
 
124
129
  const method = await PaymentMethod.findOne({
125
- where: { type: 'app_store', active: true, livemode: !!req.livemode },
130
+ where: { type: 'app_store', active: true, livemode: !!c.get('livemode') },
126
131
  });
127
132
  if (!method) {
128
- res.status(503).json({ error: 'app_store PaymentMethod not configured' });
129
- return;
133
+ return c.json({ error: 'app_store PaymentMethod not configured' }, 503);
130
134
  }
131
135
  const client = method.getAppStoreClient();
132
136
 
@@ -136,8 +140,8 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
136
140
  // through `ingestVerifiedAppStorePurchase`.
137
141
  const seen = new Set<string>();
138
142
  const items: Array<{ kind: 'jws' | 'receipt'; value: string }> = [
139
- ...(input.signedTransactions ?? []).map((v) => ({ kind: 'jws' as const, value: v })),
140
- ...(input.receipts ?? []).map((v) => ({ kind: 'receipt' as const, value: v })),
143
+ ...(input.signedTransactions ?? []).map((v: string) => ({ kind: 'jws' as const, value: v })),
144
+ ...(input.receipts ?? []).map((v: string) => ({ kind: 'receipt' as const, value: v })),
141
145
  ].filter((item) => {
142
146
  if (seen.has(item.value)) return false;
143
147
  seen.add(item.value);
@@ -188,13 +192,13 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
188
192
  results.push(...batchResults);
189
193
  }
190
194
 
191
- res.json({
195
+ return c.json({
192
196
  restored: results.filter((r) => r.ok),
193
197
  errors: results.filter((r) => !r.ok),
194
198
  });
195
199
  } catch (err: any) {
196
200
  logger.error('app_store restore failed', { error: err?.message, stack: err?.stack });
197
- res.status(400).json({ error: err?.message ?? 'restore failed' });
201
+ return c.json({ error: err?.message ?? 'restore failed' }, 400);
198
202
  }
199
203
  });
200
204
 
@@ -206,23 +210,27 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
206
210
  * Apple doesn't retry-storm; a failure while PROCESSING a verified notification
207
211
  * is transient and returns 5xx so Apple retries.
208
212
  */
209
- router.post('/webhook', async (req: Request, res: Response) => {
210
- const signedPayload = (req.body?.signedPayload as string | undefined) ?? '';
213
+ app.post('/webhook', async (c) => {
214
+ const body = c.get('sanitizedBody') ?? {};
215
+ const signedPayload = (body?.signedPayload as string | undefined) ?? '';
211
216
  if (!signedPayload) {
212
217
  logger.warn('app_store webhook missing signedPayload');
213
- res.json({ skipped: true, reason: 'no signedPayload' });
214
- return;
218
+ return c.json({ skipped: true, reason: 'no signedPayload' });
215
219
  }
216
220
 
217
221
  // --- Select the matching method + verify. Failures here are NOT retryable. ---
218
222
  let notification: any;
219
223
  let client: any;
224
+ let matched: PaymentMethod;
220
225
  try {
221
- const methods = await PaymentMethod.findAll({ where: { type: 'app_store' } });
226
+ // Reverse lookup: the App Store Server Notification arrives with NO tenant
227
+ // context — find which tenant registered this bundle_id. A genuine
228
+ // cross-tenant read (systemFindAll bypasses TenantModel scoping); the
229
+ // matched method's tenant is enforced via withTenant(resolveRowTenant).
230
+ const methods = await systemFindAll(PaymentMethod, { where: { type: 'app_store' } });
222
231
  if (methods.length === 0) {
223
232
  logger.warn('app_store webhook: no PaymentMethod configured');
224
- res.json({ skipped: true, reason: 'no app_store PaymentMethod' });
225
- return;
233
+ return c.json({ skipped: true, reason: 'no app_store PaymentMethod' });
226
234
  }
227
235
 
228
236
  // Read bundleId/environment from the UNVERIFIED payload to pick the method.
@@ -230,38 +238,50 @@ router.post('/webhook', async (req: Request, res: Response) => {
230
238
  // before the correct method is ever tried, and the old catch then 200'd —
231
239
  // silently discarding valid notifications (PR #1381 review P1).
232
240
  const routing = peekNotificationRouting(signedPayload);
233
- const matched = methods.find((m) => {
241
+ const candidates = methods.filter((m) => {
234
242
  const settings = PaymentMethod.decryptSettings(m.settings);
235
243
  if (settings.app_store?.bundle_id !== routing?.bundleId) return false;
236
244
  if (!routing?.environment) return true;
237
245
  return settings.app_store?.environment === routing.environment.toLowerCase();
238
246
  });
239
- if (!matched) {
247
+ if (candidates.length === 0) {
240
248
  logger.warn('app_store webhook: no matching PaymentMethod', {
241
249
  bundleId: routing?.bundleId,
242
250
  environment: routing?.environment,
243
251
  });
244
- res.json({ skipped: true, reason: 'no matching PaymentMethod' });
245
- return;
252
+ return c.json({ skipped: true, reason: 'no matching PaymentMethod' });
253
+ }
254
+ // Phase 6 (W1-4b): a channel identifier must map to exactly ONE tenant in
255
+ // a deployment — ambiguity means the reverse lookup could land the
256
+ // notification in the wrong tenant, so it is refused loudly.
257
+ if (candidates.length > 1) {
258
+ logger.error('app_store webhook: bundle_id registered by multiple methods, refusing', {
259
+ code: TENANT_MISMATCH,
260
+ bundleId: routing?.bundleId,
261
+ methodIds: candidates.map((m) => m.id),
262
+ });
263
+ return c.json({ skipped: true, reason: 'ambiguous bundle_id registration' });
246
264
  }
265
+ matched = candidates[0]!;
247
266
 
248
267
  client = matched.getAppStoreClient();
249
268
  notification = await client.verifyNotificationPayload(signedPayload);
250
269
  } catch (err: any) {
251
270
  // Malformed / forged / not-for-us → ack so Apple stops retrying.
252
271
  logger.warn('app_store webhook: verification/selection failed — acking', { error: err?.message });
253
- res.json({ skipped: true, reason: 'verification failed' });
254
- return;
272
+ return c.json({ skipped: true, reason: 'verification failed' });
255
273
  }
256
274
 
257
275
  // --- Process the verified notification. Failures here ARE transient. ---
258
276
  try {
259
- await handleAppStoreNotification(notification, client);
260
- res.json({ received: true });
277
+ // tenant = the method that registered this bundle_id (reverse lookup);
278
+ // the whole ingest chain (queries, events, jobs) runs under it
279
+ await withTenant(resolveRowTenant(matched!), () => handleAppStoreNotification(notification, client));
280
+ return c.json({ received: true });
261
281
  } catch (err: any) {
262
282
  logger.error('app_store webhook processing failed — will retry', { error: err?.message, stack: err?.stack });
263
- res.status(500).json({ error: err?.message ?? 'processing failed' });
283
+ return c.json({ error: err?.message ?? 'processing failed' }, 500);
264
284
  }
265
285
  });
266
286
 
267
- export default router;
287
+ export default app;
@@ -1,3 +1,4 @@
1
+ // Phase 3e (express→hono) — hono fork of routes/integrations/google-play.ts.
1
2
  // Google Play Real-Time Developer Notification webhook receiver.
2
3
  //
3
4
  // Pub/Sub Push body:
@@ -8,19 +9,26 @@
8
9
  //
9
10
  // Auth: Pub/Sub puts a Google-signed JWT in `Authorization: Bearer <jwt>`.
10
11
  // We verify the JWT claims here (signature verification is TODO — see verify.ts).
12
+ //
13
+ // NOTE: /webhook, /verify, /restore are NORMAL JSON POSTs — body is read via
14
+ // c.get('sanitizedBody') ?? {} (NOT c.req.arrayBuffer).
11
15
 
12
- import { Request, Response, Router } from 'express';
16
+ import { Hono } from 'hono';
13
17
  import Joi from 'joi';
18
+ import { googlePubsubPushServiceAccount, googlePubsubAllowUnverifiedSender, isTestEnv } from '../../../libs/env';
14
19
 
15
- import handleGooglePlayEvent, { GooglePlayRtdnPayload } from '../../integrations/google-play/handlers';
16
- import { ingestVerifiedGooglePlayPurchase } from '../../integrations/google-play/handlers/subscription';
17
- import { decodePubSubMessage, verifyPubSubJwt } from '../../integrations/google-play/verify';
18
- import logger from '../../libs/logger';
19
- import { authenticate } from '../../libs/security';
20
- import { googlePlayEndpoint } from '../../libs/util';
21
- import { Customer, PaymentMethod } from '../../store/models';
20
+ import handleGooglePlayEvent, { GooglePlayRtdnPayload } from '../../../integrations/google-play/handlers';
21
+ import { ingestVerifiedGooglePlayPurchase } from '../../../integrations/google-play/handlers/subscription';
22
+ import { decodePubSubMessage, verifyPubSubJwt } from '../../../integrations/google-play/verify';
23
+ import logger from '../../../libs/logger';
24
+ import { withTenant } from '../../../libs/context';
25
+ import { TENANT_MISMATCH, resolveRowTenant } from '../../../libs/tenant';
26
+ import { authenticate } from '../../../middlewares/hono/security';
27
+ import { googlePlayEndpoint } from '../../../libs/util';
28
+ import { Customer, PaymentMethod } from '../../../store/models';
29
+ import { systemFindAll } from '../../../store/scoped';
22
30
 
23
- const router = Router();
31
+ const app = new Hono();
24
32
  const userAuth = authenticate<Customer>({ component: false, ensureLogin: true });
25
33
 
26
34
  const verifyBodySchema = Joi.object<{
@@ -35,25 +43,24 @@ const verifyBodySchema = Joi.object<{
35
43
  * Client-initiated verify (aistro-shape).
36
44
  * Mobile client POSTs after StoreKit / BillingClient finishes the purchase.
37
45
  */
38
- router.post('/verify', userAuth, async (req: Request, res: Response) => {
46
+ app.post('/verify', userAuth, async (c) => {
39
47
  try {
40
- const did = (req as any).user?.did;
48
+ const did = (c.get('user') as any)?.did;
41
49
  if (!did) {
42
- res.status(401).json({ error: 'unauthenticated' });
43
- return;
50
+ return c.json({ error: 'unauthenticated' }, 401);
44
51
  }
45
- const input = await verifyBodySchema.validateAsync(req.body, { stripUnknown: true });
52
+ const body = c.get('sanitizedBody') ?? {};
53
+ const input = await verifyBodySchema.validateAsync(body, { stripUnknown: true });
46
54
 
47
55
  // Resolve the Google Play PaymentMethod for THIS livemode. Without the
48
56
  // livemode filter a testmode request would silently fall through to the
49
57
  // production method (and vice versa), and its encrypted credentials may
50
58
  // not even decrypt under the current process key.
51
59
  const method = await PaymentMethod.findOne({
52
- where: { type: 'google_play', active: true, livemode: !!req.livemode },
60
+ where: { type: 'google_play', active: true, livemode: !!c.get('livemode') },
53
61
  });
54
62
  if (!method) {
55
- res.status(503).json({ error: 'google_play PaymentMethod not configured' });
56
- return;
63
+ return c.json({ error: 'google_play PaymentMethod not configured' }, 503);
57
64
  }
58
65
  const client = method.getGooglePlayClient();
59
66
 
@@ -65,7 +72,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
65
72
  subscriptionId: input.subscriptionId,
66
73
  });
67
74
 
68
- res.json({
75
+ return c.json({
69
76
  success: true,
70
77
  subscription_id: result.subscription.id,
71
78
  isFirstSubscribe: result.isFirstSubscribe,
@@ -86,10 +93,13 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
86
93
  errKeys: err ? Object.keys(err) : [],
87
94
  stack: err?.stack,
88
95
  });
89
- res.status(400).json({
90
- success: false,
91
- error: { message, raw: err?.errorMessage ?? null },
92
- });
96
+ return c.json(
97
+ {
98
+ success: false,
99
+ error: { message, raw: err?.errorMessage ?? null },
100
+ },
101
+ 400
102
+ );
93
103
  }
94
104
  });
95
105
 
@@ -130,21 +140,20 @@ const restoreBodySchema = Joi.object<{
130
140
  * either return the existing local Subscription or create one. Partial
131
141
  * success is reported per item.
132
142
  */
133
- router.post('/restore', userAuth, async (req: Request, res: Response) => {
143
+ app.post('/restore', userAuth, async (c) => {
134
144
  try {
135
- const did = (req as any).user?.did;
145
+ const did = (c.get('user') as any)?.did;
136
146
  if (!did) {
137
- res.status(401).json({ error: 'unauthenticated' });
138
- return;
147
+ return c.json({ error: 'unauthenticated' }, 401);
139
148
  }
140
- const input = await restoreBodySchema.validateAsync(req.body, { stripUnknown: true });
149
+ const body = c.get('sanitizedBody') ?? {};
150
+ const input = await restoreBodySchema.validateAsync(body, { stripUnknown: true });
141
151
 
142
152
  const method = await PaymentMethod.findOne({
143
- where: { type: 'google_play', active: true, livemode: !!req.livemode },
153
+ where: { type: 'google_play', active: true, livemode: !!c.get('livemode') },
144
154
  });
145
155
  if (!method) {
146
- res.status(503).json({ error: 'google_play PaymentMethod not configured' });
147
- return;
156
+ return c.json({ error: 'google_play PaymentMethod not configured' }, 503);
148
157
  }
149
158
  const client = method.getGooglePlayClient();
150
159
 
@@ -152,7 +161,7 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
152
161
  // Play purchase, so duplicates in the request would otherwise double-
153
162
  // call Google's verifier and re-upsert the same Subscription row.
154
163
  const seen = new Set<string>();
155
- const purchases = input.purchases.filter((p) => {
164
+ const purchases = input.purchases.filter((p: { purchaseToken: string; subscriptionId: string }) => {
156
165
  if (seen.has(p.purchaseToken)) return false;
157
166
  seen.add(p.purchaseToken);
158
167
  return true;
@@ -175,7 +184,7 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
175
184
  const batch = purchases.slice(i, i + RESTORE_CONCURRENCY);
176
185
  // eslint-disable-next-line no-await-in-loop -- intentional: batches must complete sequentially to bound concurrency
177
186
  const batchResults = await Promise.all(
178
- batch.map(async (p): Promise<ItemResult> => {
187
+ batch.map(async (p: { purchaseToken: string; subscriptionId: string }): Promise<ItemResult> => {
179
188
  try {
180
189
  const r = await ingestVerifiedGooglePlayPurchase({
181
190
  customerDid: did,
@@ -202,13 +211,13 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
202
211
  results.push(...batchResults);
203
212
  }
204
213
 
205
- res.json({
214
+ return c.json({
206
215
  restored: results.filter((r) => r.ok),
207
216
  errors: results.filter((r) => !r.ok),
208
217
  });
209
218
  } catch (err: any) {
210
219
  logger.error('google_play restore failed', { error: err?.message, stack: err?.stack });
211
- res.status(400).json({ error: err?.message ?? 'restore failed' });
220
+ return c.json({ error: err?.message ?? 'restore failed' }, 400);
212
221
  }
213
222
  });
214
223
 
@@ -246,17 +255,17 @@ function markHandled(messageId: string): void {
246
255
  seenMessageIds.set(messageId, now + MESSAGE_DEDUP_TTL_MS);
247
256
  }
248
257
 
249
- router.post('/webhook', async (req: Request, res: Response) => {
250
- const expectedEmail = process.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;
258
+ app.post('/webhook', async (c) => {
259
+ const expectedEmail = googlePubsubPushServiceAccount();
251
260
  // Fail CLOSED: in production the push service account MUST be configured. A
252
261
  // sandbox/test bypass has to be explicit (PR #1381 review P1).
253
- const allowUnverifiedSender =
254
- process.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER === 'true' || process.env.NODE_ENV === 'test';
262
+ const allowUnverifiedSender = googlePubsubAllowUnverifiedSender() || isTestEnv();
255
263
 
256
264
  // --- Phase 1: authenticate + select. Failures here are rejections / not-for-us,
257
265
  // NOT processing failures. ---
258
266
  let payload: GooglePlayRtdnPayload;
259
267
  let client: ReturnType<PaymentMethod['getGooglePlayClient']>;
268
+ let method: PaymentMethod;
260
269
  let messageId: string | undefined;
261
270
  try {
262
271
  if (!expectedEmail && !allowUnverifiedSender) {
@@ -264,61 +273,72 @@ router.post('/webhook', async (req: Request, res: Response) => {
264
273
  'google_play webhook refusing: GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT unset ' +
265
274
  '(set GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER=true only for sandbox)'
266
275
  );
267
- res.status(403).json({ error: 'sender verification not configured' });
268
- return;
276
+ return c.json({ error: 'sender verification not configured' }, 403);
269
277
  }
270
278
 
271
- const authHeader = req.get('authorization') || req.get('Authorization');
279
+ const authHeader = c.req.header('authorization') || c.req.header('Authorization');
272
280
  if (authHeader) {
273
281
  const token = authHeader.replace(/^Bearer\s+/i, '');
274
282
  await verifyPubSubJwt(token, { expectedAudience: googlePlayEndpoint(), expectedEmail });
275
283
  } else if (!allowUnverifiedSender) {
276
284
  logger.warn('google_play webhook missing Authorization header');
277
- res.status(401).json({ error: 'missing authorization' });
278
- return;
285
+ return c.json({ error: 'missing authorization' }, 401);
279
286
  }
280
287
 
281
- messageId = req.body?.message?.messageId;
288
+ const body = c.get('sanitizedBody') ?? {};
289
+ messageId = (body as any)?.message?.messageId;
282
290
  // Skip only messages we already handled SUCCESSFULLY (mark happens post-success).
283
291
  if (messageId && wasHandled(messageId)) {
284
292
  logger.info('google_play webhook: duplicate Pub/Sub messageId, skipping', { messageId });
285
- res.json({ deduped: true });
286
- return;
293
+ return c.json({ deduped: true });
287
294
  }
288
295
 
289
- payload = decodePubSubMessage<GooglePlayRtdnPayload>(req.body);
296
+ payload = decodePubSubMessage<GooglePlayRtdnPayload>(body);
290
297
 
291
- const methods = await PaymentMethod.findAll({ where: { type: 'google_play' } });
292
- const method = methods.find((m) => {
298
+ // Reverse lookup: the RTDN webhook arrives with NO tenant context — find
299
+ // which tenant registered this package_name. A genuine cross-tenant read,
300
+ // so it must use systemFindAll to bypass TenantModel scoping; the matched
301
+ // method's tenant is then enforced via withTenant(resolveRowTenant) below.
302
+ const methods = await systemFindAll(PaymentMethod, { where: { type: 'google_play' } });
303
+ const candidates = methods.filter((m) => {
293
304
  const settings = PaymentMethod.decryptSettings(m.settings);
294
305
  return settings.google_play?.package_name === payload.packageName;
295
306
  });
296
- if (!method) {
307
+ if (candidates.length === 0) {
297
308
  logger.warn('google_play webhook: no matching PaymentMethod for packageName', {
298
309
  packageName: payload.packageName,
299
310
  });
300
311
  // Not for us → ack so Pub/Sub doesn't retry a misconfigured topic forever.
301
- res.json({ skipped: true });
302
- return;
312
+ return c.json({ skipped: true });
313
+ }
314
+ // Phase 6 (W1-4b): one channel identifier maps to exactly one tenant
315
+ if (candidates.length > 1) {
316
+ logger.error('google_play webhook: package_name registered by multiple methods, refusing', {
317
+ code: TENANT_MISMATCH,
318
+ packageName: payload.packageName,
319
+ methodIds: candidates.map((m) => m.id),
320
+ });
321
+ return c.json({ skipped: true, reason: 'ambiguous package_name registration' });
303
322
  }
323
+ method = candidates[0]!;
304
324
  client = method.getGooglePlayClient();
305
325
  } catch (err: any) {
306
326
  // Auth / decode / selection failure → forged or malformed; reject.
307
327
  logger.warn('google_play webhook: auth/decode failed', { error: err?.message });
308
- res.status(401).json({ error: 'unauthorized' });
309
- return;
328
+ return c.json({ error: 'unauthorized' }, 401);
310
329
  }
311
330
 
312
331
  // --- Phase 2: process the verified event. Failure here is transient → 5xx so
313
332
  // Pub/Sub retries; mark the messageId handled ONLY after success. ---
314
333
  try {
315
- await handleGooglePlayEvent(payload, client);
334
+ // tenant = the method that registered this package_name (reverse lookup)
335
+ await withTenant(resolveRowTenant(method!), () => handleGooglePlayEvent(payload, client));
316
336
  if (messageId) markHandled(messageId);
317
- res.json({ received: true });
337
+ return c.json({ received: true });
318
338
  } catch (err: any) {
319
339
  logger.error('google_play webhook processing failed — will retry', { error: err?.message, stack: err?.stack });
320
- res.status(500).json({ error: err?.message ?? 'processing failed' });
340
+ return c.json({ error: err?.message ?? 'processing failed' }, 500);
321
341
  }
322
342
  });
323
343
 
324
- export default router;
344
+ export default app;
@@ -0,0 +1,74 @@
1
+ // Phase 3e (express→hono) — hono fork of routes/integrations/stripe.ts. The
2
+ // Stripe webhook is RAW-BODY: the signature is an HMAC over the EXACT received
3
+ // bytes, so the body must NOT be parsed/sanitized before verification. xss skips
4
+ // this path (RAW_BODY_PREFIXES); the route reads c.req.arrayBuffer() directly
5
+ // (§3.1). stripeEvent/stripeClient are injected into context by verifyWebhookSig
6
+ // (the route-internal middleware, not a global one).
7
+ import { Hono } from 'hono';
8
+ import type { MiddlewareHandler } from 'hono';
9
+ // eslint-disable-next-line import/no-extraneous-dependencies
10
+ import { env } from '@blocklet/sdk/lib/env';
11
+ import get from 'lodash/get';
12
+ import { stripeWebhookSecret } from '../../../libs/env';
13
+
14
+ import handleStripeEvent from '../../../integrations/stripe/handlers';
15
+ import logger from '../../../libs/logger';
16
+ import { STRIPE_EVENTS } from '../../../libs/util';
17
+ import { PaymentMethod } from '../../../store/models';
18
+
19
+ const app = new Hono();
20
+
21
+ const verifyWebhookSig: MiddlewareHandler = async (c, next) => {
22
+ try {
23
+ const signature = c.req.header('stripe-signature');
24
+ if (!signature) {
25
+ return c.json({ error: 'No stripe webhook signature found' }, 400);
26
+ }
27
+
28
+ // RAW body — read the exact bytes once (xss did not consume this path).
29
+ const raw = Buffer.from(await c.req.arrayBuffer());
30
+ const json = JSON.parse(raw.toString('utf8'));
31
+ const method = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: json.livemode } });
32
+ if (!method) {
33
+ return c.json({ error: 'No stripe payment method found' }, 400);
34
+ }
35
+
36
+ const stripe = method.getStripeClient();
37
+ const settings = PaymentMethod.decryptSettings(method.settings);
38
+ const secret = stripeWebhookSecret() || settings.stripe?.webhook_signing_secret;
39
+ c.set('stripeEvent', stripe.webhooks.constructEvent(raw, signature, secret as string));
40
+ c.set('stripeClient', stripe);
41
+
42
+ // eslint-disable-next-line @typescript-eslint/return-await -- do NOT await: downstream route errors must not be caught by this sig-verify catch
43
+ return next();
44
+ } catch (err: any) {
45
+ logger.error('verify signature error', { error: err });
46
+ return c.json({ error: err.message }, 400);
47
+ }
48
+ };
49
+
50
+ const handleEvent: MiddlewareHandler = async (c) => {
51
+ const stripeEvent = c.get('stripeEvent');
52
+ const stripeClient = c.get('stripeClient');
53
+
54
+ if (STRIPE_EVENTS.includes(stripeEvent.type) === false) {
55
+ logger.debug('webhook event not interested', { id: stripeEvent.id, type: stripeEvent.type });
56
+ return c.json({ skipped: true });
57
+ }
58
+
59
+ // only events from this app should be processed
60
+ const appPid = get(stripeEvent, 'data.object.metadata.appPid');
61
+ if (appPid && appPid !== (env as any).appPid) {
62
+ logger.debug('webhook event for other app', { id: stripeEvent.id, type: stripeEvent.type });
63
+ return c.json({ skipped: true });
64
+ }
65
+
66
+ logger.debug('webhook received event', { id: stripeEvent.id, type: stripeEvent.type });
67
+ await handleStripeEvent(stripeEvent, stripeClient);
68
+
69
+ return c.json({ received: true });
70
+ };
71
+
72
+ app.post('/webhook', verifyWebhookSig, handleEvent);
73
+
74
+ export default app;