@dotdo/postgres 0.1.0

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 (1129) hide show
  1. package/README.md +868 -0
  2. package/dist/cdc/change-stream.d.ts +44 -0
  3. package/dist/cdc/change-stream.d.ts.map +1 -0
  4. package/dist/cdc/change-stream.js +95 -0
  5. package/dist/cdc/change-stream.js.map +1 -0
  6. package/dist/cdc/filter.d.ts +58 -0
  7. package/dist/cdc/filter.d.ts.map +1 -0
  8. package/dist/cdc/filter.js +520 -0
  9. package/dist/cdc/filter.js.map +1 -0
  10. package/dist/cdc/index.d.ts +47 -0
  11. package/dist/cdc/index.d.ts.map +1 -0
  12. package/dist/cdc/index.js +50 -0
  13. package/dist/cdc/index.js.map +1 -0
  14. package/dist/cdc/resume-token.d.ts +60 -0
  15. package/dist/cdc/resume-token.d.ts.map +1 -0
  16. package/dist/cdc/resume-token.js +228 -0
  17. package/dist/cdc/resume-token.js.map +1 -0
  18. package/dist/cdc/transport/index.d.ts +7 -0
  19. package/dist/cdc/transport/index.d.ts.map +1 -0
  20. package/dist/cdc/transport/index.js +7 -0
  21. package/dist/cdc/transport/index.js.map +1 -0
  22. package/dist/cdc/transport/sse.d.ts +120 -0
  23. package/dist/cdc/transport/sse.d.ts.map +1 -0
  24. package/dist/cdc/transport/sse.js +590 -0
  25. package/dist/cdc/transport/sse.js.map +1 -0
  26. package/dist/cdc/transport/websocket.d.ts +130 -0
  27. package/dist/cdc/transport/websocket.d.ts.map +1 -0
  28. package/dist/cdc/transport/websocket.js +688 -0
  29. package/dist/cdc/transport/websocket.js.map +1 -0
  30. package/dist/cdc/types.d.ts +306 -0
  31. package/dist/cdc/types.d.ts.map +1 -0
  32. package/dist/cdc/types.js +8 -0
  33. package/dist/cdc/types.js.map +1 -0
  34. package/dist/config/index.d.ts +25 -0
  35. package/dist/config/index.d.ts.map +1 -0
  36. package/dist/config/index.js +25 -0
  37. package/dist/config/index.js.map +1 -0
  38. package/dist/config/memory.d.ts +139 -0
  39. package/dist/config/memory.d.ts.map +1 -0
  40. package/dist/config/memory.js +157 -0
  41. package/dist/config/memory.js.map +1 -0
  42. package/dist/config/storage.d.ts +157 -0
  43. package/dist/config/storage.d.ts.map +1 -0
  44. package/dist/config/storage.js +178 -0
  45. package/dist/config/storage.js.map +1 -0
  46. package/dist/config/streaming.d.ts +117 -0
  47. package/dist/config/streaming.d.ts.map +1 -0
  48. package/dist/config/streaming.js +132 -0
  49. package/dist/config/streaming.js.map +1 -0
  50. package/dist/config/timeouts.d.ts +168 -0
  51. package/dist/config/timeouts.d.ts.map +1 -0
  52. package/dist/config/timeouts.js +192 -0
  53. package/dist/config/timeouts.js.map +1 -0
  54. package/dist/extensions/config.d.ts +89 -0
  55. package/dist/extensions/config.d.ts.map +1 -0
  56. package/dist/extensions/config.js +216 -0
  57. package/dist/extensions/config.js.map +1 -0
  58. package/dist/extensions/geo.d.ts +452 -0
  59. package/dist/extensions/geo.d.ts.map +1 -0
  60. package/dist/extensions/geo.js +583 -0
  61. package/dist/extensions/geo.js.map +1 -0
  62. package/dist/extensions/index.d.ts +167 -0
  63. package/dist/extensions/index.d.ts.map +1 -0
  64. package/dist/extensions/index.js +99 -0
  65. package/dist/extensions/index.js.map +1 -0
  66. package/dist/extensions/loader.d.ts +226 -0
  67. package/dist/extensions/loader.d.ts.map +1 -0
  68. package/dist/extensions/loader.js +456 -0
  69. package/dist/extensions/loader.js.map +1 -0
  70. package/dist/extensions/pgmq-lite.d.ts +330 -0
  71. package/dist/extensions/pgmq-lite.d.ts.map +1 -0
  72. package/dist/extensions/pgmq-lite.js +648 -0
  73. package/dist/extensions/pgmq-lite.js.map +1 -0
  74. package/dist/extensions/plugins.d.ts +260 -0
  75. package/dist/extensions/plugins.d.ts.map +1 -0
  76. package/dist/extensions/plugins.js +535 -0
  77. package/dist/extensions/plugins.js.map +1 -0
  78. package/dist/extensions/registry.d.ts +93 -0
  79. package/dist/extensions/registry.d.ts.map +1 -0
  80. package/dist/extensions/registry.js +182 -0
  81. package/dist/extensions/registry.js.map +1 -0
  82. package/dist/extensions/vector.d.ts +106 -0
  83. package/dist/extensions/vector.d.ts.map +1 -0
  84. package/dist/extensions/vector.js +129 -0
  85. package/dist/extensions/vector.js.map +1 -0
  86. package/dist/iceberg/analytics.d.ts +279 -0
  87. package/dist/iceberg/analytics.d.ts.map +1 -0
  88. package/dist/iceberg/analytics.js +448 -0
  89. package/dist/iceberg/analytics.js.map +1 -0
  90. package/dist/iceberg/catalog-api.d.ts +39 -0
  91. package/dist/iceberg/catalog-api.d.ts.map +1 -0
  92. package/dist/iceberg/catalog-api.js +388 -0
  93. package/dist/iceberg/catalog-api.js.map +1 -0
  94. package/dist/iceberg/catalog.d.ts +401 -0
  95. package/dist/iceberg/catalog.d.ts.map +1 -0
  96. package/dist/iceberg/catalog.js +677 -0
  97. package/dist/iceberg/catalog.js.map +1 -0
  98. package/dist/iceberg/duckdb-wasm.d.ts +447 -0
  99. package/dist/iceberg/duckdb-wasm.d.ts.map +1 -0
  100. package/dist/iceberg/duckdb-wasm.js +600 -0
  101. package/dist/iceberg/duckdb-wasm.js.map +1 -0
  102. package/dist/iceberg/index.d.ts +92 -0
  103. package/dist/iceberg/index.d.ts.map +1 -0
  104. package/dist/iceberg/index.js +119 -0
  105. package/dist/iceberg/index.js.map +1 -0
  106. package/dist/iceberg/metadata.d.ts +214 -0
  107. package/dist/iceberg/metadata.d.ts.map +1 -0
  108. package/dist/iceberg/metadata.js +535 -0
  109. package/dist/iceberg/metadata.js.map +1 -0
  110. package/dist/iceberg/optimizer.d.ts +296 -0
  111. package/dist/iceberg/optimizer.d.ts.map +1 -0
  112. package/dist/iceberg/optimizer.js +889 -0
  113. package/dist/iceberg/optimizer.js.map +1 -0
  114. package/dist/iceberg/parquet.d.ts +447 -0
  115. package/dist/iceberg/parquet.d.ts.map +1 -0
  116. package/dist/iceberg/parquet.js +1225 -0
  117. package/dist/iceberg/parquet.js.map +1 -0
  118. package/dist/iceberg/r2-organization.d.ts +422 -0
  119. package/dist/iceberg/r2-organization.d.ts.map +1 -0
  120. package/dist/iceberg/r2-organization.js +672 -0
  121. package/dist/iceberg/r2-organization.js.map +1 -0
  122. package/dist/iceberg/scheduler-do-example.d.ts +158 -0
  123. package/dist/iceberg/scheduler-do-example.d.ts.map +1 -0
  124. package/dist/iceberg/scheduler-do-example.js +261 -0
  125. package/dist/iceberg/scheduler-do-example.js.map +1 -0
  126. package/dist/iceberg/scheduler.d.ts +434 -0
  127. package/dist/iceberg/scheduler.d.ts.map +1 -0
  128. package/dist/iceberg/scheduler.js +818 -0
  129. package/dist/iceberg/scheduler.js.map +1 -0
  130. package/dist/iceberg/schema.d.ts +149 -0
  131. package/dist/iceberg/schema.d.ts.map +1 -0
  132. package/dist/iceberg/schema.js +525 -0
  133. package/dist/iceberg/schema.js.map +1 -0
  134. package/dist/iceberg/snapshot-manager.d.ts +406 -0
  135. package/dist/iceberg/snapshot-manager.d.ts.map +1 -0
  136. package/dist/iceberg/snapshot-manager.js +934 -0
  137. package/dist/iceberg/snapshot-manager.js.map +1 -0
  138. package/dist/iceberg/sql-router.d.ts +194 -0
  139. package/dist/iceberg/sql-router.d.ts.map +1 -0
  140. package/dist/iceberg/sql-router.js +180 -0
  141. package/dist/iceberg/sql-router.js.map +1 -0
  142. package/dist/iceberg/test-fixtures.d.ts +151 -0
  143. package/dist/iceberg/test-fixtures.d.ts.map +1 -0
  144. package/dist/iceberg/test-fixtures.js +446 -0
  145. package/dist/iceberg/test-fixtures.js.map +1 -0
  146. package/dist/iceberg/time-travel-api.d.ts +102 -0
  147. package/dist/iceberg/time-travel-api.d.ts.map +1 -0
  148. package/dist/iceberg/time-travel-api.js +437 -0
  149. package/dist/iceberg/time-travel-api.js.map +1 -0
  150. package/dist/iceberg/time-travel.d.ts +293 -0
  151. package/dist/iceberg/time-travel.d.ts.map +1 -0
  152. package/dist/iceberg/time-travel.js +689 -0
  153. package/dist/iceberg/time-travel.js.map +1 -0
  154. package/dist/iceberg/transformer.d.ts +356 -0
  155. package/dist/iceberg/transformer.d.ts.map +1 -0
  156. package/dist/iceberg/transformer.js +770 -0
  157. package/dist/iceberg/transformer.js.map +1 -0
  158. package/dist/iceberg/types.d.ts +318 -0
  159. package/dist/iceberg/types.d.ts.map +1 -0
  160. package/dist/iceberg/types.js +9 -0
  161. package/dist/iceberg/types.js.map +1 -0
  162. package/dist/iceberg/writer.d.ts +144 -0
  163. package/dist/iceberg/writer.d.ts.map +1 -0
  164. package/dist/iceberg/writer.js +452 -0
  165. package/dist/iceberg/writer.js.map +1 -0
  166. package/dist/index.d.ts +50 -0
  167. package/dist/index.d.ts.map +1 -0
  168. package/dist/index.js +69 -0
  169. package/dist/index.js.map +1 -0
  170. package/dist/lineage/index.d.ts +11 -0
  171. package/dist/lineage/index.d.ts.map +1 -0
  172. package/dist/lineage/index.js +11 -0
  173. package/dist/lineage/index.js.map +1 -0
  174. package/dist/lineage/integration.d.ts +134 -0
  175. package/dist/lineage/integration.d.ts.map +1 -0
  176. package/dist/lineage/integration.js +258 -0
  177. package/dist/lineage/integration.js.map +1 -0
  178. package/dist/lineage/tracker.d.ts +189 -0
  179. package/dist/lineage/tracker.d.ts.map +1 -0
  180. package/dist/lineage/tracker.js +1352 -0
  181. package/dist/lineage/tracker.js.map +1 -0
  182. package/dist/lineage/types.d.ts +318 -0
  183. package/dist/lineage/types.d.ts.map +1 -0
  184. package/dist/lineage/types.js +9 -0
  185. package/dist/lineage/types.js.map +1 -0
  186. package/dist/middleware/index.d.ts +11 -0
  187. package/dist/middleware/index.d.ts.map +1 -0
  188. package/dist/middleware/index.js +16 -0
  189. package/dist/middleware/index.js.map +1 -0
  190. package/dist/middleware/rate-limit.d.ts +397 -0
  191. package/dist/middleware/rate-limit.d.ts.map +1 -0
  192. package/dist/middleware/rate-limit.js +507 -0
  193. package/dist/middleware/rate-limit.js.map +1 -0
  194. package/dist/migration-tooling/external-migration.d.ts +601 -0
  195. package/dist/migration-tooling/external-migration.d.ts.map +1 -0
  196. package/dist/migration-tooling/external-migration.js +1612 -0
  197. package/dist/migration-tooling/external-migration.js.map +1 -0
  198. package/dist/migration-tooling/index.d.ts +19 -0
  199. package/dist/migration-tooling/index.d.ts.map +1 -0
  200. package/dist/migration-tooling/index.js +19 -0
  201. package/dist/migration-tooling/index.js.map +1 -0
  202. package/dist/migrations/auto-migrator.d.ts +289 -0
  203. package/dist/migrations/auto-migrator.d.ts.map +1 -0
  204. package/dist/migrations/auto-migrator.js +396 -0
  205. package/dist/migrations/auto-migrator.js.map +1 -0
  206. package/dist/migrations/bulk-orchestrator.d.ts +403 -0
  207. package/dist/migrations/bulk-orchestrator.d.ts.map +1 -0
  208. package/dist/migrations/bulk-orchestrator.js +646 -0
  209. package/dist/migrations/bulk-orchestrator.js.map +1 -0
  210. package/dist/migrations/compatibility.d.ts +216 -0
  211. package/dist/migrations/compatibility.d.ts.map +1 -0
  212. package/dist/migrations/compatibility.js +651 -0
  213. package/dist/migrations/compatibility.js.map +1 -0
  214. package/dist/migrations/do-migrations.d.ts +101 -0
  215. package/dist/migrations/do-migrations.d.ts.map +1 -0
  216. package/dist/migrations/do-migrations.js +1060 -0
  217. package/dist/migrations/do-migrations.js.map +1 -0
  218. package/dist/migrations/do-migrations.types.d.ts +550 -0
  219. package/dist/migrations/do-migrations.types.d.ts.map +1 -0
  220. package/dist/migrations/do-migrations.types.js +15 -0
  221. package/dist/migrations/do-migrations.types.js.map +1 -0
  222. package/dist/migrations/drizzle-compat.d.ts +163 -0
  223. package/dist/migrations/drizzle-compat.d.ts.map +1 -0
  224. package/dist/migrations/drizzle-compat.js +273 -0
  225. package/dist/migrations/drizzle-compat.js.map +1 -0
  226. package/dist/migrations/index.d.ts +109 -0
  227. package/dist/migrations/index.d.ts.map +1 -0
  228. package/dist/migrations/index.js +127 -0
  229. package/dist/migrations/index.js.map +1 -0
  230. package/dist/migrations/migration-api.d.ts +161 -0
  231. package/dist/migrations/migration-api.d.ts.map +1 -0
  232. package/dist/migrations/migration-api.js +499 -0
  233. package/dist/migrations/migration-api.js.map +1 -0
  234. package/dist/migrations/progress-tracker-do.d.ts +195 -0
  235. package/dist/migrations/progress-tracker-do.d.ts.map +1 -0
  236. package/dist/migrations/progress-tracker-do.js +339 -0
  237. package/dist/migrations/progress-tracker-do.js.map +1 -0
  238. package/dist/migrations/progress-tracker-kv.d.ts +103 -0
  239. package/dist/migrations/progress-tracker-kv.d.ts.map +1 -0
  240. package/dist/migrations/progress-tracker-kv.js +231 -0
  241. package/dist/migrations/progress-tracker-kv.js.map +1 -0
  242. package/dist/migrations/progress-tracker.d.ts +320 -0
  243. package/dist/migrations/progress-tracker.d.ts.map +1 -0
  244. package/dist/migrations/progress-tracker.js +443 -0
  245. package/dist/migrations/progress-tracker.js.map +1 -0
  246. package/dist/migrations/registry.d.ts +231 -0
  247. package/dist/migrations/registry.d.ts.map +1 -0
  248. package/dist/migrations/registry.js +376 -0
  249. package/dist/migrations/registry.js.map +1 -0
  250. package/dist/migrations/runner.d.ts +197 -0
  251. package/dist/migrations/runner.d.ts.map +1 -0
  252. package/dist/migrations/runner.js +1167 -0
  253. package/dist/migrations/runner.js.map +1 -0
  254. package/dist/migrations/schema-generator.d.ts +111 -0
  255. package/dist/migrations/schema-generator.d.ts.map +1 -0
  256. package/dist/migrations/schema-generator.js +335 -0
  257. package/dist/migrations/schema-generator.js.map +1 -0
  258. package/dist/migrations/testing.d.ts +321 -0
  259. package/dist/migrations/testing.d.ts.map +1 -0
  260. package/dist/migrations/testing.js +645 -0
  261. package/dist/migrations/testing.js.map +1 -0
  262. package/dist/migrations/types.d.ts +503 -0
  263. package/dist/migrations/types.d.ts.map +1 -0
  264. package/dist/migrations/types.js +11 -0
  265. package/dist/migrations/types.js.map +1 -0
  266. package/dist/migrations/validator.d.ts +215 -0
  267. package/dist/migrations/validator.d.ts.map +1 -0
  268. package/dist/migrations/validator.js +494 -0
  269. package/dist/migrations/validator.js.map +1 -0
  270. package/dist/observability/alerting.d.ts +116 -0
  271. package/dist/observability/alerting.d.ts.map +1 -0
  272. package/dist/observability/alerting.js +353 -0
  273. package/dist/observability/alerting.js.map +1 -0
  274. package/dist/observability/analytics-engine.d.ts +357 -0
  275. package/dist/observability/analytics-engine.d.ts.map +1 -0
  276. package/dist/observability/analytics-engine.js +430 -0
  277. package/dist/observability/analytics-engine.js.map +1 -0
  278. package/dist/observability/cost-metrics.d.ts +269 -0
  279. package/dist/observability/cost-metrics.d.ts.map +1 -0
  280. package/dist/observability/cost-metrics.js +560 -0
  281. package/dist/observability/cost-metrics.js.map +1 -0
  282. package/dist/observability/cross-do-tracing.d.ts +305 -0
  283. package/dist/observability/cross-do-tracing.d.ts.map +1 -0
  284. package/dist/observability/cross-do-tracing.js +431 -0
  285. package/dist/observability/cross-do-tracing.js.map +1 -0
  286. package/dist/observability/error-rate-collector.d.ts +163 -0
  287. package/dist/observability/error-rate-collector.d.ts.map +1 -0
  288. package/dist/observability/error-rate-collector.js +306 -0
  289. package/dist/observability/error-rate-collector.js.map +1 -0
  290. package/dist/observability/exporters.d.ts +231 -0
  291. package/dist/observability/exporters.d.ts.map +1 -0
  292. package/dist/observability/exporters.js +479 -0
  293. package/dist/observability/exporters.js.map +1 -0
  294. package/dist/observability/health-check.d.ts +106 -0
  295. package/dist/observability/health-check.d.ts.map +1 -0
  296. package/dist/observability/health-check.js +243 -0
  297. package/dist/observability/health-check.js.map +1 -0
  298. package/dist/observability/index.d.ts +297 -0
  299. package/dist/observability/index.d.ts.map +1 -0
  300. package/dist/observability/index.js +455 -0
  301. package/dist/observability/index.js.map +1 -0
  302. package/dist/observability/instrumentation.d.ts +222 -0
  303. package/dist/observability/instrumentation.d.ts.map +1 -0
  304. package/dist/observability/instrumentation.js +532 -0
  305. package/dist/observability/instrumentation.js.map +1 -0
  306. package/dist/observability/memory-metrics.d.ts +227 -0
  307. package/dist/observability/memory-metrics.d.ts.map +1 -0
  308. package/dist/observability/memory-metrics.js +688 -0
  309. package/dist/observability/memory-metrics.js.map +1 -0
  310. package/dist/observability/metrics-endpoint.d.ts +91 -0
  311. package/dist/observability/metrics-endpoint.d.ts.map +1 -0
  312. package/dist/observability/metrics-endpoint.js +246 -0
  313. package/dist/observability/metrics-endpoint.js.map +1 -0
  314. package/dist/observability/metrics.d.ts +88 -0
  315. package/dist/observability/metrics.d.ts.map +1 -0
  316. package/dist/observability/metrics.js +253 -0
  317. package/dist/observability/metrics.js.map +1 -0
  318. package/dist/observability/observability-features.d.ts +488 -0
  319. package/dist/observability/observability-features.d.ts.map +1 -0
  320. package/dist/observability/observability-features.js +773 -0
  321. package/dist/observability/observability-features.js.map +1 -0
  322. package/dist/observability/prometheus.d.ts +39 -0
  323. package/dist/observability/prometheus.d.ts.map +1 -0
  324. package/dist/observability/prometheus.js +120 -0
  325. package/dist/observability/prometheus.js.map +1 -0
  326. package/dist/observability/propagation.d.ts +126 -0
  327. package/dist/observability/propagation.d.ts.map +1 -0
  328. package/dist/observability/propagation.js +234 -0
  329. package/dist/observability/propagation.js.map +1 -0
  330. package/dist/observability/query-latency.d.ts +243 -0
  331. package/dist/observability/query-latency.d.ts.map +1 -0
  332. package/dist/observability/query-latency.js +292 -0
  333. package/dist/observability/query-latency.js.map +1 -0
  334. package/dist/observability/query-performance.d.ts +169 -0
  335. package/dist/observability/query-performance.d.ts.map +1 -0
  336. package/dist/observability/query-performance.js +290 -0
  337. package/dist/observability/query-performance.js.map +1 -0
  338. package/dist/observability/storage-tier-metrics.d.ts +174 -0
  339. package/dist/observability/storage-tier-metrics.d.ts.map +1 -0
  340. package/dist/observability/storage-tier-metrics.js +306 -0
  341. package/dist/observability/storage-tier-metrics.js.map +1 -0
  342. package/dist/observability/tier-cost-optimizer.d.ts +155 -0
  343. package/dist/observability/tier-cost-optimizer.d.ts.map +1 -0
  344. package/dist/observability/tier-cost-optimizer.js +536 -0
  345. package/dist/observability/tier-cost-optimizer.js.map +1 -0
  346. package/dist/observability/tracer.d.ts +149 -0
  347. package/dist/observability/tracer.d.ts.map +1 -0
  348. package/dist/observability/tracer.js +435 -0
  349. package/dist/observability/tracer.js.map +1 -0
  350. package/dist/observability/types.d.ts +402 -0
  351. package/dist/observability/types.d.ts.map +1 -0
  352. package/dist/observability/types.js +103 -0
  353. package/dist/observability/types.js.map +1 -0
  354. package/dist/pglite/workers-pglite.d.ts +138 -0
  355. package/dist/pglite/workers-pglite.d.ts.map +1 -0
  356. package/dist/pglite/workers-pglite.js +143 -0
  357. package/dist/pglite/workers-pglite.js.map +1 -0
  358. package/dist/pglite-assets/pglite.data +0 -0
  359. package/dist/pglite-assets/pglite.wasm +0 -0
  360. package/dist/playground/index.d.ts +52 -0
  361. package/dist/playground/index.d.ts.map +1 -0
  362. package/dist/playground/index.js +55 -0
  363. package/dist/playground/index.js.map +1 -0
  364. package/dist/playground/keyboard-shortcuts.d.ts +116 -0
  365. package/dist/playground/keyboard-shortcuts.d.ts.map +1 -0
  366. package/dist/playground/keyboard-shortcuts.js +588 -0
  367. package/dist/playground/keyboard-shortcuts.js.map +1 -0
  368. package/dist/playground/playground.d.ts +82 -0
  369. package/dist/playground/playground.d.ts.map +1 -0
  370. package/dist/playground/playground.js +271 -0
  371. package/dist/playground/playground.js.map +1 -0
  372. package/dist/playground/query-executor.d.ts +115 -0
  373. package/dist/playground/query-executor.d.ts.map +1 -0
  374. package/dist/playground/query-executor.js +558 -0
  375. package/dist/playground/query-executor.js.map +1 -0
  376. package/dist/playground/query-history.d.ts +92 -0
  377. package/dist/playground/query-history.d.ts.map +1 -0
  378. package/dist/playground/query-history.js +259 -0
  379. package/dist/playground/query-history.js.map +1 -0
  380. package/dist/playground/result-formatter.d.ts +59 -0
  381. package/dist/playground/result-formatter.d.ts.map +1 -0
  382. package/dist/playground/result-formatter.js +341 -0
  383. package/dist/playground/result-formatter.js.map +1 -0
  384. package/dist/playground/sample-datasets.d.ts +77 -0
  385. package/dist/playground/sample-datasets.d.ts.map +1 -0
  386. package/dist/playground/sample-datasets.js +641 -0
  387. package/dist/playground/sample-datasets.js.map +1 -0
  388. package/dist/playground/sample-queries.d.ts +73 -0
  389. package/dist/playground/sample-queries.d.ts.map +1 -0
  390. package/dist/playground/sample-queries.js +1095 -0
  391. package/dist/playground/sample-queries.js.map +1 -0
  392. package/dist/playground/schema-explorer.d.ts +55 -0
  393. package/dist/playground/schema-explorer.d.ts.map +1 -0
  394. package/dist/playground/schema-explorer.js +473 -0
  395. package/dist/playground/schema-explorer.js.map +1 -0
  396. package/dist/playground/types.d.ts +430 -0
  397. package/dist/playground/types.d.ts.map +1 -0
  398. package/dist/playground/types.js +10 -0
  399. package/dist/playground/types.js.map +1 -0
  400. package/dist/readonly/cache-reader.d.ts +145 -0
  401. package/dist/readonly/cache-reader.d.ts.map +1 -0
  402. package/dist/readonly/cache-reader.js +198 -0
  403. package/dist/readonly/cache-reader.js.map +1 -0
  404. package/dist/readonly/config.d.ts +74 -0
  405. package/dist/readonly/config.d.ts.map +1 -0
  406. package/dist/readonly/config.js +67 -0
  407. package/dist/readonly/config.js.map +1 -0
  408. package/dist/readonly/index.d.ts +22 -0
  409. package/dist/readonly/index.d.ts.map +1 -0
  410. package/dist/readonly/index.js +17 -0
  411. package/dist/readonly/index.js.map +1 -0
  412. package/dist/readonly/pglite-wrapper.d.ts +82 -0
  413. package/dist/readonly/pglite-wrapper.d.ts.map +1 -0
  414. package/dist/readonly/pglite-wrapper.js +123 -0
  415. package/dist/readonly/pglite-wrapper.js.map +1 -0
  416. package/dist/readonly/worker.d.ts +142 -0
  417. package/dist/readonly/worker.d.ts.map +1 -0
  418. package/dist/readonly/worker.js +187 -0
  419. package/dist/readonly/worker.js.map +1 -0
  420. package/dist/readonly/write-blocker.d.ts +47 -0
  421. package/dist/readonly/write-blocker.d.ts.map +1 -0
  422. package/dist/readonly/write-blocker.js +136 -0
  423. package/dist/readonly/write-blocker.js.map +1 -0
  424. package/dist/recovery/disaster-recovery.d.ts +326 -0
  425. package/dist/recovery/disaster-recovery.d.ts.map +1 -0
  426. package/dist/recovery/disaster-recovery.js +799 -0
  427. package/dist/recovery/disaster-recovery.js.map +1 -0
  428. package/dist/recovery/index.d.ts +12 -0
  429. package/dist/recovery/index.d.ts.map +1 -0
  430. package/dist/recovery/index.js +12 -0
  431. package/dist/recovery/index.js.map +1 -0
  432. package/dist/recovery/parquet-parser.d.ts +321 -0
  433. package/dist/recovery/parquet-parser.d.ts.map +1 -0
  434. package/dist/recovery/parquet-parser.js +797 -0
  435. package/dist/recovery/parquet-parser.js.map +1 -0
  436. package/dist/retention/index.d.ts +50 -0
  437. package/dist/retention/index.d.ts.map +1 -0
  438. package/dist/retention/index.js +50 -0
  439. package/dist/retention/index.js.map +1 -0
  440. package/dist/retention/policy.d.ts +344 -0
  441. package/dist/retention/policy.d.ts.map +1 -0
  442. package/dist/retention/policy.js +472 -0
  443. package/dist/retention/policy.js.map +1 -0
  444. package/dist/retention/purger.d.ts +187 -0
  445. package/dist/retention/purger.d.ts.map +1 -0
  446. package/dist/retention/purger.js +411 -0
  447. package/dist/retention/purger.js.map +1 -0
  448. package/dist/rls/auth-integration.d.ts +280 -0
  449. package/dist/rls/auth-integration.d.ts.map +1 -0
  450. package/dist/rls/auth-integration.js +399 -0
  451. package/dist/rls/auth-integration.js.map +1 -0
  452. package/dist/rls/generator.d.ts +249 -0
  453. package/dist/rls/generator.d.ts.map +1 -0
  454. package/dist/rls/generator.js +495 -0
  455. package/dist/rls/generator.js.map +1 -0
  456. package/dist/rls/index.d.ts +26 -0
  457. package/dist/rls/index.d.ts.map +1 -0
  458. package/dist/rls/index.js +58 -0
  459. package/dist/rls/index.js.map +1 -0
  460. package/dist/rls/policy.d.ts +116 -0
  461. package/dist/rls/policy.d.ts.map +1 -0
  462. package/dist/rls/policy.js +77 -0
  463. package/dist/rls/policy.js.map +1 -0
  464. package/dist/rls/validator.d.ts +155 -0
  465. package/dist/rls/validator.d.ts.map +1 -0
  466. package/dist/rls/validator.js +792 -0
  467. package/dist/rls/validator.js.map +1 -0
  468. package/dist/routing/adaptive-router.d.ts +317 -0
  469. package/dist/routing/adaptive-router.d.ts.map +1 -0
  470. package/dist/routing/adaptive-router.js +554 -0
  471. package/dist/routing/adaptive-router.js.map +1 -0
  472. package/dist/routing/circuit-breaker.d.ts +339 -0
  473. package/dist/routing/circuit-breaker.d.ts.map +1 -0
  474. package/dist/routing/circuit-breaker.js +620 -0
  475. package/dist/routing/circuit-breaker.js.map +1 -0
  476. package/dist/routing/cost-metrics.d.ts +133 -0
  477. package/dist/routing/cost-metrics.d.ts.map +1 -0
  478. package/dist/routing/cost-metrics.js +259 -0
  479. package/dist/routing/cost-metrics.js.map +1 -0
  480. package/dist/routing/do-connection-pool.d.ts +243 -0
  481. package/dist/routing/do-connection-pool.d.ts.map +1 -0
  482. package/dist/routing/do-connection-pool.js +572 -0
  483. package/dist/routing/do-connection-pool.js.map +1 -0
  484. package/dist/routing/index.d.ts +59 -0
  485. package/dist/routing/index.d.ts.map +1 -0
  486. package/dist/routing/index.js +59 -0
  487. package/dist/routing/index.js.map +1 -0
  488. package/dist/routing/query-complexity-estimator.d.ts +73 -0
  489. package/dist/routing/query-complexity-estimator.d.ts.map +1 -0
  490. package/dist/routing/query-complexity-estimator.js +327 -0
  491. package/dist/routing/query-complexity-estimator.js.map +1 -0
  492. package/dist/routing/request-coalescing.d.ts +178 -0
  493. package/dist/routing/request-coalescing.d.ts.map +1 -0
  494. package/dist/routing/request-coalescing.js +325 -0
  495. package/dist/routing/request-coalescing.js.map +1 -0
  496. package/dist/routing/runtime-router.d.ts +107 -0
  497. package/dist/routing/runtime-router.d.ts.map +1 -0
  498. package/dist/routing/runtime-router.js +246 -0
  499. package/dist/routing/runtime-router.js.map +1 -0
  500. package/dist/routing/tenant-router.d.ts +848 -0
  501. package/dist/routing/tenant-router.d.ts.map +1 -0
  502. package/dist/routing/tenant-router.js +1056 -0
  503. package/dist/routing/tenant-router.js.map +1 -0
  504. package/dist/routing/websocket-pool.d.ts +119 -0
  505. package/dist/routing/websocket-pool.d.ts.map +1 -0
  506. package/dist/routing/websocket-pool.js +436 -0
  507. package/dist/routing/websocket-pool.js.map +1 -0
  508. package/dist/storage/cache-layer.d.ts +159 -0
  509. package/dist/storage/cache-layer.d.ts.map +1 -0
  510. package/dist/storage/cache-layer.js +245 -0
  511. package/dist/storage/cache-layer.js.map +1 -0
  512. package/dist/storage/cost-aware-tiering.d.ts +258 -0
  513. package/dist/storage/cost-aware-tiering.d.ts.map +1 -0
  514. package/dist/storage/cost-aware-tiering.js +526 -0
  515. package/dist/storage/cost-aware-tiering.js.map +1 -0
  516. package/dist/storage/index.d.ts +87 -0
  517. package/dist/storage/index.d.ts.map +1 -0
  518. package/dist/storage/index.js +78 -0
  519. package/dist/storage/index.js.map +1 -0
  520. package/dist/storage/interfaces.d.ts +856 -0
  521. package/dist/storage/interfaces.d.ts.map +1 -0
  522. package/dist/storage/interfaces.js +69 -0
  523. package/dist/storage/interfaces.js.map +1 -0
  524. package/dist/storage/r2-layer.d.ts +226 -0
  525. package/dist/storage/r2-layer.d.ts.map +1 -0
  526. package/dist/storage/r2-layer.js +307 -0
  527. package/dist/storage/r2-layer.js.map +1 -0
  528. package/dist/storage/r2-overflow.d.ts +344 -0
  529. package/dist/storage/r2-overflow.d.ts.map +1 -0
  530. package/dist/storage/r2-overflow.js +730 -0
  531. package/dist/storage/r2-overflow.js.map +1 -0
  532. package/dist/storage/r2-page-vfs.d.ts +374 -0
  533. package/dist/storage/r2-page-vfs.d.ts.map +1 -0
  534. package/dist/storage/r2-page-vfs.js +754 -0
  535. package/dist/storage/r2-page-vfs.js.map +1 -0
  536. package/dist/storage/swr-cache.d.ts +181 -0
  537. package/dist/storage/swr-cache.d.ts.map +1 -0
  538. package/dist/storage/swr-cache.js +295 -0
  539. package/dist/storage/swr-cache.js.map +1 -0
  540. package/dist/storage/tiered-orchestrator.d.ts +951 -0
  541. package/dist/storage/tiered-orchestrator.d.ts.map +1 -0
  542. package/dist/storage/tiered-orchestrator.js +1731 -0
  543. package/dist/storage/tiered-orchestrator.js.map +1 -0
  544. package/dist/storage/tiered-vfs-swr.d.ts +279 -0
  545. package/dist/storage/tiered-vfs-swr.d.ts.map +1 -0
  546. package/dist/storage/tiered-vfs-swr.js +584 -0
  547. package/dist/storage/tiered-vfs-swr.js.map +1 -0
  548. package/dist/storage/tiered-vfs.d.ts +405 -0
  549. package/dist/storage/tiered-vfs.d.ts.map +1 -0
  550. package/dist/storage/tiered-vfs.js +833 -0
  551. package/dist/storage/tiered-vfs.js.map +1 -0
  552. package/dist/streaming/backpressure-controller.d.ts +173 -0
  553. package/dist/streaming/backpressure-controller.d.ts.map +1 -0
  554. package/dist/streaming/backpressure-controller.js +344 -0
  555. package/dist/streaming/backpressure-controller.js.map +1 -0
  556. package/dist/streaming/buffer-pool.d.ts +241 -0
  557. package/dist/streaming/buffer-pool.d.ts.map +1 -0
  558. package/dist/streaming/buffer-pool.js +381 -0
  559. package/dist/streaming/buffer-pool.js.map +1 -0
  560. package/dist/streaming/cdc-iceberg-connector.d.ts +272 -0
  561. package/dist/streaming/cdc-iceberg-connector.d.ts.map +1 -0
  562. package/dist/streaming/cdc-iceberg-connector.js +408 -0
  563. package/dist/streaming/cdc-iceberg-connector.js.map +1 -0
  564. package/dist/streaming/index.d.ts +111 -0
  565. package/dist/streaming/index.d.ts.map +1 -0
  566. package/dist/streaming/index.js +128 -0
  567. package/dist/streaming/index.js.map +1 -0
  568. package/dist/streaming/live-cdc-stream.d.ts +400 -0
  569. package/dist/streaming/live-cdc-stream.d.ts.map +1 -0
  570. package/dist/streaming/live-cdc-stream.js +703 -0
  571. package/dist/streaming/live-cdc-stream.js.map +1 -0
  572. package/dist/streaming/memory-bounded-stream.d.ts +207 -0
  573. package/dist/streaming/memory-bounded-stream.d.ts.map +1 -0
  574. package/dist/streaming/memory-bounded-stream.js +340 -0
  575. package/dist/streaming/memory-bounded-stream.js.map +1 -0
  576. package/dist/streaming/query-streamer.d.ts +379 -0
  577. package/dist/streaming/query-streamer.d.ts.map +1 -0
  578. package/dist/streaming/query-streamer.js +495 -0
  579. package/dist/streaming/query-streamer.js.map +1 -0
  580. package/dist/streaming/response-streaming.d.ts +203 -0
  581. package/dist/streaming/response-streaming.d.ts.map +1 -0
  582. package/dist/streaming/response-streaming.js +449 -0
  583. package/dist/streaming/response-streaming.js.map +1 -0
  584. package/dist/types/branded.d.ts +859 -0
  585. package/dist/types/branded.d.ts.map +1 -0
  586. package/dist/types/branded.js +891 -0
  587. package/dist/types/branded.js.map +1 -0
  588. package/dist/types/utilities.d.ts +757 -0
  589. package/dist/types/utilities.d.ts.map +1 -0
  590. package/dist/types/utilities.js +447 -0
  591. package/dist/types/utilities.js.map +1 -0
  592. package/dist/wal/replay-engine.d.ts +344 -0
  593. package/dist/wal/replay-engine.d.ts.map +1 -0
  594. package/dist/wal/replay-engine.js +975 -0
  595. package/dist/wal/replay-engine.js.map +1 -0
  596. package/dist/worker/__mocks__/capnweb.d.ts +13 -0
  597. package/dist/worker/__mocks__/capnweb.d.ts.map +1 -0
  598. package/dist/worker/__mocks__/capnweb.js +15 -0
  599. package/dist/worker/__mocks__/capnweb.js.map +1 -0
  600. package/dist/worker/__mocks__/cloudflare-workers.d.ts +31 -0
  601. package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +1 -0
  602. package/dist/worker/__mocks__/cloudflare-workers.js +33 -0
  603. package/dist/worker/__mocks__/cloudflare-workers.js.map +1 -0
  604. package/dist/worker/__mocks__/pglite.data.d.ts +3 -0
  605. package/dist/worker/__mocks__/pglite.data.d.ts.map +1 -0
  606. package/dist/worker/__mocks__/pglite.data.js +20 -0
  607. package/dist/worker/__mocks__/pglite.data.js.map +1 -0
  608. package/dist/worker/__mocks__/pglite.wasm.d.ts +3 -0
  609. package/dist/worker/__mocks__/pglite.wasm.d.ts.map +1 -0
  610. package/dist/worker/__mocks__/pglite.wasm.js +30 -0
  611. package/dist/worker/__mocks__/pglite.wasm.js.map +1 -0
  612. package/dist/worker/auth-rate-limiter.d.ts +270 -0
  613. package/dist/worker/auth-rate-limiter.d.ts.map +1 -0
  614. package/dist/worker/auth-rate-limiter.js +332 -0
  615. package/dist/worker/auth-rate-limiter.js.map +1 -0
  616. package/dist/worker/auth.d.ts +345 -0
  617. package/dist/worker/auth.d.ts.map +1 -0
  618. package/dist/worker/auth.js +837 -0
  619. package/dist/worker/auth.js.map +1 -0
  620. package/dist/worker/cdc-backpressure.d.ts +338 -0
  621. package/dist/worker/cdc-backpressure.d.ts.map +1 -0
  622. package/dist/worker/cdc-backpressure.js +619 -0
  623. package/dist/worker/cdc-backpressure.js.map +1 -0
  624. package/dist/worker/cdc-sse.d.ts +277 -0
  625. package/dist/worker/cdc-sse.d.ts.map +1 -0
  626. package/dist/worker/cdc-sse.js +528 -0
  627. package/dist/worker/cdc-sse.js.map +1 -0
  628. package/dist/worker/cdc-websocket.d.ts +252 -0
  629. package/dist/worker/cdc-websocket.d.ts.map +1 -0
  630. package/dist/worker/cdc-websocket.js +940 -0
  631. package/dist/worker/cdc-websocket.js.map +1 -0
  632. package/dist/worker/cdc.d.ts +95 -0
  633. package/dist/worker/cdc.d.ts.map +1 -0
  634. package/dist/worker/cdc.js +211 -0
  635. package/dist/worker/cdc.js.map +1 -0
  636. package/dist/worker/concerns/auth-concern.d.ts +50 -0
  637. package/dist/worker/concerns/auth-concern.d.ts.map +1 -0
  638. package/dist/worker/concerns/auth-concern.js +131 -0
  639. package/dist/worker/concerns/auth-concern.js.map +1 -0
  640. package/dist/worker/concerns/cdc-concern.d.ts +99 -0
  641. package/dist/worker/concerns/cdc-concern.d.ts.map +1 -0
  642. package/dist/worker/concerns/cdc-concern.js +137 -0
  643. package/dist/worker/concerns/cdc-concern.js.map +1 -0
  644. package/dist/worker/concerns/index.d.ts +22 -0
  645. package/dist/worker/concerns/index.d.ts.map +1 -0
  646. package/dist/worker/concerns/index.js +13 -0
  647. package/dist/worker/concerns/index.js.map +1 -0
  648. package/dist/worker/concerns/query-execution-concern.d.ts +104 -0
  649. package/dist/worker/concerns/query-execution-concern.d.ts.map +1 -0
  650. package/dist/worker/concerns/query-execution-concern.js +95 -0
  651. package/dist/worker/concerns/query-execution-concern.js.map +1 -0
  652. package/dist/worker/concerns/storage-orchestration-concern.d.ts +78 -0
  653. package/dist/worker/concerns/storage-orchestration-concern.d.ts.map +1 -0
  654. package/dist/worker/concerns/storage-orchestration-concern.js +240 -0
  655. package/dist/worker/concerns/storage-orchestration-concern.js.map +1 -0
  656. package/dist/worker/do-auth-manager.d.ts +108 -0
  657. package/dist/worker/do-auth-manager.d.ts.map +1 -0
  658. package/dist/worker/do-auth-manager.js +212 -0
  659. package/dist/worker/do-auth-manager.js.map +1 -0
  660. package/dist/worker/do-pglite-manager.d.ts +137 -0
  661. package/dist/worker/do-pglite-manager.d.ts.map +1 -0
  662. package/dist/worker/do-pglite-manager.js +228 -0
  663. package/dist/worker/do-pglite-manager.js.map +1 -0
  664. package/dist/worker/do.d.ts +556 -0
  665. package/dist/worker/do.d.ts.map +1 -0
  666. package/dist/worker/do.js +1441 -0
  667. package/dist/worker/do.js.map +1 -0
  668. package/dist/worker/entry.d.ts +23 -0
  669. package/dist/worker/entry.d.ts.map +1 -0
  670. package/dist/worker/entry.js +362 -0
  671. package/dist/worker/entry.js.map +1 -0
  672. package/dist/worker/errors.d.ts +106 -0
  673. package/dist/worker/errors.d.ts.map +1 -0
  674. package/dist/worker/errors.js +178 -0
  675. package/dist/worker/errors.js.map +1 -0
  676. package/dist/worker/health-check-manager.d.ts +141 -0
  677. package/dist/worker/health-check-manager.d.ts.map +1 -0
  678. package/dist/worker/health-check-manager.js +145 -0
  679. package/dist/worker/health-check-manager.js.map +1 -0
  680. package/dist/worker/index.d.ts +60 -0
  681. package/dist/worker/index.d.ts.map +1 -0
  682. package/dist/worker/index.js +67 -0
  683. package/dist/worker/index.js.map +1 -0
  684. package/dist/worker/memory-pressure.d.ts +892 -0
  685. package/dist/worker/memory-pressure.d.ts.map +1 -0
  686. package/dist/worker/memory-pressure.js +1990 -0
  687. package/dist/worker/memory-pressure.js.map +1 -0
  688. package/dist/worker/migration-manager.d.ts +153 -0
  689. package/dist/worker/migration-manager.d.ts.map +1 -0
  690. package/dist/worker/migration-manager.js +461 -0
  691. package/dist/worker/migration-manager.js.map +1 -0
  692. package/dist/worker/plugin-manager.d.ts +147 -0
  693. package/dist/worker/plugin-manager.d.ts.map +1 -0
  694. package/dist/worker/plugin-manager.js +408 -0
  695. package/dist/worker/plugin-manager.js.map +1 -0
  696. package/dist/worker/proxy.d.ts +330 -0
  697. package/dist/worker/proxy.d.ts.map +1 -0
  698. package/dist/worker/proxy.js +504 -0
  699. package/dist/worker/proxy.js.map +1 -0
  700. package/dist/worker/query-execution-manager.d.ts +107 -0
  701. package/dist/worker/query-execution-manager.d.ts.map +1 -0
  702. package/dist/worker/query-execution-manager.js +155 -0
  703. package/dist/worker/query-execution-manager.js.map +1 -0
  704. package/dist/worker/query-executor.d.ts +163 -0
  705. package/dist/worker/query-executor.d.ts.map +1 -0
  706. package/dist/worker/query-executor.js +413 -0
  707. package/dist/worker/query-executor.js.map +1 -0
  708. package/dist/worker/query-stats-manager.d.ts +117 -0
  709. package/dist/worker/query-stats-manager.d.ts.map +1 -0
  710. package/dist/worker/query-stats-manager.js +162 -0
  711. package/dist/worker/query-stats-manager.js.map +1 -0
  712. package/dist/worker/result-handler.d.ts +192 -0
  713. package/dist/worker/result-handler.d.ts.map +1 -0
  714. package/dist/worker/result-handler.js +346 -0
  715. package/dist/worker/result-handler.js.map +1 -0
  716. package/dist/worker/routes.d.ts +135 -0
  717. package/dist/worker/routes.d.ts.map +1 -0
  718. package/dist/worker/routes.js +460 -0
  719. package/dist/worker/routes.js.map +1 -0
  720. package/dist/worker/rpc-methods-manager.d.ts +142 -0
  721. package/dist/worker/rpc-methods-manager.d.ts.map +1 -0
  722. package/dist/worker/rpc-methods-manager.js +195 -0
  723. package/dist/worker/rpc-methods-manager.js.map +1 -0
  724. package/dist/worker/rpc.d.ts +259 -0
  725. package/dist/worker/rpc.d.ts.map +1 -0
  726. package/dist/worker/rpc.js +398 -0
  727. package/dist/worker/rpc.js.map +1 -0
  728. package/dist/worker/schema-version.d.ts +209 -0
  729. package/dist/worker/schema-version.d.ts.map +1 -0
  730. package/dist/worker/schema-version.js +450 -0
  731. package/dist/worker/schema-version.js.map +1 -0
  732. package/dist/worker/session-manager.d.ts +282 -0
  733. package/dist/worker/session-manager.d.ts.map +1 -0
  734. package/dist/worker/session-manager.js +523 -0
  735. package/dist/worker/session-manager.js.map +1 -0
  736. package/dist/worker/shutdown-manager.d.ts +188 -0
  737. package/dist/worker/shutdown-manager.d.ts.map +1 -0
  738. package/dist/worker/shutdown-manager.js +347 -0
  739. package/dist/worker/shutdown-manager.js.map +1 -0
  740. package/dist/worker/sql-transform.d.ts +61 -0
  741. package/dist/worker/sql-transform.d.ts.map +1 -0
  742. package/dist/worker/sql-transform.js +312 -0
  743. package/dist/worker/sql-transform.js.map +1 -0
  744. package/dist/worker/types.d.ts +738 -0
  745. package/dist/worker/types.d.ts.map +1 -0
  746. package/dist/worker/types.js +6 -0
  747. package/dist/worker/types.js.map +1 -0
  748. package/dist/worker/user-routes.d.ts +76 -0
  749. package/dist/worker/user-routes.d.ts.map +1 -0
  750. package/dist/worker/user-routes.js +188 -0
  751. package/dist/worker/user-routes.js.map +1 -0
  752. package/dist/worker/wal-facade.d.ts +138 -0
  753. package/dist/worker/wal-facade.d.ts.map +1 -0
  754. package/dist/worker/wal-facade.js +184 -0
  755. package/dist/worker/wal-facade.js.map +1 -0
  756. package/dist/worker/wal-r2.d.ts +271 -0
  757. package/dist/worker/wal-r2.d.ts.map +1 -0
  758. package/dist/worker/wal-r2.js +689 -0
  759. package/dist/worker/wal-r2.js.map +1 -0
  760. package/dist/worker/wal-replay.d.ts +361 -0
  761. package/dist/worker/wal-replay.d.ts.map +1 -0
  762. package/dist/worker/wal-replay.js +628 -0
  763. package/dist/worker/wal-replay.js.map +1 -0
  764. package/dist/worker/wal-retention.d.ts +389 -0
  765. package/dist/worker/wal-retention.d.ts.map +1 -0
  766. package/dist/worker/wal-retention.js +763 -0
  767. package/dist/worker/wal-retention.js.map +1 -0
  768. package/dist/worker/wal.d.ts +278 -0
  769. package/dist/worker/wal.d.ts.map +1 -0
  770. package/dist/worker/wal.js +467 -0
  771. package/dist/worker/wal.js.map +1 -0
  772. package/dist/worker/websocket.d.ts +85 -0
  773. package/dist/worker/websocket.d.ts.map +1 -0
  774. package/dist/worker/websocket.js +227 -0
  775. package/dist/worker/websocket.js.map +1 -0
  776. package/package.json +108 -0
  777. package/src/cdc/change-stream.ts +137 -0
  778. package/src/cdc/filter.ts +646 -0
  779. package/src/cdc/index.ts +112 -0
  780. package/src/cdc/resume-token.ts +280 -0
  781. package/src/cdc/transport/index.ts +7 -0
  782. package/src/cdc/transport/sse.ts +723 -0
  783. package/src/cdc/transport/websocket.ts +873 -0
  784. package/src/cdc/types.ts +346 -0
  785. package/src/config/index.ts +25 -0
  786. package/src/config/memory.ts +177 -0
  787. package/src/config/storage.ts +204 -0
  788. package/src/config/streaming.ts +147 -0
  789. package/src/config/timeouts.ts +221 -0
  790. package/src/extensions/config.test.ts +187 -0
  791. package/src/extensions/config.ts +278 -0
  792. package/src/extensions/geo.test.ts +455 -0
  793. package/src/extensions/geo.ts +858 -0
  794. package/src/extensions/index.test.ts +259 -0
  795. package/src/extensions/index.ts +227 -0
  796. package/src/extensions/loader.test.ts +555 -0
  797. package/src/extensions/loader.ts +588 -0
  798. package/src/extensions/pgmq-lite.test.ts +727 -0
  799. package/src/extensions/pgmq-lite.ts +770 -0
  800. package/src/extensions/plugins.test.ts +528 -0
  801. package/src/extensions/plugins.ts +718 -0
  802. package/src/extensions/registry.test.ts +202 -0
  803. package/src/extensions/registry.ts +267 -0
  804. package/src/extensions/vector.test.ts +195 -0
  805. package/src/extensions/vector.ts +217 -0
  806. package/src/iceberg/SCHEDULER.md +580 -0
  807. package/src/iceberg/analytics.test.ts +703 -0
  808. package/src/iceberg/analytics.ts +727 -0
  809. package/src/iceberg/catalog-api.test.ts +838 -0
  810. package/src/iceberg/catalog-api.ts +520 -0
  811. package/src/iceberg/catalog.test.ts +680 -0
  812. package/src/iceberg/catalog.ts +1007 -0
  813. package/src/iceberg/iceberg.test.ts +705 -0
  814. package/src/iceberg/index.ts +406 -0
  815. package/src/iceberg/metadata.test.ts +632 -0
  816. package/src/iceberg/metadata.ts +649 -0
  817. package/src/iceberg/optimizer.test.ts +868 -0
  818. package/src/iceberg/optimizer.ts +1287 -0
  819. package/src/iceberg/parquet.test.ts +899 -0
  820. package/src/iceberg/parquet.ts +1640 -0
  821. package/src/iceberg/r2-organization.test.ts +615 -0
  822. package/src/iceberg/r2-organization.ts +951 -0
  823. package/src/iceberg/scheduler-do-example.ts +364 -0
  824. package/src/iceberg/scheduler.test.ts +861 -0
  825. package/src/iceberg/scheduler.ts +1201 -0
  826. package/src/iceberg/schema.test.ts +547 -0
  827. package/src/iceberg/schema.ts +616 -0
  828. package/src/iceberg/snapshot-manager.test.ts +919 -0
  829. package/src/iceberg/snapshot-manager.ts +1369 -0
  830. package/src/iceberg/sql-router.test.ts +334 -0
  831. package/src/iceberg/sql-router.ts +337 -0
  832. package/src/iceberg/test-fixtures.ts +605 -0
  833. package/src/iceberg/time-travel-api.test.ts +1029 -0
  834. package/src/iceberg/time-travel-api.ts +731 -0
  835. package/src/iceberg/time-travel.test.ts +1218 -0
  836. package/src/iceberg/time-travel.ts +1052 -0
  837. package/src/iceberg/transformer.test.ts +689 -0
  838. package/src/iceberg/transformer.ts +1029 -0
  839. package/src/iceberg/types.ts +373 -0
  840. package/src/iceberg/writer.test.ts +716 -0
  841. package/src/iceberg/writer.ts +590 -0
  842. package/src/index.ts +212 -0
  843. package/src/lineage/index.ts +42 -0
  844. package/src/lineage/integration.ts +334 -0
  845. package/src/lineage/tracker.ts +1618 -0
  846. package/src/lineage/types.ts +354 -0
  847. package/src/middleware/index.ts +36 -0
  848. package/src/middleware/rate-limit-concurrent.test.ts +794 -0
  849. package/src/middleware/rate-limit.test.ts +1568 -0
  850. package/src/middleware/rate-limit.ts +840 -0
  851. package/src/migration-tooling/external-migration.test.ts +1864 -0
  852. package/src/migration-tooling/external-migration.ts +2355 -0
  853. package/src/migration-tooling/index.ts +19 -0
  854. package/src/migrations/ARCHITECTURE.md +474 -0
  855. package/src/migrations/PROGRESS_TRACKING.md +485 -0
  856. package/src/migrations/auto-migrator.test.ts +732 -0
  857. package/src/migrations/auto-migrator.ts +531 -0
  858. package/src/migrations/bulk-orchestrator.test.ts +801 -0
  859. package/src/migrations/bulk-orchestrator.ts +1039 -0
  860. package/src/migrations/compatibility.test.ts +958 -0
  861. package/src/migrations/compatibility.ts +902 -0
  862. package/src/migrations/do-migrations.test.ts +2620 -0
  863. package/src/migrations/do-migrations.ts +1289 -0
  864. package/src/migrations/do-migrations.types.ts +715 -0
  865. package/src/migrations/drizzle-compat.test.ts +210 -0
  866. package/src/migrations/drizzle-compat.ts +337 -0
  867. package/src/migrations/index.ts +334 -0
  868. package/src/migrations/migration-api.test.ts +438 -0
  869. package/src/migrations/migration-api.ts +704 -0
  870. package/src/migrations/progress-tracker-do.ts +518 -0
  871. package/src/migrations/progress-tracker-kv.ts +305 -0
  872. package/src/migrations/progress-tracker.test.ts +937 -0
  873. package/src/migrations/progress-tracker.ts +665 -0
  874. package/src/migrations/registry.test.ts +331 -0
  875. package/src/migrations/registry.ts +468 -0
  876. package/src/migrations/rollback.test.ts +644 -0
  877. package/src/migrations/runner.test.ts +807 -0
  878. package/src/migrations/runner.test.ts.backup +759 -0
  879. package/src/migrations/runner.ts +1459 -0
  880. package/src/migrations/schema-generator.test.ts +649 -0
  881. package/src/migrations/schema-generator.ts +513 -0
  882. package/src/migrations/testing.ts +1037 -0
  883. package/src/migrations/types.ts +573 -0
  884. package/src/migrations/validator.test.ts +660 -0
  885. package/src/migrations/validator.ts +741 -0
  886. package/src/observability/alerting.test.ts +1133 -0
  887. package/src/observability/alerting.ts +455 -0
  888. package/src/observability/analytics-engine.ts +733 -0
  889. package/src/observability/cost-metrics.ts +804 -0
  890. package/src/observability/cross-do-tracing.test.ts +516 -0
  891. package/src/observability/cross-do-tracing.ts +588 -0
  892. package/src/observability/dashboards/postgres-do-overview.json +1656 -0
  893. package/src/observability/error-rate-collector.test.ts +977 -0
  894. package/src/observability/error-rate-collector.ts +518 -0
  895. package/src/observability/exporters.test.ts +365 -0
  896. package/src/observability/exporters.ts +650 -0
  897. package/src/observability/health-check.test.ts +353 -0
  898. package/src/observability/health-check.ts +341 -0
  899. package/src/observability/index.test.ts +298 -0
  900. package/src/observability/index.ts +885 -0
  901. package/src/observability/instrumentation.test.ts +428 -0
  902. package/src/observability/instrumentation.ts +788 -0
  903. package/src/observability/memory-metrics.test.ts +355 -0
  904. package/src/observability/memory-metrics.ts +990 -0
  905. package/src/observability/metrics-endpoint.test.ts +402 -0
  906. package/src/observability/metrics-endpoint.ts +374 -0
  907. package/src/observability/metrics.test.ts +291 -0
  908. package/src/observability/metrics.ts +315 -0
  909. package/src/observability/observability-features.ts +1296 -0
  910. package/src/observability/prometheus.test.ts +292 -0
  911. package/src/observability/prometheus.ts +170 -0
  912. package/src/observability/propagation.test.ts +417 -0
  913. package/src/observability/propagation.ts +294 -0
  914. package/src/observability/query-latency.ts +586 -0
  915. package/src/observability/query-performance.test.ts +406 -0
  916. package/src/observability/query-performance.ts +491 -0
  917. package/src/observability/storage-tier-metrics.test.ts +633 -0
  918. package/src/observability/storage-tier-metrics.ts +570 -0
  919. package/src/observability/tier-cost-optimizer.ts +740 -0
  920. package/src/observability/tracer.test.ts +346 -0
  921. package/src/observability/tracer.ts +585 -0
  922. package/src/observability/types.test.ts +726 -0
  923. package/src/observability/types.ts +434 -0
  924. package/src/pglite/auto-demotion.test.ts +477 -0
  925. package/src/pglite/auto-demotion.ts +385 -0
  926. package/src/pglite/auto-promotion.test.ts +824 -0
  927. package/src/pglite/auto-promotion.ts +547 -0
  928. package/src/pglite/cache-layer.test.ts +469 -0
  929. package/src/pglite/cache-layer.ts +271 -0
  930. package/src/pglite/cold-start-manager.ts +1260 -0
  931. package/src/pglite/cold-start-optimizer.test.ts +937 -0
  932. package/src/pglite/cold-start-optimizer.ts +1895 -0
  933. package/src/pglite/dovfs-adapter.ts +1122 -0
  934. package/src/pglite/dovfs.ts +1258 -0
  935. package/src/pglite/etag-cache.test.ts +844 -0
  936. package/src/pglite/etag-cache.ts +526 -0
  937. package/src/pglite/index.ts +442 -0
  938. package/src/pglite/init.test.ts +455 -0
  939. package/src/pglite/init.ts +574 -0
  940. package/src/pglite/lifecycle.test.ts +599 -0
  941. package/src/pglite/lifecycle.ts +704 -0
  942. package/src/pglite/parallel-loader.test.ts +586 -0
  943. package/src/pglite/parallel-loader.ts +481 -0
  944. package/src/pglite/production-pglite.test.ts +666 -0
  945. package/src/pglite/production-pglite.ts +537 -0
  946. package/src/pglite/query-executor.ts +614 -0
  947. package/src/pglite/r2-layer.test.ts +501 -0
  948. package/src/pglite/r2-layer.ts +322 -0
  949. package/src/pglite/tiered-init.test.ts +725 -0
  950. package/src/pglite/tiered-init.ts +556 -0
  951. package/src/pglite/tiered-vfs.test.ts +726 -0
  952. package/src/pglite/tiered-vfs.ts +33 -0
  953. package/src/pglite/tiering-stats.test.ts +531 -0
  954. package/src/pglite/tiering-stats.ts +407 -0
  955. package/src/pglite/transaction-hooks.ts +343 -0
  956. package/src/pglite/warm-loader.test.ts +1701 -0
  957. package/src/pglite/warm-loader.ts +528 -0
  958. package/src/pglite/workers-pglite.ts +224 -0
  959. package/src/pglite-assets/pglite.data +0 -0
  960. package/src/pglite-assets/pglite.wasm +0 -0
  961. package/src/pglite.d.ts +47 -0
  962. package/src/playground/index.ts +137 -0
  963. package/src/playground/keyboard-shortcuts.ts +677 -0
  964. package/src/playground/playground.ts +323 -0
  965. package/src/playground/query-executor.ts +669 -0
  966. package/src/playground/query-history.ts +328 -0
  967. package/src/playground/result-formatter.ts +420 -0
  968. package/src/playground/sample-datasets.ts +674 -0
  969. package/src/playground/sample-queries.ts +1168 -0
  970. package/src/playground/schema-explorer.ts +558 -0
  971. package/src/playground/types.ts +518 -0
  972. package/src/readonly/cache-reader.test.ts +460 -0
  973. package/src/readonly/cache-reader.ts +313 -0
  974. package/src/readonly/config.test.ts +187 -0
  975. package/src/readonly/config.ts +128 -0
  976. package/src/readonly/index.ts +50 -0
  977. package/src/readonly/pglite-wrapper.test.ts +278 -0
  978. package/src/readonly/pglite-wrapper.ts +184 -0
  979. package/src/readonly/worker.test.ts +533 -0
  980. package/src/readonly/worker.ts +341 -0
  981. package/src/readonly/write-blocker.test.ts +459 -0
  982. package/src/readonly/write-blocker.ts +175 -0
  983. package/src/recovery/disaster-recovery.test.ts +618 -0
  984. package/src/recovery/disaster-recovery.ts +1181 -0
  985. package/src/recovery/index.ts +43 -0
  986. package/src/recovery/parquet-parser.ts +974 -0
  987. package/src/retention/index.ts +74 -0
  988. package/src/retention/policy.test.ts +571 -0
  989. package/src/retention/policy.ts +774 -0
  990. package/src/retention/purger.test.ts +465 -0
  991. package/src/retention/purger.ts +558 -0
  992. package/src/rls/auth-integration.test.ts +752 -0
  993. package/src/rls/auth-integration.ts +533 -0
  994. package/src/rls/generator.test.ts +829 -0
  995. package/src/rls/generator.ts +573 -0
  996. package/src/rls/index.ts +128 -0
  997. package/src/rls/policy.ts +208 -0
  998. package/src/rls/rls.test.ts +1071 -0
  999. package/src/rls/validator.test.ts +930 -0
  1000. package/src/rls/validator.ts +895 -0
  1001. package/src/routing/adaptive-router.test.ts +884 -0
  1002. package/src/routing/adaptive-router.ts +845 -0
  1003. package/src/routing/circuit-breaker.test.ts +1505 -0
  1004. package/src/routing/circuit-breaker.ts +852 -0
  1005. package/src/routing/cost-metrics.test.ts +565 -0
  1006. package/src/routing/cost-metrics.ts +408 -0
  1007. package/src/routing/do-connection-pool.test.ts +1109 -0
  1008. package/src/routing/do-connection-pool.ts +828 -0
  1009. package/src/routing/index.ts +158 -0
  1010. package/src/routing/query-complexity-estimator.test.ts +356 -0
  1011. package/src/routing/query-complexity-estimator.ts +444 -0
  1012. package/src/routing/request-coalescing.test.ts +738 -0
  1013. package/src/routing/request-coalescing.ts +475 -0
  1014. package/src/routing/runtime-router.test.ts +436 -0
  1015. package/src/routing/runtime-router.ts +357 -0
  1016. package/src/routing/tenant-router.test.ts +2493 -0
  1017. package/src/routing/tenant-router.ts +1908 -0
  1018. package/src/routing/websocket-pool.test.ts +551 -0
  1019. package/src/routing/websocket-pool.ts +577 -0
  1020. package/src/storage/access-pattern-tracker.test.ts +874 -0
  1021. package/src/storage/cache-layer.test.ts +560 -0
  1022. package/src/storage/cache-layer.ts +328 -0
  1023. package/src/storage/cost-aware-tiering.test.ts +652 -0
  1024. package/src/storage/cost-aware-tiering.ts +794 -0
  1025. package/src/storage/do-sqlite-blobs.test.ts +937 -0
  1026. package/src/storage/index.ts +272 -0
  1027. package/src/storage/interfaces.ts +974 -0
  1028. package/src/storage/r2-layer.test.ts +653 -0
  1029. package/src/storage/r2-layer.ts +434 -0
  1030. package/src/storage/r2-overflow.ts +920 -0
  1031. package/src/storage/r2-page-vfs.test.ts +2348 -0
  1032. package/src/storage/r2-page-vfs.ts +1054 -0
  1033. package/src/storage/swr-cache.test.ts +832 -0
  1034. package/src/storage/swr-cache.ts +398 -0
  1035. package/src/storage/swr-tiered-integration.test.ts +617 -0
  1036. package/src/storage/tiered-orchestrator.test.ts +2441 -0
  1037. package/src/storage/tiered-orchestrator.ts +2081 -0
  1038. package/src/storage/tiered-vfs-swr.test.ts +736 -0
  1039. package/src/storage/tiered-vfs-swr.ts +735 -0
  1040. package/src/storage/tiered-vfs.test.ts +793 -0
  1041. package/src/storage/tiered-vfs.ts +1082 -0
  1042. package/src/streaming/backpressure-controller.ts +452 -0
  1043. package/src/streaming/buffer-pool.ts +484 -0
  1044. package/src/streaming/cdc-iceberg-connector.ts +605 -0
  1045. package/src/streaming/index.ts +225 -0
  1046. package/src/streaming/live-cdc-stream.ts +985 -0
  1047. package/src/streaming/memory-bounded-stream.ts +443 -0
  1048. package/src/streaming/query-streamer.ts +662 -0
  1049. package/src/streaming/response-streaming.ts +557 -0
  1050. package/src/types/branded.ts +1075 -0
  1051. package/src/types/branded.ts.backup +273 -0
  1052. package/src/types/utilities.ts +1023 -0
  1053. package/src/types/wasm.d.ts +30 -0
  1054. package/src/validation/typed-errors.test.ts +420 -0
  1055. package/src/wal/replay-engine.ts +1264 -0
  1056. package/src/worker/__mocks__/capnweb.ts +15 -0
  1057. package/src/worker/__mocks__/pglite.data.ts +22 -0
  1058. package/src/worker/__mocks__/pglite.wasm.ts +33 -0
  1059. package/src/worker/auth-rate-limiter.test.ts +272 -0
  1060. package/src/worker/auth-rate-limiter.ts +448 -0
  1061. package/src/worker/auth.security-red.test.ts +1236 -0
  1062. package/src/worker/auth.security.test.ts +822 -0
  1063. package/src/worker/auth.test.ts +469 -0
  1064. package/src/worker/auth.ts +1104 -0
  1065. package/src/worker/cdc-backpressure.test.ts +726 -0
  1066. package/src/worker/cdc-backpressure.ts +866 -0
  1067. package/src/worker/cdc-sse.test.ts +780 -0
  1068. package/src/worker/cdc-sse.ts +728 -0
  1069. package/src/worker/cdc-websocket.ts +1229 -0
  1070. package/src/worker/cdc-ws.test.ts +1009 -0
  1071. package/src/worker/cdc.test.ts +327 -0
  1072. package/src/worker/cdc.ts +289 -0
  1073. package/src/worker/concerns/auth-concern.ts +179 -0
  1074. package/src/worker/concerns/cdc-concern.ts +247 -0
  1075. package/src/worker/concerns/index.ts +58 -0
  1076. package/src/worker/concerns/query-execution-concern.ts +194 -0
  1077. package/src/worker/concerns/storage-orchestration-concern.ts +373 -0
  1078. package/src/worker/discriminated-types.test.ts +280 -0
  1079. package/src/worker/do-auth-manager.ts +257 -0
  1080. package/src/worker/do-decomposition.test.ts +1236 -0
  1081. package/src/worker/do-pglite-manager.ts +302 -0
  1082. package/src/worker/do.test.ts +2254 -0
  1083. package/src/worker/do.ts +1878 -0
  1084. package/src/worker/entry.ts +417 -0
  1085. package/src/worker/errors.ts +285 -0
  1086. package/src/worker/health-check-manager.test.ts +261 -0
  1087. package/src/worker/health-check-manager.ts +231 -0
  1088. package/src/worker/index.ts +389 -0
  1089. package/src/worker/memory-pressure.test.ts +1460 -0
  1090. package/src/worker/memory-pressure.ts +2650 -0
  1091. package/src/worker/migration-manager.ts +582 -0
  1092. package/src/worker/neon-compat.test.ts +332 -0
  1093. package/src/worker/plugin-manager.ts +485 -0
  1094. package/src/worker/postgres.do-rpc.d.ts +76 -0
  1095. package/src/worker/proxy.ts +694 -0
  1096. package/src/worker/query-execution-manager.test.ts +303 -0
  1097. package/src/worker/query-execution-manager.ts +219 -0
  1098. package/src/worker/query-executor.test.ts +282 -0
  1099. package/src/worker/query-executor.ts +560 -0
  1100. package/src/worker/query-stats-manager.ts +229 -0
  1101. package/src/worker/result-handler.test.ts +364 -0
  1102. package/src/worker/result-handler.ts +510 -0
  1103. package/src/worker/routes.test.ts +795 -0
  1104. package/src/worker/routes.ts +650 -0
  1105. package/src/worker/rpc-methods-manager.test.ts +326 -0
  1106. package/src/worker/rpc-methods-manager.ts +276 -0
  1107. package/src/worker/rpc.ts +524 -0
  1108. package/src/worker/schema-version.ts +605 -0
  1109. package/src/worker/session-manager.test.ts +506 -0
  1110. package/src/worker/session-manager.ts +732 -0
  1111. package/src/worker/shutdown-manager.ts +469 -0
  1112. package/src/worker/sql-transform.test.ts +286 -0
  1113. package/src/worker/sql-transform.ts +368 -0
  1114. package/src/worker/supabase-compat.test.ts +621 -0
  1115. package/src/worker/types.test.ts +292 -0
  1116. package/src/worker/types.ts +873 -0
  1117. package/src/worker/user-routes.test.ts +703 -0
  1118. package/src/worker/user-routes.ts +303 -0
  1119. package/src/worker/wal-facade.ts +235 -0
  1120. package/src/worker/wal-r2.test.ts +570 -0
  1121. package/src/worker/wal-r2.ts +930 -0
  1122. package/src/worker/wal-replay.test.ts +845 -0
  1123. package/src/worker/wal-replay.ts +897 -0
  1124. package/src/worker/wal-retention.test.ts +758 -0
  1125. package/src/worker/wal-retention.ts +1075 -0
  1126. package/src/worker/wal.test.ts +618 -0
  1127. package/src/worker/wal.ts +697 -0
  1128. package/src/worker/websocket.test.ts +296 -0
  1129. package/src/worker/websocket.ts +284 -0
