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
@@ -0,0 +1,116 @@
1
+ /* eslint-disable no-console */
2
+ // D1 (S3.0) E2E harness — really runs the embedded factory with the three input
3
+ // matrices and prints JSON-shaped results. Invoked by build-phases Layer 2; the
4
+ // raw output is captured into planning/.../logs/sD1-e2e.log.
5
+ //
6
+ // Run: npx tsx scripts/e2e-d1-tenancy.ts
7
+ import { Sequelize } from 'sequelize';
8
+ import { setCoreConfig, getCoreConfig } from '../api/src/libs/env';
9
+ import { getTenantMode, setDefaultInstanceDid } from '../api/src/libs/tenant';
10
+ import {
11
+ createDefaultIdentityDriver,
12
+ setIdentityDriver,
13
+ createDefaultSecretsDriver,
14
+ setSecretsDriver,
15
+ } from '../api/src/libs/drivers';
16
+ import { createEmbeddedPaymentService } from '../api/src/service';
17
+
18
+ const identity = { resolveInstanceDidForHost: () => null } as any;
19
+ const secrets = createDefaultSecretsDriver();
20
+
21
+ function freshDb() {
22
+ return { sequelize: new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) as any };
23
+ }
24
+ function reset() {
25
+ setCoreConfig(undefined);
26
+ setDefaultInstanceDid(undefined);
27
+ setIdentityDriver(createDefaultIdentityDriver());
28
+ setSecretsDriver(createDefaultSecretsDriver());
29
+ delete process.env.PAYMENT_TENANT_MODE;
30
+ }
31
+
32
+ function emit(label: string, obj: unknown) {
33
+ console.log(`=== ${label} ===`);
34
+ console.log(JSON.stringify(obj, null, 2));
35
+ }
36
+
37
+ // matrix 1 — slot drives multi (no env)
38
+ reset();
39
+ createEmbeddedPaymentService({ config: {}, db: freshDb(), tenancy: { mode: 'multi' }, identity, secrets });
40
+ emit('M1 slot-multi-no-env', { tenantMode: getTenantMode(), success: getTenantMode() === 'multi' });
41
+
42
+ // matrix 2 — config carries multi, slot agrees
43
+ reset();
44
+ createEmbeddedPaymentService({
45
+ config: { PAYMENT_TENANT_MODE: 'multi' },
46
+ db: freshDb(),
47
+ tenancy: { mode: 'multi' },
48
+ identity,
49
+ secrets,
50
+ });
51
+ emit('M2 config-multi-slot-agrees', { tenantMode: getTenantMode(), success: getTenantMode() === 'multi' });
52
+
53
+ // matrix 3 (NEGATIVE) — config single conflicts with slot multi -> must throw, no mode written
54
+ reset();
55
+ let conflictErr: any = null;
56
+ try {
57
+ createEmbeddedPaymentService({
58
+ config: { PAYMENT_TENANT_MODE: 'single' },
59
+ db: freshDb(),
60
+ tenancy: { mode: 'multi' },
61
+ identity,
62
+ secrets,
63
+ });
64
+ } catch (err: any) {
65
+ conflictErr = err;
66
+ }
67
+ emit('M3 conflict-negative', {
68
+ threw: conflictErr !== null,
69
+ name: conflictErr?.name,
70
+ code: conflictErr?.code,
71
+ message: conflictErr?.message,
72
+ coreConfigAfter: getCoreConfig() === undefined ? 'undefined (no half write)' : 'WRITTEN (bug!)',
73
+ success: conflictErr !== null && getCoreConfig() === undefined,
74
+ });
75
+
76
+ // matrix 4 (NEGATIVE) — multi without secrets -> fail-closed (no silent single)
77
+ reset();
78
+ let slotErr: any = null;
79
+ try {
80
+ createEmbeddedPaymentService({ config: {}, db: freshDb(), tenancy: { mode: 'multi' }, identity });
81
+ } catch (err: any) {
82
+ slotErr = err;
83
+ }
84
+ emit('M4 multi-missing-secrets-negative', {
85
+ threw: slotErr !== null,
86
+ name: slotErr?.name,
87
+ code: slotErr?.code,
88
+ slot: slotErr?.slot,
89
+ success: slotErr !== null && slotErr?.slot === 'secrets',
90
+ });
91
+
92
+ // matrix 4b (NEGATIVE) — multi without identity -> fail-closed (no silent single)
93
+ reset();
94
+ let identityErr: any = null;
95
+ try {
96
+ createEmbeddedPaymentService({ config: {}, db: freshDb(), tenancy: { mode: 'multi' }, secrets } as any);
97
+ } catch (err: any) {
98
+ identityErr = err;
99
+ }
100
+ emit('M4b multi-missing-identity-negative', {
101
+ threw: identityErr !== null,
102
+ name: identityErr?.name,
103
+ code: identityErr?.code,
104
+ slot: identityErr?.slot,
105
+ success: identityErr !== null && identityErr?.slot === 'identity',
106
+ });
107
+
108
+ // matrix 5 — legacy single compat (no tenancy + APP_PID)
109
+ reset();
110
+ process.env.BLOCKLET_APP_PID = 'did:abt:zLEGACYAPP';
111
+ createEmbeddedPaymentService({ config: { BLOCKLET_APP_PID: 'did:abt:zLEGACYAPP' }, db: freshDb() });
112
+ emit('M5 legacy-single-compat', { tenantMode: getTenantMode(), success: getTenantMode() === 'single' });
113
+ delete process.env.BLOCKLET_APP_PID;
114
+
115
+ console.log('=== DONE ===');
116
+ process.exit(0);
@@ -0,0 +1,139 @@
1
+ /* eslint-disable no-console */
2
+ // D2 (S3.0) E2E harness — really runs the cron driver + queue teardown and
3
+ // prints JSON-shaped results. Captured into planning/.../logs/sD2-e2e.log.
4
+ //
5
+ // Run: NODE_ENV=test npx tsx scripts/e2e-d2-cron-queue.ts
6
+ //
7
+ // Covers: (1) crons/index conservation — every declared job flows through the
8
+ // injected driver, no bare @abtnode/cron; (2) node-cron self-schedules a real
9
+ // 1s cron with NO external runDue tick; (3) teardown — stop() clears the cron
10
+ // timer (active-handle count drops); (4) cf-cron stays passive (negative).
11
+ process.env.NODE_ENV = 'test';
12
+ process.env.BLOCKLET_MODE = 'test';
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { createCronRegistry, setCronDriver } from '../api/src/libs/drivers/cron';
17
+
18
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
19
+ const cronTimers = () => (process as any).getActiveResourcesInfo().filter((r: string) => r === 'Timeout').length;
20
+ function emit(label: string, obj: unknown) {
21
+ console.log(`=== ${label} ===`);
22
+ console.log(JSON.stringify(obj, null, 2));
23
+ }
24
+
25
+ async function main() {
26
+ // NOTE: crons/index conservation (every declared job flows through the
27
+ // injected driver) is verified in jest where the db/env is set up —
28
+ // tests/libs/crons-conservation-d2.spec.ts — because importing crons/index
29
+ // eagerly builds queues that need a real sequelize. Here we exercise the
30
+ // driver self-scheduling + teardown directly (no db needed).
31
+ setCronDriver(createCronRegistry('node-cron'));
32
+
33
+ // 1) job-names list — the full declared cron set crons/index registers through
34
+ // the driver (parsed from source; importing the graph needs a real DB/ESM).
35
+ const cronsSrc = fs.readFileSync(path.resolve(__dirname, '../api/src/crons/index.ts'), 'utf8');
36
+ const declaredNames = [...cronsSrc.matchAll(/name:\s*'([^']+)'/g)].map((m) => m[1]);
37
+ emit('C1 crons-conservation-job-names', {
38
+ registerCalls: (cronsSrc.match(/getCronDriver\(\)\.register\(/g) || []).length,
39
+ bareCronInit: /Cron\.init\s*\(/.test(cronsSrc),
40
+ count: declaredNames.length,
41
+ names: declaredNames,
42
+ success: declaredNames.length >= 18 && (cronsSrc.match(/getCronDriver\(\)\.register\(/g) || []).length === 1,
43
+ });
44
+
45
+ // 2) node-cron self-schedules a real 1s cron with no external runDue
46
+ const node = createCronRegistry('node-cron');
47
+ let ticks = 0;
48
+ const beforeReg = cronTimers();
49
+ node.register([
50
+ {
51
+ name: 'recon',
52
+ time: '*/1 * * * * *',
53
+ fn: () => {
54
+ ticks += 1;
55
+ },
56
+ },
57
+ ]);
58
+ const withTimer = cronTimers();
59
+ await wait(2200);
60
+ const ticksWhileRunning = ticks;
61
+ emit('C2 node-self-schedule', {
62
+ ticksWhileRunning,
63
+ timerAddedOnRegister: withTimer > beforeReg,
64
+ success: ticksWhileRunning >= 1 && withTimer > beforeReg,
65
+ });
66
+
67
+ // 3) teardown — stop() clears the cron timer; no more ticks
68
+ node.stop();
69
+ const afterStop = cronTimers();
70
+ const ticksAtStop = ticks;
71
+ await wait(2200);
72
+ emit('C3 teardown-stop', {
73
+ timersWithCron: withTimer,
74
+ timersAfterStop: afterStop,
75
+ timerCleared: afterStop < withTimer,
76
+ ticksAfterStop: ticks - ticksAtStop,
77
+ success: afterStop < withTimer && ticks - ticksAtStop === 0,
78
+ });
79
+
80
+ // 4) restart idempotent — re-register after stop, no double scheduler
81
+ let ran2 = 0;
82
+ node.register([
83
+ {
84
+ name: 'init2',
85
+ time: '0 0 0 1 1 *',
86
+ fn: () => {
87
+ ran2 += 1;
88
+ },
89
+ options: { runOnInit: true },
90
+ },
91
+ ]);
92
+ await wait(50);
93
+ const afterFirst = ran2;
94
+ node.stop();
95
+ node.register([
96
+ {
97
+ name: 'init2',
98
+ time: '0 0 0 1 1 *',
99
+ fn: () => {
100
+ ran2 += 1;
101
+ },
102
+ options: { runOnInit: true },
103
+ },
104
+ ]);
105
+ await wait(50);
106
+ node.stop();
107
+ emit('C4 restart-idempotent', {
108
+ afterFirstStart: afterFirst,
109
+ afterRestart: ran2,
110
+ success: afterFirst === 1 && ran2 === 2, // exactly one run per start, no double
111
+ });
112
+
113
+ // 5) NEGATIVE — cf-cron stays passive: register does NOT self-fire
114
+ const cf = createCronRegistry('cf-cron');
115
+ let cfRan = 0;
116
+ cf.register([
117
+ {
118
+ name: 'cf',
119
+ time: '*/1 * * * * *',
120
+ fn: () => {
121
+ cfRan += 1;
122
+ },
123
+ options: { runOnInit: true },
124
+ },
125
+ ]);
126
+ await wait(1200);
127
+ const cfSelfFired = cfRan;
128
+ const due = await cf.runDue(new Date());
129
+ emit('C5 cf-passive-negative', {
130
+ selfFiredWithoutHostTick: cfSelfFired,
131
+ ranViaRunDue: due.ran,
132
+ success: cfSelfFired === 0 && due.ran.includes('cf'),
133
+ });
134
+
135
+ console.log('=== DONE ===');
136
+ process.exit(0);
137
+ }
138
+
139
+ main();
@@ -0,0 +1,171 @@
1
+ /* eslint-disable no-console */
2
+ // D3 (S3.0) E2E harness — builds a REAL embedded multi-mode payment service over
3
+ // a file-backed sqlite (46-table schema) and exercises the background engine,
4
+ // emitting JSON. Captured into planning/.../logs/sD3-e2e.log.
5
+ //
6
+ // NODE_ENV=test npx tsx scripts/e2e-d3-embedded-multi.ts
7
+ process.env.NODE_ENV = 'test';
8
+ process.env.BLOCKLET_MODE = 'test';
9
+
10
+ import fs from 'fs';
11
+ import os from 'os';
12
+ import path from 'path';
13
+ import { Sequelize } from 'sequelize';
14
+
15
+ import { withTenant, getInstanceDid } from '../api/src/libs/context';
16
+ import { createNodeDbDriver } from '../api/src/libs/drivers/db';
17
+ import {
18
+ applyPaymentCoreMigrations,
19
+ createMemoryLocksDriver,
20
+ createCronRegistry,
21
+ createKeyringSecretsDriver,
22
+ nodeQueueHostHooks,
23
+ } from '../api/src/libs/drivers';
24
+ import { setQueueRuntimeMode } from '../api/src/libs/queue/runtime';
25
+ import { createEmbeddedPaymentService } from '../api/src/service';
26
+
27
+ const TENANT_A = 'did:abt:zD3A';
28
+ const TENANT_B = 'did:abt:zD3B';
29
+ const identity = {
30
+ resolveInstanceDidForHost: (h: string | undefined) => (h === 'a' ? TENANT_A : h === 'b' ? TENANT_B : null),
31
+ getAppEk: (id: string) => (id === TENANT_A ? 'a'.repeat(64) : 'b'.repeat(64)),
32
+ };
33
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
34
+ const settle = (e: any): Promise<string> =>
35
+ new Promise((res) => ['finished', 'failed', 'cancelled'].forEach((ev) => e.on(ev, () => res(ev))));
36
+ function emit(label: string, obj: unknown) {
37
+ console.log(`=== ${label} ===`);
38
+ console.log(JSON.stringify(obj, null, 2));
39
+ }
40
+
41
+ async function main() {
42
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'd3-e2e-'));
43
+ const sequelize = new Sequelize({
44
+ dialect: 'sqlite',
45
+ storage: path.join(dir, 'p.db'),
46
+ logging: false,
47
+ pool: { max: 5 },
48
+ });
49
+ const driver = createNodeDbDriver(sequelize);
50
+ await applyPaymentCoreMigrations(driver);
51
+ const tables = await driver.all<{ name: string }>(
52
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
53
+ );
54
+
55
+ const slots = () => ({
56
+ config: { BLOCKLET_APP_PID: TENANT_A, PAYMENT_LIVEMODE: 'true' },
57
+ db: { sequelize: sequelize as any },
58
+ tenancy: { mode: 'multi' as const },
59
+ identity,
60
+ secrets: createKeyringSecretsDriver(identity),
61
+ queue: nodeQueueHostHooks,
62
+ cron: createCronRegistry('node-cron'),
63
+ locks: createMemoryLocksDriver(),
64
+ });
65
+ const svc = createEmbeddedPaymentService(slots());
66
+ setQueueRuntimeMode('node');
67
+ const createQueue = require('../api/src/libs/queue').default;
68
+ const createQueueStore = require('../api/src/libs/queue/store').default;
69
+
70
+ // happy — assembly
71
+ emit('D3.1 assembly', {
72
+ tableCount: tables.length,
73
+ hasEntitlementsCheck: typeof svc.rpc.entitlements.check === 'function',
74
+ hasMeterReport: typeof svc.rpc.meterEvents.report === 'function',
75
+ success: tables.length === 46,
76
+ });
77
+
78
+ // bad input — slot misconfig (NEGATIVE)
79
+ const noSecrets: any = slots();
80
+ delete noSecrets.secrets;
81
+ const noIdentity: any = slots();
82
+ delete noIdentity.identity;
83
+ const tryBuild = (s: any) => {
84
+ try {
85
+ createEmbeddedPaymentService(s);
86
+ return { threw: false, slot: null };
87
+ } catch (e: any) {
88
+ return { threw: true, slot: e?.slot, code: e?.code };
89
+ }
90
+ };
91
+ const s1 = tryBuild(noSecrets);
92
+ const s2 = tryBuild(noIdentity);
93
+ emit('D3.2 slot-misconfig-negative', {
94
+ missingSecrets: s1,
95
+ missingIdentity: s2,
96
+ success: s1.threw && s1.slot === 'secrets' && s2.threw && s2.slot === 'identity',
97
+ });
98
+
99
+ // retry trace
100
+ const trace: number[] = [];
101
+ let successes = 0;
102
+ const rq = createQueue({
103
+ name: 'd3-retry',
104
+ onJob: async () => {
105
+ trace.push(trace.length + 1);
106
+ if (trace.length < 3) throw new Error('transient');
107
+ successes += 1;
108
+ },
109
+ options: { maxRetries: 5, retryDelay: 1 },
110
+ });
111
+ const rEv = await withTenant(TENANT_A, async () => rq.push({ job: { instance_did: TENANT_A } }));
112
+ const rEnd = await settle(rEv);
113
+ emit('D3.3 retry-trace', {
114
+ attempts: trace.length,
115
+ successSideEffects: successes,
116
+ terminal: rEnd,
117
+ success: trace.length === 3 && successes === 1 && rEnd === 'finished',
118
+ });
119
+
120
+ // restart recovery — job-id set diff
121
+ const jobId = 'recover-1';
122
+ const store = createQueueStore('d3-recover');
123
+ await store.addJob(jobId, { instance_did: TENANT_A }, {});
124
+ const before = (await store.getJobs()).map((r: any) => r.id);
125
+ let ranId = '';
126
+ const recoverQ = createQueue({
127
+ name: 'd3-recover',
128
+ onJob: async () => {},
129
+ });
130
+ // the recovered row's id comes from the runtime finished event (the payload
131
+ // carries no id) — proving the recovered id is the original, not regenerated.
132
+ recoverQ.on('finished', (data: { id: string }) => {
133
+ ranId = data.id;
134
+ });
135
+ await wait(400);
136
+ const after = (await store.getJobs()).map((r: any) => r.id);
137
+ emit('D3.4 restart-recovery', {
138
+ beforeJobIds: before,
139
+ afterJobIds: after,
140
+ executedId: ranId,
141
+ success: before.includes(jobId) && !after.includes(jobId) && ranId === jobId,
142
+ });
143
+
144
+ // cross-tenant rejection (NEGATIVE)
145
+ const { assertJobObjectTenant } = require('../api/src/libs/queue');
146
+ let leaked = false;
147
+ let observed = '';
148
+ const xq = createQueue({
149
+ name: 'd3-xtenant',
150
+ onJob: async () => {
151
+ observed = getInstanceDid();
152
+ assertJobObjectTenant({ instance_did: TENANT_B });
153
+ leaked = true;
154
+ },
155
+ });
156
+ const xEv = await withTenant(TENANT_A, async () => xq.push({ job: { instance_did: TENANT_A } }));
157
+ const xEnd = await settle(xEv);
158
+ emit('D3.5 cross-tenant-rejection-negative', {
159
+ observedTenant: observed,
160
+ leaked,
161
+ terminal: xEnd,
162
+ success: observed === TENANT_A && !leaked && xEnd === 'failed',
163
+ });
164
+
165
+ console.log('=== DONE ===');
166
+ await sequelize.close();
167
+ fs.rmSync(dir, { recursive: true, force: true });
168
+ process.exit(0);
169
+ }
170
+
171
+ main();
@@ -0,0 +1,125 @@
1
+ /* eslint-disable no-console, global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
2
+ // Phase 2 (express→hono) E2E — SELF-VALIDATING harness. Serves the REAL
3
+ // buildConnectRoutesHono() (DID-Connect via did-connect-js native attachHono)
4
+ // through REAL @hono/node-server serve(), then drives the surface over a real TCP
5
+ // socket and prints PASS/FAIL for each spec-table category. Uses the spike's env
6
+ // bootstrap (testSetup → a valid appInfo so generateSession succeeds — jest's
7
+ // bare env cannot, which is why these session-dependent checks live here).
8
+ const os = require('os');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const testSetup = require('@blocklet/sdk/lib/util/test-setup').default;
13
+
14
+ testSetup();
15
+ process.env.BLOCKLET_DATA_DIR = process.env.BLOCKLET_DATA_DIR || fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-s2-'));
16
+ const { fromRandom } = require('@ocap/wallet');
17
+ const { types } = require('@ocap/mcrypto');
18
+
19
+ const appWallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
20
+ process.env.BLOCKLET_APP_SK = appWallet.secretKey;
21
+ process.env.BLOCKLET_APP_PK = appWallet.publicKey;
22
+ process.env.BLOCKLET_APP_ID = appWallet.address;
23
+ process.env.BLOCKLET_APP_PID = appWallet.address;
24
+
25
+ const { serve } = require('@hono/node-server');
26
+ const { buildConnectRoutesHono } = require('../api/src/service');
27
+
28
+ const PORT = Number(process.env.E2E_PORT || 9272);
29
+ const connectApp = buildConnectRoutesHono();
30
+
31
+ let pass = 0;
32
+ let fail = 0;
33
+ const check = (name: string, cond: boolean, detail = '') => {
34
+ if (cond) {
35
+ pass += 1;
36
+ console.log(` PASS ${name}`);
37
+ } else {
38
+ fail += 1;
39
+ console.log(` FAIL ${name} ${detail}`);
40
+ }
41
+ };
42
+
43
+ const server = serve({ fetch: connectApp.fetch, port: PORT }, async (info: { port: number }) => {
44
+ const base = `http://127.0.0.1:${info.port}`;
45
+ console.log(`E2E_READY ${info.port} (real @hono/node-server socket)`);
46
+ try {
47
+ // E1 happy path: generateSession returns the full session shape
48
+ const r1 = await fetch(`${base}/api/did/payment/token`);
49
+ const s1: any = await r1.json();
50
+ console.log(`E1 GET /api/did/payment/token → ${r1.status} ${JSON.stringify(s1).slice(0, 240)}`);
51
+ check('E1 happy: status 200', r1.status === 200);
52
+ check('E1 happy: status="created"', s1.status === 'created');
53
+ check('E1 happy: token present', !!s1.token);
54
+
55
+ // Data loss (deep-link): the url carries the session token as _t_
56
+ const decoded = decodeURIComponent(decodeURIComponent(s1.url || ''));
57
+ check('Data-loss: deep-link url carries _t_=<token>', decoded.includes(`_t_=${s1.token}`), decoded.slice(0, 120));
58
+
59
+ // Data damage: appInfo intact (name + publisher/nodeDid), not stripped/garbled
60
+ check(
61
+ 'Data-damage: appInfo.name present',
62
+ !!(s1.appInfo && s1.appInfo.name),
63
+ JSON.stringify(s1.appInfo).slice(0, 120)
64
+ );
65
+ check('Data-damage: appInfo.publisher (did) present', !!(s1.appInfo && s1.appInfo.publisher));
66
+
67
+ // Data leak: two sessions get DISTINCT tokens (no reuse/bleed)
68
+ const r2 = await fetch(`${base}/api/did/collect/token`);
69
+ const s2: any = await r2.json();
70
+ check(
71
+ 'Data-leak: a second session has a DISTINCT token',
72
+ !!s2.token && s2.token !== s1.token,
73
+ `${s1.token} vs ${s2.token}`
74
+ );
75
+
76
+ // Security: with a VALID session token, an UNSIGNED/forged auth POST is not
77
+ // accepted — onAuthResponse refuses the unsigned payload, so the session never
78
+ // advances to succeed and no authInfo is returned (the auth verification path
79
+ // runs unchanged under the hono adapter).
80
+ const rAuth = await fetch(`${base}/api/did/payment/auth?_t_=${s1.token}`, {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json' },
83
+ body: JSON.stringify({ userDid: 'did:abt:zForgedUser', claims: [] }),
84
+ });
85
+ const authBody = await rAuth.text();
86
+ let authJson: any = {};
87
+ try {
88
+ authJson = JSON.parse(authBody);
89
+ } catch {
90
+ /* non-json error body */
91
+ }
92
+ console.log(`Security POST /auth (unsigned, valid _t_) → ${rAuth.status} ${authBody.slice(0, 160)}`);
93
+ check(
94
+ 'Security: unsigned/forged auth POST does NOT succeed (onAuthResponse refuses it)',
95
+ authJson.status !== 'succeed' && !authJson.authInfo,
96
+ `status=${rAuth.status}`
97
+ );
98
+ // and the session must NOT have advanced to succeed
99
+ const rStatus = await fetch(`${base}/api/did/payment/status?_t_=${s1.token}`);
100
+ const st: any = await rStatus.json().catch(() => ({}));
101
+ check(
102
+ 'Security: session stays unauthenticated after the unsigned POST',
103
+ st.status !== 'succeed',
104
+ `status=${st.status}`
105
+ );
106
+
107
+ // E3 negative: a forged/non-existent token never resolves to a session
108
+ const rForged = await fetch(`${base}/api/did/payment/auth?_t_=deadbeef-not-a-token`);
109
+ const forgedBody = await rForged.text();
110
+ console.log(`E3 negative GET /auth?_t_=forged → ${rForged.status} ${forgedBody.slice(0, 100)}`);
111
+ check(
112
+ 'E3 negative: forged token does not return a created session/authInfo',
113
+ !forgedBody.includes('"status":"created"') && !forgedBody.includes('authInfo')
114
+ );
115
+ } catch (err: any) {
116
+ fail += 1;
117
+ console.log(` FAIL harness error: ${err.message}`);
118
+ } finally {
119
+ console.log(`\nphase2-e2e: ${pass} passed, ${fail} failed`);
120
+ server.close();
121
+ process.exit(fail ? 1 : 0);
122
+ }
123
+ });
124
+
125
+ export {}; // make this file a module so its top-level consts do not collide with sibling e2e scripts in tsc global scope
@@ -0,0 +1,135 @@
1
+ /* eslint-disable no-console, global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
2
+ // Phase 3e (express→hono) E2E — SELF-VALIDATING. Proves the Stripe webhook RAW
3
+ // BODY survives the FULL native pipeline (cors→xss→csrf→ensureI18n→cdn→context→
4
+ // livemode, applied via mountResourceGroup) over a REAL @hono/node-server socket:
5
+ // xss SKIPS the webhook path, so the handler's c.req.arrayBuffer() gets the exact
6
+ // bytes and stripe.constructEvent verifies. Tampered bytes → reject (§3.1).
7
+ process.env.NODE_ENV = 'test';
8
+ process.env.BLOCKLET_APP_SK = process.env.BLOCKLET_APP_SK || 'e2e_s3e_secret';
9
+ process.env.BLOCKLET_APP_PID = process.env.BLOCKLET_APP_PID || 'did:abt:zE2ES3eTenant';
10
+
11
+ const Stripe = require('stripe');
12
+ const { serve } = require('@hono/node-server');
13
+ const { Hono } = require('hono');
14
+ const { buildHonoApp } = require('../api/src/service');
15
+ const { mountResourceGroup } = require('../api/src/middlewares/hono/pipeline');
16
+
17
+ const secret = 'whsec_e2e_s3e';
18
+ const stripe = new Stripe('sk_test_dummy', { apiVersion: '2023-08-16' });
19
+ const payload = JSON.stringify({
20
+ id: 'evt_s3e_1',
21
+ type: 'payment_intent.succeeded',
22
+ livemode: false,
23
+ data: { object: { metadata: {}, note: '欧元 €42 — naïve café 🚀 "x"' } },
24
+ });
25
+ const sigHeader = stripe.webhooks.generateTestHeaderString({ payload, secret });
26
+
27
+ // A representative stripe webhook mounted at /api/integrations/stripe — exercises
28
+ // the REAL native pipeline (xss skip) + raw-body read + constructEvent. (The real
29
+ // route's extra PaymentMethod lookup is orthogonal to raw-body fidelity.)
30
+ const stripeApp = new Hono();
31
+ stripeApp.post('/webhook', async (c: any) => {
32
+ const signature = c.req.header('stripe-signature');
33
+ if (!signature) return c.json({ error: 'no sig' }, 400);
34
+ const raw = Buffer.from(await c.req.arrayBuffer());
35
+ try {
36
+ const event = stripe.webhooks.constructEvent(raw, signature, secret);
37
+ return c.json({ received: true, id: event.id, bytesIn: raw.length });
38
+ } catch (err: any) {
39
+ return c.json({ error: err.message }, 400);
40
+ }
41
+ });
42
+
43
+ const bridge = (() => Promise.resolve(new Response('bridge', { status: 404 }))) as any;
44
+ bridge.close = () => Promise.resolve();
45
+ const app = buildHonoApp(
46
+ () => bridge,
47
+ (native: any) => mountResourceGroup(native, '/api/integrations/stripe', stripeApp)
48
+ );
49
+
50
+ let pass = 0;
51
+ let fail = 0;
52
+ const check = (name: string, cond: boolean, extra = '') => {
53
+ if (cond) {
54
+ pass += 1;
55
+ console.log(` PASS ${name}`);
56
+ } else {
57
+ fail += 1;
58
+ console.log(` FAIL ${name} ${extra}`);
59
+ }
60
+ };
61
+
62
+ const PORT = Number(process.env.E2E_PORT || 9278);
63
+ const server = serve({ fetch: app.fetch, port: PORT }, async (info: { port: number }) => {
64
+ const base = `http://127.0.0.1:${info.port}`;
65
+ console.log(`E2E_READY ${info.port} (real @hono/node-server socket)`);
66
+ try {
67
+ // 1) valid signature over a real socket → constructEvent verifies through the native pipeline
68
+ const r1 = await fetch(`${base}/api/integrations/stripe/webhook`, {
69
+ method: 'POST',
70
+ headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
71
+ body: Buffer.from(payload, 'utf8'),
72
+ });
73
+ const b1: any = await r1.json();
74
+ console.log(`E1 signed webhook → ${r1.status} ${JSON.stringify(b1)}`);
75
+ check(
76
+ 'E1 raw-body verified through native pipeline (xss skip)',
77
+ r1.status === 200 && b1.received === true && b1.id === 'evt_s3e_1'
78
+ );
79
+ check(
80
+ 'E1 exact byte count delivered (no consumption/re-encode)',
81
+ b1.bytesIn === Buffer.byteLength(payload, 'utf8'),
82
+ `${b1.bytesIn} vs ${Buffer.byteLength(payload, 'utf8')}`
83
+ );
84
+
85
+ // 2) tampered bytes over the wire → reject
86
+ const buf = Buffer.from(payload, 'utf8');
87
+ // eslint-disable-next-line no-bitwise -- intentional single-byte flip to tamper the payload
88
+ buf[10] = (buf[10] as number) ^ 0x01;
89
+ const r2 = await fetch(`${base}/api/integrations/stripe/webhook`, {
90
+ method: 'POST',
91
+ headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
92
+ body: buf,
93
+ });
94
+ console.log(`E2 tampered webhook → ${r2.status}`);
95
+ check('E2 tampered body → 400 (signature mismatch)', r2.status === 400);
96
+
97
+ // 3) chunked transfer-encoding (streamed body) → still verifies
98
+ const stream = new ReadableStream({
99
+ start(controller) {
100
+ const b = Buffer.from(payload, 'utf8');
101
+ let i = 0;
102
+ const push = () => {
103
+ if (i >= b.length) {
104
+ controller.close();
105
+ return;
106
+ }
107
+ const end = Math.min(i + 5, b.length);
108
+ controller.enqueue(new Uint8Array(b.subarray(i, end)));
109
+ i = end;
110
+ setTimeout(push, 1);
111
+ };
112
+ push();
113
+ },
114
+ });
115
+ const r3 = await fetch(`${base}/api/integrations/stripe/webhook`, {
116
+ method: 'POST',
117
+ headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
118
+ body: stream,
119
+ // @ts-ignore duplex is required for a streamed request body
120
+ duplex: 'half',
121
+ });
122
+ const b3: any = await r3.json();
123
+ console.log(`E3 chunked webhook → ${r3.status} ${JSON.stringify(b3)}`);
124
+ check('E3 chunked transfer-encoding raw-body verified', r3.status === 200 && b3.received === true);
125
+ } catch (err: any) {
126
+ fail += 1;
127
+ console.log(` FAIL harness error: ${err.message}`);
128
+ } finally {
129
+ console.log(`\nphase3e-e2e: ${pass} passed, ${fail} failed`);
130
+ server.close();
131
+ process.exit(fail ? 1 : 0);
132
+ }
133
+ });
134
+
135
+ export {}; // make this file a module so its top-level consts do not collide with sibling e2e scripts in tsc global scope