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,209 @@
1
+ // D1 (S3.0) — TenancySlot is the authoritative source for PAYMENT_TENANT_MODE.
2
+ //
3
+ // The factory derives effectiveConfig.PAYMENT_TENANT_MODE from slots.tenancy.mode
4
+ // (so getTenantMode(), which reads the libs/env config boundary, sees the slot
5
+ // mode without any env). A config PAYMENT_TENANT_MODE that disagrees with the
6
+ // slot is a fail-fast. Intending multi must never silently degrade to single:
7
+ // the default identity/secrets drivers ARE single-tenant behaviors, so a
8
+ // multi-mode host that omits them fails closed. The legacy single path (no
9
+ // tenancy slot + BLOCKLET_APP_PID) stays valid.
10
+
11
+ import { Sequelize } from 'sequelize';
12
+ import { setCoreConfig, getCoreConfig, readConfig } from '../../src/libs/env';
13
+ import { getTenantMode, setDefaultInstanceDid } from '../../src/libs/tenant';
14
+ import {
15
+ createDefaultIdentityDriver,
16
+ setIdentityDriver,
17
+ createDefaultSecretsDriver,
18
+ setSecretsDriver,
19
+ } from '../../src/libs/drivers';
20
+ import { createEmbeddedPaymentService, PaymentCoreSlotError, TenancySlotError } from '../../src/service';
21
+
22
+ const ORIG_MODE = process.env.PAYMENT_TENANT_MODE;
23
+ const ORIG_PID = process.env.BLOCKLET_APP_PID;
24
+
25
+ // minimal slot stubs — only presence matters for D1 (the factory wires them,
26
+ // it does not exercise their methods at construction time).
27
+ const identityStub = { resolveInstanceDidForHost: () => null } as any;
28
+ const secretsStub = createDefaultSecretsDriver();
29
+ // a real in-memory sqlite sequelize: the factory's initialize() defines models
30
+ // on it (no queries at construction), so it must be a genuine Sequelize, not a
31
+ // stub. The data-damage/bad-input cases throw BEFORE initialize() so they never
32
+ // touch it, but the happy/leak cases run the factory to completion.
33
+ const db = { sequelize: new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) as any };
34
+
35
+ afterEach(() => {
36
+ setCoreConfig(undefined);
37
+ setDefaultInstanceDid(undefined);
38
+ // restore module-level driver singletons to their defaults so the multi
39
+ // tests do not leak an injected identity/secrets driver into later specs.
40
+ setIdentityDriver(createDefaultIdentityDriver());
41
+ setSecretsDriver(createDefaultSecretsDriver());
42
+ if (ORIG_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
43
+ else process.env.PAYMENT_TENANT_MODE = ORIG_MODE;
44
+ if (ORIG_PID === undefined) delete process.env.BLOCKLET_APP_PID;
45
+ else process.env.BLOCKLET_APP_PID = ORIG_PID;
46
+ });
47
+
48
+ describe('D1 — happy path: tenancy slot drives getTenantMode without env', () => {
49
+ it('tenancy.mode=multi makes getTenantMode() === "multi" with no env', () => {
50
+ delete process.env.PAYMENT_TENANT_MODE;
51
+ createEmbeddedPaymentService({
52
+ config: {},
53
+ db,
54
+ tenancy: { mode: 'multi' },
55
+ identity: identityStub,
56
+ secrets: secretsStub,
57
+ });
58
+ expect(getTenantMode()).toBe('multi');
59
+ });
60
+
61
+ it('tenancy.mode=single makes getTenantMode() === "single" with no env', () => {
62
+ delete process.env.PAYMENT_TENANT_MODE;
63
+ createEmbeddedPaymentService({
64
+ config: {},
65
+ db,
66
+ tenancy: { mode: 'single', instanceDid: 'did:abt:zSINGLEAPP' },
67
+ });
68
+ expect(getTenantMode()).toBe('single');
69
+ });
70
+ });
71
+
72
+ describe('D1 — bad input: invalid tenancy.mode fails fast', () => {
73
+ it('an unknown tenancy.mode throws at construction with the legal enum in the message', () => {
74
+ delete process.env.PAYMENT_TENANT_MODE;
75
+ let thrown: any;
76
+ try {
77
+ createEmbeddedPaymentService({
78
+ config: {},
79
+ db,
80
+ tenancy: { mode: 'foo' } as any,
81
+ });
82
+ throw new Error('expected a TenancySlotError');
83
+ } catch (err: any) {
84
+ thrown = err;
85
+ }
86
+ expect(thrown).toBeInstanceOf(TenancySlotError);
87
+ expect(thrown.message).toMatch(/single/);
88
+ expect(thrown.message).toMatch(/multi/);
89
+ // fail-fast: it did not silently write a mode into the config boundary
90
+ expect(getCoreConfig()).toBeUndefined();
91
+ });
92
+ });
93
+
94
+ describe('D1 — security: intending multi never silently degrades to single', () => {
95
+ it('multi mode without an identity slot fails closed', () => {
96
+ delete process.env.PAYMENT_TENANT_MODE;
97
+ expect(() =>
98
+ createEmbeddedPaymentService({
99
+ config: {},
100
+ db,
101
+ tenancy: { mode: 'multi' },
102
+ secrets: secretsStub,
103
+ // identity omitted
104
+ })
105
+ ).toThrow(PaymentCoreSlotError);
106
+ });
107
+
108
+ it('multi mode without a secrets slot fails closed', () => {
109
+ delete process.env.PAYMENT_TENANT_MODE;
110
+ expect(() =>
111
+ createEmbeddedPaymentService({
112
+ config: {},
113
+ db,
114
+ tenancy: { mode: 'multi' },
115
+ identity: identityStub,
116
+ // secrets omitted
117
+ })
118
+ ).toThrow(PaymentCoreSlotError);
119
+ });
120
+
121
+ it('multi mode with both slots does NOT fall back to single', () => {
122
+ delete process.env.PAYMENT_TENANT_MODE;
123
+ createEmbeddedPaymentService({
124
+ config: {},
125
+ db,
126
+ tenancy: { mode: 'multi' },
127
+ identity: identityStub,
128
+ secrets: secretsStub,
129
+ });
130
+ expect(getTenantMode()).toBe('multi');
131
+ expect(getTenantMode()).not.toBe('single');
132
+ });
133
+ });
134
+
135
+ describe('D1 — data loss surrogate: legacy single compat preserved', () => {
136
+ it('no tenancy slot + BLOCKLET_APP_PID present resolves as a valid single deployment', () => {
137
+ delete process.env.PAYMENT_TENANT_MODE;
138
+ process.env.BLOCKLET_APP_PID = 'did:abt:zLEGACYAPP';
139
+ expect(() =>
140
+ createEmbeddedPaymentService({
141
+ config: { BLOCKLET_APP_PID: 'did:abt:zLEGACYAPP' },
142
+ db,
143
+ })
144
+ ).not.toThrow();
145
+ expect(getTenantMode()).toBe('single');
146
+ });
147
+ });
148
+
149
+ describe('D1 — data damage: config<->slot conflict fails fast with no half config', () => {
150
+ it('config.PAYMENT_TENANT_MODE that disagrees with tenancy.mode throws and writes nothing', () => {
151
+ delete process.env.PAYMENT_TENANT_MODE;
152
+ let thrown: any;
153
+ try {
154
+ createEmbeddedPaymentService({
155
+ config: { PAYMENT_TENANT_MODE: 'single' },
156
+ db,
157
+ tenancy: { mode: 'multi' },
158
+ identity: identityStub,
159
+ secrets: secretsStub,
160
+ });
161
+ throw new Error('expected a TenancySlotError');
162
+ } catch (err: any) {
163
+ thrown = err;
164
+ }
165
+ expect(thrown).toBeInstanceOf(TenancySlotError);
166
+ // no mode was written into the config boundary (setCoreConfig never ran)
167
+ expect(getCoreConfig()).toBeUndefined();
168
+ });
169
+
170
+ it('a config that AGREES with the slot is accepted (no false conflict)', () => {
171
+ delete process.env.PAYMENT_TENANT_MODE;
172
+ createEmbeddedPaymentService({
173
+ config: { PAYMENT_TENANT_MODE: 'multi' },
174
+ db,
175
+ tenancy: { mode: 'multi' },
176
+ identity: identityStub,
177
+ secrets: secretsStub,
178
+ });
179
+ expect(getTenantMode()).toBe('multi');
180
+ });
181
+ });
182
+
183
+ describe('D1 — data leak surrogate: mode has a single source (the config boundary)', () => {
184
+ it('setDefaultInstanceDid does not flip the resolved tenant mode', () => {
185
+ delete process.env.PAYMENT_TENANT_MODE;
186
+ createEmbeddedPaymentService({
187
+ config: {},
188
+ db,
189
+ tenancy: { mode: 'multi' },
190
+ identity: identityStub,
191
+ secrets: secretsStub,
192
+ });
193
+ expect(getTenantMode()).toBe('multi');
194
+ setDefaultInstanceDid('did:abt:zSOMEONE'); // the only other tenancy setter
195
+ expect(getTenantMode()).toBe('multi'); // mode is unaffected — config is the one source
196
+ });
197
+
198
+ it('effectiveConfig carries the slot mode through the readConfig boundary', () => {
199
+ delete process.env.PAYMENT_TENANT_MODE;
200
+ createEmbeddedPaymentService({
201
+ config: {},
202
+ db,
203
+ tenancy: { mode: 'multi' },
204
+ identity: identityStub,
205
+ secrets: secretsStub,
206
+ });
207
+ expect(readConfig('PAYMENT_TENANT_MODE')).toBe('multi');
208
+ });
209
+ });
@@ -0,0 +1,42 @@
1
+ // Phase 10 (W2-2): libs/tenant default-tenant + row-tenant resolution.
2
+ //
3
+ // The HTTP-driven tenant-middleware scenarios (single mode, multi-mode Host→tenant,
4
+ // fail-closed, X-Forwarded-Host security, malformed Host) moved to the hono
5
+ // middlewares/hono/context.spec.ts when the express contextMiddleware was deleted
6
+ // (express→hono Phase 4). What remains here is the framework-agnostic libs/tenant
7
+ // unit coverage that has no HTTP surface.
8
+ import { getDefaultInstanceDid, setDefaultInstanceDid, resolveRowTenant } from '../../src/libs/tenant';
9
+
10
+ const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
11
+
12
+ // Data damage — the single-mode tenancy slot value drives the default tenant, and
13
+ // pre-backfill (null instance_did) rows resolve to it (upgrade path intact).
14
+ describe('single-mode tenancy slot value (no "tenant swap" on upgrade)', () => {
15
+ const ORIG = (() => {
16
+ const saved = process.env.PAYMENT_TENANT_MODE;
17
+ process.env.PAYMENT_TENANT_MODE = 'single';
18
+ const v = getDefaultInstanceDid();
19
+ if (saved === undefined) delete process.env.PAYMENT_TENANT_MODE;
20
+ else process.env.PAYMENT_TENANT_MODE = saved;
21
+ return v;
22
+ })();
23
+
24
+ beforeEach(() => {
25
+ process.env.PAYMENT_TENANT_MODE = 'single';
26
+ });
27
+ afterEach(() => {
28
+ setDefaultInstanceDid(ORIG); // restore to the env-derived default
29
+ if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
30
+ else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
31
+ });
32
+
33
+ it('the configured instanceDid becomes the default tenant and old null rows map to it', () => {
34
+ setDefaultInstanceDid('did:abt:zCONFIGURED');
35
+ expect(getDefaultInstanceDid()).toBe('did:abt:zCONFIGURED');
36
+ // a pre-backfill row (instance_did = null) resolves to the configured default,
37
+ // not some other tenant — old data stays visible, no corruption
38
+ expect(resolveRowTenant({ instance_did: null })).toBe('did:abt:zCONFIGURED');
39
+ // a row that already carries a tenant is unaffected
40
+ expect(resolveRowTenant({ instance_did: 'did:abt:zEXISTING' })).toBe('did:abt:zEXISTING');
41
+ });
42
+ });
@@ -0,0 +1,120 @@
1
+ // Phase 5 (W1′) — the tenant scanner is now a STRUCTURAL backstop, not a
2
+ // fail-open denylist. These tests pin the new behavior:
3
+ // - the live api/src scan passes with ZERO violations and an EMPTY whitelist
4
+ // (the 151-file fail-open whitelist collapsed because Phase 3/4 made the
5
+ // invariants load-bearing);
6
+ // - assertion ② (raw query on a tenant table must bind a tenant) fires on an
7
+ // unguarded raw read and stays quiet on a guarded one / a non-tenant table.
8
+ // Assertion ① (model must extend TenantModel) is exercised by the test-the-test
9
+ // in logs/s5-e2e.log (revert a model to `extends Model` -> scanner exits 1).
10
+ import { execFileSync } from 'child_process';
11
+ import fs from 'fs';
12
+ import os from 'os';
13
+ import path from 'path';
14
+
15
+ const SCANNER = path.join(__dirname, '../../../scripts/scan-tenant-queries.js');
16
+ const WHITELIST = path.join(__dirname, '../../../scripts/tenant-scan-whitelist.json');
17
+
18
+ function runScanner(args: string[], env: Record<string, string> = {}): { exit: number; result: any } {
19
+ try {
20
+ const out = execFileSync('node', [SCANNER, '--json', ...args], {
21
+ encoding: 'utf8',
22
+ env: { ...process.env, ...env },
23
+ });
24
+ return { exit: 0, result: JSON.parse(out) };
25
+ } catch (err: any) {
26
+ return { exit: err.status ?? 1, result: JSON.parse(err.stdout || '{}') };
27
+ }
28
+ }
29
+
30
+ function writeFixture(name: string, content: string): string {
31
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-'));
32
+ const file = path.join(dir, name);
33
+ fs.writeFileSync(file, content);
34
+ return file;
35
+ }
36
+
37
+ describe('tenant scanner — structural backstop (phase 5)', () => {
38
+ it('the live api/src scan passes with zero violations and an empty whitelist', () => {
39
+ const { exit, result } = runScanner([]);
40
+ expect(exit).toBe(0);
41
+ expect(result.violations).toEqual([]);
42
+ expect(result.staleWhitelistEntries).toEqual([]);
43
+ // the fail-open 151-file whitelist has collapsed
44
+ const whitelist = JSON.parse(fs.readFileSync(WHITELIST, 'utf8'));
45
+ expect(whitelist).toEqual([]);
46
+ });
47
+
48
+ it('assertion ②: an unguarded raw read on a tenant table is flagged', () => {
49
+ const file = writeFixture(
50
+ 'unguarded.ts',
51
+ `export const f = (s: any) => s.query("SELECT * FROM coupons WHERE livemode = 1");`
52
+ );
53
+ const { exit, result } = runScanner([file]);
54
+ expect(exit).toBe(1);
55
+ expect(result.violations[0].kind).toBe('raw-unguarded');
56
+ });
57
+
58
+ it('assertion ②: a tenant-bound raw read is NOT flagged', () => {
59
+ const file = writeFixture(
60
+ 'guarded.ts',
61
+ `export const f = (s: any) => s.query("SELECT * FROM coupons WHERE instance_did = :instance_did");`
62
+ );
63
+ const { exit, result } = runScanner([file]);
64
+ expect(exit).toBe(0);
65
+ expect(result.violations).toEqual([]);
66
+ });
67
+
68
+ it('assertion ②: a DDL statement on a tenant table is exempt (not flagged)', () => {
69
+ const file = writeFixture(
70
+ 'ddl.ts',
71
+ `export const f = (s: any) => s.query('ALTER TABLE "coupons" ADD COLUMN foo TEXT');`
72
+ );
73
+ const { exit } = runScanner([file]);
74
+ expect(exit).toBe(0);
75
+ });
76
+
77
+ it('assertion ②: a raw read on a NON-tenant table is not flagged', () => {
78
+ const file = writeFixture('jobs.ts', `export const f = (s: any) => s.query("SELECT * FROM jobs WHERE id = 1");`);
79
+ const { exit } = runScanner([file]);
80
+ expect(exit).toBe(0);
81
+ });
82
+
83
+ it('Security: DDL is judged by the statement verb — a SELECT cannot hide behind /* CREATE */', () => {
84
+ const file = writeFixture(
85
+ 'evasion.ts',
86
+ `export const f = (s: any) => s.query("SELECT * FROM coupons WHERE id=1 /* CREATE */");`
87
+ );
88
+ const { exit, result } = runScanner([file]);
89
+ expect(exit).toBe(1);
90
+ expect(result.violations[0].kind).toBe('raw-unguarded');
91
+ });
92
+
93
+ it('Bad input: an unparseable (binary / non-utf8) file fails loudly, never a silent skip', () => {
94
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-'));
95
+ const file = path.join(dir, 'binary.ts');
96
+ fs.writeFileSync(file, Buffer.from([0x53, 0x45, 0x00, 0x01, 0xff, 0xfe]));
97
+ const { exit, result } = runScanner([file]);
98
+ expect(exit).toBe(1);
99
+ expect(result.violations[0].kind).toBe('unreadable');
100
+ });
101
+
102
+ it('Bad input: a whitelist entry pointing at a non-existent file is reported stale (exit 1)', () => {
103
+ const wl = writeFixture('wl.json', JSON.stringify([{ file: 'api/src/store/models/does-not-exist.ts', reason: 'x' }]));
104
+ const { exit, result } = runScanner([], { TENANT_SCAN_WHITELIST: wl });
105
+ expect(exit).toBe(1);
106
+ expect(result.staleWhitelistEntries).toContain('api/src/store/models/does-not-exist.ts');
107
+ });
108
+
109
+ it('Security: a wildcard whitelist entry cannot swallow a real violation (exact-path match only)', () => {
110
+ const violating = writeFixture(
111
+ 'leak.ts',
112
+ `export const f = (s: any) => s.query("SELECT * FROM coupons WHERE id=1");`
113
+ );
114
+ // a glob-looking entry must NOT match the violating file by prefix/wildcard
115
+ const wl = writeFixture('wl2.json', JSON.stringify([{ file: 'api/src/**', reason: 'wildcard attempt' }]));
116
+ const { exit, result } = runScanner([violating], { TENANT_SCAN_WHITELIST: wl });
117
+ expect(exit).toBe(1);
118
+ expect(result.violations.some((v: any) => v.kind === 'raw-unguarded')).toBe(true);
119
+ });
120
+ });
@@ -0,0 +1,70 @@
1
+ // Phase 1 (express→hono) — hono cdn fork. Rewrites asset URLs to the CDN host on
2
+ // outgoing HTML in production; inert for JSON /api responses (the only surface in
3
+ // Phases 1-3). The transform core (AssetHostTransformer) is the real SDK util;
4
+ // only the config env (assetCdnHost / componentDid) is mocked here.
5
+ import { Hono } from 'hono';
6
+
7
+ jest.mock('@blocklet/sdk/lib/config', () => ({
8
+ __esModule: true,
9
+ env: { assetCdnHost: 'cdn.example.com', componentDid: 'z-comp' },
10
+ }));
11
+
12
+ // eslint-disable-next-line import/first
13
+ import { cdn } from '../../../src/middlewares/hono/cdn';
14
+
15
+ const ASSET = '/.blocklet/proxy/z-comp/app.js';
16
+ const HTML = `<html><head><script src="${ASSET}"></script></head><body>hi</body></html>`;
17
+
18
+ const prevServiceEnv = process.env.ABT_NODE_SERVICE_ENV;
19
+ beforeAll(() => {
20
+ process.env.ABT_NODE_SERVICE_ENV = 'production';
21
+ });
22
+ afterAll(() => {
23
+ if (prevServiceEnv === undefined) delete process.env.ABT_NODE_SERVICE_ENV;
24
+ else process.env.ABT_NODE_SERVICE_ENV = prevServiceEnv;
25
+ });
26
+
27
+ function buildApp() {
28
+ const app = new Hono();
29
+ app.use('*', cdn());
30
+ app.get('/page', (c) => c.html(HTML));
31
+ app.get('/api/data', (c) => c.json({ asset: ASSET })); // JSON must NOT be rewritten
32
+ return app;
33
+ }
34
+
35
+ const get = (app: Hono, p: string, accept = 'text/html') =>
36
+ app.fetch(new Request(`http://x${p}`, { headers: { accept } }));
37
+
38
+ describe('hono cdn — happy path', () => {
39
+ it('rewrites HTML asset URLs to the CDN host in production', async () => {
40
+ const res = await get(buildApp(), '/page');
41
+ const body = await res.text();
42
+ expect(body).toContain('//cdn.example.com/.blocklet/proxy/z-comp/app.js');
43
+ expect(body).not.toMatch(/"\/\.blocklet\/proxy\/z-comp\/app\.js"/); // original quoted path is gone
44
+ });
45
+ });
46
+
47
+ describe('hono cdn — security / inert for API', () => {
48
+ it('does NOT rewrite a JSON response (content-type guard — inert for /api)', async () => {
49
+ const res = await get(buildApp(), '/api/data', 'application/json');
50
+ const body = await res.json();
51
+ expect(body.asset).toBe(ASSET); // untouched
52
+ });
53
+
54
+ it('does NOT rewrite when the client does not accept html', async () => {
55
+ const res = await get(buildApp(), '/page', 'application/json');
56
+ const body = await res.text();
57
+ // not html-accepting → shouldProcess false → original asset path retained
58
+ expect(body).toContain(`"${ASSET}"`);
59
+ });
60
+ });
61
+
62
+ describe('hono cdn — bad input (non-production inert)', () => {
63
+ it('does NOT rewrite outside production', async () => {
64
+ delete process.env.ABT_NODE_SERVICE_ENV;
65
+ const res = await get(buildApp(), '/page');
66
+ const body = await res.text();
67
+ expect(body).toContain(`"${ASSET}"`); // original retained
68
+ process.env.ABT_NODE_SERVICE_ENV = 'production';
69
+ });
70
+ });
@@ -0,0 +1,113 @@
1
+ // Phase 1 (express→hono) — hono ensureI18n + contextMiddleware fork. Mirrors the
2
+ // express tenant-middleware spec: single-point Host→tenant resolution, raw Host
3
+ // only, multi-mode fail-closed. Driven via app.fetch with an explicit host
4
+ // header (the fork reads c.req.header('host') — never a proxy header).
5
+ import { Hono } from 'hono';
6
+ import { ensureI18n, contextMiddleware } from '../../../src/middlewares/hono/context';
7
+ import { getInstanceDid } from '../../../src/libs/context';
8
+ import { getDefaultInstanceDid } from '../../../src/libs/tenant';
9
+ import {
10
+ setIdentityDriver,
11
+ createDefaultIdentityDriver,
12
+ type IdentityDriver,
13
+ } from '../../../src/libs/drivers/identity';
14
+
15
+ const TENANT_A = 'did:abt:zHOSTA';
16
+ const TENANT_B = 'did:abt:zHOSTB';
17
+
18
+ function buildApp() {
19
+ const app = new Hono();
20
+ app.use('*', ensureI18n());
21
+ app.use('*', contextMiddleware());
22
+ app.get('/whoami', (c) => c.json({ tenant: getInstanceDid(), locale: c.get('locale') }));
23
+ return app;
24
+ }
25
+
26
+ const get = (app: Hono, host: string, extra: Record<string, string> = {}, path = '/whoami') =>
27
+ app.fetch(new Request(`http://${host}${path}`, { headers: { host, ...extra } }));
28
+
29
+ afterEach(() => {
30
+ delete process.env.PAYMENT_TENANT_MODE;
31
+ setIdentityDriver(createDefaultIdentityDriver());
32
+ });
33
+
34
+ describe('hono ensureI18n', () => {
35
+ it('sets locale from ?locale and defaults to en', async () => {
36
+ const app = buildApp();
37
+ expect((await (await get(app, 'x.example.com', {}, '/whoami?locale=zh')).json()).locale).toBe('zh');
38
+ expect((await (await get(app, 'x.example.com')).json()).locale).toBe('en');
39
+ });
40
+ });
41
+
42
+ describe('hono contextMiddleware — single mode (default test env)', () => {
43
+ it('resolves to the default tenant regardless of host', async () => {
44
+ const app = buildApp();
45
+ const expected = getDefaultInstanceDid();
46
+ expect((await (await get(app, 'a.example.com')).json()).tenant).toBe(expected);
47
+ expect((await (await get(app, 'other.example.com')).json()).tenant).toBe(expected);
48
+ });
49
+ });
50
+
51
+ describe('hono contextMiddleware — multi mode (Host→tenant, fail-closed)', () => {
52
+ const identity: IdentityDriver = {
53
+ resolveInstanceDidForHost(host) {
54
+ if (host === 'a.example.com') return TENANT_A;
55
+ if (host === 'b.example.com') return TENANT_B;
56
+ return null;
57
+ },
58
+ };
59
+
60
+ beforeEach(() => {
61
+ process.env.PAYMENT_TENANT_MODE = 'multi';
62
+ setIdentityDriver(identity);
63
+ });
64
+
65
+ it('two hosts see two tenants (data isolation)', async () => {
66
+ const app = buildApp();
67
+ expect((await (await get(app, 'a.example.com')).json()).tenant).toBe(TENANT_A);
68
+ expect((await (await get(app, 'b.example.com')).json()).tenant).toBe(TENANT_B);
69
+ });
70
+
71
+ it('unknown host fails closed 400 + error code, no tenant leak', async () => {
72
+ const r = await get(buildApp(), 'unknown.example.com');
73
+ expect(r.status).toBe(400);
74
+ const body = await r.json();
75
+ expect(body.error.code).toBe('TENANT_HOST_UNRESOLVED');
76
+ expect(body.tenant).toBeUndefined();
77
+ });
78
+
79
+ it('SECURITY: a forged X-Forwarded-Host cannot change resolution (raw Host only)', async () => {
80
+ const r = await get(buildApp(), 'a.example.com', { 'x-forwarded-host': 'b.example.com' });
81
+ expect((await r.json()).tenant).toBe(TENANT_A);
82
+ });
83
+
84
+ it('SECURITY: X-Forwarded-Host pointing at a known tenant cannot rescue an unknown raw Host', async () => {
85
+ const r = await get(buildApp(), 'unknown.example.com', { 'x-forwarded-host': 'a.example.com' });
86
+ expect(r.status).toBe(400);
87
+ });
88
+
89
+ it('an empty / malformed Host fails closed without crashing', async () => {
90
+ // host supplied via the Host header (fixed URL) so an empty/garbage value
91
+ // does not break URL construction; the fork reads c.req.header('host').
92
+ const app = buildApp();
93
+ const empty = await app.fetch(new Request('http://req.local/whoami', { headers: { host: '' } }));
94
+ expect(empty.status).toBe(400);
95
+ const garbage = await app.fetch(new Request('http://req.local/whoami', { headers: { host: ':::garbage:::' } }));
96
+ expect(garbage.status).toBe(400);
97
+ });
98
+
99
+ it('a 4xx-rejected request never reaches the route handler (no write)', async () => {
100
+ let handlerRuns = 0;
101
+ const app = new Hono();
102
+ app.use('*', contextMiddleware());
103
+ app.get('/whoami', (c) => {
104
+ handlerRuns += 1;
105
+ return c.json({ tenant: getInstanceDid() });
106
+ });
107
+ const r = await app.fetch(
108
+ new Request('http://unknown.example.com/whoami', { headers: { host: 'unknown.example.com' } })
109
+ );
110
+ expect(r.status).toBe(400);
111
+ expect(handlerRuns).toBe(0);
112
+ });
113
+ });