payment-kit 1.29.1 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +36 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +27 -24
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,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
@@ -0,0 +1,114 @@
1
+ /* eslint-disable no-console */
2
+ // express→hono Phase 4 E2E — adapter collapse evidence.
3
+ //
4
+ // Proves the two production paths the collapse must preserve, over a REAL
5
+ // @hono/node-server socket (not app.fetch mock) where it matters:
6
+ // 1. the full hono app (buildHonoApp) serves /api/healthz over a real socket —
7
+ // the same serve({ fetch }) path the blocklet server uses;
8
+ // 2. svc.http.fetch (createFetchHandler) strips the host mount prefix and
9
+ // forwards RAW body bytes byte-for-byte (Stripe webhook signature fidelity).
10
+ // Raw JSON evidence only — appended to logs/s4-e2e.log.
11
+ import fs from 'fs';
12
+ import http from 'http';
13
+ import path from 'path';
14
+ import crypto from 'crypto';
15
+ import { Hono } from 'hono';
16
+ import { serve } from '@hono/node-server';
17
+ import { buildHonoApp } from '../api/src/service';
18
+ import { createFetchHandler } from '../api/src/libs/http-fetch-adapter';
19
+
20
+ const LOG = path.resolve(__dirname, '../../../docs/arc-integration/planning/express-to-hono/logs/s4-e2e.log');
21
+ fs.mkdirSync(path.dirname(LOG), { recursive: true });
22
+ fs.writeFileSync(LOG, '');
23
+ const log = (header: string, payload: unknown) => {
24
+ fs.appendFileSync(LOG, `=== ${header} ===\n${JSON.stringify(payload)}\n`);
25
+ console.log(header, JSON.stringify(payload));
26
+ };
27
+
28
+ const socketGet = (port: number, p: string): Promise<{ status: number; body: string }> =>
29
+ new Promise((resolve, reject) => {
30
+ const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET' }, (res) => {
31
+ let data = '';
32
+ res.on('data', (c) => {
33
+ data += c;
34
+ });
35
+ res.on('end', () => resolve({ status: res.statusCode || 0, body: data }));
36
+ });
37
+ req.on('error', reject);
38
+ req.end();
39
+ });
40
+
41
+ async function main() {
42
+ let failures = 0;
43
+ const expect = (name: string, cond: boolean) => {
44
+ if (!cond) {
45
+ failures += 1;
46
+ log(`FAIL ${name}`, { ok: false });
47
+ }
48
+ };
49
+
50
+ // ── E1: full hono app over a REAL @hono/node-server socket ───────────────────
51
+ const app = buildHonoApp();
52
+ const server = serve({ fetch: app.fetch, port: 0, hostname: '127.0.0.1' }) as any;
53
+ const port: number = await new Promise((resolve) => {
54
+ if (server.address()?.port) return resolve(server.address().port);
55
+ server.on('listening', () => resolve(server.address().port));
56
+ // @hono/node-server resolves synchronously in most versions; fall back to a tick
57
+ setImmediate(() => resolve(server.address().port));
58
+ });
59
+
60
+ const health = await socketGet(port, '/api/healthz');
61
+ log('E1 GET /api/healthz (real @hono/node-server socket)', { status: health.status, body: JSON.parse(health.body) });
62
+ expect('E1 healthz 200', health.status === 200);
63
+ expect('E1 healthz {ok:true}', JSON.parse(health.body).ok === true);
64
+
65
+ const notFound = await socketGet(port, '/api/__does_not_exist__');
66
+ log('E3(negative) GET /api/__does_not_exist__ (unmatched → 404)', { status: notFound.status });
67
+ expect('E3 unmatched 404', notFound.status === 404);
68
+
69
+ await new Promise<void>((resolve) => server.close(() => resolve()));
70
+
71
+ // ── E2: svc.http.fetch base-strip + RAW body fidelity ────────────────────────
72
+ const core = new Hono();
73
+ core.post('/api/raw', async (c) => {
74
+ const buf = Buffer.from(await c.req.arrayBuffer());
75
+ return c.json({ len: buf.length, sig: crypto.createHmac('sha256', 'whsec_s4').update(buf).digest('hex') });
76
+ });
77
+ const httpFetch = createFetchHandler(core);
78
+
79
+ const payload = JSON.stringify({ id: 'evt_1', data: { nested: ['<b>', 1, null] } });
80
+ const expectedSig = crypto.createHmac('sha256', 'whsec_s4').update(payload).digest('hex');
81
+ const stripped = await httpFetch(
82
+ new Request('http://app.local/.well-known/payment/api/raw', {
83
+ method: 'POST',
84
+ headers: { 'content-type': 'application/json' },
85
+ body: payload,
86
+ }),
87
+ { basePath: '/.well-known/payment' }
88
+ );
89
+ const strippedBody = await stripped.json();
90
+ log('E2 svc.http.fetch base-strip + raw-body HMAC', {
91
+ status: stripped.status,
92
+ forwardedBytes: strippedBody.len,
93
+ expectedBytes: Buffer.byteLength(payload),
94
+ sigMatch: strippedBody.sig === expectedSig,
95
+ });
96
+ expect('E2 status 200', stripped.status === 200);
97
+ expect('E2 byte-exact', strippedBody.len === Buffer.byteLength(payload));
98
+ expect('E2 sig match', strippedBody.sig === expectedSig);
99
+
100
+ // negative: segment boundary — "/mntbeta" not stripped by basePath "/mnt"
101
+ const boundary = await httpFetch(new Request('http://app.local/mntbeta/api/raw', { method: 'POST', body: 'x' }), {
102
+ basePath: '/mnt',
103
+ });
104
+ log('E2(negative) segment-boundary "/mntbeta" not stripped by "/mnt"', { status: boundary.status });
105
+ expect('E2 boundary 404', boundary.status === 404);
106
+
107
+ log('SUMMARY', { failures, success: failures === 0 });
108
+ if (failures > 0) process.exit(1);
109
+ }
110
+
111
+ main().catch((err) => {
112
+ log('FATAL', { error: err?.message, stack: err?.stack?.split('\n').slice(0, 5) });
113
+ process.exit(1);
114
+ });