@@ -0,0 +1,2493 @@
1
+ /**
2
+ * RED PHASE TESTS: Hostname-based Multi-tenant DO Routing
3
+ *
4
+ * Purpose: Route incoming requests to the correct Durable Object based on
5
+ * tenant identification extracted from hostname, path, or headers.
6
+ *
7
+ * Key features:
8
+ * - Extract tenant from subdomain (tenant1.app.com)
9
+ * - Extract tenant from full hostname (custom domains)
10
+ * - Extract tenant from path (/tenant1/...)
11
+ * - Extract tenant from header (X-Tenant-ID)
12
+ * - Route to correct DO with tenant isolation
13
+ * - Support blocked tenant lists
14
+ * - Custom tenant resolver functions
15
+ */
16
+
17
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
18
+ import {
19
+ TenantRouter,
20
+ TenantRouterConfig,
21
+ TenantExtractor,
22
+ TenantExtractionResult,
23
+ TenantRoutingResult,
24
+ RoutingMetadata,
25
+ RoutingTiming,
26
+ TenantContext,
27
+ createTenantRouter,
28
+ extractors,
29
+ DEFAULT_HEADER_NAME,
30
+ DEFAULT_PATH_PREFIX,
31
+ SimpleTenantExtractor,
32
+ TenantRateLimiter,
33
+ RateLimitConfig,
34
+ RateLimitResult,
35
+ createDomainMappingCache,
36
+ MetricsCollector,
37
+ RequestMetrics,
38
+ TenantRouterLogger,
39
+ LogEntry,
40
+ TraceContext,
41
+ TraceContextExtractor,
42
+ DomainMappingCache,
43
+ DomainCacheConfig,
44
+ DomainCacheEntry,
45
+ CacheStats,
46
+ } from './tenant-router'
47
+ import {
48
+ createMockRequest,
49
+ createMockDONamespace,
50
+ } from '../__tests__/test-utils'
51
+
52
+ describe('TenantRouter', () => {
53
+ describe('hostname extraction', () => {
54
+ it('should extract tenant from subdomain: tenant1.app.com -> tenant1', () => {
55
+ const extractor = extractors.subdomain('app.com')
56
+ const request = createMockRequest('https://tenant1.app.com/api/users')
57
+ expect(extractor(request)).toBe('tenant1')
58
+ })
59
+
60
+ it('should extract tenant from subdomain with multiple levels: tenant1.api.app.com -> tenant1', () => {
61
+ const extractor = extractors.subdomain('app.com')
62
+ const request = createMockRequest('https://tenant1.api.app.com/api/users')
63
+ expect(extractor(request)).toBe('tenant1')
64
+ })
65
+
66
+ it('should extract tenant from full hostname: tenant1.example.com -> tenant1.example.com', () => {
67
+ // Without baseDomain, return the first subdomain segment
68
+ const extractor = extractors.subdomain()
69
+ const request = createMockRequest('https://tenant1.example.com/api/users')
70
+ expect(extractor(request)).toBe('tenant1')
71
+ })
72
+
73
+ it('should handle www prefix: www.tenant1.app.com -> tenant1', () => {
74
+ const extractor = extractors.subdomain('app.com')
75
+ const request = createMockRequest('https://www.tenant1.app.com/api/users')
76
+ expect(extractor(request)).toBe('tenant1')
77
+ })
78
+
79
+ it('should handle custom domain mapping lookup', async () => {
80
+ // Custom extractor that does domain mapping lookup
81
+ const domainMap: Record<string, string> = {
82
+ 'custom.example.org': 'tenant1',
83
+ 'another.example.org': 'tenant2',
84
+ }
85
+ const customExtractor: SimpleTenantExtractor = (request: Request) => {
86
+ const url = new URL(request.url)
87
+ return domainMap[url.hostname] || null
88
+ }
89
+
90
+ const request1 = createMockRequest('https://custom.example.org/api')
91
+ const request2 = createMockRequest('https://another.example.org/api')
92
+
93
+ expect(customExtractor(request1)).toBe('tenant1')
94
+ expect(customExtractor(request2)).toBe('tenant2')
95
+ })
96
+
97
+ it('should return null for apex domain: app.com -> null', () => {
98
+ const extractor = extractors.subdomain('app.com')
99
+ const request = createMockRequest('https://app.com/api/users')
100
+ expect(extractor(request)).toBe(null)
101
+ })
102
+
103
+ it('should handle localhost with port: tenant1.localhost:8787 -> tenant1', () => {
104
+ const extractor = extractors.subdomain('localhost')
105
+ const request = createMockRequest('http://tenant1.localhost:8787/api/users')
106
+ expect(extractor(request)).toBe('tenant1')
107
+ })
108
+
109
+ it('should normalize tenant identifiers to lowercase', () => {
110
+ const extractor = extractors.subdomain('app.com')
111
+ const request = createMockRequest('https://TenAnt1.app.com/api/users')
112
+ // The URL parser already lowercases hostnames
113
+ expect(extractor(request)).toBe('tenant1')
114
+ })
115
+
116
+ it('should strip invalid characters from tenant identifiers', async () => {
117
+ const mockNamespace = createMockDONamespace()
118
+ const router = createTenantRouter({
119
+ doNamespace: mockNamespace,
120
+ extractTenant: 'path',
121
+ transformTenant: (id) => id.replace(/[^a-z0-9-]/gi, ''),
122
+ })
123
+
124
+ const request = createMockRequest('https://app.com/tenant@1!/api')
125
+ const tenantId = await router.getTenantId(request)
126
+ expect(tenantId).toBe('tenant1')
127
+ })
128
+
129
+ it('should handle IP addresses gracefully: 192.168.1.1 -> null', () => {
130
+ const extractor = extractors.subdomain()
131
+ const request = createMockRequest('http://192.168.1.1:8080/api/users')
132
+ expect(extractor(request)).toBe(null)
133
+ })
134
+ })
135
+
136
+ describe('path-based extraction', () => {
137
+ it('should extract tenant from path: /tenant1/api/users -> tenant1', () => {
138
+ const extractor = extractors.path()
139
+ const request = createMockRequest('https://app.com/tenant1/api/users')
140
+ expect(extractor(request)).toBe('tenant1')
141
+ })
142
+
143
+ it('should extract tenant with custom prefix: /orgs/tenant1/api -> tenant1', () => {
144
+ const extractor = extractors.path('/orgs')
145
+ const request = createMockRequest('https://app.com/orgs/tenant1/api')
146
+ expect(extractor(request)).toBe('tenant1')
147
+ })
148
+
149
+ it('should return null for root path: / -> null', () => {
150
+ const extractor = extractors.path()
151
+ const request = createMockRequest('https://app.com/')
152
+ expect(extractor(request)).toBe(null)
153
+ })
154
+
155
+ it('should handle URL-encoded tenant names: /tenant%20name/api -> tenant name', () => {
156
+ const extractor = extractors.path()
157
+ const request = createMockRequest('https://app.com/tenant%20name/api')
158
+ expect(extractor(request)).toBe('tenant name')
159
+ })
160
+
161
+ it('should preserve remaining path after tenant extraction', async () => {
162
+ const mockNamespace = createMockDONamespace()
163
+ const router = createTenantRouter({
164
+ doNamespace: mockNamespace,
165
+ extractTenant: 'path',
166
+ stripTenantFromPath: true,
167
+ })
168
+
169
+ const request = createMockRequest('https://app.com/tenant1/api/users')
170
+ const result = await router.extractTenant(request)
171
+ expect(result.modifiedPath).toBe('/api/users')
172
+ })
173
+
174
+ it('should handle trailing slashes: /tenant1/ -> tenant1', () => {
175
+ const extractor = extractors.path()
176
+ const request = createMockRequest('https://app.com/tenant1/')
177
+ expect(extractor(request)).toBe('tenant1')
178
+ })
179
+ })
180
+
181
+ describe('header-based extraction', () => {
182
+ it('should extract tenant from X-Tenant-ID header', () => {
183
+ const extractor = extractors.header()
184
+ const request = createMockRequest('https://app.com/api', {
185
+ headers: { 'X-Tenant-ID': 'tenant1' },
186
+ })
187
+ expect(extractor(request)).toBe('tenant1')
188
+ })
189
+
190
+ it('should support custom header name via config', () => {
191
+ const extractor = extractors.header('X-Org-ID')
192
+ const request = createMockRequest('https://app.com/api', {
193
+ headers: { 'X-Org-ID': 'org123' },
194
+ })
195
+ expect(extractor(request)).toBe('org123')
196
+ })
197
+
198
+ it('should return null when header is missing', () => {
199
+ const extractor = extractors.header()
200
+ const request = createMockRequest('https://app.com/api')
201
+ expect(extractor(request)).toBe(null)
202
+ })
203
+
204
+ it('should trim whitespace from header value', () => {
205
+ const extractor = extractors.header()
206
+ const request = createMockRequest('https://app.com/api', {
207
+ headers: { 'X-Tenant-ID': ' tenant1 ' },
208
+ })
209
+ expect(extractor(request)).toBe('tenant1')
210
+ })
211
+
212
+ it('should handle empty header value: X-Tenant-ID: "" -> null', () => {
213
+ const extractor = extractors.header()
214
+ const request = createMockRequest('https://app.com/api', {
215
+ headers: { 'X-Tenant-ID': '' },
216
+ })
217
+ expect(extractor(request)).toBe(null)
218
+ })
219
+
220
+ it('should be case-insensitive for header name lookup', () => {
221
+ const extractor = extractors.header('X-Tenant-ID')
222
+ const request = createMockRequest('https://app.com/api', {
223
+ headers: { 'x-tenant-id': 'tenant1' },
224
+ })
225
+ expect(extractor(request)).toBe('tenant1')
226
+ })
227
+ })
228
+
229
+ describe('custom extractor', () => {
230
+ it('should support custom tenant resolver function', async () => {
231
+ const mockNamespace = createMockDONamespace()
232
+ const customExtractor: SimpleTenantExtractor = (request: Request) => {
233
+ const url = new URL(request.url)
234
+ return url.searchParams.get('tenant')
235
+ }
236
+
237
+ const router = createTenantRouter({
238
+ doNamespace: mockNamespace,
239
+ extractTenant: customExtractor,
240
+ })
241
+
242
+ const request = createMockRequest('https://app.com/api?tenant=custom1')
243
+ const tenantId = await router.getTenantId(request)
244
+ expect(tenantId).toBe('custom1')
245
+ })
246
+
247
+ it('should pass full request to custom extractor', async () => {
248
+ const mockNamespace = createMockDONamespace()
249
+ let receivedRequest: Request | null = null
250
+
251
+ const customExtractor: SimpleTenantExtractor = (request: Request) => {
252
+ receivedRequest = request
253
+ return 'tenant1'
254
+ }
255
+
256
+ const router = createTenantRouter({
257
+ doNamespace: mockNamespace,
258
+ extractTenant: customExtractor,
259
+ })
260
+
261
+ const request = createMockRequest('https://app.com/api', {
262
+ method: 'POST',
263
+ headers: { 'Content-Type': 'application/json' },
264
+ })
265
+
266
+ await router.getTenantId(request)
267
+ expect(receivedRequest).not.toBe(null)
268
+ expect(receivedRequest!.method).toBe('POST')
269
+ expect(receivedRequest!.headers.get('Content-Type')).toBe('application/json')
270
+ })
271
+
272
+ it('should handle async custom extractors', async () => {
273
+ const mockNamespace = createMockDONamespace()
274
+ const asyncExtractor: TenantExtractor = async (request: Request) => {
275
+ await new Promise((resolve) => setTimeout(resolve, 10))
276
+ const url = new URL(request.url)
277
+ return {
278
+ tenantId: url.searchParams.get('tenant'),
279
+ source: 'custom' as const,
280
+ originalHostname: url.hostname,
281
+ }
282
+ }
283
+
284
+ const router = createTenantRouter({
285
+ doNamespace: mockNamespace,
286
+ extractTenant: asyncExtractor,
287
+ })
288
+
289
+ const request = createMockRequest('https://app.com/api?tenant=async1')
290
+ const tenantId = await router.getTenantId(request)
291
+ expect(tenantId).toBe('async1')
292
+ })
293
+
294
+ it('should allow custom extractor to return metadata', async () => {
295
+ const mockNamespace = createMockDONamespace()
296
+ const customExtractor: TenantExtractor = (request: Request) => {
297
+ const url = new URL(request.url)
298
+ return {
299
+ tenantId: url.searchParams.get('tenant'),
300
+ source: 'custom' as const,
301
+ originalHostname: url.hostname,
302
+ metadata: {
303
+ plan: 'enterprise',
304
+ region: 'us-east-1',
305
+ },
306
+ }
307
+ }
308
+
309
+ const router = createTenantRouter({
310
+ doNamespace: mockNamespace,
311
+ extractTenant: customExtractor,
312
+ })
313
+
314
+ const request = createMockRequest('https://app.com/api?tenant=meta1')
315
+ const result = await router.extractTenant(request)
316
+ expect(result.metadata).toEqual({ plan: 'enterprise', region: 'us-east-1' })
317
+ })
318
+
319
+ it('should fallback to null when custom extractor throws', async () => {
320
+ const mockNamespace = createMockDONamespace()
321
+ const throwingExtractor: SimpleTenantExtractor = () => {
322
+ throw new Error('Extractor error')
323
+ }
324
+
325
+ const router = createTenantRouter({
326
+ doNamespace: mockNamespace,
327
+ extractTenant: throwingExtractor,
328
+ })
329
+
330
+ const request = createMockRequest('https://app.com/api')
331
+ // The route method should handle the error
332
+ const response = await router.route(request)
333
+ expect(response.status).toBe(500)
334
+ })
335
+ })
336
+
337
+ describe('DO routing', () => {
338
+ let mockNamespace: DurableObjectNamespace
339
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
340
+
341
+ beforeEach(() => {
342
+ mockStub = {
343
+ fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
344
+ }
345
+ mockNamespace = {
346
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
347
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
348
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
349
+ get: vi.fn(() => mockStub),
350
+ jurisdiction: vi.fn(),
351
+ } as unknown as DurableObjectNamespace
352
+ })
353
+
354
+ it('should create DO ID from tenant identifier using idFromName', async () => {
355
+ const router = createTenantRouter({
356
+ doNamespace: mockNamespace,
357
+ extractTenant: 'header',
358
+ })
359
+
360
+ const request = createMockRequest('https://app.com/api', {
361
+ headers: { 'X-Tenant-ID': 'tenant1' },
362
+ })
363
+
364
+ await router.route(request)
365
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('tenant1')
366
+ })
367
+
368
+ it('should route request to correct DO stub', async () => {
369
+ const router = createTenantRouter({
370
+ doNamespace: mockNamespace,
371
+ extractTenant: 'header',
372
+ })
373
+
374
+ const request = createMockRequest('https://app.com/api', {
375
+ headers: { 'X-Tenant-ID': 'tenant1' },
376
+ })
377
+
378
+ await router.route(request)
379
+ expect(mockNamespace.get).toHaveBeenCalled()
380
+ expect(mockStub.fetch).toHaveBeenCalled()
381
+ })
382
+
383
+ it('should forward all original headers to DO', async () => {
384
+ const router = createTenantRouter({
385
+ doNamespace: mockNamespace,
386
+ extractTenant: 'header',
387
+ })
388
+
389
+ const request = createMockRequest('https://app.com/api', {
390
+ headers: {
391
+ 'X-Tenant-ID': 'tenant1',
392
+ 'Content-Type': 'application/json',
393
+ 'Authorization': 'Bearer token123',
394
+ },
395
+ })
396
+
397
+ await router.route(request)
398
+
399
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
400
+ expect(forwardedRequest.headers.get('Content-Type')).toBe('application/json')
401
+ expect(forwardedRequest.headers.get('Authorization')).toBe('Bearer token123')
402
+ })
403
+
404
+ it('should forward request body to DO', async () => {
405
+ const router = createTenantRouter({
406
+ doNamespace: mockNamespace,
407
+ extractTenant: 'header',
408
+ })
409
+
410
+ const body = JSON.stringify({ data: 'test' })
411
+ const request = createMockRequest('https://app.com/api', {
412
+ method: 'POST',
413
+ headers: {
414
+ 'X-Tenant-ID': 'tenant1',
415
+ 'Content-Type': 'application/json',
416
+ },
417
+ body,
418
+ })
419
+
420
+ await router.route(request)
421
+
422
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
423
+ const forwardedBody = await forwardedRequest.text()
424
+ expect(forwardedBody).toBe(body)
425
+ })
426
+
427
+ it('should forward request method to DO', async () => {
428
+ const router = createTenantRouter({
429
+ doNamespace: mockNamespace,
430
+ extractTenant: 'header',
431
+ })
432
+
433
+ const request = createMockRequest('https://app.com/api', {
434
+ method: 'PUT',
435
+ headers: { 'X-Tenant-ID': 'tenant1' },
436
+ })
437
+
438
+ await router.route(request)
439
+
440
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
441
+ expect(forwardedRequest.method).toBe('PUT')
442
+ })
443
+
444
+ it('should add X-Tenant-ID header to forwarded request', async () => {
445
+ const router = createTenantRouter({
446
+ doNamespace: mockNamespace,
447
+ extractTenant: 'subdomain',
448
+ baseDomain: 'app.com',
449
+ })
450
+
451
+ const request = createMockRequest('https://tenant1.app.com/api')
452
+
453
+ await router.route(request)
454
+
455
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
456
+ expect(forwardedRequest.headers.get('X-Tenant-ID')).toBe('tenant1')
457
+ })
458
+
459
+ it('should preserve query string in forwarded request', async () => {
460
+ const router = createTenantRouter({
461
+ doNamespace: mockNamespace,
462
+ extractTenant: 'header',
463
+ })
464
+
465
+ const request = createMockRequest('https://app.com/api?foo=bar&baz=qux', {
466
+ headers: { 'X-Tenant-ID': 'tenant1' },
467
+ })
468
+
469
+ await router.route(request)
470
+
471
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
472
+ const url = new URL(forwardedRequest.url)
473
+ expect(url.searchParams.get('foo')).toBe('bar')
474
+ expect(url.searchParams.get('baz')).toBe('qux')
475
+ })
476
+
477
+ it('should handle streaming request bodies', async () => {
478
+ const router = createTenantRouter({
479
+ doNamespace: mockNamespace,
480
+ extractTenant: 'header',
481
+ })
482
+
483
+ // Create a streaming body using ReadableStream
484
+ const stream = new ReadableStream({
485
+ start(controller) {
486
+ controller.enqueue(new TextEncoder().encode('chunk1'))
487
+ controller.enqueue(new TextEncoder().encode('chunk2'))
488
+ controller.close()
489
+ },
490
+ })
491
+
492
+ const request = new Request('https://app.com/api', {
493
+ method: 'POST',
494
+ headers: { 'X-Tenant-ID': 'tenant1' },
495
+ body: stream,
496
+ // @ts-expect-error duplex is needed for streaming
497
+ duplex: 'half',
498
+ })
499
+
500
+ await router.route(request)
501
+ expect(mockStub.fetch).toHaveBeenCalled()
502
+ })
503
+
504
+ it('should return DO response directly to caller', async () => {
505
+ mockStub.fetch.mockResolvedValue(
506
+ new Response(JSON.stringify({ result: 'success' }), {
507
+ status: 201,
508
+ headers: { 'Content-Type': 'application/json' },
509
+ })
510
+ )
511
+
512
+ const router = createTenantRouter({
513
+ doNamespace: mockNamespace,
514
+ extractTenant: 'header',
515
+ })
516
+
517
+ const request = createMockRequest('https://app.com/api', {
518
+ headers: { 'X-Tenant-ID': 'tenant1' },
519
+ })
520
+
521
+ const response = await router.route(request)
522
+ expect(response.status).toBe(201)
523
+ expect(response.headers.get('Content-Type')).toBe('application/json')
524
+ const body = await response.json()
525
+ expect(body).toEqual({ result: 'success' })
526
+ })
527
+
528
+ it('should timeout DO requests after configured duration', async () => {
529
+ // The timeout is implemented using AbortController
530
+ // For this test, we simulate the abort by having fetch reject with AbortError
531
+ const abortError = new Error('Aborted')
532
+ abortError.name = 'AbortError'
533
+ mockStub.fetch.mockRejectedValue(abortError)
534
+
535
+ const router = createTenantRouter({
536
+ doNamespace: mockNamespace,
537
+ extractTenant: 'header',
538
+ requestTimeoutMs: 50, // Very short timeout for testing
539
+ })
540
+
541
+ const request = createMockRequest('https://app.com/api', {
542
+ headers: { 'X-Tenant-ID': 'tenant1' },
543
+ })
544
+
545
+ const response = await router.route(request)
546
+ expect(response.status).toBe(504)
547
+ })
548
+ })
549
+
550
+ describe('tenant isolation', () => {
551
+ let mockNamespace: DurableObjectNamespace
552
+ let stubCalls: Map<string, DurableObjectStub>
553
+
554
+ beforeEach(() => {
555
+ stubCalls = new Map()
556
+ mockNamespace = {
557
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
558
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
559
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
560
+ get: vi.fn((id: { name: string }) => {
561
+ const stub = {
562
+ fetch: vi.fn().mockResolvedValue(new Response(`Response for ${id.name}`)),
563
+ }
564
+ stubCalls.set(id.name, stub as unknown as DurableObjectStub)
565
+ return stub
566
+ }),
567
+ jurisdiction: vi.fn(),
568
+ } as unknown as DurableObjectNamespace
569
+ })
570
+
571
+ it('should never route tenant1 requests to tenant2 DO', async () => {
572
+ const router = createTenantRouter({
573
+ doNamespace: mockNamespace,
574
+ extractTenant: 'header',
575
+ })
576
+
577
+ const request1 = createMockRequest('https://app.com/api', {
578
+ headers: { 'X-Tenant-ID': 'tenant1' },
579
+ })
580
+ const request2 = createMockRequest('https://app.com/api', {
581
+ headers: { 'X-Tenant-ID': 'tenant2' },
582
+ })
583
+
584
+ await router.route(request1)
585
+ await router.route(request2)
586
+
587
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('tenant1')
588
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('tenant2')
589
+ expect(stubCalls.has('tenant1')).toBe(true)
590
+ expect(stubCalls.has('tenant2')).toBe(true)
591
+ })
592
+
593
+ it('should create separate DO for each unique tenant', async () => {
594
+ const router = createTenantRouter({
595
+ doNamespace: mockNamespace,
596
+ extractTenant: 'header',
597
+ })
598
+
599
+ for (const tenant of ['tenant1', 'tenant2', 'tenant3']) {
600
+ const request = createMockRequest('https://app.com/api', {
601
+ headers: { 'X-Tenant-ID': tenant },
602
+ })
603
+ await router.route(request)
604
+ }
605
+
606
+ expect(mockNamespace.idFromName).toHaveBeenCalledTimes(3)
607
+ expect(stubCalls.size).toBe(3)
608
+ })
609
+
610
+ it('should use consistent DO ID for same tenant across requests', async () => {
611
+ const router = createTenantRouter({
612
+ doNamespace: mockNamespace,
613
+ extractTenant: 'header',
614
+ })
615
+
616
+ for (let i = 0; i < 3; i++) {
617
+ const request = createMockRequest('https://app.com/api', {
618
+ headers: { 'X-Tenant-ID': 'tenant1' },
619
+ })
620
+ await router.route(request)
621
+ }
622
+
623
+ // All calls should use the same tenant ID
624
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('tenant1')
625
+ expect(mockNamespace.idFromName).toHaveBeenCalledTimes(3)
626
+ })
627
+
628
+ it('should not leak tenant information in error responses', async () => {
629
+ const router = createTenantRouter({
630
+ doNamespace: mockNamespace,
631
+ extractTenant: 'header',
632
+ blockedTenants: ['blocked-tenant'],
633
+ })
634
+
635
+ const request = createMockRequest('https://app.com/api', {
636
+ headers: { 'X-Tenant-ID': 'blocked-tenant' },
637
+ })
638
+
639
+ const response = await router.route(request)
640
+ const body = await response.json() as { error: string }
641
+
642
+ // Should return generic 404, not mention tenant
643
+ expect(response.status).toBe(404)
644
+ expect(body.error).toBe('Not Found')
645
+ expect(body.error).not.toContain('blocked-tenant')
646
+ })
647
+
648
+ it('should validate tenant ID format before routing', async () => {
649
+ const router = createTenantRouter({
650
+ doNamespace: mockNamespace,
651
+ extractTenant: 'header',
652
+ })
653
+
654
+ const request = createMockRequest('https://app.com/api', {
655
+ headers: { 'X-Tenant-ID': '../malicious' },
656
+ })
657
+
658
+ const response = await router.route(request)
659
+ expect(response.status).toBe(400)
660
+ })
661
+
662
+ it('should prevent tenant ID injection via path traversal', async () => {
663
+ const router = createTenantRouter({
664
+ doNamespace: mockNamespace,
665
+ extractTenant: 'header',
666
+ })
667
+
668
+ // Using header-based extraction to test the validation directly
669
+ // Path-based URLs get normalized by URL parser before we see them
670
+ const request = createMockRequest('https://app.com/api', {
671
+ headers: { 'X-Tenant-ID': 'tenant/../../etc/passwd' },
672
+ })
673
+
674
+ const response = await router.route(request)
675
+ expect(response.status).toBe(400)
676
+ })
677
+ })
678
+
679
+ describe('configuration', () => {
680
+ let mockNamespace: DurableObjectNamespace
681
+
682
+ beforeEach(() => {
683
+ mockNamespace = createMockDONamespace()
684
+ })
685
+
686
+ it('should support subdomain-based tenancy mode', async () => {
687
+ const router = createTenantRouter({
688
+ doNamespace: mockNamespace,
689
+ extractTenant: 'subdomain',
690
+ baseDomain: 'app.com',
691
+ })
692
+
693
+ const request = createMockRequest('https://tenant1.app.com/api')
694
+ const tenantId = await router.getTenantId(request)
695
+ expect(tenantId).toBe('tenant1')
696
+ })
697
+
698
+ it('should support path-based tenancy mode: /tenant1/...', async () => {
699
+ const router = createTenantRouter({
700
+ doNamespace: mockNamespace,
701
+ extractTenant: 'path',
702
+ })
703
+
704
+ const request = createMockRequest('https://app.com/tenant1/api')
705
+ const tenantId = await router.getTenantId(request)
706
+ expect(tenantId).toBe('tenant1')
707
+ })
708
+
709
+ it('should support header-based tenancy mode: X-Tenant-ID', async () => {
710
+ const router = createTenantRouter({
711
+ doNamespace: mockNamespace,
712
+ extractTenant: 'header',
713
+ })
714
+
715
+ const request = createMockRequest('https://app.com/api', {
716
+ headers: { 'X-Tenant-ID': 'tenant1' },
717
+ })
718
+ const tenantId = await router.getTenantId(request)
719
+ expect(tenantId).toBe('tenant1')
720
+ })
721
+
722
+ it('should support custom tenant resolver function', async () => {
723
+ const customResolver: SimpleTenantExtractor = (request: Request) => {
724
+ const url = new URL(request.url)
725
+ return url.searchParams.get('org')
726
+ }
727
+
728
+ const router = createTenantRouter({
729
+ doNamespace: mockNamespace,
730
+ extractTenant: customResolver,
731
+ })
732
+
733
+ const request = createMockRequest('https://app.com/api?org=myorg')
734
+ const tenantId = await router.getTenantId(request)
735
+ expect(tenantId).toBe('myorg')
736
+ })
737
+
738
+ it('should allow combining multiple extraction strategies with priority', async () => {
739
+ const combinedExtractor = extractors.combined([
740
+ extractors.header(),
741
+ extractors.subdomain('app.com'),
742
+ extractors.path(),
743
+ ])
744
+
745
+ const router = createTenantRouter({
746
+ doNamespace: mockNamespace,
747
+ extractTenant: combinedExtractor,
748
+ })
749
+
750
+ // Header takes priority
751
+ const req1 = createMockRequest('https://tenant2.app.com/tenant3/api', {
752
+ headers: { 'X-Tenant-ID': 'tenant1' },
753
+ })
754
+ expect(await router.getTenantId(req1)).toBe('tenant1')
755
+
756
+ // Subdomain is next
757
+ const req2 = createMockRequest('https://tenant2.app.com/tenant3/api')
758
+ expect(await router.getTenantId(req2)).toBe('tenant2')
759
+ })
760
+
761
+ it('should support tenant ID transformation function', async () => {
762
+ const router = createTenantRouter({
763
+ doNamespace: mockNamespace,
764
+ extractTenant: 'header',
765
+ transformTenant: (id) => `prefix_${id.toLowerCase()}`,
766
+ })
767
+
768
+ const request = createMockRequest('https://app.com/api', {
769
+ headers: { 'X-Tenant-ID': 'MyTenant' },
770
+ })
771
+ const tenantId = await router.getTenantId(request)
772
+ expect(tenantId).toBe('prefix_mytenant')
773
+ })
774
+
775
+ it('should support configurable DO namespace', async () => {
776
+ const namespace1 = createMockDONamespace()
777
+ const namespace2 = createMockDONamespace()
778
+
779
+ const router1 = createTenantRouter({
780
+ doNamespace: namespace1,
781
+ extractTenant: 'header',
782
+ })
783
+ const router2 = createTenantRouter({
784
+ doNamespace: namespace2,
785
+ extractTenant: 'header',
786
+ })
787
+
788
+ const request = createMockRequest('https://app.com/api', {
789
+ headers: { 'X-Tenant-ID': 'tenant1' },
790
+ })
791
+
792
+ await router1.route(request)
793
+ await router2.route(request)
794
+
795
+ expect(namespace1.idFromName).toHaveBeenCalledWith('tenant1')
796
+ expect(namespace2.idFromName).toHaveBeenCalledWith('tenant1')
797
+ })
798
+
799
+ it('should support request timeout configuration', () => {
800
+ // This is validated by the timeout test in DO routing section
801
+ const router = createTenantRouter({
802
+ doNamespace: mockNamespace,
803
+ extractTenant: 'header',
804
+ requestTimeoutMs: 5000,
805
+ })
806
+
807
+ expect(router).toBeDefined()
808
+ })
809
+
810
+ it('should support base path stripping for path-based tenancy', async () => {
811
+ const router = createTenantRouter({
812
+ doNamespace: mockNamespace,
813
+ extractTenant: 'path',
814
+ pathPrefix: '/v1/tenants',
815
+ stripTenantFromPath: true,
816
+ })
817
+
818
+ const request = createMockRequest('https://app.com/v1/tenants/tenant1/users/123')
819
+ const result = await router.extractTenant(request)
820
+
821
+ expect(result.tenantId).toBe('tenant1')
822
+ expect(result.modifiedPath).toBe('/users/123')
823
+ })
824
+ })
825
+
826
+ describe('blocked tenants', () => {
827
+ let mockNamespace: DurableObjectNamespace
828
+
829
+ beforeEach(() => {
830
+ mockNamespace = createMockDONamespace()
831
+ })
832
+
833
+ it('should return 404 for blocked tenants', async () => {
834
+ const router = createTenantRouter({
835
+ doNamespace: mockNamespace,
836
+ extractTenant: 'header',
837
+ blockedTenants: ['blocked'],
838
+ })
839
+
840
+ const request = createMockRequest('https://app.com/api', {
841
+ headers: { 'X-Tenant-ID': 'blocked' },
842
+ })
843
+
844
+ const response = await router.route(request)
845
+ expect(response.status).toBe(404)
846
+ })
847
+
848
+ it('should check blocked list before routing', async () => {
849
+ const router = createTenantRouter({
850
+ doNamespace: mockNamespace,
851
+ extractTenant: 'header',
852
+ blockedTenants: ['blocked'],
853
+ })
854
+
855
+ const request = createMockRequest('https://app.com/api', {
856
+ headers: { 'X-Tenant-ID': 'blocked' },
857
+ })
858
+
859
+ await router.route(request)
860
+
861
+ // Should not have routed to DO
862
+ expect(mockNamespace.get).not.toHaveBeenCalled()
863
+ })
864
+
865
+ it('should support wildcard patterns in blocked list: admin*', async () => {
866
+ const router = createTenantRouter({
867
+ doNamespace: mockNamespace,
868
+ extractTenant: 'header',
869
+ blockedTenants: ['admin*'],
870
+ })
871
+
872
+ const request1 = createMockRequest('https://app.com/api', {
873
+ headers: { 'X-Tenant-ID': 'admin' },
874
+ })
875
+ const request2 = createMockRequest('https://app.com/api', {
876
+ headers: { 'X-Tenant-ID': 'admin123' },
877
+ })
878
+ const request3 = createMockRequest('https://app.com/api', {
879
+ headers: { 'X-Tenant-ID': 'administrator' },
880
+ })
881
+
882
+ expect((await router.route(request1)).status).toBe(404)
883
+ expect((await router.route(request2)).status).toBe(404)
884
+ expect((await router.route(request3)).status).toBe(404)
885
+ })
886
+
887
+ it('should support blocked tenant callback for dynamic checks', async () => {
888
+ const blockedCallback = vi.fn().mockResolvedValue(true)
889
+
890
+ const router = createTenantRouter({
891
+ doNamespace: mockNamespace,
892
+ extractTenant: 'header',
893
+ isBlocked: blockedCallback,
894
+ })
895
+
896
+ const request = createMockRequest('https://app.com/api', {
897
+ headers: { 'X-Tenant-ID': 'dynamically-blocked' },
898
+ })
899
+
900
+ const response = await router.route(request)
901
+ expect(response.status).toBe(404)
902
+ expect(blockedCallback).toHaveBeenCalledWith('dynamically-blocked', expect.any(Request))
903
+ })
904
+
905
+ it('should log blocked tenant access attempts', async () => {
906
+ // The implementation uses error formatter for blocked tenants
907
+ // We can verify through the response that includes correlation ID
908
+ const router = createTenantRouter({
909
+ doNamespace: mockNamespace,
910
+ extractTenant: 'header',
911
+ blockedTenants: ['blocked'],
912
+ })
913
+
914
+ const request = createMockRequest('https://app.com/api', {
915
+ headers: { 'X-Tenant-ID': 'blocked' },
916
+ })
917
+
918
+ const response = await router.route(request)
919
+ expect(response.headers.get('X-Correlation-ID')).toBeTruthy()
920
+ })
921
+ })
922
+
923
+ describe('error handling', () => {
924
+ let mockNamespace: DurableObjectNamespace
925
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
926
+
927
+ beforeEach(() => {
928
+ mockStub = {
929
+ fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
930
+ }
931
+ mockNamespace = {
932
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
933
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
934
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
935
+ get: vi.fn(() => mockStub),
936
+ jurisdiction: vi.fn(),
937
+ } as unknown as DurableObjectNamespace
938
+ })
939
+
940
+ it('should return 400 for missing tenant identifier', async () => {
941
+ const router = createTenantRouter({
942
+ doNamespace: mockNamespace,
943
+ extractTenant: 'header',
944
+ })
945
+
946
+ const request = createMockRequest('https://app.com/api')
947
+ const response = await router.route(request)
948
+ expect(response.status).toBe(400)
949
+ })
950
+
951
+ it('should return 400 for invalid tenant identifier format', async () => {
952
+ const router = createTenantRouter({
953
+ doNamespace: mockNamespace,
954
+ extractTenant: 'header',
955
+ })
956
+
957
+ const request = createMockRequest('https://app.com/api', {
958
+ headers: { 'X-Tenant-ID': 'tenant/../hack' },
959
+ })
960
+
961
+ const response = await router.route(request)
962
+ expect(response.status).toBe(400)
963
+ })
964
+
965
+ it('should return 404 for blocked tenants', async () => {
966
+ const router = createTenantRouter({
967
+ doNamespace: mockNamespace,
968
+ extractTenant: 'header',
969
+ blockedTenants: ['blocked'],
970
+ })
971
+
972
+ const request = createMockRequest('https://app.com/api', {
973
+ headers: { 'X-Tenant-ID': 'blocked' },
974
+ })
975
+
976
+ const response = await router.route(request)
977
+ expect(response.status).toBe(404)
978
+ })
979
+
980
+ it('should return 502 when DO is unreachable', async () => {
981
+ mockStub.fetch.mockRejectedValue(new Error('Network error'))
982
+
983
+ const router = createTenantRouter({
984
+ doNamespace: mockNamespace,
985
+ extractTenant: 'header',
986
+ })
987
+
988
+ const request = createMockRequest('https://app.com/api', {
989
+ headers: { 'X-Tenant-ID': 'tenant1' },
990
+ })
991
+
992
+ const response = await router.route(request)
993
+ expect(response.status).toBe(502)
994
+ })
995
+
996
+ it('should return 504 when DO request times out', async () => {
997
+ // Simulate AbortError which is what happens when timeout triggers
998
+ const abortError = new Error('Aborted')
999
+ abortError.name = 'AbortError'
1000
+ mockStub.fetch.mockRejectedValue(abortError)
1001
+
1002
+ const router = createTenantRouter({
1003
+ doNamespace: mockNamespace,
1004
+ extractTenant: 'header',
1005
+ requestTimeoutMs: 50,
1006
+ })
1007
+
1008
+ const request = createMockRequest('https://app.com/api', {
1009
+ headers: { 'X-Tenant-ID': 'tenant1' },
1010
+ })
1011
+
1012
+ const response = await router.route(request)
1013
+ expect(response.status).toBe(504)
1014
+ })
1015
+
1016
+ it('should handle DO errors gracefully with proper status codes', async () => {
1017
+ mockStub.fetch.mockResolvedValue(new Response('Service Error', { status: 500 }))
1018
+
1019
+ const router = createTenantRouter({
1020
+ doNamespace: mockNamespace,
1021
+ extractTenant: 'header',
1022
+ })
1023
+
1024
+ const request = createMockRequest('https://app.com/api', {
1025
+ headers: { 'X-Tenant-ID': 'tenant1' },
1026
+ })
1027
+
1028
+ // DO errors should pass through
1029
+ const response = await router.route(request)
1030
+ expect(response.status).toBe(500)
1031
+ })
1032
+
1033
+ it('should include correlation ID in error responses', async () => {
1034
+ const router = createTenantRouter({
1035
+ doNamespace: mockNamespace,
1036
+ extractTenant: 'header',
1037
+ })
1038
+
1039
+ const request = createMockRequest('https://app.com/api')
1040
+ const response = await router.route(request)
1041
+
1042
+ expect(response.headers.get('X-Correlation-ID')).toBeTruthy()
1043
+ })
1044
+
1045
+ it('should not expose internal error details to client', async () => {
1046
+ mockStub.fetch.mockRejectedValue(new Error('Internal database connection failed'))
1047
+
1048
+ const router = createTenantRouter({
1049
+ doNamespace: mockNamespace,
1050
+ extractTenant: 'header',
1051
+ })
1052
+
1053
+ const request = createMockRequest('https://app.com/api', {
1054
+ headers: { 'X-Tenant-ID': 'tenant1' },
1055
+ })
1056
+
1057
+ const response = await router.route(request)
1058
+ const body = await response.json() as { error: string }
1059
+
1060
+ expect(body.error).not.toContain('database')
1061
+ expect(body.error).toBe('Bad Gateway')
1062
+ })
1063
+
1064
+ it('should support custom error response formatter', async () => {
1065
+ const customFormatter = vi.fn().mockReturnValue(
1066
+ new Response('Custom Error', { status: 418 })
1067
+ )
1068
+
1069
+ const router = createTenantRouter({
1070
+ doNamespace: mockNamespace,
1071
+ extractTenant: 'header',
1072
+ formatError: customFormatter,
1073
+ })
1074
+
1075
+ const request = createMockRequest('https://app.com/api')
1076
+ const response = await router.route(request)
1077
+
1078
+ expect(response.status).toBe(418)
1079
+ expect(customFormatter).toHaveBeenCalled()
1080
+ })
1081
+ })
1082
+
1083
+ describe('observability', () => {
1084
+ let mockNamespace: DurableObjectNamespace
1085
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
1086
+
1087
+ beforeEach(() => {
1088
+ mockStub = {
1089
+ fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
1090
+ }
1091
+ mockNamespace = {
1092
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
1093
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
1094
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
1095
+ get: vi.fn(() => mockStub),
1096
+ jurisdiction: vi.fn(),
1097
+ } as unknown as DurableObjectNamespace
1098
+ })
1099
+
1100
+ it('should emit metrics for each routed request', async () => {
1101
+ const recordedMetrics: RequestMetrics[] = []
1102
+ const metricsCollector: MetricsCollector = {
1103
+ recordRequest: vi.fn((metrics: RequestMetrics) => {
1104
+ recordedMetrics.push(metrics)
1105
+ }),
1106
+ incrementRequestCount: vi.fn(),
1107
+ recordLatency: vi.fn(),
1108
+ recordDoResponseTime: vi.fn(),
1109
+ }
1110
+
1111
+ const router = createTenantRouter({
1112
+ doNamespace: mockNamespace,
1113
+ extractTenant: 'header',
1114
+ metricsCollector,
1115
+ })
1116
+
1117
+ const request = createMockRequest('https://app.com/api/users', {
1118
+ headers: { 'X-Tenant-ID': 'tenant1' },
1119
+ })
1120
+
1121
+ await router.route(request)
1122
+
1123
+ expect(metricsCollector.recordRequest).toHaveBeenCalledTimes(1)
1124
+ expect(recordedMetrics[0].tenantId).toBe('tenant1')
1125
+ expect(recordedMetrics[0].method).toBe('GET')
1126
+ expect(recordedMetrics[0].path).toBe('/api/users')
1127
+ expect(recordedMetrics[0].status).toBe(200)
1128
+ })
1129
+
1130
+ it('should track request latency', async () => {
1131
+ const latencies: Array<{ tenantId: string; latencyMs: number }> = []
1132
+ const metricsCollector: MetricsCollector = {
1133
+ recordRequest: vi.fn(),
1134
+ incrementRequestCount: vi.fn(),
1135
+ recordLatency: vi.fn((tenantId: string, latencyMs: number) => {
1136
+ latencies.push({ tenantId, latencyMs })
1137
+ }),
1138
+ recordDoResponseTime: vi.fn(),
1139
+ }
1140
+
1141
+ const router = createTenantRouter({
1142
+ doNamespace: mockNamespace,
1143
+ extractTenant: 'header',
1144
+ metricsCollector,
1145
+ })
1146
+
1147
+ const request = createMockRequest('https://app.com/api', {
1148
+ headers: { 'X-Tenant-ID': 'tenant1' },
1149
+ })
1150
+
1151
+ await router.route(request)
1152
+
1153
+ expect(metricsCollector.recordLatency).toHaveBeenCalledTimes(1)
1154
+ expect(latencies[0].tenantId).toBe('tenant1')
1155
+ expect(latencies[0].latencyMs).toBeGreaterThanOrEqual(0)
1156
+ })
1157
+
1158
+ it('should count requests per tenant', async () => {
1159
+ const requestCounts: string[] = []
1160
+ const metricsCollector: MetricsCollector = {
1161
+ recordRequest: vi.fn(),
1162
+ incrementRequestCount: vi.fn((tenantId: string) => {
1163
+ requestCounts.push(tenantId)
1164
+ }),
1165
+ recordLatency: vi.fn(),
1166
+ recordDoResponseTime: vi.fn(),
1167
+ }
1168
+
1169
+ const router = createTenantRouter({
1170
+ doNamespace: mockNamespace,
1171
+ extractTenant: 'header',
1172
+ metricsCollector,
1173
+ })
1174
+
1175
+ // Make requests for different tenants
1176
+ const request1 = createMockRequest('https://app.com/api', {
1177
+ headers: { 'X-Tenant-ID': 'tenant1' },
1178
+ })
1179
+ const request2 = createMockRequest('https://app.com/api', {
1180
+ headers: { 'X-Tenant-ID': 'tenant2' },
1181
+ })
1182
+ const request3 = createMockRequest('https://app.com/api', {
1183
+ headers: { 'X-Tenant-ID': 'tenant1' },
1184
+ })
1185
+
1186
+ await router.route(request1)
1187
+ await router.route(request2)
1188
+ await router.route(request3)
1189
+
1190
+ expect(metricsCollector.incrementRequestCount).toHaveBeenCalledTimes(3)
1191
+ expect(requestCounts).toEqual(['tenant1', 'tenant2', 'tenant1'])
1192
+ })
1193
+
1194
+ it('should support custom metrics collector', async () => {
1195
+ const doResponseTimes: Array<{ tenantId: string; responseTimeMs: number }> = []
1196
+ const customCollector: MetricsCollector = {
1197
+ recordRequest: vi.fn(),
1198
+ incrementRequestCount: vi.fn(),
1199
+ recordLatency: vi.fn(),
1200
+ recordDoResponseTime: vi.fn((tenantId: string, responseTimeMs: number) => {
1201
+ doResponseTimes.push({ tenantId, responseTimeMs })
1202
+ }),
1203
+ }
1204
+
1205
+ const router = createTenantRouter({
1206
+ doNamespace: mockNamespace,
1207
+ extractTenant: 'header',
1208
+ metricsCollector: customCollector,
1209
+ })
1210
+
1211
+ const request = createMockRequest('https://app.com/api', {
1212
+ headers: { 'X-Tenant-ID': 'custom-tenant' },
1213
+ })
1214
+
1215
+ await router.route(request)
1216
+
1217
+ expect(customCollector.recordDoResponseTime).toHaveBeenCalledTimes(1)
1218
+ expect(doResponseTimes[0].tenantId).toBe('custom-tenant')
1219
+ expect(doResponseTimes[0].responseTimeMs).toBeGreaterThanOrEqual(0)
1220
+ })
1221
+
1222
+ it('should include trace context in forwarded requests', async () => {
1223
+ const traceContextExtractor: TraceContextExtractor = (request: Request): TraceContext | null => {
1224
+ const traceId = request.headers.get('X-Trace-ID')
1225
+ const spanId = request.headers.get('X-Span-ID')
1226
+ if (traceId && spanId) {
1227
+ return {
1228
+ traceId,
1229
+ spanId,
1230
+ parentSpanId: request.headers.get('X-Parent-Span-ID') || undefined,
1231
+ }
1232
+ }
1233
+ return null
1234
+ }
1235
+
1236
+ const router = createTenantRouter({
1237
+ doNamespace: mockNamespace,
1238
+ extractTenant: 'header',
1239
+ traceContextExtractor,
1240
+ propagateTraceContext: true,
1241
+ })
1242
+
1243
+ const request = createMockRequest('https://app.com/api', {
1244
+ headers: {
1245
+ 'X-Tenant-ID': 'tenant1',
1246
+ 'X-Trace-ID': 'trace-abc123',
1247
+ 'X-Span-ID': 'span-def456',
1248
+ 'X-Parent-Span-ID': 'parent-ghi789',
1249
+ },
1250
+ })
1251
+
1252
+ await router.route(request)
1253
+
1254
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
1255
+ expect(forwardedRequest.headers.get('X-Trace-ID')).toBe('trace-abc123')
1256
+ expect(forwardedRequest.headers.get('X-Span-ID')).toBe('span-def456')
1257
+ expect(forwardedRequest.headers.get('X-Parent-Span-ID')).toBe('parent-ghi789')
1258
+ })
1259
+
1260
+ it('should log tenant extraction failures', async () => {
1261
+ const logEntries: Array<{ level: string; message: string; data?: Record<string, unknown> }> = []
1262
+ const mockLogger: TenantRouterLogger = {
1263
+ log: vi.fn(),
1264
+ debug: vi.fn((message: string, data?: Record<string, unknown>) => {
1265
+ logEntries.push({ level: 'debug', message, data })
1266
+ }),
1267
+ info: vi.fn(),
1268
+ warn: vi.fn((message: string, data?: Record<string, unknown>) => {
1269
+ logEntries.push({ level: 'warn', message, data })
1270
+ }),
1271
+ error: vi.fn(),
1272
+ }
1273
+
1274
+ const router = createTenantRouter({
1275
+ doNamespace: mockNamespace,
1276
+ extractTenant: 'header',
1277
+ logger: mockLogger,
1278
+ })
1279
+
1280
+ // Request without tenant header - should fail extraction
1281
+ const request = createMockRequest('https://app.com/api')
1282
+
1283
+ await router.route(request)
1284
+
1285
+ expect(mockLogger.warn).toHaveBeenCalled()
1286
+ const failureLog = logEntries.find(e => e.level === 'warn' && e.message.includes('extraction failed'))
1287
+ expect(failureLog).toBeDefined()
1288
+ expect(failureLog?.data?.path).toBe('/api')
1289
+ })
1290
+ })
1291
+
1292
+ describe('caching', () => {
1293
+ it('should cache custom domain to tenant mappings', () => {
1294
+ const cache = createDomainMappingCache({ ttlMs: 300000 }) // 5 minute TTL
1295
+
1296
+ // Set some domain mappings
1297
+ cache.set('custom.example.org', 'tenant1')
1298
+ cache.set('another.example.org', 'tenant2')
1299
+
1300
+ // Verify they are cached
1301
+ const entry1 = cache.get('custom.example.org')
1302
+ const entry2 = cache.get('another.example.org')
1303
+
1304
+ expect(entry1).not.toBeNull()
1305
+ expect(entry1!.tenantId).toBe('tenant1')
1306
+ expect(entry2).not.toBeNull()
1307
+ expect(entry2!.tenantId).toBe('tenant2')
1308
+
1309
+ // Verify cache stats
1310
+ const stats = cache.stats()
1311
+ expect(stats.hits).toBe(2)
1312
+ expect(stats.size).toBe(2)
1313
+ })
1314
+
1315
+ it('should invalidate cache on TTL expiry', async () => {
1316
+ // Use a very short TTL for testing
1317
+ const cache = createDomainMappingCache({ ttlMs: 50 }) // 50ms TTL
1318
+
1319
+ cache.set('expires.example.org', 'tenant1')
1320
+
1321
+ // Verify entry exists immediately
1322
+ expect(cache.get('expires.example.org')).not.toBeNull()
1323
+
1324
+ // Wait for TTL to expire
1325
+ await new Promise((resolve) => setTimeout(resolve, 60))
1326
+
1327
+ // Entry should now be expired and return null
1328
+ const expiredEntry = cache.get('expires.example.org')
1329
+ expect(expiredEntry).toBeNull()
1330
+
1331
+ // Stats should show a miss for the expired entry
1332
+ const stats = cache.stats()
1333
+ expect(stats.misses).toBeGreaterThanOrEqual(1)
1334
+ })
1335
+
1336
+ it('should support cache warming for known tenants', () => {
1337
+ const cache = createDomainMappingCache({ ttlMs: 300000 })
1338
+
1339
+ // Warm cache with known domain mappings
1340
+ cache.warm({
1341
+ 'acme.com': 'acme-inc',
1342
+ 'bigcorp.io': 'bigcorp',
1343
+ 'startup.dev': 'startup-tenant',
1344
+ })
1345
+
1346
+ // Verify all mappings are available
1347
+ expect(cache.size()).toBe(3)
1348
+ expect(cache.get('acme.com')?.tenantId).toBe('acme-inc')
1349
+ expect(cache.get('bigcorp.io')?.tenantId).toBe('bigcorp')
1350
+ expect(cache.get('startup.dev')?.tenantId).toBe('startup-tenant')
1351
+
1352
+ // Verify cache has entries
1353
+ expect(cache.has('acme.com')).toBe(true)
1354
+ expect(cache.has('bigcorp.io')).toBe(true)
1355
+ })
1356
+
1357
+ it('should handle cache miss gracefully', () => {
1358
+ const cache = createDomainMappingCache({ ttlMs: 300000 })
1359
+
1360
+ // Get a non-existent domain
1361
+ const entry = cache.get('nonexistent.example.org')
1362
+ expect(entry).toBeNull()
1363
+
1364
+ // has() should return false
1365
+ expect(cache.has('nonexistent.example.org')).toBe(false)
1366
+
1367
+ // Stats should show a miss
1368
+ const stats = cache.stats()
1369
+ expect(stats.misses).toBeGreaterThanOrEqual(1)
1370
+
1371
+ // Invalidating non-existent entry should not throw
1372
+ cache.invalidate('nonexistent.example.org')
1373
+ expect(cache.stats().invalidations).toBe(0) // No actual invalidation happened
1374
+
1375
+ // invalidateAll should work even on empty cache
1376
+ cache.invalidateAll()
1377
+ expect(cache.size()).toBe(0)
1378
+ })
1379
+ })
1380
+
1381
+ describe('rate limiting', () => {
1382
+ it('should enforce per-tenant rate limits', async () => {
1383
+ const rateLimiter = new TenantRateLimiter({
1384
+ maxRequests: 3,
1385
+ windowMs: 60000,
1386
+ })
1387
+
1388
+ const request = createMockRequest('https://app.com/api')
1389
+
1390
+ // First 3 requests should be allowed
1391
+ const result1 = await rateLimiter.check('tenant1', request)
1392
+ const result2 = await rateLimiter.check('tenant1', request)
1393
+ const result3 = await rateLimiter.check('tenant1', request)
1394
+
1395
+ expect(result1.allowed).toBe(true)
1396
+ expect(result1.currentCount).toBe(1)
1397
+ expect(result2.allowed).toBe(true)
1398
+ expect(result2.currentCount).toBe(2)
1399
+ expect(result3.allowed).toBe(true)
1400
+ expect(result3.currentCount).toBe(3)
1401
+
1402
+ // 4th request should be denied
1403
+ const result4 = await rateLimiter.check('tenant1', request)
1404
+ expect(result4.allowed).toBe(false)
1405
+ expect(result4.currentCount).toBe(3)
1406
+ })
1407
+
1408
+ it('should return 429 when rate limit exceeded', async () => {
1409
+ const rateLimiter = new TenantRateLimiter({
1410
+ maxRequests: 2,
1411
+ windowMs: 60000,
1412
+ })
1413
+
1414
+ const request = createMockRequest('https://app.com/api')
1415
+
1416
+ // Exhaust the rate limit
1417
+ await rateLimiter.check('tenant1', request)
1418
+ await rateLimiter.check('tenant1', request)
1419
+
1420
+ // Third request should be rate limited
1421
+ const result = await rateLimiter.check('tenant1', request)
1422
+ expect(result.allowed).toBe(false)
1423
+ expect(result.maxRequests).toBe(2)
1424
+ expect(result.retryAfterSeconds).toBeGreaterThan(0)
1425
+
1426
+ // Simulate 429 response
1427
+ if (!result.allowed) {
1428
+ const response = new Response(
1429
+ JSON.stringify({ error: 'Rate limit exceeded' }),
1430
+ {
1431
+ status: 429,
1432
+ headers: {
1433
+ 'Content-Type': 'application/json',
1434
+ 'Retry-After': String(result.retryAfterSeconds),
1435
+ },
1436
+ }
1437
+ )
1438
+ expect(response.status).toBe(429)
1439
+ expect(response.headers.get('Retry-After')).toBeTruthy()
1440
+ }
1441
+ })
1442
+
1443
+ it('should support configurable rate limit windows', async () => {
1444
+ // Create rate limiter with a very short window
1445
+ const rateLimiter = new TenantRateLimiter({
1446
+ maxRequests: 2,
1447
+ windowMs: 100, // 100ms window
1448
+ })
1449
+
1450
+ const request = createMockRequest('https://app.com/api')
1451
+
1452
+ // Exhaust the rate limit
1453
+ await rateLimiter.check('tenant1', request)
1454
+ await rateLimiter.check('tenant1', request)
1455
+
1456
+ // Should be rate limited
1457
+ const resultBefore = await rateLimiter.check('tenant1', request)
1458
+ expect(resultBefore.allowed).toBe(false)
1459
+
1460
+ // Wait for window to expire
1461
+ await new Promise((resolve) => setTimeout(resolve, 150))
1462
+
1463
+ // Should be allowed again after window expires
1464
+ const resultAfter = await rateLimiter.check('tenant1', request)
1465
+ expect(resultAfter.allowed).toBe(true)
1466
+ expect(resultAfter.currentCount).toBe(1)
1467
+ })
1468
+
1469
+ it('should allow rate limit bypass for specific tenants', async () => {
1470
+ const rateLimiter = new TenantRateLimiter({
1471
+ maxRequests: 1,
1472
+ windowMs: 60000,
1473
+ bypassTenants: ['premium-tenant', 'admin-tenant'],
1474
+ })
1475
+
1476
+ const request = createMockRequest('https://app.com/api')
1477
+
1478
+ // Regular tenant should be limited after 1 request
1479
+ const normalResult1 = await rateLimiter.check('regular-tenant', request)
1480
+ const normalResult2 = await rateLimiter.check('regular-tenant', request)
1481
+ expect(normalResult1.allowed).toBe(true)
1482
+ expect(normalResult2.allowed).toBe(false)
1483
+
1484
+ // Bypass tenant should never be limited
1485
+ for (let i = 0; i < 10; i++) {
1486
+ const result = await rateLimiter.check('premium-tenant', request)
1487
+ expect(result.allowed).toBe(true)
1488
+ expect(result.bypassed).toBe(true)
1489
+ }
1490
+
1491
+ // Another bypass tenant
1492
+ for (let i = 0; i < 10; i++) {
1493
+ const result = await rateLimiter.check('admin-tenant', request)
1494
+ expect(result.allowed).toBe(true)
1495
+ expect(result.bypassed).toBe(true)
1496
+ }
1497
+ })
1498
+ })
1499
+
1500
+ describe('preset extractors', () => {
1501
+ describe('extractors.subdomain', () => {
1502
+ it('should extract first subdomain segment', () => {
1503
+ const extractor = extractors.subdomain('example.com')
1504
+ const request = createMockRequest('https://tenant1.example.com/api')
1505
+ expect(extractor(request)).toBe('tenant1')
1506
+ })
1507
+
1508
+ it('should handle two-part TLDs: tenant1.app.co.uk -> tenant1', () => {
1509
+ const extractor = extractors.subdomain('app.co.uk')
1510
+ const request = createMockRequest('https://tenant1.app.co.uk/api')
1511
+ expect(extractor(request)).toBe('tenant1')
1512
+ })
1513
+
1514
+ it('should return null for apex domains', () => {
1515
+ const extractor = extractors.subdomain('example.com')
1516
+ const request = createMockRequest('https://example.com/api')
1517
+ expect(extractor(request)).toBe(null)
1518
+ })
1519
+ })
1520
+
1521
+ describe('extractors.path', () => {
1522
+ it('should extract first path segment by default', () => {
1523
+ const extractor = extractors.path()
1524
+ const request = createMockRequest('https://app.com/tenant1/api/users')
1525
+ expect(extractor(request)).toBe('tenant1')
1526
+ })
1527
+
1528
+ it('should support configurable path prefix', () => {
1529
+ const extractor = extractors.path('/v1/orgs')
1530
+ const request = createMockRequest('https://app.com/v1/orgs/myorg/users')
1531
+ expect(extractor(request)).toBe('myorg')
1532
+ })
1533
+
1534
+ it('should decode URL-encoded segments', () => {
1535
+ const extractor = extractors.path()
1536
+ const request = createMockRequest('https://app.com/my%20org/users')
1537
+ expect(extractor(request)).toBe('my org')
1538
+ })
1539
+ })
1540
+
1541
+ describe('extractors.header', () => {
1542
+ it('should return extractor function for given header name', () => {
1543
+ const extractor = extractors.header('X-Custom-Tenant')
1544
+ const request = createMockRequest('https://app.com/api', {
1545
+ headers: { 'X-Custom-Tenant': 'custom-tenant' },
1546
+ })
1547
+ expect(extractor(request)).toBe('custom-tenant')
1548
+ })
1549
+
1550
+ it('should use X-Tenant-ID as default header name', () => {
1551
+ const extractor = extractors.header()
1552
+ const request = createMockRequest('https://app.com/api', {
1553
+ headers: { 'X-Tenant-ID': 'default-header' },
1554
+ })
1555
+ expect(extractor(request)).toBe('default-header')
1556
+ })
1557
+ })
1558
+
1559
+ describe('extractors.combined', () => {
1560
+ it('should try extractors in order until one succeeds', async () => {
1561
+ const combined = extractors.combined([
1562
+ extractors.header('X-Missing'),
1563
+ extractors.header('X-Tenant-ID'),
1564
+ extractors.path(),
1565
+ ])
1566
+
1567
+ const request = createMockRequest('https://app.com/path-tenant/api', {
1568
+ headers: { 'X-Tenant-ID': 'header-tenant' },
1569
+ })
1570
+
1571
+ expect(await combined(request)).toBe('header-tenant')
1572
+ })
1573
+
1574
+ it('should return null if all extractors fail', async () => {
1575
+ const combined = extractors.combined([
1576
+ extractors.header('X-Missing'),
1577
+ extractors.subdomain('example.com'),
1578
+ ])
1579
+
1580
+ const request = createMockRequest('https://example.com/')
1581
+ expect(await combined(request)).toBe(null)
1582
+ })
1583
+ })
1584
+ })
1585
+ })
1586
+
1587
+ describe('TenantExtractionResult', () => {
1588
+ let mockNamespace: DurableObjectNamespace
1589
+
1590
+ beforeEach(() => {
1591
+ mockNamespace = createMockDONamespace()
1592
+ })
1593
+
1594
+ it('should include tenantId when extraction succeeds', async () => {
1595
+ const router = createTenantRouter({
1596
+ doNamespace: mockNamespace,
1597
+ extractTenant: 'header',
1598
+ })
1599
+
1600
+ const request = createMockRequest('https://app.com/api', {
1601
+ headers: { 'X-Tenant-ID': 'tenant1' },
1602
+ })
1603
+
1604
+ const result = await router.extractTenant(request)
1605
+ expect(result.tenantId).toBe('tenant1')
1606
+ })
1607
+
1608
+ it('should include extraction source (subdomain, path, header, custom)', async () => {
1609
+ const headerRouter = createTenantRouter({
1610
+ doNamespace: mockNamespace,
1611
+ extractTenant: 'header',
1612
+ })
1613
+ const pathRouter = createTenantRouter({
1614
+ doNamespace: mockNamespace,
1615
+ extractTenant: 'path',
1616
+ })
1617
+ const subdomainRouter = createTenantRouter({
1618
+ doNamespace: mockNamespace,
1619
+ extractTenant: 'subdomain',
1620
+ baseDomain: 'app.com',
1621
+ })
1622
+
1623
+ const headerRequest = createMockRequest('https://app.com/api', {
1624
+ headers: { 'X-Tenant-ID': 'tenant1' },
1625
+ })
1626
+ const pathRequest = createMockRequest('https://app.com/tenant1/api')
1627
+ const subdomainRequest = createMockRequest('https://tenant1.app.com/api')
1628
+
1629
+ expect((await headerRouter.extractTenant(headerRequest)).source).toBe('header')
1630
+ expect((await pathRouter.extractTenant(pathRequest)).source).toBe('path')
1631
+ expect((await subdomainRouter.extractTenant(subdomainRequest)).source).toBe('subdomain')
1632
+ })
1633
+
1634
+ it('should include original hostname', async () => {
1635
+ const router = createTenantRouter({
1636
+ doNamespace: mockNamespace,
1637
+ extractTenant: 'header',
1638
+ })
1639
+
1640
+ const request = createMockRequest('https://my-app.example.com/api', {
1641
+ headers: { 'X-Tenant-ID': 'tenant1' },
1642
+ })
1643
+
1644
+ const result = await router.extractTenant(request)
1645
+ expect(result.originalHostname).toBe('my-app.example.com')
1646
+ })
1647
+
1648
+ it('should include modified path for path-based extraction', async () => {
1649
+ const router = createTenantRouter({
1650
+ doNamespace: mockNamespace,
1651
+ extractTenant: 'path',
1652
+ stripTenantFromPath: true,
1653
+ })
1654
+
1655
+ const request = createMockRequest('https://app.com/tenant1/api/users/123')
1656
+
1657
+ const result = await router.extractTenant(request)
1658
+ expect(result.modifiedPath).toBe('/api/users/123')
1659
+ })
1660
+
1661
+ it('should include metadata from custom extractors', async () => {
1662
+ const customExtractor: TenantExtractor = (request: Request) => {
1663
+ const url = new URL(request.url)
1664
+ return {
1665
+ tenantId: 'tenant1',
1666
+ source: 'custom' as const,
1667
+ originalHostname: url.hostname,
1668
+ metadata: {
1669
+ customField: 'customValue',
1670
+ tier: 'premium',
1671
+ },
1672
+ }
1673
+ }
1674
+
1675
+ const router = createTenantRouter({
1676
+ doNamespace: mockNamespace,
1677
+ extractTenant: customExtractor,
1678
+ })
1679
+
1680
+ const request = createMockRequest('https://app.com/api')
1681
+ const result = await router.extractTenant(request)
1682
+
1683
+ expect(result.metadata).toEqual({
1684
+ customField: 'customValue',
1685
+ tier: 'premium',
1686
+ })
1687
+ })
1688
+ })
1689
+
1690
+ describe('TenantRoutingResult', () => {
1691
+ let mockNamespace: DurableObjectNamespace
1692
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
1693
+
1694
+ beforeEach(() => {
1695
+ mockStub = {
1696
+ fetch: vi.fn().mockResolvedValue(new Response('DO Response', { status: 200 })),
1697
+ }
1698
+ mockNamespace = {
1699
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
1700
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
1701
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
1702
+ get: vi.fn(() => mockStub),
1703
+ jurisdiction: vi.fn(),
1704
+ } as unknown as DurableObjectNamespace
1705
+ })
1706
+
1707
+ it('should include response from DO', async () => {
1708
+ mockStub.fetch.mockResolvedValue(
1709
+ new Response(JSON.stringify({ data: 'test-data' }), {
1710
+ status: 200,
1711
+ headers: { 'Content-Type': 'application/json' },
1712
+ })
1713
+ )
1714
+
1715
+ const router = createTenantRouter({
1716
+ doNamespace: mockNamespace,
1717
+ extractTenant: 'header',
1718
+ })
1719
+
1720
+ const request = createMockRequest('https://app.com/api', {
1721
+ headers: { 'X-Tenant-ID': 'tenant1' },
1722
+ })
1723
+
1724
+ const result = await router.routeWithResult(request)
1725
+
1726
+ expect(result.response).toBeInstanceOf(Response)
1727
+ expect(result.response.status).toBe(200)
1728
+ const body = await result.response.json()
1729
+ expect(body).toEqual({ data: 'test-data' })
1730
+ })
1731
+
1732
+ it('should include routing metadata', async () => {
1733
+ const router = createTenantRouter({
1734
+ doNamespace: mockNamespace,
1735
+ extractTenant: 'header',
1736
+ })
1737
+
1738
+ const request = createMockRequest('https://app.com/api', {
1739
+ headers: { 'X-Tenant-ID': 'tenant1' },
1740
+ })
1741
+
1742
+ const result = await router.routeWithResult(request)
1743
+
1744
+ expect(result.routingMetadata).toBeDefined()
1745
+ expect(result.routingMetadata.extractorType).toBe('header')
1746
+ expect(result.routingMetadata.doId).toBe('id-tenant1')
1747
+ })
1748
+
1749
+ it('should include timing information', async () => {
1750
+ const router = createTenantRouter({
1751
+ doNamespace: mockNamespace,
1752
+ extractTenant: 'header',
1753
+ })
1754
+
1755
+ const request = createMockRequest('https://app.com/api', {
1756
+ headers: { 'X-Tenant-ID': 'tenant1' },
1757
+ })
1758
+
1759
+ const beforeTime = Date.now()
1760
+ const result = await router.routeWithResult(request)
1761
+ const afterTime = Date.now()
1762
+
1763
+ expect(result.timing).toBeDefined()
1764
+ expect(result.timing.extractionTimeMs).toBeGreaterThanOrEqual(0)
1765
+ expect(result.timing.routingTimeMs).toBeGreaterThanOrEqual(0)
1766
+ expect(result.timing.totalTimeMs).toBeGreaterThanOrEqual(0)
1767
+ expect(result.timing.startTimestamp).toBeGreaterThanOrEqual(beforeTime)
1768
+ expect(result.timing.startTimestamp).toBeLessThanOrEqual(afterTime)
1769
+ })
1770
+
1771
+ it('should include tenant context', async () => {
1772
+ const customExtractor: TenantExtractor = (request: Request) => {
1773
+ const url = new URL(request.url)
1774
+ return {
1775
+ tenantId: url.searchParams.get('tenant'),
1776
+ source: 'custom' as const,
1777
+ originalHostname: url.hostname,
1778
+ metadata: {
1779
+ plan: 'enterprise',
1780
+ region: 'us-west-2',
1781
+ },
1782
+ }
1783
+ }
1784
+
1785
+ const router = createTenantRouter({
1786
+ doNamespace: mockNamespace,
1787
+ extractTenant: customExtractor,
1788
+ })
1789
+
1790
+ const request = createMockRequest('https://app.com/api?tenant=tenant1')
1791
+
1792
+ const result = await router.routeWithResult(request)
1793
+
1794
+ expect(result.tenantContext).toBeDefined()
1795
+ expect(result.tenantContext.tenantId).toBe('tenant1')
1796
+ expect(result.tenantContext.source).toBe('custom')
1797
+ expect(result.tenantContext.metadata).toEqual({
1798
+ plan: 'enterprise',
1799
+ region: 'us-west-2',
1800
+ })
1801
+ })
1802
+ })
1803
+
1804
+ describe('createTenantRouter', () => {
1805
+ let mockNamespace: DurableObjectNamespace
1806
+
1807
+ beforeEach(() => {
1808
+ mockNamespace = createMockDONamespace()
1809
+ })
1810
+
1811
+ it('should create router with subdomain extraction when extractTenant is "subdomain"', async () => {
1812
+ const router = createTenantRouter({
1813
+ doNamespace: mockNamespace,
1814
+ extractTenant: 'subdomain',
1815
+ baseDomain: 'myapp.com',
1816
+ })
1817
+
1818
+ const request = createMockRequest('https://acme.myapp.com/api')
1819
+ const tenantId = await router.getTenantId(request)
1820
+ expect(tenantId).toBe('acme')
1821
+ })
1822
+
1823
+ it('should create router with path extraction when extractTenant is "path"', async () => {
1824
+ const router = createTenantRouter({
1825
+ doNamespace: mockNamespace,
1826
+ extractTenant: 'path',
1827
+ })
1828
+
1829
+ const request = createMockRequest('https://app.com/acme/api')
1830
+ const tenantId = await router.getTenantId(request)
1831
+ expect(tenantId).toBe('acme')
1832
+ })
1833
+
1834
+ it('should create router with header extraction when extractTenant is "header"', async () => {
1835
+ const router = createTenantRouter({
1836
+ doNamespace: mockNamespace,
1837
+ extractTenant: 'header',
1838
+ })
1839
+
1840
+ const request = createMockRequest('https://app.com/api', {
1841
+ headers: { 'X-Tenant-ID': 'acme' },
1842
+ })
1843
+ const tenantId = await router.getTenantId(request)
1844
+ expect(tenantId).toBe('acme')
1845
+ })
1846
+
1847
+ it('should create router with custom function when extractTenant is a function', async () => {
1848
+ const customFn: SimpleTenantExtractor = (req) => {
1849
+ const url = new URL(req.url)
1850
+ return url.searchParams.get('t')
1851
+ }
1852
+
1853
+ const router = createTenantRouter({
1854
+ doNamespace: mockNamespace,
1855
+ extractTenant: customFn,
1856
+ })
1857
+
1858
+ const request = createMockRequest('https://app.com/api?t=custom-tenant')
1859
+ const tenantId = await router.getTenantId(request)
1860
+ expect(tenantId).toBe('custom-tenant')
1861
+ })
1862
+
1863
+ it('should throw error for invalid extractTenant value', () => {
1864
+ expect(() => {
1865
+ createTenantRouter({
1866
+ doNamespace: mockNamespace,
1867
+ // @ts-expect-error - intentionally passing invalid value
1868
+ extractTenant: 'invalid',
1869
+ })
1870
+ }).toThrow('Invalid extractTenant value: invalid')
1871
+ })
1872
+
1873
+ it('should apply default configuration values', async () => {
1874
+ // Default header name should be X-Tenant-ID
1875
+ const router = createTenantRouter({
1876
+ doNamespace: mockNamespace,
1877
+ extractTenant: 'header',
1878
+ })
1879
+
1880
+ const request = createMockRequest('https://app.com/api', {
1881
+ headers: { 'X-Tenant-ID': 'default-tenant' },
1882
+ })
1883
+
1884
+ const tenantId = await router.getTenantId(request)
1885
+ expect(tenantId).toBe('default-tenant')
1886
+ })
1887
+ })
1888
+
1889
+ describe('nested sub-config support', () => {
1890
+ let mockNamespace: DurableObjectNamespace
1891
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
1892
+
1893
+ beforeEach(() => {
1894
+ mockStub = {
1895
+ fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
1896
+ }
1897
+ mockNamespace = {
1898
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
1899
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
1900
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
1901
+ get: vi.fn(() => mockStub),
1902
+ jurisdiction: vi.fn(),
1903
+ } as unknown as DurableObjectNamespace
1904
+ })
1905
+
1906
+ describe('observability config', () => {
1907
+ it('should use nested observability.metricsCollector', async () => {
1908
+ const recordedMetrics: RequestMetrics[] = []
1909
+ const metricsCollector: MetricsCollector = {
1910
+ recordRequest: vi.fn((metrics: RequestMetrics) => {
1911
+ recordedMetrics.push(metrics)
1912
+ }),
1913
+ incrementRequestCount: vi.fn(),
1914
+ recordLatency: vi.fn(),
1915
+ recordDoResponseTime: vi.fn(),
1916
+ }
1917
+
1918
+ const router = createTenantRouter({
1919
+ doNamespace: mockNamespace,
1920
+ extractTenant: 'header',
1921
+ observability: {
1922
+ metricsCollector,
1923
+ },
1924
+ })
1925
+
1926
+ const request = createMockRequest('https://app.com/api/users', {
1927
+ headers: { 'X-Tenant-ID': 'tenant1' },
1928
+ })
1929
+
1930
+ await router.route(request)
1931
+
1932
+ expect(metricsCollector.recordRequest).toHaveBeenCalledTimes(1)
1933
+ expect(recordedMetrics[0].tenantId).toBe('tenant1')
1934
+ })
1935
+
1936
+ it('should use nested observability.logger', async () => {
1937
+ const logEntries: Array<{ level: string; message: string; data?: Record<string, unknown> }> = []
1938
+ const mockLogger: TenantRouterLogger = {
1939
+ log: vi.fn(),
1940
+ debug: vi.fn((message: string, data?: Record<string, unknown>) => {
1941
+ logEntries.push({ level: 'debug', message, data })
1942
+ }),
1943
+ info: vi.fn(),
1944
+ warn: vi.fn((message: string, data?: Record<string, unknown>) => {
1945
+ logEntries.push({ level: 'warn', message, data })
1946
+ }),
1947
+ error: vi.fn(),
1948
+ }
1949
+
1950
+ const router = createTenantRouter({
1951
+ doNamespace: mockNamespace,
1952
+ extractTenant: 'header',
1953
+ observability: {
1954
+ logger: mockLogger,
1955
+ },
1956
+ })
1957
+
1958
+ const request = createMockRequest('https://app.com/api')
1959
+ await router.route(request)
1960
+
1961
+ expect(mockLogger.warn).toHaveBeenCalled()
1962
+ })
1963
+
1964
+ it('should use nested observability.traceContextExtractor and propagateTraceContext', async () => {
1965
+ const traceContextExtractor: TraceContextExtractor = (request: Request): TraceContext | null => {
1966
+ const traceId = request.headers.get('X-Trace-ID')
1967
+ const spanId = request.headers.get('X-Span-ID')
1968
+ if (traceId && spanId) {
1969
+ return { traceId, spanId }
1970
+ }
1971
+ return null
1972
+ }
1973
+
1974
+ const router = createTenantRouter({
1975
+ doNamespace: mockNamespace,
1976
+ extractTenant: 'header',
1977
+ observability: {
1978
+ traceContextExtractor,
1979
+ propagateTraceContext: true,
1980
+ },
1981
+ })
1982
+
1983
+ const request = createMockRequest('https://app.com/api', {
1984
+ headers: {
1985
+ 'X-Tenant-ID': 'tenant1',
1986
+ 'X-Trace-ID': 'nested-trace-123',
1987
+ 'X-Span-ID': 'nested-span-456',
1988
+ },
1989
+ })
1990
+
1991
+ await router.route(request)
1992
+
1993
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
1994
+ expect(forwardedRequest.headers.get('X-Trace-ID')).toBe('nested-trace-123')
1995
+ expect(forwardedRequest.headers.get('X-Span-ID')).toBe('nested-span-456')
1996
+ })
1997
+
1998
+ it('should disable trace propagation when observability.propagateTraceContext is false', async () => {
1999
+ const traceContextExtractor: TraceContextExtractor = (request: Request): TraceContext | null => {
2000
+ const traceId = request.headers.get('X-Trace-ID')
2001
+ const spanId = request.headers.get('X-Span-ID')
2002
+ if (traceId && spanId) {
2003
+ return { traceId, spanId }
2004
+ }
2005
+ return null
2006
+ }
2007
+
2008
+ const router = createTenantRouter({
2009
+ doNamespace: mockNamespace,
2010
+ extractTenant: 'header',
2011
+ observability: {
2012
+ traceContextExtractor,
2013
+ propagateTraceContext: false,
2014
+ },
2015
+ })
2016
+
2017
+ const request = createMockRequest('https://app.com/api', {
2018
+ headers: {
2019
+ 'X-Tenant-ID': 'tenant1',
2020
+ 'X-Trace-ID': 'trace-abc',
2021
+ 'X-Span-ID': 'span-def',
2022
+ },
2023
+ })
2024
+
2025
+ await router.route(request)
2026
+
2027
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
2028
+ // Trace headers should NOT be propagated
2029
+ expect(forwardedRequest.headers.get('X-Trace-ID')).toBeNull()
2030
+ expect(forwardedRequest.headers.get('X-Span-ID')).toBeNull()
2031
+ })
2032
+ })
2033
+
2034
+ describe('backward compatibility with legacy flat config', () => {
2035
+ it('should still support legacy flat metricsCollector option', async () => {
2036
+ const recordedMetrics: RequestMetrics[] = []
2037
+ const metricsCollector: MetricsCollector = {
2038
+ recordRequest: vi.fn((metrics: RequestMetrics) => {
2039
+ recordedMetrics.push(metrics)
2040
+ }),
2041
+ incrementRequestCount: vi.fn(),
2042
+ recordLatency: vi.fn(),
2043
+ recordDoResponseTime: vi.fn(),
2044
+ }
2045
+
2046
+ const router = createTenantRouter({
2047
+ doNamespace: mockNamespace,
2048
+ extractTenant: 'header',
2049
+ metricsCollector, // legacy flat option
2050
+ })
2051
+
2052
+ const request = createMockRequest('https://app.com/api', {
2053
+ headers: { 'X-Tenant-ID': 'tenant1' },
2054
+ })
2055
+
2056
+ await router.route(request)
2057
+
2058
+ expect(metricsCollector.recordRequest).toHaveBeenCalledTimes(1)
2059
+ })
2060
+
2061
+ it('should still support legacy flat logger option', async () => {
2062
+ const mockLogger: TenantRouterLogger = {
2063
+ log: vi.fn(),
2064
+ debug: vi.fn(),
2065
+ info: vi.fn(),
2066
+ warn: vi.fn(),
2067
+ error: vi.fn(),
2068
+ }
2069
+
2070
+ const router = createTenantRouter({
2071
+ doNamespace: mockNamespace,
2072
+ extractTenant: 'header',
2073
+ logger: mockLogger, // legacy flat option
2074
+ })
2075
+
2076
+ const request = createMockRequest('https://app.com/api')
2077
+ await router.route(request)
2078
+
2079
+ expect(mockLogger.warn).toHaveBeenCalled()
2080
+ })
2081
+
2082
+ it('should still support legacy flat traceContextExtractor option', async () => {
2083
+ const traceContextExtractor: TraceContextExtractor = (request: Request): TraceContext | null => {
2084
+ const traceId = request.headers.get('X-Trace-ID')
2085
+ const spanId = request.headers.get('X-Span-ID')
2086
+ if (traceId && spanId) {
2087
+ return { traceId, spanId }
2088
+ }
2089
+ return null
2090
+ }
2091
+
2092
+ const router = createTenantRouter({
2093
+ doNamespace: mockNamespace,
2094
+ extractTenant: 'header',
2095
+ traceContextExtractor, // legacy flat option
2096
+ propagateTraceContext: true, // legacy flat option
2097
+ })
2098
+
2099
+ const request = createMockRequest('https://app.com/api', {
2100
+ headers: {
2101
+ 'X-Tenant-ID': 'tenant1',
2102
+ 'X-Trace-ID': 'legacy-trace-123',
2103
+ 'X-Span-ID': 'legacy-span-456',
2104
+ },
2105
+ })
2106
+
2107
+ await router.route(request)
2108
+
2109
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
2110
+ expect(forwardedRequest.headers.get('X-Trace-ID')).toBe('legacy-trace-123')
2111
+ })
2112
+
2113
+ it('should prefer nested config over legacy flat config', async () => {
2114
+ const nestedMetrics: RequestMetrics[] = []
2115
+ const legacyMetrics: RequestMetrics[] = []
2116
+
2117
+ const nestedCollector: MetricsCollector = {
2118
+ recordRequest: vi.fn((metrics: RequestMetrics) => {
2119
+ nestedMetrics.push(metrics)
2120
+ }),
2121
+ incrementRequestCount: vi.fn(),
2122
+ recordLatency: vi.fn(),
2123
+ recordDoResponseTime: vi.fn(),
2124
+ }
2125
+
2126
+ const legacyCollector: MetricsCollector = {
2127
+ recordRequest: vi.fn((metrics: RequestMetrics) => {
2128
+ legacyMetrics.push(metrics)
2129
+ }),
2130
+ incrementRequestCount: vi.fn(),
2131
+ recordLatency: vi.fn(),
2132
+ recordDoResponseTime: vi.fn(),
2133
+ }
2134
+
2135
+ const router = createTenantRouter({
2136
+ doNamespace: mockNamespace,
2137
+ extractTenant: 'header',
2138
+ observability: {
2139
+ metricsCollector: nestedCollector, // this should take precedence
2140
+ },
2141
+ metricsCollector: legacyCollector, // this should be ignored
2142
+ })
2143
+
2144
+ const request = createMockRequest('https://app.com/api', {
2145
+ headers: { 'X-Tenant-ID': 'tenant1' },
2146
+ })
2147
+
2148
+ await router.route(request)
2149
+
2150
+ // Nested config should be used
2151
+ expect(nestedCollector.recordRequest).toHaveBeenCalledTimes(1)
2152
+ expect(nestedMetrics.length).toBe(1)
2153
+
2154
+ // Legacy config should NOT be used
2155
+ expect(legacyCollector.recordRequest).not.toHaveBeenCalled()
2156
+ expect(legacyMetrics.length).toBe(0)
2157
+ })
2158
+ })
2159
+
2160
+ describe('blockedTenantCallback config', () => {
2161
+ it('should use blockedTenantCallback for dynamic blocking', async () => {
2162
+ const blockedCallback = vi.fn().mockResolvedValue(true)
2163
+
2164
+ const router = createTenantRouter({
2165
+ doNamespace: mockNamespace,
2166
+ extractTenant: 'header',
2167
+ blockedTenantCallback: blockedCallback,
2168
+ })
2169
+
2170
+ const request = createMockRequest('https://app.com/api', {
2171
+ headers: { 'X-Tenant-ID': 'dynamically-blocked' },
2172
+ })
2173
+
2174
+ const response = await router.route(request)
2175
+ expect(response.status).toBe(404)
2176
+ expect(blockedCallback).toHaveBeenCalledWith('dynamically-blocked', expect.any(Request))
2177
+ })
2178
+
2179
+ it('should prefer blockedTenantCallback over legacy isBlocked', async () => {
2180
+ const newCallback = vi.fn().mockResolvedValue(false)
2181
+ const legacyCallback = vi.fn().mockResolvedValue(true)
2182
+
2183
+ const router = createTenantRouter({
2184
+ doNamespace: mockNamespace,
2185
+ extractTenant: 'header',
2186
+ blockedTenantCallback: newCallback, // should be used
2187
+ isBlocked: legacyCallback, // should be ignored
2188
+ })
2189
+
2190
+ const request = createMockRequest('https://app.com/api', {
2191
+ headers: { 'X-Tenant-ID': 'test-tenant' },
2192
+ })
2193
+
2194
+ await router.route(request)
2195
+
2196
+ // New callback should be used
2197
+ expect(newCallback).toHaveBeenCalled()
2198
+ // Legacy callback should NOT be used
2199
+ expect(legacyCallback).not.toHaveBeenCalled()
2200
+ })
2201
+ })
2202
+
2203
+ describe('errorResponseFormatter config', () => {
2204
+ it('should use errorResponseFormatter for custom error responses', async () => {
2205
+ const customFormatter = vi.fn().mockReturnValue(
2206
+ new Response('Custom Error', { status: 418 })
2207
+ )
2208
+
2209
+ const router = createTenantRouter({
2210
+ doNamespace: mockNamespace,
2211
+ extractTenant: 'header',
2212
+ errorResponseFormatter: customFormatter,
2213
+ })
2214
+
2215
+ const request = createMockRequest('https://app.com/api')
2216
+ const response = await router.route(request)
2217
+
2218
+ expect(response.status).toBe(418)
2219
+ expect(customFormatter).toHaveBeenCalled()
2220
+ })
2221
+
2222
+ it('should prefer errorResponseFormatter over legacy formatError', async () => {
2223
+ const newFormatter = vi.fn().mockReturnValue(
2224
+ new Response('New Formatter', { status: 418 })
2225
+ )
2226
+ const legacyFormatter = vi.fn().mockReturnValue(
2227
+ new Response('Legacy Formatter', { status: 500 })
2228
+ )
2229
+
2230
+ const router = createTenantRouter({
2231
+ doNamespace: mockNamespace,
2232
+ extractTenant: 'header',
2233
+ errorResponseFormatter: newFormatter, // should be used
2234
+ formatError: legacyFormatter, // should be ignored
2235
+ })
2236
+
2237
+ const request = createMockRequest('https://app.com/api')
2238
+ const response = await router.route(request)
2239
+
2240
+ expect(response.status).toBe(418)
2241
+ expect(newFormatter).toHaveBeenCalled()
2242
+ expect(legacyFormatter).not.toHaveBeenCalled()
2243
+ })
2244
+ })
2245
+ })
2246
+
2247
+ describe('integration scenarios', () => {
2248
+ let mockNamespace: DurableObjectNamespace
2249
+ let mockStub: { fetch: ReturnType<typeof vi.fn> }
2250
+
2251
+ beforeEach(() => {
2252
+ mockStub = {
2253
+ fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
2254
+ }
2255
+ mockNamespace = {
2256
+ idFromName: vi.fn((name: string) => ({ name, toString: () => `id-${name}` })),
2257
+ idFromString: vi.fn((id: string) => ({ id, toString: () => id })),
2258
+ newUniqueId: vi.fn(() => ({ toString: () => 'unique-id' })),
2259
+ get: vi.fn(() => mockStub),
2260
+ jurisdiction: vi.fn(),
2261
+ } as unknown as DurableObjectNamespace
2262
+ })
2263
+
2264
+ describe('SaaS multi-tenant application', () => {
2265
+ it('should route customer1.myapp.com to customer1 DO', async () => {
2266
+ const router = createTenantRouter({
2267
+ doNamespace: mockNamespace,
2268
+ extractTenant: 'subdomain',
2269
+ baseDomain: 'myapp.com',
2270
+ })
2271
+
2272
+ const request = createMockRequest('https://customer1.myapp.com/dashboard')
2273
+ await router.route(request)
2274
+
2275
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('customer1')
2276
+ })
2277
+
2278
+ it('should route customer2.myapp.com to customer2 DO', async () => {
2279
+ const router = createTenantRouter({
2280
+ doNamespace: mockNamespace,
2281
+ extractTenant: 'subdomain',
2282
+ baseDomain: 'myapp.com',
2283
+ })
2284
+
2285
+ const request = createMockRequest('https://customer2.myapp.com/dashboard')
2286
+ await router.route(request)
2287
+
2288
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('customer2')
2289
+ })
2290
+
2291
+ it('should route custom domain mappings correctly', async () => {
2292
+ const domainToTenant: Record<string, string> = {
2293
+ 'acme.com': 'acme-inc',
2294
+ 'bigcorp.io': 'bigcorp',
2295
+ }
2296
+
2297
+ const customExtractor: SimpleTenantExtractor = (request: Request) => {
2298
+ const url = new URL(request.url)
2299
+ return domainToTenant[url.hostname] || null
2300
+ }
2301
+
2302
+ const router = createTenantRouter({
2303
+ doNamespace: mockNamespace,
2304
+ extractTenant: customExtractor,
2305
+ })
2306
+
2307
+ const request1 = createMockRequest('https://acme.com/dashboard')
2308
+ const request2 = createMockRequest('https://bigcorp.io/dashboard')
2309
+
2310
+ await router.route(request1)
2311
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('acme-inc')
2312
+
2313
+ await router.route(request2)
2314
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('bigcorp')
2315
+ })
2316
+ })
2317
+
2318
+ describe('API gateway with path-based routing', () => {
2319
+ it('should route /v1/tenants/acme/users to acme DO', async () => {
2320
+ const router = createTenantRouter({
2321
+ doNamespace: mockNamespace,
2322
+ extractTenant: 'path',
2323
+ pathPrefix: '/v1/tenants',
2324
+ })
2325
+
2326
+ const request = createMockRequest('https://api.example.com/v1/tenants/acme/users')
2327
+ await router.route(request)
2328
+
2329
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('acme')
2330
+ })
2331
+
2332
+ it('should strip tenant prefix from forwarded path', async () => {
2333
+ const router = createTenantRouter({
2334
+ doNamespace: mockNamespace,
2335
+ extractTenant: 'path',
2336
+ pathPrefix: '/v1/tenants',
2337
+ stripTenantFromPath: true,
2338
+ })
2339
+
2340
+ const request = createMockRequest('https://api.example.com/v1/tenants/acme/users/123')
2341
+ await router.route(request)
2342
+
2343
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
2344
+ const url = new URL(forwardedRequest.url)
2345
+ expect(url.pathname).toBe('/users/123')
2346
+ })
2347
+
2348
+ it('should preserve API versioning in path', async () => {
2349
+ // Using header-based extraction so path is not modified
2350
+ const router = createTenantRouter({
2351
+ doNamespace: mockNamespace,
2352
+ extractTenant: 'header',
2353
+ })
2354
+
2355
+ const request = createMockRequest('https://api.example.com/v2/users/123', {
2356
+ headers: { 'X-Tenant-ID': 'acme' },
2357
+ })
2358
+
2359
+ await router.route(request)
2360
+
2361
+ const forwardedRequest = mockStub.fetch.mock.calls[0][0] as Request
2362
+ const url = new URL(forwardedRequest.url)
2363
+ expect(url.pathname).toBe('/v2/users/123')
2364
+ })
2365
+ })
2366
+
2367
+ describe('internal service with header-based routing', () => {
2368
+ it('should route based on X-Tenant-ID from upstream proxy', async () => {
2369
+ const router = createTenantRouter({
2370
+ doNamespace: mockNamespace,
2371
+ extractTenant: 'header',
2372
+ headerName: 'X-Tenant-ID',
2373
+ })
2374
+
2375
+ const request = createMockRequest('https://internal-service/api/data', {
2376
+ headers: {
2377
+ 'X-Tenant-ID': 'internal-tenant',
2378
+ 'X-Forwarded-For': '10.0.0.1',
2379
+ },
2380
+ })
2381
+
2382
+ await router.route(request)
2383
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith('internal-tenant')
2384
+ })
2385
+
2386
+ it('should reject requests without tenant header', async () => {
2387
+ const router = createTenantRouter({
2388
+ doNamespace: mockNamespace,
2389
+ extractTenant: 'header',
2390
+ })
2391
+
2392
+ const request = createMockRequest('https://internal-service/api/data')
2393
+ const response = await router.route(request)
2394
+
2395
+ expect(response.status).toBe(400)
2396
+ })
2397
+ })
2398
+
2399
+ describe('tenant ID boundary cases', () => {
2400
+ it('should handle very long tenant ID at 128 character boundary', async () => {
2401
+ const router = createTenantRouter({
2402
+ doNamespace: mockNamespace,
2403
+ extractTenant: 'header',
2404
+ })
2405
+
2406
+ // Exactly 128 characters - typical maximum for identifiers
2407
+ const tenantId128 = 'a'.repeat(128)
2408
+ const request = createMockRequest('https://app.com/api', {
2409
+ headers: { 'X-Tenant-ID': tenantId128 },
2410
+ })
2411
+
2412
+ await router.route(request)
2413
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith(tenantId128)
2414
+ })
2415
+
2416
+ it('should handle tenant ID just under 128 character boundary', async () => {
2417
+ const router = createTenantRouter({
2418
+ doNamespace: mockNamespace,
2419
+ extractTenant: 'header',
2420
+ })
2421
+
2422
+ const tenantId127 = 'b'.repeat(127)
2423
+ const request = createMockRequest('https://app.com/api', {
2424
+ headers: { 'X-Tenant-ID': tenantId127 },
2425
+ })
2426
+
2427
+ await router.route(request)
2428
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith(tenantId127)
2429
+ })
2430
+
2431
+ it('should reject tenant ID exceeding 128 character boundary', async () => {
2432
+ const router = createTenantRouter({
2433
+ doNamespace: mockNamespace,
2434
+ extractTenant: 'header',
2435
+ maxTenantIdLength: 128, // Explicit limit
2436
+ })
2437
+
2438
+ const tenantId129 = 'c'.repeat(129)
2439
+ const request = createMockRequest('https://app.com/api', {
2440
+ headers: { 'X-Tenant-ID': tenantId129 },
2441
+ })
2442
+
2443
+ const response = await router.route(request)
2444
+ expect(response.status).toBe(400)
2445
+ })
2446
+
2447
+ it('should handle empty tenant ID as missing tenant', async () => {
2448
+ const router = createTenantRouter({
2449
+ doNamespace: mockNamespace,
2450
+ extractTenant: 'header',
2451
+ })
2452
+
2453
+ const request = createMockRequest('https://app.com/api', {
2454
+ headers: { 'X-Tenant-ID': '' },
2455
+ })
2456
+
2457
+ const response = await router.route(request)
2458
+ expect(response.status).toBe(400) // Should fail validation
2459
+ expect(mockNamespace.get).not.toHaveBeenCalled()
2460
+ })
2461
+
2462
+ it('should handle whitespace-only tenant ID as empty', async () => {
2463
+ const router = createTenantRouter({
2464
+ doNamespace: mockNamespace,
2465
+ extractTenant: 'header',
2466
+ })
2467
+
2468
+ const request = createMockRequest('https://app.com/api', {
2469
+ headers: { 'X-Tenant-ID': ' ' },
2470
+ })
2471
+
2472
+ const response = await router.route(request)
2473
+ // After trimming, empty string should be treated as missing tenant
2474
+ expect(response.status).toBe(400)
2475
+ })
2476
+
2477
+ it('should handle tenant ID with special characters within 128 char limit', async () => {
2478
+ const router = createTenantRouter({
2479
+ doNamespace: mockNamespace,
2480
+ extractTenant: 'header',
2481
+ })
2482
+
2483
+ // Valid tenant ID with allowed special characters (hyphens, underscores)
2484
+ const validTenantId = 'tenant-name_with-special_chars-123'
2485
+ const request = createMockRequest('https://app.com/api', {
2486
+ headers: { 'X-Tenant-ID': validTenantId },
2487
+ })
2488
+
2489
+ await router.route(request)
2490
+ expect(mockNamespace.idFromName).toHaveBeenCalledWith(validTenantId)
2491
+ })
2492
+ })
2493
+ })