@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.
- package/README.md +868 -0
- package/dist/cdc/change-stream.d.ts +44 -0
- package/dist/cdc/change-stream.d.ts.map +1 -0
- package/dist/cdc/change-stream.js +95 -0
- package/dist/cdc/change-stream.js.map +1 -0
- package/dist/cdc/filter.d.ts +58 -0
- package/dist/cdc/filter.d.ts.map +1 -0
- package/dist/cdc/filter.js +520 -0
- package/dist/cdc/filter.js.map +1 -0
- package/dist/cdc/index.d.ts +47 -0
- package/dist/cdc/index.d.ts.map +1 -0
- package/dist/cdc/index.js +50 -0
- package/dist/cdc/index.js.map +1 -0
- package/dist/cdc/resume-token.d.ts +60 -0
- package/dist/cdc/resume-token.d.ts.map +1 -0
- package/dist/cdc/resume-token.js +228 -0
- package/dist/cdc/resume-token.js.map +1 -0
- package/dist/cdc/transport/index.d.ts +7 -0
- package/dist/cdc/transport/index.d.ts.map +1 -0
- package/dist/cdc/transport/index.js +7 -0
- package/dist/cdc/transport/index.js.map +1 -0
- package/dist/cdc/transport/sse.d.ts +120 -0
- package/dist/cdc/transport/sse.d.ts.map +1 -0
- package/dist/cdc/transport/sse.js +590 -0
- package/dist/cdc/transport/sse.js.map +1 -0
- package/dist/cdc/transport/websocket.d.ts +130 -0
- package/dist/cdc/transport/websocket.d.ts.map +1 -0
- package/dist/cdc/transport/websocket.js +688 -0
- package/dist/cdc/transport/websocket.js.map +1 -0
- package/dist/cdc/types.d.ts +306 -0
- package/dist/cdc/types.d.ts.map +1 -0
- package/dist/cdc/types.js +8 -0
- package/dist/cdc/types.js.map +1 -0
- package/dist/config/index.d.ts +25 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +25 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/memory.d.ts +139 -0
- package/dist/config/memory.d.ts.map +1 -0
- package/dist/config/memory.js +157 -0
- package/dist/config/memory.js.map +1 -0
- package/dist/config/storage.d.ts +157 -0
- package/dist/config/storage.d.ts.map +1 -0
- package/dist/config/storage.js +178 -0
- package/dist/config/storage.js.map +1 -0
- package/dist/config/streaming.d.ts +117 -0
- package/dist/config/streaming.d.ts.map +1 -0
- package/dist/config/streaming.js +132 -0
- package/dist/config/streaming.js.map +1 -0
- package/dist/config/timeouts.d.ts +168 -0
- package/dist/config/timeouts.d.ts.map +1 -0
- package/dist/config/timeouts.js +192 -0
- package/dist/config/timeouts.js.map +1 -0
- package/dist/extensions/config.d.ts +89 -0
- package/dist/extensions/config.d.ts.map +1 -0
- package/dist/extensions/config.js +216 -0
- package/dist/extensions/config.js.map +1 -0
- package/dist/extensions/geo.d.ts +452 -0
- package/dist/extensions/geo.d.ts.map +1 -0
- package/dist/extensions/geo.js +583 -0
- package/dist/extensions/geo.js.map +1 -0
- package/dist/extensions/index.d.ts +167 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +99 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/loader.d.ts +226 -0
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/extensions/loader.js +456 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/pgmq-lite.d.ts +330 -0
- package/dist/extensions/pgmq-lite.d.ts.map +1 -0
- package/dist/extensions/pgmq-lite.js +648 -0
- package/dist/extensions/pgmq-lite.js.map +1 -0
- package/dist/extensions/plugins.d.ts +260 -0
- package/dist/extensions/plugins.d.ts.map +1 -0
- package/dist/extensions/plugins.js +535 -0
- package/dist/extensions/plugins.js.map +1 -0
- package/dist/extensions/registry.d.ts +93 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/extensions/registry.js +182 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/extensions/vector.d.ts +106 -0
- package/dist/extensions/vector.d.ts.map +1 -0
- package/dist/extensions/vector.js +129 -0
- package/dist/extensions/vector.js.map +1 -0
- package/dist/iceberg/analytics.d.ts +279 -0
- package/dist/iceberg/analytics.d.ts.map +1 -0
- package/dist/iceberg/analytics.js +448 -0
- package/dist/iceberg/analytics.js.map +1 -0
- package/dist/iceberg/catalog-api.d.ts +39 -0
- package/dist/iceberg/catalog-api.d.ts.map +1 -0
- package/dist/iceberg/catalog-api.js +388 -0
- package/dist/iceberg/catalog-api.js.map +1 -0
- package/dist/iceberg/catalog.d.ts +401 -0
- package/dist/iceberg/catalog.d.ts.map +1 -0
- package/dist/iceberg/catalog.js +677 -0
- package/dist/iceberg/catalog.js.map +1 -0
- package/dist/iceberg/duckdb-wasm.d.ts +447 -0
- package/dist/iceberg/duckdb-wasm.d.ts.map +1 -0
- package/dist/iceberg/duckdb-wasm.js +600 -0
- package/dist/iceberg/duckdb-wasm.js.map +1 -0
- package/dist/iceberg/index.d.ts +92 -0
- package/dist/iceberg/index.d.ts.map +1 -0
- package/dist/iceberg/index.js +119 -0
- package/dist/iceberg/index.js.map +1 -0
- package/dist/iceberg/metadata.d.ts +214 -0
- package/dist/iceberg/metadata.d.ts.map +1 -0
- package/dist/iceberg/metadata.js +535 -0
- package/dist/iceberg/metadata.js.map +1 -0
- package/dist/iceberg/optimizer.d.ts +296 -0
- package/dist/iceberg/optimizer.d.ts.map +1 -0
- package/dist/iceberg/optimizer.js +889 -0
- package/dist/iceberg/optimizer.js.map +1 -0
- package/dist/iceberg/parquet.d.ts +447 -0
- package/dist/iceberg/parquet.d.ts.map +1 -0
- package/dist/iceberg/parquet.js +1225 -0
- package/dist/iceberg/parquet.js.map +1 -0
- package/dist/iceberg/r2-organization.d.ts +422 -0
- package/dist/iceberg/r2-organization.d.ts.map +1 -0
- package/dist/iceberg/r2-organization.js +672 -0
- package/dist/iceberg/r2-organization.js.map +1 -0
- package/dist/iceberg/scheduler-do-example.d.ts +158 -0
- package/dist/iceberg/scheduler-do-example.d.ts.map +1 -0
- package/dist/iceberg/scheduler-do-example.js +261 -0
- package/dist/iceberg/scheduler-do-example.js.map +1 -0
- package/dist/iceberg/scheduler.d.ts +434 -0
- package/dist/iceberg/scheduler.d.ts.map +1 -0
- package/dist/iceberg/scheduler.js +818 -0
- package/dist/iceberg/scheduler.js.map +1 -0
- package/dist/iceberg/schema.d.ts +149 -0
- package/dist/iceberg/schema.d.ts.map +1 -0
- package/dist/iceberg/schema.js +525 -0
- package/dist/iceberg/schema.js.map +1 -0
- package/dist/iceberg/snapshot-manager.d.ts +406 -0
- package/dist/iceberg/snapshot-manager.d.ts.map +1 -0
- package/dist/iceberg/snapshot-manager.js +934 -0
- package/dist/iceberg/snapshot-manager.js.map +1 -0
- package/dist/iceberg/sql-router.d.ts +194 -0
- package/dist/iceberg/sql-router.d.ts.map +1 -0
- package/dist/iceberg/sql-router.js +180 -0
- package/dist/iceberg/sql-router.js.map +1 -0
- package/dist/iceberg/test-fixtures.d.ts +151 -0
- package/dist/iceberg/test-fixtures.d.ts.map +1 -0
- package/dist/iceberg/test-fixtures.js +446 -0
- package/dist/iceberg/test-fixtures.js.map +1 -0
- package/dist/iceberg/time-travel-api.d.ts +102 -0
- package/dist/iceberg/time-travel-api.d.ts.map +1 -0
- package/dist/iceberg/time-travel-api.js +437 -0
- package/dist/iceberg/time-travel-api.js.map +1 -0
- package/dist/iceberg/time-travel.d.ts +293 -0
- package/dist/iceberg/time-travel.d.ts.map +1 -0
- package/dist/iceberg/time-travel.js +689 -0
- package/dist/iceberg/time-travel.js.map +1 -0
- package/dist/iceberg/transformer.d.ts +356 -0
- package/dist/iceberg/transformer.d.ts.map +1 -0
- package/dist/iceberg/transformer.js +770 -0
- package/dist/iceberg/transformer.js.map +1 -0
- package/dist/iceberg/types.d.ts +318 -0
- package/dist/iceberg/types.d.ts.map +1 -0
- package/dist/iceberg/types.js +9 -0
- package/dist/iceberg/types.js.map +1 -0
- package/dist/iceberg/writer.d.ts +144 -0
- package/dist/iceberg/writer.d.ts.map +1 -0
- package/dist/iceberg/writer.js +452 -0
- package/dist/iceberg/writer.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/lineage/index.d.ts +11 -0
- package/dist/lineage/index.d.ts.map +1 -0
- package/dist/lineage/index.js +11 -0
- package/dist/lineage/index.js.map +1 -0
- package/dist/lineage/integration.d.ts +134 -0
- package/dist/lineage/integration.d.ts.map +1 -0
- package/dist/lineage/integration.js +258 -0
- package/dist/lineage/integration.js.map +1 -0
- package/dist/lineage/tracker.d.ts +189 -0
- package/dist/lineage/tracker.d.ts.map +1 -0
- package/dist/lineage/tracker.js +1352 -0
- package/dist/lineage/tracker.js.map +1 -0
- package/dist/lineage/types.d.ts +318 -0
- package/dist/lineage/types.d.ts.map +1 -0
- package/dist/lineage/types.js +9 -0
- package/dist/lineage/types.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +16 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +397 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +507 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/migration-tooling/external-migration.d.ts +601 -0
- package/dist/migration-tooling/external-migration.d.ts.map +1 -0
- package/dist/migration-tooling/external-migration.js +1612 -0
- package/dist/migration-tooling/external-migration.js.map +1 -0
- package/dist/migration-tooling/index.d.ts +19 -0
- package/dist/migration-tooling/index.d.ts.map +1 -0
- package/dist/migration-tooling/index.js +19 -0
- package/dist/migration-tooling/index.js.map +1 -0
- package/dist/migrations/auto-migrator.d.ts +289 -0
- package/dist/migrations/auto-migrator.d.ts.map +1 -0
- package/dist/migrations/auto-migrator.js +396 -0
- package/dist/migrations/auto-migrator.js.map +1 -0
- package/dist/migrations/bulk-orchestrator.d.ts +403 -0
- package/dist/migrations/bulk-orchestrator.d.ts.map +1 -0
- package/dist/migrations/bulk-orchestrator.js +646 -0
- package/dist/migrations/bulk-orchestrator.js.map +1 -0
- package/dist/migrations/compatibility.d.ts +216 -0
- package/dist/migrations/compatibility.d.ts.map +1 -0
- package/dist/migrations/compatibility.js +651 -0
- package/dist/migrations/compatibility.js.map +1 -0
- package/dist/migrations/do-migrations.d.ts +101 -0
- package/dist/migrations/do-migrations.d.ts.map +1 -0
- package/dist/migrations/do-migrations.js +1060 -0
- package/dist/migrations/do-migrations.js.map +1 -0
- package/dist/migrations/do-migrations.types.d.ts +550 -0
- package/dist/migrations/do-migrations.types.d.ts.map +1 -0
- package/dist/migrations/do-migrations.types.js +15 -0
- package/dist/migrations/do-migrations.types.js.map +1 -0
- package/dist/migrations/drizzle-compat.d.ts +163 -0
- package/dist/migrations/drizzle-compat.d.ts.map +1 -0
- package/dist/migrations/drizzle-compat.js +273 -0
- package/dist/migrations/drizzle-compat.js.map +1 -0
- package/dist/migrations/index.d.ts +109 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +127 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migration-api.d.ts +161 -0
- package/dist/migrations/migration-api.d.ts.map +1 -0
- package/dist/migrations/migration-api.js +499 -0
- package/dist/migrations/migration-api.js.map +1 -0
- package/dist/migrations/progress-tracker-do.d.ts +195 -0
- package/dist/migrations/progress-tracker-do.d.ts.map +1 -0
- package/dist/migrations/progress-tracker-do.js +339 -0
- package/dist/migrations/progress-tracker-do.js.map +1 -0
- package/dist/migrations/progress-tracker-kv.d.ts +103 -0
- package/dist/migrations/progress-tracker-kv.d.ts.map +1 -0
- package/dist/migrations/progress-tracker-kv.js +231 -0
- package/dist/migrations/progress-tracker-kv.js.map +1 -0
- package/dist/migrations/progress-tracker.d.ts +320 -0
- package/dist/migrations/progress-tracker.d.ts.map +1 -0
- package/dist/migrations/progress-tracker.js +443 -0
- package/dist/migrations/progress-tracker.js.map +1 -0
- package/dist/migrations/registry.d.ts +231 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +376 -0
- package/dist/migrations/registry.js.map +1 -0
- package/dist/migrations/runner.d.ts +197 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +1167 -0
- package/dist/migrations/runner.js.map +1 -0
- package/dist/migrations/schema-generator.d.ts +111 -0
- package/dist/migrations/schema-generator.d.ts.map +1 -0
- package/dist/migrations/schema-generator.js +335 -0
- package/dist/migrations/schema-generator.js.map +1 -0
- package/dist/migrations/testing.d.ts +321 -0
- package/dist/migrations/testing.d.ts.map +1 -0
- package/dist/migrations/testing.js +645 -0
- package/dist/migrations/testing.js.map +1 -0
- package/dist/migrations/types.d.ts +503 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +11 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/migrations/validator.d.ts +215 -0
- package/dist/migrations/validator.d.ts.map +1 -0
- package/dist/migrations/validator.js +494 -0
- package/dist/migrations/validator.js.map +1 -0
- package/dist/observability/alerting.d.ts +116 -0
- package/dist/observability/alerting.d.ts.map +1 -0
- package/dist/observability/alerting.js +353 -0
- package/dist/observability/alerting.js.map +1 -0
- package/dist/observability/analytics-engine.d.ts +357 -0
- package/dist/observability/analytics-engine.d.ts.map +1 -0
- package/dist/observability/analytics-engine.js +430 -0
- package/dist/observability/analytics-engine.js.map +1 -0
- package/dist/observability/cost-metrics.d.ts +269 -0
- package/dist/observability/cost-metrics.d.ts.map +1 -0
- package/dist/observability/cost-metrics.js +560 -0
- package/dist/observability/cost-metrics.js.map +1 -0
- package/dist/observability/cross-do-tracing.d.ts +305 -0
- package/dist/observability/cross-do-tracing.d.ts.map +1 -0
- package/dist/observability/cross-do-tracing.js +431 -0
- package/dist/observability/cross-do-tracing.js.map +1 -0
- package/dist/observability/error-rate-collector.d.ts +163 -0
- package/dist/observability/error-rate-collector.d.ts.map +1 -0
- package/dist/observability/error-rate-collector.js +306 -0
- package/dist/observability/error-rate-collector.js.map +1 -0
- package/dist/observability/exporters.d.ts +231 -0
- package/dist/observability/exporters.d.ts.map +1 -0
- package/dist/observability/exporters.js +479 -0
- package/dist/observability/exporters.js.map +1 -0
- package/dist/observability/health-check.d.ts +106 -0
- package/dist/observability/health-check.d.ts.map +1 -0
- package/dist/observability/health-check.js +243 -0
- package/dist/observability/health-check.js.map +1 -0
- package/dist/observability/index.d.ts +297 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +455 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/instrumentation.d.ts +222 -0
- package/dist/observability/instrumentation.d.ts.map +1 -0
- package/dist/observability/instrumentation.js +532 -0
- package/dist/observability/instrumentation.js.map +1 -0
- package/dist/observability/memory-metrics.d.ts +227 -0
- package/dist/observability/memory-metrics.d.ts.map +1 -0
- package/dist/observability/memory-metrics.js +688 -0
- package/dist/observability/memory-metrics.js.map +1 -0
- package/dist/observability/metrics-endpoint.d.ts +91 -0
- package/dist/observability/metrics-endpoint.d.ts.map +1 -0
- package/dist/observability/metrics-endpoint.js +246 -0
- package/dist/observability/metrics-endpoint.js.map +1 -0
- package/dist/observability/metrics.d.ts +88 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +253 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/observability-features.d.ts +488 -0
- package/dist/observability/observability-features.d.ts.map +1 -0
- package/dist/observability/observability-features.js +773 -0
- package/dist/observability/observability-features.js.map +1 -0
- package/dist/observability/prometheus.d.ts +39 -0
- package/dist/observability/prometheus.d.ts.map +1 -0
- package/dist/observability/prometheus.js +120 -0
- package/dist/observability/prometheus.js.map +1 -0
- package/dist/observability/propagation.d.ts +126 -0
- package/dist/observability/propagation.d.ts.map +1 -0
- package/dist/observability/propagation.js +234 -0
- package/dist/observability/propagation.js.map +1 -0
- package/dist/observability/query-latency.d.ts +243 -0
- package/dist/observability/query-latency.d.ts.map +1 -0
- package/dist/observability/query-latency.js +292 -0
- package/dist/observability/query-latency.js.map +1 -0
- package/dist/observability/query-performance.d.ts +169 -0
- package/dist/observability/query-performance.d.ts.map +1 -0
- package/dist/observability/query-performance.js +290 -0
- package/dist/observability/query-performance.js.map +1 -0
- package/dist/observability/storage-tier-metrics.d.ts +174 -0
- package/dist/observability/storage-tier-metrics.d.ts.map +1 -0
- package/dist/observability/storage-tier-metrics.js +306 -0
- package/dist/observability/storage-tier-metrics.js.map +1 -0
- package/dist/observability/tier-cost-optimizer.d.ts +155 -0
- package/dist/observability/tier-cost-optimizer.d.ts.map +1 -0
- package/dist/observability/tier-cost-optimizer.js +536 -0
- package/dist/observability/tier-cost-optimizer.js.map +1 -0
- package/dist/observability/tracer.d.ts +149 -0
- package/dist/observability/tracer.d.ts.map +1 -0
- package/dist/observability/tracer.js +435 -0
- package/dist/observability/tracer.js.map +1 -0
- package/dist/observability/types.d.ts +402 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +103 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/pglite/workers-pglite.d.ts +138 -0
- package/dist/pglite/workers-pglite.d.ts.map +1 -0
- package/dist/pglite/workers-pglite.js +143 -0
- package/dist/pglite/workers-pglite.js.map +1 -0
- package/dist/pglite-assets/pglite.data +0 -0
- package/dist/pglite-assets/pglite.wasm +0 -0
- package/dist/playground/index.d.ts +52 -0
- package/dist/playground/index.d.ts.map +1 -0
- package/dist/playground/index.js +55 -0
- package/dist/playground/index.js.map +1 -0
- package/dist/playground/keyboard-shortcuts.d.ts +116 -0
- package/dist/playground/keyboard-shortcuts.d.ts.map +1 -0
- package/dist/playground/keyboard-shortcuts.js +588 -0
- package/dist/playground/keyboard-shortcuts.js.map +1 -0
- package/dist/playground/playground.d.ts +82 -0
- package/dist/playground/playground.d.ts.map +1 -0
- package/dist/playground/playground.js +271 -0
- package/dist/playground/playground.js.map +1 -0
- package/dist/playground/query-executor.d.ts +115 -0
- package/dist/playground/query-executor.d.ts.map +1 -0
- package/dist/playground/query-executor.js +558 -0
- package/dist/playground/query-executor.js.map +1 -0
- package/dist/playground/query-history.d.ts +92 -0
- package/dist/playground/query-history.d.ts.map +1 -0
- package/dist/playground/query-history.js +259 -0
- package/dist/playground/query-history.js.map +1 -0
- package/dist/playground/result-formatter.d.ts +59 -0
- package/dist/playground/result-formatter.d.ts.map +1 -0
- package/dist/playground/result-formatter.js +341 -0
- package/dist/playground/result-formatter.js.map +1 -0
- package/dist/playground/sample-datasets.d.ts +77 -0
- package/dist/playground/sample-datasets.d.ts.map +1 -0
- package/dist/playground/sample-datasets.js +641 -0
- package/dist/playground/sample-datasets.js.map +1 -0
- package/dist/playground/sample-queries.d.ts +73 -0
- package/dist/playground/sample-queries.d.ts.map +1 -0
- package/dist/playground/sample-queries.js +1095 -0
- package/dist/playground/sample-queries.js.map +1 -0
- package/dist/playground/schema-explorer.d.ts +55 -0
- package/dist/playground/schema-explorer.d.ts.map +1 -0
- package/dist/playground/schema-explorer.js +473 -0
- package/dist/playground/schema-explorer.js.map +1 -0
- package/dist/playground/types.d.ts +430 -0
- package/dist/playground/types.d.ts.map +1 -0
- package/dist/playground/types.js +10 -0
- package/dist/playground/types.js.map +1 -0
- package/dist/readonly/cache-reader.d.ts +145 -0
- package/dist/readonly/cache-reader.d.ts.map +1 -0
- package/dist/readonly/cache-reader.js +198 -0
- package/dist/readonly/cache-reader.js.map +1 -0
- package/dist/readonly/config.d.ts +74 -0
- package/dist/readonly/config.d.ts.map +1 -0
- package/dist/readonly/config.js +67 -0
- package/dist/readonly/config.js.map +1 -0
- package/dist/readonly/index.d.ts +22 -0
- package/dist/readonly/index.d.ts.map +1 -0
- package/dist/readonly/index.js +17 -0
- package/dist/readonly/index.js.map +1 -0
- package/dist/readonly/pglite-wrapper.d.ts +82 -0
- package/dist/readonly/pglite-wrapper.d.ts.map +1 -0
- package/dist/readonly/pglite-wrapper.js +123 -0
- package/dist/readonly/pglite-wrapper.js.map +1 -0
- package/dist/readonly/worker.d.ts +142 -0
- package/dist/readonly/worker.d.ts.map +1 -0
- package/dist/readonly/worker.js +187 -0
- package/dist/readonly/worker.js.map +1 -0
- package/dist/readonly/write-blocker.d.ts +47 -0
- package/dist/readonly/write-blocker.d.ts.map +1 -0
- package/dist/readonly/write-blocker.js +136 -0
- package/dist/readonly/write-blocker.js.map +1 -0
- package/dist/recovery/disaster-recovery.d.ts +326 -0
- package/dist/recovery/disaster-recovery.d.ts.map +1 -0
- package/dist/recovery/disaster-recovery.js +799 -0
- package/dist/recovery/disaster-recovery.js.map +1 -0
- package/dist/recovery/index.d.ts +12 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +12 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/recovery/parquet-parser.d.ts +321 -0
- package/dist/recovery/parquet-parser.d.ts.map +1 -0
- package/dist/recovery/parquet-parser.js +797 -0
- package/dist/recovery/parquet-parser.js.map +1 -0
- package/dist/retention/index.d.ts +50 -0
- package/dist/retention/index.d.ts.map +1 -0
- package/dist/retention/index.js +50 -0
- package/dist/retention/index.js.map +1 -0
- package/dist/retention/policy.d.ts +344 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +472 -0
- package/dist/retention/policy.js.map +1 -0
- package/dist/retention/purger.d.ts +187 -0
- package/dist/retention/purger.d.ts.map +1 -0
- package/dist/retention/purger.js +411 -0
- package/dist/retention/purger.js.map +1 -0
- package/dist/rls/auth-integration.d.ts +280 -0
- package/dist/rls/auth-integration.d.ts.map +1 -0
- package/dist/rls/auth-integration.js +399 -0
- package/dist/rls/auth-integration.js.map +1 -0
- package/dist/rls/generator.d.ts +249 -0
- package/dist/rls/generator.d.ts.map +1 -0
- package/dist/rls/generator.js +495 -0
- package/dist/rls/generator.js.map +1 -0
- package/dist/rls/index.d.ts +26 -0
- package/dist/rls/index.d.ts.map +1 -0
- package/dist/rls/index.js +58 -0
- package/dist/rls/index.js.map +1 -0
- package/dist/rls/policy.d.ts +116 -0
- package/dist/rls/policy.d.ts.map +1 -0
- package/dist/rls/policy.js +77 -0
- package/dist/rls/policy.js.map +1 -0
- package/dist/rls/validator.d.ts +155 -0
- package/dist/rls/validator.d.ts.map +1 -0
- package/dist/rls/validator.js +792 -0
- package/dist/rls/validator.js.map +1 -0
- package/dist/routing/adaptive-router.d.ts +317 -0
- package/dist/routing/adaptive-router.d.ts.map +1 -0
- package/dist/routing/adaptive-router.js +554 -0
- package/dist/routing/adaptive-router.js.map +1 -0
- package/dist/routing/circuit-breaker.d.ts +339 -0
- package/dist/routing/circuit-breaker.d.ts.map +1 -0
- package/dist/routing/circuit-breaker.js +620 -0
- package/dist/routing/circuit-breaker.js.map +1 -0
- package/dist/routing/cost-metrics.d.ts +133 -0
- package/dist/routing/cost-metrics.d.ts.map +1 -0
- package/dist/routing/cost-metrics.js +259 -0
- package/dist/routing/cost-metrics.js.map +1 -0
- package/dist/routing/do-connection-pool.d.ts +243 -0
- package/dist/routing/do-connection-pool.d.ts.map +1 -0
- package/dist/routing/do-connection-pool.js +572 -0
- package/dist/routing/do-connection-pool.js.map +1 -0
- package/dist/routing/index.d.ts +59 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +59 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/routing/query-complexity-estimator.d.ts +73 -0
- package/dist/routing/query-complexity-estimator.d.ts.map +1 -0
- package/dist/routing/query-complexity-estimator.js +327 -0
- package/dist/routing/query-complexity-estimator.js.map +1 -0
- package/dist/routing/request-coalescing.d.ts +178 -0
- package/dist/routing/request-coalescing.d.ts.map +1 -0
- package/dist/routing/request-coalescing.js +325 -0
- package/dist/routing/request-coalescing.js.map +1 -0
- package/dist/routing/runtime-router.d.ts +107 -0
- package/dist/routing/runtime-router.d.ts.map +1 -0
- package/dist/routing/runtime-router.js +246 -0
- package/dist/routing/runtime-router.js.map +1 -0
- package/dist/routing/tenant-router.d.ts +848 -0
- package/dist/routing/tenant-router.d.ts.map +1 -0
- package/dist/routing/tenant-router.js +1056 -0
- package/dist/routing/tenant-router.js.map +1 -0
- package/dist/routing/websocket-pool.d.ts +119 -0
- package/dist/routing/websocket-pool.d.ts.map +1 -0
- package/dist/routing/websocket-pool.js +436 -0
- package/dist/routing/websocket-pool.js.map +1 -0
- package/dist/storage/cache-layer.d.ts +159 -0
- package/dist/storage/cache-layer.d.ts.map +1 -0
- package/dist/storage/cache-layer.js +245 -0
- package/dist/storage/cache-layer.js.map +1 -0
- package/dist/storage/cost-aware-tiering.d.ts +258 -0
- package/dist/storage/cost-aware-tiering.d.ts.map +1 -0
- package/dist/storage/cost-aware-tiering.js +526 -0
- package/dist/storage/cost-aware-tiering.js.map +1 -0
- package/dist/storage/index.d.ts +87 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +78 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/interfaces.d.ts +856 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +69 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/r2-layer.d.ts +226 -0
- package/dist/storage/r2-layer.d.ts.map +1 -0
- package/dist/storage/r2-layer.js +307 -0
- package/dist/storage/r2-layer.js.map +1 -0
- package/dist/storage/r2-overflow.d.ts +344 -0
- package/dist/storage/r2-overflow.d.ts.map +1 -0
- package/dist/storage/r2-overflow.js +730 -0
- package/dist/storage/r2-overflow.js.map +1 -0
- package/dist/storage/r2-page-vfs.d.ts +374 -0
- package/dist/storage/r2-page-vfs.d.ts.map +1 -0
- package/dist/storage/r2-page-vfs.js +754 -0
- package/dist/storage/r2-page-vfs.js.map +1 -0
- package/dist/storage/swr-cache.d.ts +181 -0
- package/dist/storage/swr-cache.d.ts.map +1 -0
- package/dist/storage/swr-cache.js +295 -0
- package/dist/storage/swr-cache.js.map +1 -0
- package/dist/storage/tiered-orchestrator.d.ts +951 -0
- package/dist/storage/tiered-orchestrator.d.ts.map +1 -0
- package/dist/storage/tiered-orchestrator.js +1731 -0
- package/dist/storage/tiered-orchestrator.js.map +1 -0
- package/dist/storage/tiered-vfs-swr.d.ts +279 -0
- package/dist/storage/tiered-vfs-swr.d.ts.map +1 -0
- package/dist/storage/tiered-vfs-swr.js +584 -0
- package/dist/storage/tiered-vfs-swr.js.map +1 -0
- package/dist/storage/tiered-vfs.d.ts +405 -0
- package/dist/storage/tiered-vfs.d.ts.map +1 -0
- package/dist/storage/tiered-vfs.js +833 -0
- package/dist/storage/tiered-vfs.js.map +1 -0
- package/dist/streaming/backpressure-controller.d.ts +173 -0
- package/dist/streaming/backpressure-controller.d.ts.map +1 -0
- package/dist/streaming/backpressure-controller.js +344 -0
- package/dist/streaming/backpressure-controller.js.map +1 -0
- package/dist/streaming/buffer-pool.d.ts +241 -0
- package/dist/streaming/buffer-pool.d.ts.map +1 -0
- package/dist/streaming/buffer-pool.js +381 -0
- package/dist/streaming/buffer-pool.js.map +1 -0
- package/dist/streaming/cdc-iceberg-connector.d.ts +272 -0
- package/dist/streaming/cdc-iceberg-connector.d.ts.map +1 -0
- package/dist/streaming/cdc-iceberg-connector.js +408 -0
- package/dist/streaming/cdc-iceberg-connector.js.map +1 -0
- package/dist/streaming/index.d.ts +111 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +128 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/live-cdc-stream.d.ts +400 -0
- package/dist/streaming/live-cdc-stream.d.ts.map +1 -0
- package/dist/streaming/live-cdc-stream.js +703 -0
- package/dist/streaming/live-cdc-stream.js.map +1 -0
- package/dist/streaming/memory-bounded-stream.d.ts +207 -0
- package/dist/streaming/memory-bounded-stream.d.ts.map +1 -0
- package/dist/streaming/memory-bounded-stream.js +340 -0
- package/dist/streaming/memory-bounded-stream.js.map +1 -0
- package/dist/streaming/query-streamer.d.ts +379 -0
- package/dist/streaming/query-streamer.d.ts.map +1 -0
- package/dist/streaming/query-streamer.js +495 -0
- package/dist/streaming/query-streamer.js.map +1 -0
- package/dist/streaming/response-streaming.d.ts +203 -0
- package/dist/streaming/response-streaming.d.ts.map +1 -0
- package/dist/streaming/response-streaming.js +449 -0
- package/dist/streaming/response-streaming.js.map +1 -0
- package/dist/types/branded.d.ts +859 -0
- package/dist/types/branded.d.ts.map +1 -0
- package/dist/types/branded.js +891 -0
- package/dist/types/branded.js.map +1 -0
- package/dist/types/utilities.d.ts +757 -0
- package/dist/types/utilities.d.ts.map +1 -0
- package/dist/types/utilities.js +447 -0
- package/dist/types/utilities.js.map +1 -0
- package/dist/wal/replay-engine.d.ts +344 -0
- package/dist/wal/replay-engine.d.ts.map +1 -0
- package/dist/wal/replay-engine.js +975 -0
- package/dist/wal/replay-engine.js.map +1 -0
- package/dist/worker/__mocks__/capnweb.d.ts +13 -0
- package/dist/worker/__mocks__/capnweb.d.ts.map +1 -0
- package/dist/worker/__mocks__/capnweb.js +15 -0
- package/dist/worker/__mocks__/capnweb.js.map +1 -0
- package/dist/worker/__mocks__/cloudflare-workers.d.ts +31 -0
- package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +1 -0
- package/dist/worker/__mocks__/cloudflare-workers.js +33 -0
- package/dist/worker/__mocks__/cloudflare-workers.js.map +1 -0
- package/dist/worker/__mocks__/pglite.data.d.ts +3 -0
- package/dist/worker/__mocks__/pglite.data.d.ts.map +1 -0
- package/dist/worker/__mocks__/pglite.data.js +20 -0
- package/dist/worker/__mocks__/pglite.data.js.map +1 -0
- package/dist/worker/__mocks__/pglite.wasm.d.ts +3 -0
- package/dist/worker/__mocks__/pglite.wasm.d.ts.map +1 -0
- package/dist/worker/__mocks__/pglite.wasm.js +30 -0
- package/dist/worker/__mocks__/pglite.wasm.js.map +1 -0
- package/dist/worker/auth-rate-limiter.d.ts +270 -0
- package/dist/worker/auth-rate-limiter.d.ts.map +1 -0
- package/dist/worker/auth-rate-limiter.js +332 -0
- package/dist/worker/auth-rate-limiter.js.map +1 -0
- package/dist/worker/auth.d.ts +345 -0
- package/dist/worker/auth.d.ts.map +1 -0
- package/dist/worker/auth.js +837 -0
- package/dist/worker/auth.js.map +1 -0
- package/dist/worker/cdc-backpressure.d.ts +338 -0
- package/dist/worker/cdc-backpressure.d.ts.map +1 -0
- package/dist/worker/cdc-backpressure.js +619 -0
- package/dist/worker/cdc-backpressure.js.map +1 -0
- package/dist/worker/cdc-sse.d.ts +277 -0
- package/dist/worker/cdc-sse.d.ts.map +1 -0
- package/dist/worker/cdc-sse.js +528 -0
- package/dist/worker/cdc-sse.js.map +1 -0
- package/dist/worker/cdc-websocket.d.ts +252 -0
- package/dist/worker/cdc-websocket.d.ts.map +1 -0
- package/dist/worker/cdc-websocket.js +940 -0
- package/dist/worker/cdc-websocket.js.map +1 -0
- package/dist/worker/cdc.d.ts +95 -0
- package/dist/worker/cdc.d.ts.map +1 -0
- package/dist/worker/cdc.js +211 -0
- package/dist/worker/cdc.js.map +1 -0
- package/dist/worker/concerns/auth-concern.d.ts +50 -0
- package/dist/worker/concerns/auth-concern.d.ts.map +1 -0
- package/dist/worker/concerns/auth-concern.js +131 -0
- package/dist/worker/concerns/auth-concern.js.map +1 -0
- package/dist/worker/concerns/cdc-concern.d.ts +99 -0
- package/dist/worker/concerns/cdc-concern.d.ts.map +1 -0
- package/dist/worker/concerns/cdc-concern.js +137 -0
- package/dist/worker/concerns/cdc-concern.js.map +1 -0
- package/dist/worker/concerns/index.d.ts +22 -0
- package/dist/worker/concerns/index.d.ts.map +1 -0
- package/dist/worker/concerns/index.js +13 -0
- package/dist/worker/concerns/index.js.map +1 -0
- package/dist/worker/concerns/query-execution-concern.d.ts +104 -0
- package/dist/worker/concerns/query-execution-concern.d.ts.map +1 -0
- package/dist/worker/concerns/query-execution-concern.js +95 -0
- package/dist/worker/concerns/query-execution-concern.js.map +1 -0
- package/dist/worker/concerns/storage-orchestration-concern.d.ts +78 -0
- package/dist/worker/concerns/storage-orchestration-concern.d.ts.map +1 -0
- package/dist/worker/concerns/storage-orchestration-concern.js +240 -0
- package/dist/worker/concerns/storage-orchestration-concern.js.map +1 -0
- package/dist/worker/do-auth-manager.d.ts +108 -0
- package/dist/worker/do-auth-manager.d.ts.map +1 -0
- package/dist/worker/do-auth-manager.js +212 -0
- package/dist/worker/do-auth-manager.js.map +1 -0
- package/dist/worker/do-pglite-manager.d.ts +137 -0
- package/dist/worker/do-pglite-manager.d.ts.map +1 -0
- package/dist/worker/do-pglite-manager.js +228 -0
- package/dist/worker/do-pglite-manager.js.map +1 -0
- package/dist/worker/do.d.ts +556 -0
- package/dist/worker/do.d.ts.map +1 -0
- package/dist/worker/do.js +1441 -0
- package/dist/worker/do.js.map +1 -0
- package/dist/worker/entry.d.ts +23 -0
- package/dist/worker/entry.d.ts.map +1 -0
- package/dist/worker/entry.js +362 -0
- package/dist/worker/entry.js.map +1 -0
- package/dist/worker/errors.d.ts +106 -0
- package/dist/worker/errors.d.ts.map +1 -0
- package/dist/worker/errors.js +178 -0
- package/dist/worker/errors.js.map +1 -0
- package/dist/worker/health-check-manager.d.ts +141 -0
- package/dist/worker/health-check-manager.d.ts.map +1 -0
- package/dist/worker/health-check-manager.js +145 -0
- package/dist/worker/health-check-manager.js.map +1 -0
- package/dist/worker/index.d.ts +60 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +67 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/memory-pressure.d.ts +892 -0
- package/dist/worker/memory-pressure.d.ts.map +1 -0
- package/dist/worker/memory-pressure.js +1990 -0
- package/dist/worker/memory-pressure.js.map +1 -0
- package/dist/worker/migration-manager.d.ts +153 -0
- package/dist/worker/migration-manager.d.ts.map +1 -0
- package/dist/worker/migration-manager.js +461 -0
- package/dist/worker/migration-manager.js.map +1 -0
- package/dist/worker/plugin-manager.d.ts +147 -0
- package/dist/worker/plugin-manager.d.ts.map +1 -0
- package/dist/worker/plugin-manager.js +408 -0
- package/dist/worker/plugin-manager.js.map +1 -0
- package/dist/worker/proxy.d.ts +330 -0
- package/dist/worker/proxy.d.ts.map +1 -0
- package/dist/worker/proxy.js +504 -0
- package/dist/worker/proxy.js.map +1 -0
- package/dist/worker/query-execution-manager.d.ts +107 -0
- package/dist/worker/query-execution-manager.d.ts.map +1 -0
- package/dist/worker/query-execution-manager.js +155 -0
- package/dist/worker/query-execution-manager.js.map +1 -0
- package/dist/worker/query-executor.d.ts +163 -0
- package/dist/worker/query-executor.d.ts.map +1 -0
- package/dist/worker/query-executor.js +413 -0
- package/dist/worker/query-executor.js.map +1 -0
- package/dist/worker/query-stats-manager.d.ts +117 -0
- package/dist/worker/query-stats-manager.d.ts.map +1 -0
- package/dist/worker/query-stats-manager.js +162 -0
- package/dist/worker/query-stats-manager.js.map +1 -0
- package/dist/worker/result-handler.d.ts +192 -0
- package/dist/worker/result-handler.d.ts.map +1 -0
- package/dist/worker/result-handler.js +346 -0
- package/dist/worker/result-handler.js.map +1 -0
- package/dist/worker/routes.d.ts +135 -0
- package/dist/worker/routes.d.ts.map +1 -0
- package/dist/worker/routes.js +460 -0
- package/dist/worker/routes.js.map +1 -0
- package/dist/worker/rpc-methods-manager.d.ts +142 -0
- package/dist/worker/rpc-methods-manager.d.ts.map +1 -0
- package/dist/worker/rpc-methods-manager.js +195 -0
- package/dist/worker/rpc-methods-manager.js.map +1 -0
- package/dist/worker/rpc.d.ts +259 -0
- package/dist/worker/rpc.d.ts.map +1 -0
- package/dist/worker/rpc.js +398 -0
- package/dist/worker/rpc.js.map +1 -0
- package/dist/worker/schema-version.d.ts +209 -0
- package/dist/worker/schema-version.d.ts.map +1 -0
- package/dist/worker/schema-version.js +450 -0
- package/dist/worker/schema-version.js.map +1 -0
- package/dist/worker/session-manager.d.ts +282 -0
- package/dist/worker/session-manager.d.ts.map +1 -0
- package/dist/worker/session-manager.js +523 -0
- package/dist/worker/session-manager.js.map +1 -0
- package/dist/worker/shutdown-manager.d.ts +188 -0
- package/dist/worker/shutdown-manager.d.ts.map +1 -0
- package/dist/worker/shutdown-manager.js +347 -0
- package/dist/worker/shutdown-manager.js.map +1 -0
- package/dist/worker/sql-transform.d.ts +61 -0
- package/dist/worker/sql-transform.d.ts.map +1 -0
- package/dist/worker/sql-transform.js +312 -0
- package/dist/worker/sql-transform.js.map +1 -0
- package/dist/worker/types.d.ts +738 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +6 -0
- package/dist/worker/types.js.map +1 -0
- package/dist/worker/user-routes.d.ts +76 -0
- package/dist/worker/user-routes.d.ts.map +1 -0
- package/dist/worker/user-routes.js +188 -0
- package/dist/worker/user-routes.js.map +1 -0
- package/dist/worker/wal-facade.d.ts +138 -0
- package/dist/worker/wal-facade.d.ts.map +1 -0
- package/dist/worker/wal-facade.js +184 -0
- package/dist/worker/wal-facade.js.map +1 -0
- package/dist/worker/wal-r2.d.ts +271 -0
- package/dist/worker/wal-r2.d.ts.map +1 -0
- package/dist/worker/wal-r2.js +689 -0
- package/dist/worker/wal-r2.js.map +1 -0
- package/dist/worker/wal-replay.d.ts +361 -0
- package/dist/worker/wal-replay.d.ts.map +1 -0
- package/dist/worker/wal-replay.js +628 -0
- package/dist/worker/wal-replay.js.map +1 -0
- package/dist/worker/wal-retention.d.ts +389 -0
- package/dist/worker/wal-retention.d.ts.map +1 -0
- package/dist/worker/wal-retention.js +763 -0
- package/dist/worker/wal-retention.js.map +1 -0
- package/dist/worker/wal.d.ts +278 -0
- package/dist/worker/wal.d.ts.map +1 -0
- package/dist/worker/wal.js +467 -0
- package/dist/worker/wal.js.map +1 -0
- package/dist/worker/websocket.d.ts +85 -0
- package/dist/worker/websocket.d.ts.map +1 -0
- package/dist/worker/websocket.js +227 -0
- package/dist/worker/websocket.js.map +1 -0
- package/package.json +108 -0
- package/src/cdc/change-stream.ts +137 -0
- package/src/cdc/filter.ts +646 -0
- package/src/cdc/index.ts +112 -0
- package/src/cdc/resume-token.ts +280 -0
- package/src/cdc/transport/index.ts +7 -0
- package/src/cdc/transport/sse.ts +723 -0
- package/src/cdc/transport/websocket.ts +873 -0
- package/src/cdc/types.ts +346 -0
- package/src/config/index.ts +25 -0
- package/src/config/memory.ts +177 -0
- package/src/config/storage.ts +204 -0
- package/src/config/streaming.ts +147 -0
- package/src/config/timeouts.ts +221 -0
- package/src/extensions/config.test.ts +187 -0
- package/src/extensions/config.ts +278 -0
- package/src/extensions/geo.test.ts +455 -0
- package/src/extensions/geo.ts +858 -0
- package/src/extensions/index.test.ts +259 -0
- package/src/extensions/index.ts +227 -0
- package/src/extensions/loader.test.ts +555 -0
- package/src/extensions/loader.ts +588 -0
- package/src/extensions/pgmq-lite.test.ts +727 -0
- package/src/extensions/pgmq-lite.ts +770 -0
- package/src/extensions/plugins.test.ts +528 -0
- package/src/extensions/plugins.ts +718 -0
- package/src/extensions/registry.test.ts +202 -0
- package/src/extensions/registry.ts +267 -0
- package/src/extensions/vector.test.ts +195 -0
- package/src/extensions/vector.ts +217 -0
- package/src/iceberg/SCHEDULER.md +580 -0
- package/src/iceberg/analytics.test.ts +703 -0
- package/src/iceberg/analytics.ts +727 -0
- package/src/iceberg/catalog-api.test.ts +838 -0
- package/src/iceberg/catalog-api.ts +520 -0
- package/src/iceberg/catalog.test.ts +680 -0
- package/src/iceberg/catalog.ts +1007 -0
- package/src/iceberg/iceberg.test.ts +705 -0
- package/src/iceberg/index.ts +406 -0
- package/src/iceberg/metadata.test.ts +632 -0
- package/src/iceberg/metadata.ts +649 -0
- package/src/iceberg/optimizer.test.ts +868 -0
- package/src/iceberg/optimizer.ts +1287 -0
- package/src/iceberg/parquet.test.ts +899 -0
- package/src/iceberg/parquet.ts +1640 -0
- package/src/iceberg/r2-organization.test.ts +615 -0
- package/src/iceberg/r2-organization.ts +951 -0
- package/src/iceberg/scheduler-do-example.ts +364 -0
- package/src/iceberg/scheduler.test.ts +861 -0
- package/src/iceberg/scheduler.ts +1201 -0
- package/src/iceberg/schema.test.ts +547 -0
- package/src/iceberg/schema.ts +616 -0
- package/src/iceberg/snapshot-manager.test.ts +919 -0
- package/src/iceberg/snapshot-manager.ts +1369 -0
- package/src/iceberg/sql-router.test.ts +334 -0
- package/src/iceberg/sql-router.ts +337 -0
- package/src/iceberg/test-fixtures.ts +605 -0
- package/src/iceberg/time-travel-api.test.ts +1029 -0
- package/src/iceberg/time-travel-api.ts +731 -0
- package/src/iceberg/time-travel.test.ts +1218 -0
- package/src/iceberg/time-travel.ts +1052 -0
- package/src/iceberg/transformer.test.ts +689 -0
- package/src/iceberg/transformer.ts +1029 -0
- package/src/iceberg/types.ts +373 -0
- package/src/iceberg/writer.test.ts +716 -0
- package/src/iceberg/writer.ts +590 -0
- package/src/index.ts +212 -0
- package/src/lineage/index.ts +42 -0
- package/src/lineage/integration.ts +334 -0
- package/src/lineage/tracker.ts +1618 -0
- package/src/lineage/types.ts +354 -0
- package/src/middleware/index.ts +36 -0
- package/src/middleware/rate-limit-concurrent.test.ts +794 -0
- package/src/middleware/rate-limit.test.ts +1568 -0
- package/src/middleware/rate-limit.ts +840 -0
- package/src/migration-tooling/external-migration.test.ts +1864 -0
- package/src/migration-tooling/external-migration.ts +2355 -0
- package/src/migration-tooling/index.ts +19 -0
- package/src/migrations/ARCHITECTURE.md +474 -0
- package/src/migrations/PROGRESS_TRACKING.md +485 -0
- package/src/migrations/auto-migrator.test.ts +732 -0
- package/src/migrations/auto-migrator.ts +531 -0
- package/src/migrations/bulk-orchestrator.test.ts +801 -0
- package/src/migrations/bulk-orchestrator.ts +1039 -0
- package/src/migrations/compatibility.test.ts +958 -0
- package/src/migrations/compatibility.ts +902 -0
- package/src/migrations/do-migrations.test.ts +2620 -0
- package/src/migrations/do-migrations.ts +1289 -0
- package/src/migrations/do-migrations.types.ts +715 -0
- package/src/migrations/drizzle-compat.test.ts +210 -0
- package/src/migrations/drizzle-compat.ts +337 -0
- package/src/migrations/index.ts +334 -0
- package/src/migrations/migration-api.test.ts +438 -0
- package/src/migrations/migration-api.ts +704 -0
- package/src/migrations/progress-tracker-do.ts +518 -0
- package/src/migrations/progress-tracker-kv.ts +305 -0
- package/src/migrations/progress-tracker.test.ts +937 -0
- package/src/migrations/progress-tracker.ts +665 -0
- package/src/migrations/registry.test.ts +331 -0
- package/src/migrations/registry.ts +468 -0
- package/src/migrations/rollback.test.ts +644 -0
- package/src/migrations/runner.test.ts +807 -0
- package/src/migrations/runner.test.ts.backup +759 -0
- package/src/migrations/runner.ts +1459 -0
- package/src/migrations/schema-generator.test.ts +649 -0
- package/src/migrations/schema-generator.ts +513 -0
- package/src/migrations/testing.ts +1037 -0
- package/src/migrations/types.ts +573 -0
- package/src/migrations/validator.test.ts +660 -0
- package/src/migrations/validator.ts +741 -0
- package/src/observability/alerting.test.ts +1133 -0
- package/src/observability/alerting.ts +455 -0
- package/src/observability/analytics-engine.ts +733 -0
- package/src/observability/cost-metrics.ts +804 -0
- package/src/observability/cross-do-tracing.test.ts +516 -0
- package/src/observability/cross-do-tracing.ts +588 -0
- package/src/observability/dashboards/postgres-do-overview.json +1656 -0
- package/src/observability/error-rate-collector.test.ts +977 -0
- package/src/observability/error-rate-collector.ts +518 -0
- package/src/observability/exporters.test.ts +365 -0
- package/src/observability/exporters.ts +650 -0
- package/src/observability/health-check.test.ts +353 -0
- package/src/observability/health-check.ts +341 -0
- package/src/observability/index.test.ts +298 -0
- package/src/observability/index.ts +885 -0
- package/src/observability/instrumentation.test.ts +428 -0
- package/src/observability/instrumentation.ts +788 -0
- package/src/observability/memory-metrics.test.ts +355 -0
- package/src/observability/memory-metrics.ts +990 -0
- package/src/observability/metrics-endpoint.test.ts +402 -0
- package/src/observability/metrics-endpoint.ts +374 -0
- package/src/observability/metrics.test.ts +291 -0
- package/src/observability/metrics.ts +315 -0
- package/src/observability/observability-features.ts +1296 -0
- package/src/observability/prometheus.test.ts +292 -0
- package/src/observability/prometheus.ts +170 -0
- package/src/observability/propagation.test.ts +417 -0
- package/src/observability/propagation.ts +294 -0
- package/src/observability/query-latency.ts +586 -0
- package/src/observability/query-performance.test.ts +406 -0
- package/src/observability/query-performance.ts +491 -0
- package/src/observability/storage-tier-metrics.test.ts +633 -0
- package/src/observability/storage-tier-metrics.ts +570 -0
- package/src/observability/tier-cost-optimizer.ts +740 -0
- package/src/observability/tracer.test.ts +346 -0
- package/src/observability/tracer.ts +585 -0
- package/src/observability/types.test.ts +726 -0
- package/src/observability/types.ts +434 -0
- package/src/pglite/auto-demotion.test.ts +477 -0
- package/src/pglite/auto-demotion.ts +385 -0
- package/src/pglite/auto-promotion.test.ts +824 -0
- package/src/pglite/auto-promotion.ts +547 -0
- package/src/pglite/cache-layer.test.ts +469 -0
- package/src/pglite/cache-layer.ts +271 -0
- package/src/pglite/cold-start-manager.ts +1260 -0
- package/src/pglite/cold-start-optimizer.test.ts +937 -0
- package/src/pglite/cold-start-optimizer.ts +1895 -0
- package/src/pglite/dovfs-adapter.ts +1122 -0
- package/src/pglite/dovfs.ts +1258 -0
- package/src/pglite/etag-cache.test.ts +844 -0
- package/src/pglite/etag-cache.ts +526 -0
- package/src/pglite/index.ts +442 -0
- package/src/pglite/init.test.ts +455 -0
- package/src/pglite/init.ts +574 -0
- package/src/pglite/lifecycle.test.ts +599 -0
- package/src/pglite/lifecycle.ts +704 -0
- package/src/pglite/parallel-loader.test.ts +586 -0
- package/src/pglite/parallel-loader.ts +481 -0
- package/src/pglite/production-pglite.test.ts +666 -0
- package/src/pglite/production-pglite.ts +537 -0
- package/src/pglite/query-executor.ts +614 -0
- package/src/pglite/r2-layer.test.ts +501 -0
- package/src/pglite/r2-layer.ts +322 -0
- package/src/pglite/tiered-init.test.ts +725 -0
- package/src/pglite/tiered-init.ts +556 -0
- package/src/pglite/tiered-vfs.test.ts +726 -0
- package/src/pglite/tiered-vfs.ts +33 -0
- package/src/pglite/tiering-stats.test.ts +531 -0
- package/src/pglite/tiering-stats.ts +407 -0
- package/src/pglite/transaction-hooks.ts +343 -0
- package/src/pglite/warm-loader.test.ts +1701 -0
- package/src/pglite/warm-loader.ts +528 -0
- package/src/pglite/workers-pglite.ts +224 -0
- package/src/pglite-assets/pglite.data +0 -0
- package/src/pglite-assets/pglite.wasm +0 -0
- package/src/pglite.d.ts +47 -0
- package/src/playground/index.ts +137 -0
- package/src/playground/keyboard-shortcuts.ts +677 -0
- package/src/playground/playground.ts +323 -0
- package/src/playground/query-executor.ts +669 -0
- package/src/playground/query-history.ts +328 -0
- package/src/playground/result-formatter.ts +420 -0
- package/src/playground/sample-datasets.ts +674 -0
- package/src/playground/sample-queries.ts +1168 -0
- package/src/playground/schema-explorer.ts +558 -0
- package/src/playground/types.ts +518 -0
- package/src/readonly/cache-reader.test.ts +460 -0
- package/src/readonly/cache-reader.ts +313 -0
- package/src/readonly/config.test.ts +187 -0
- package/src/readonly/config.ts +128 -0
- package/src/readonly/index.ts +50 -0
- package/src/readonly/pglite-wrapper.test.ts +278 -0
- package/src/readonly/pglite-wrapper.ts +184 -0
- package/src/readonly/worker.test.ts +533 -0
- package/src/readonly/worker.ts +341 -0
- package/src/readonly/write-blocker.test.ts +459 -0
- package/src/readonly/write-blocker.ts +175 -0
- package/src/recovery/disaster-recovery.test.ts +618 -0
- package/src/recovery/disaster-recovery.ts +1181 -0
- package/src/recovery/index.ts +43 -0
- package/src/recovery/parquet-parser.ts +974 -0
- package/src/retention/index.ts +74 -0
- package/src/retention/policy.test.ts +571 -0
- package/src/retention/policy.ts +774 -0
- package/src/retention/purger.test.ts +465 -0
- package/src/retention/purger.ts +558 -0
- package/src/rls/auth-integration.test.ts +752 -0
- package/src/rls/auth-integration.ts +533 -0
- package/src/rls/generator.test.ts +829 -0
- package/src/rls/generator.ts +573 -0
- package/src/rls/index.ts +128 -0
- package/src/rls/policy.ts +208 -0
- package/src/rls/rls.test.ts +1071 -0
- package/src/rls/validator.test.ts +930 -0
- package/src/rls/validator.ts +895 -0
- package/src/routing/adaptive-router.test.ts +884 -0
- package/src/routing/adaptive-router.ts +845 -0
- package/src/routing/circuit-breaker.test.ts +1505 -0
- package/src/routing/circuit-breaker.ts +852 -0
- package/src/routing/cost-metrics.test.ts +565 -0
- package/src/routing/cost-metrics.ts +408 -0
- package/src/routing/do-connection-pool.test.ts +1109 -0
- package/src/routing/do-connection-pool.ts +828 -0
- package/src/routing/index.ts +158 -0
- package/src/routing/query-complexity-estimator.test.ts +356 -0
- package/src/routing/query-complexity-estimator.ts +444 -0
- package/src/routing/request-coalescing.test.ts +738 -0
- package/src/routing/request-coalescing.ts +475 -0
- package/src/routing/runtime-router.test.ts +436 -0
- package/src/routing/runtime-router.ts +357 -0
- package/src/routing/tenant-router.test.ts +2493 -0
- package/src/routing/tenant-router.ts +1908 -0
- package/src/routing/websocket-pool.test.ts +551 -0
- package/src/routing/websocket-pool.ts +577 -0
- package/src/storage/access-pattern-tracker.test.ts +874 -0
- package/src/storage/cache-layer.test.ts +560 -0
- package/src/storage/cache-layer.ts +328 -0
- package/src/storage/cost-aware-tiering.test.ts +652 -0
- package/src/storage/cost-aware-tiering.ts +794 -0
- package/src/storage/do-sqlite-blobs.test.ts +937 -0
- package/src/storage/index.ts +272 -0
- package/src/storage/interfaces.ts +974 -0
- package/src/storage/r2-layer.test.ts +653 -0
- package/src/storage/r2-layer.ts +434 -0
- package/src/storage/r2-overflow.ts +920 -0
- package/src/storage/r2-page-vfs.test.ts +2348 -0
- package/src/storage/r2-page-vfs.ts +1054 -0
- package/src/storage/swr-cache.test.ts +832 -0
- package/src/storage/swr-cache.ts +398 -0
- package/src/storage/swr-tiered-integration.test.ts +617 -0
- package/src/storage/tiered-orchestrator.test.ts +2441 -0
- package/src/storage/tiered-orchestrator.ts +2081 -0
- package/src/storage/tiered-vfs-swr.test.ts +736 -0
- package/src/storage/tiered-vfs-swr.ts +735 -0
- package/src/storage/tiered-vfs.test.ts +793 -0
- package/src/storage/tiered-vfs.ts +1082 -0
- package/src/streaming/backpressure-controller.ts +452 -0
- package/src/streaming/buffer-pool.ts +484 -0
- package/src/streaming/cdc-iceberg-connector.ts +605 -0
- package/src/streaming/index.ts +225 -0
- package/src/streaming/live-cdc-stream.ts +985 -0
- package/src/streaming/memory-bounded-stream.ts +443 -0
- package/src/streaming/query-streamer.ts +662 -0
- package/src/streaming/response-streaming.ts +557 -0
- package/src/types/branded.ts +1075 -0
- package/src/types/branded.ts.backup +273 -0
- package/src/types/utilities.ts +1023 -0
- package/src/types/wasm.d.ts +30 -0
- package/src/validation/typed-errors.test.ts +420 -0
- package/src/wal/replay-engine.ts +1264 -0
- package/src/worker/__mocks__/capnweb.ts +15 -0
- package/src/worker/__mocks__/pglite.data.ts +22 -0
- package/src/worker/__mocks__/pglite.wasm.ts +33 -0
- package/src/worker/auth-rate-limiter.test.ts +272 -0
- package/src/worker/auth-rate-limiter.ts +448 -0
- package/src/worker/auth.security-red.test.ts +1236 -0
- package/src/worker/auth.security.test.ts +822 -0
- package/src/worker/auth.test.ts +469 -0
- package/src/worker/auth.ts +1104 -0
- package/src/worker/cdc-backpressure.test.ts +726 -0
- package/src/worker/cdc-backpressure.ts +866 -0
- package/src/worker/cdc-sse.test.ts +780 -0
- package/src/worker/cdc-sse.ts +728 -0
- package/src/worker/cdc-websocket.ts +1229 -0
- package/src/worker/cdc-ws.test.ts +1009 -0
- package/src/worker/cdc.test.ts +327 -0
- package/src/worker/cdc.ts +289 -0
- package/src/worker/concerns/auth-concern.ts +179 -0
- package/src/worker/concerns/cdc-concern.ts +247 -0
- package/src/worker/concerns/index.ts +58 -0
- package/src/worker/concerns/query-execution-concern.ts +194 -0
- package/src/worker/concerns/storage-orchestration-concern.ts +373 -0
- package/src/worker/discriminated-types.test.ts +280 -0
- package/src/worker/do-auth-manager.ts +257 -0
- package/src/worker/do-decomposition.test.ts +1236 -0
- package/src/worker/do-pglite-manager.ts +302 -0
- package/src/worker/do.test.ts +2254 -0
- package/src/worker/do.ts +1878 -0
- package/src/worker/entry.ts +417 -0
- package/src/worker/errors.ts +285 -0
- package/src/worker/health-check-manager.test.ts +261 -0
- package/src/worker/health-check-manager.ts +231 -0
- package/src/worker/index.ts +389 -0
- package/src/worker/memory-pressure.test.ts +1460 -0
- package/src/worker/memory-pressure.ts +2650 -0
- package/src/worker/migration-manager.ts +582 -0
- package/src/worker/neon-compat.test.ts +332 -0
- package/src/worker/plugin-manager.ts +485 -0
- package/src/worker/postgres.do-rpc.d.ts +76 -0
- package/src/worker/proxy.ts +694 -0
- package/src/worker/query-execution-manager.test.ts +303 -0
- package/src/worker/query-execution-manager.ts +219 -0
- package/src/worker/query-executor.test.ts +282 -0
- package/src/worker/query-executor.ts +560 -0
- package/src/worker/query-stats-manager.ts +229 -0
- package/src/worker/result-handler.test.ts +364 -0
- package/src/worker/result-handler.ts +510 -0
- package/src/worker/routes.test.ts +795 -0
- package/src/worker/routes.ts +650 -0
- package/src/worker/rpc-methods-manager.test.ts +326 -0
- package/src/worker/rpc-methods-manager.ts +276 -0
- package/src/worker/rpc.ts +524 -0
- package/src/worker/schema-version.ts +605 -0
- package/src/worker/session-manager.test.ts +506 -0
- package/src/worker/session-manager.ts +732 -0
- package/src/worker/shutdown-manager.ts +469 -0
- package/src/worker/sql-transform.test.ts +286 -0
- package/src/worker/sql-transform.ts +368 -0
- package/src/worker/supabase-compat.test.ts +621 -0
- package/src/worker/types.test.ts +292 -0
- package/src/worker/types.ts +873 -0
- package/src/worker/user-routes.test.ts +703 -0
- package/src/worker/user-routes.ts +303 -0
- package/src/worker/wal-facade.ts +235 -0
- package/src/worker/wal-r2.test.ts +570 -0
- package/src/worker/wal-r2.ts +930 -0
- package/src/worker/wal-replay.test.ts +845 -0
- package/src/worker/wal-replay.ts +897 -0
- package/src/worker/wal-retention.test.ts +758 -0
- package/src/worker/wal-retention.ts +1075 -0
- package/src/worker/wal.test.ts +618 -0
- package/src/worker/wal.ts +697 -0
- package/src/worker/websocket.test.ts +296 -0
- package/src/worker/websocket.ts +284 -0
|
@@ -0,0 +1,1908 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 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
|
+
* This module provides a flexible tenant routing system that supports:
|
|
8
|
+
* - Subdomain-based tenancy (tenant1.app.com)
|
|
9
|
+
* - Path-based tenancy (/tenant1/api/...)
|
|
10
|
+
* - Header-based tenancy (X-Tenant-ID)
|
|
11
|
+
* - Custom extraction functions
|
|
12
|
+
* - Blocked tenant lists
|
|
13
|
+
* - Tenant isolation guarantees
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Standard HTTP header names used by the tenant router
|
|
22
|
+
*/
|
|
23
|
+
export const HEADERS = {
|
|
24
|
+
/** Header containing the tenant identifier */
|
|
25
|
+
TENANT_ID: 'X-Tenant-ID',
|
|
26
|
+
/** Header containing the correlation/request ID */
|
|
27
|
+
CORRELATION_ID: 'X-Correlation-ID',
|
|
28
|
+
/** Header containing the trace ID for distributed tracing */
|
|
29
|
+
TRACE_ID: 'X-Trace-ID',
|
|
30
|
+
/** Header containing the span ID for distributed tracing */
|
|
31
|
+
SPAN_ID: 'X-Span-ID',
|
|
32
|
+
/** Header containing the parent span ID for distributed tracing */
|
|
33
|
+
PARENT_SPAN_ID: 'X-Parent-Span-ID',
|
|
34
|
+
/** Header indicating whether the trace is sampled */
|
|
35
|
+
TRACE_SAMPLED: 'X-Trace-Sampled',
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
export type HeaderName = (typeof HEADERS)[keyof typeof HEADERS]
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_HEADER_NAME = HEADERS.TENANT_ID
|
|
41
|
+
export const DEFAULT_PATH_PREFIX = '/'
|
|
42
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
43
|
+
|
|
44
|
+
// Tenant ID validation constants
|
|
45
|
+
const TENANT_ID_MAX_LENGTH = 128
|
|
46
|
+
// Must start with alphanumeric, followed by alphanumeric, underscore, or hyphen
|
|
47
|
+
const TENANT_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a path prefix to ensure consistent format.
|
|
51
|
+
* - Ensures path starts with '/'
|
|
52
|
+
* - Removes trailing '/' (except for root '/')
|
|
53
|
+
*
|
|
54
|
+
* @param prefix The path prefix to normalize
|
|
55
|
+
* @returns Normalized path prefix
|
|
56
|
+
*/
|
|
57
|
+
function normalizePathPrefix(prefix: string): string {
|
|
58
|
+
let normalized = prefix
|
|
59
|
+
if (!normalized.startsWith('/')) {
|
|
60
|
+
normalized = '/' + normalized
|
|
61
|
+
}
|
|
62
|
+
if (normalized.endsWith('/') && normalized.length > 1) {
|
|
63
|
+
normalized = normalized.slice(0, -1)
|
|
64
|
+
}
|
|
65
|
+
return normalized
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate that a tenant ID is safe and well-formed.
|
|
70
|
+
*
|
|
71
|
+
* Security requirements:
|
|
72
|
+
* - Non-empty and within length limits
|
|
73
|
+
* - Starts with alphanumeric character
|
|
74
|
+
* - Contains only alphanumeric, underscore, or hyphen
|
|
75
|
+
* - No control characters (including null bytes)
|
|
76
|
+
* - No path traversal sequences
|
|
77
|
+
*
|
|
78
|
+
* @param tenantId The tenant ID to validate
|
|
79
|
+
* @returns true if the tenant ID is valid, false otherwise
|
|
80
|
+
*/
|
|
81
|
+
export function isValidTenantId(tenantId: string): boolean {
|
|
82
|
+
// Check length bounds
|
|
83
|
+
if (tenantId.length === 0 || tenantId.length > TENANT_ID_MAX_LENGTH) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check pattern (alphanumeric start, alphanumeric/underscore/hyphen continuation)
|
|
88
|
+
if (!TENANT_ID_PATTERN.test(tenantId)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for control characters (ASCII 0-31 and 127)
|
|
93
|
+
for (let i = 0; i < tenantId.length; i++) {
|
|
94
|
+
const charCode = tenantId.charCodeAt(i)
|
|
95
|
+
if (charCode < 32 || charCode === 127) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Core Types
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Source of tenant identification
|
|
109
|
+
*/
|
|
110
|
+
export type TenantExtractionSource = 'subdomain' | 'path' | 'header' | 'custom'
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Result of tenant extraction from a request
|
|
114
|
+
*/
|
|
115
|
+
export interface TenantExtractionResult {
|
|
116
|
+
/** The extracted tenant identifier, or null if extraction failed */
|
|
117
|
+
tenantId: string | null
|
|
118
|
+
/** How the tenant was identified */
|
|
119
|
+
source: TenantExtractionSource
|
|
120
|
+
/** Original hostname from the request */
|
|
121
|
+
originalHostname: string
|
|
122
|
+
/** Modified path (with tenant prefix stripped for path-based extraction) */
|
|
123
|
+
modifiedPath?: string | undefined
|
|
124
|
+
/** Additional metadata from custom extractors */
|
|
125
|
+
metadata?: Record<string, unknown> | undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Routing metadata about how the tenant was resolved
|
|
130
|
+
*/
|
|
131
|
+
export interface RoutingMetadata {
|
|
132
|
+
/** Which extractor type was used */
|
|
133
|
+
extractorType: TenantExtractionSource
|
|
134
|
+
/** Whether the tenant was found in cache (if caching is enabled) */
|
|
135
|
+
cached?: boolean
|
|
136
|
+
/** The DO ID that was generated */
|
|
137
|
+
doId: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Timing information for the routing operation
|
|
142
|
+
*/
|
|
143
|
+
export interface RoutingTiming {
|
|
144
|
+
/** Time taken to extract tenant from request (ms) */
|
|
145
|
+
extractionTimeMs: number
|
|
146
|
+
/** Time taken to route to DO (ms) */
|
|
147
|
+
routingTimeMs: number
|
|
148
|
+
/** Total time for the entire operation (ms) */
|
|
149
|
+
totalTimeMs: number
|
|
150
|
+
/** Timestamp when routing started */
|
|
151
|
+
startTimestamp: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Tenant context information
|
|
156
|
+
*/
|
|
157
|
+
export interface TenantContext {
|
|
158
|
+
/** The tenant identifier */
|
|
159
|
+
tenantId: string
|
|
160
|
+
/** How the tenant was identified */
|
|
161
|
+
source: TenantExtractionSource
|
|
162
|
+
/** Additional metadata from extraction */
|
|
163
|
+
metadata?: Record<string, unknown> | undefined
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Result of routing a request to a tenant's DO
|
|
168
|
+
*/
|
|
169
|
+
export interface TenantRoutingResult {
|
|
170
|
+
/** The response from the DO */
|
|
171
|
+
response: Response
|
|
172
|
+
/** Routing metadata about how tenant was resolved */
|
|
173
|
+
routingMetadata: RoutingMetadata
|
|
174
|
+
/** Timing information for the operation */
|
|
175
|
+
timing: RoutingTiming
|
|
176
|
+
/** Tenant context information */
|
|
177
|
+
tenantContext: TenantContext
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Function signature for custom tenant extraction
|
|
182
|
+
*/
|
|
183
|
+
export type TenantExtractor = (
|
|
184
|
+
request: Request
|
|
185
|
+
) => TenantExtractionResult | Promise<TenantExtractionResult>
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Simplified extractor that just returns tenant ID or null
|
|
189
|
+
*/
|
|
190
|
+
export type SimpleTenantExtractor = (request: Request) => string | null | Promise<string | null>
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Preset extractor types
|
|
194
|
+
*/
|
|
195
|
+
export type PresetExtractorType = 'subdomain' | 'path' | 'header'
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Callback for dynamic blocked tenant checks
|
|
199
|
+
*/
|
|
200
|
+
export type BlockedTenantCallback = (
|
|
201
|
+
tenantId: string,
|
|
202
|
+
request: Request
|
|
203
|
+
) => boolean | Promise<boolean>
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Function to transform tenant IDs before routing
|
|
207
|
+
*/
|
|
208
|
+
export type TenantTransformer = (tenantId: string) => string
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Custom error response formatter
|
|
212
|
+
*/
|
|
213
|
+
export type ErrorResponseFormatter = (
|
|
214
|
+
status: number,
|
|
215
|
+
message: string,
|
|
216
|
+
correlationId: string,
|
|
217
|
+
tenantId?: string
|
|
218
|
+
) => Response
|
|
219
|
+
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// Observability Types
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Log levels for structured logging
|
|
226
|
+
*/
|
|
227
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Structured log entry
|
|
231
|
+
*/
|
|
232
|
+
export interface LogEntry {
|
|
233
|
+
level: LogLevel
|
|
234
|
+
message: string
|
|
235
|
+
tenantId?: string
|
|
236
|
+
correlationId?: string
|
|
237
|
+
timestamp: number
|
|
238
|
+
data?: Record<string, unknown>
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Logger interface for observability
|
|
243
|
+
*/
|
|
244
|
+
export interface TenantRouterLogger {
|
|
245
|
+
log(entry: LogEntry): void
|
|
246
|
+
debug(message: string, data?: Record<string, unknown>): void
|
|
247
|
+
info(message: string, data?: Record<string, unknown>): void
|
|
248
|
+
warn(message: string, data?: Record<string, unknown>): void
|
|
249
|
+
error(message: string, data?: Record<string, unknown>): void
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Metrics data for a single request
|
|
254
|
+
*/
|
|
255
|
+
export interface RequestMetrics {
|
|
256
|
+
tenantId: string
|
|
257
|
+
method: string
|
|
258
|
+
path: string
|
|
259
|
+
status: number
|
|
260
|
+
routingLatencyMs: number
|
|
261
|
+
doResponseTimeMs: number
|
|
262
|
+
timestamp: number
|
|
263
|
+
correlationId: string
|
|
264
|
+
traceId?: string | undefined
|
|
265
|
+
spanId?: string | undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Metrics collector interface for observability
|
|
270
|
+
*/
|
|
271
|
+
export interface MetricsCollector {
|
|
272
|
+
/**
|
|
273
|
+
* Record metrics for a routed request
|
|
274
|
+
*/
|
|
275
|
+
recordRequest(metrics: RequestMetrics): void
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Increment request count for a tenant
|
|
279
|
+
*/
|
|
280
|
+
incrementRequestCount(tenantId: string): void
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Record routing latency
|
|
284
|
+
*/
|
|
285
|
+
recordLatency(tenantId: string, latencyMs: number): void
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Record DO response time
|
|
289
|
+
*/
|
|
290
|
+
recordDoResponseTime(tenantId: string, responseTimeMs: number): void
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Trace context for distributed tracing
|
|
295
|
+
*/
|
|
296
|
+
export interface TraceContext {
|
|
297
|
+
traceId: string
|
|
298
|
+
spanId: string
|
|
299
|
+
parentSpanId?: string
|
|
300
|
+
sampled?: boolean
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Function to extract trace context from request
|
|
305
|
+
*/
|
|
306
|
+
export type TraceContextExtractor = (request: Request) => TraceContext | null
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Function to generate a new span ID
|
|
310
|
+
*/
|
|
311
|
+
export type SpanIdGenerator = () => string
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// Configuration Sub-interfaces
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Core routing configuration
|
|
319
|
+
*/
|
|
320
|
+
export interface RoutingConfig {
|
|
321
|
+
/** The Durable Object namespace to route requests to */
|
|
322
|
+
doNamespace: DurableObjectNamespace
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* How to extract tenant from requests:
|
|
326
|
+
* - 'subdomain': Extract from hostname subdomain
|
|
327
|
+
* - 'path': Extract from URL path
|
|
328
|
+
* - 'header': Extract from HTTP header
|
|
329
|
+
* - TenantExtractor: Custom extraction function
|
|
330
|
+
*/
|
|
331
|
+
extractTenant: TenantExtractor | SimpleTenantExtractor | PresetExtractorType
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Base domain for subdomain extraction (e.g., 'app.com')
|
|
335
|
+
* Required for proper subdomain extraction with multi-level TLDs
|
|
336
|
+
*/
|
|
337
|
+
baseDomain?: string
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Path prefix for path-based extraction
|
|
341
|
+
* e.g., '/orgs' would extract from /orgs/{tenant}/...
|
|
342
|
+
* @default '/'
|
|
343
|
+
*/
|
|
344
|
+
pathPrefix?: string
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Header name for header-based extraction
|
|
348
|
+
* @default 'X-Tenant-ID'
|
|
349
|
+
*/
|
|
350
|
+
headerName?: string
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Request timeout in milliseconds
|
|
354
|
+
* @default 30000
|
|
355
|
+
*/
|
|
356
|
+
requestTimeoutMs?: number
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* List of blocked tenant identifiers
|
|
360
|
+
* Supports exact matches and wildcard patterns (e.g., 'admin*')
|
|
361
|
+
*/
|
|
362
|
+
blockedTenants?: string[]
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Dynamic callback for checking if a tenant is blocked
|
|
366
|
+
*/
|
|
367
|
+
blockedTenantCallback?: BlockedTenantCallback
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Custom error response formatter
|
|
371
|
+
*/
|
|
372
|
+
errorResponseFormatter?: ErrorResponseFormatter
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Observability configuration
|
|
377
|
+
*/
|
|
378
|
+
export interface ObservabilityConfig {
|
|
379
|
+
/** Metrics collector for recording request metrics */
|
|
380
|
+
metricsCollector?: MetricsCollector
|
|
381
|
+
|
|
382
|
+
/** Logger for structured logging */
|
|
383
|
+
logger?: TenantRouterLogger
|
|
384
|
+
|
|
385
|
+
/** Function to extract trace context from incoming requests */
|
|
386
|
+
traceContextExtractor?: TraceContextExtractor
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Whether to propagate trace context to forwarded requests
|
|
390
|
+
* @default true
|
|
391
|
+
*/
|
|
392
|
+
propagateTraceContext?: boolean
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Rate limiting configuration
|
|
397
|
+
*/
|
|
398
|
+
export interface RateLimitingConfig {
|
|
399
|
+
/**
|
|
400
|
+
* Enable rate limiting
|
|
401
|
+
* @default false
|
|
402
|
+
*/
|
|
403
|
+
enabled?: boolean
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Maximum requests allowed within the window
|
|
407
|
+
*/
|
|
408
|
+
maxRequests?: number
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Time window in milliseconds
|
|
412
|
+
* @default 60000 (1 minute)
|
|
413
|
+
*/
|
|
414
|
+
windowMs?: number
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* List of tenant IDs that bypass rate limiting
|
|
418
|
+
*/
|
|
419
|
+
bypassTenants?: string[]
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Dynamic callback for checking if a tenant should bypass rate limiting
|
|
423
|
+
*/
|
|
424
|
+
bypassCallback?: (tenantId: string, request: Request) => boolean | Promise<boolean>
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Domain caching configuration
|
|
429
|
+
*/
|
|
430
|
+
export interface DomainCachingConfig {
|
|
431
|
+
/**
|
|
432
|
+
* Enable domain mapping cache
|
|
433
|
+
* @default false
|
|
434
|
+
*/
|
|
435
|
+
enabled?: boolean
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Cache TTL in milliseconds
|
|
439
|
+
* @default 300000 (5 minutes)
|
|
440
|
+
*/
|
|
441
|
+
ttlMs?: number
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Maximum number of cache entries
|
|
445
|
+
* @default 10000
|
|
446
|
+
*/
|
|
447
|
+
maxEntries?: number
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Complete tenant router configuration
|
|
452
|
+
* Extends RoutingConfig with nested sub-configs for better organization
|
|
453
|
+
*/
|
|
454
|
+
export interface TenantRouterConfig extends RoutingConfig {
|
|
455
|
+
/**
|
|
456
|
+
* Whether to strip the tenant segment from the forwarded path
|
|
457
|
+
* @default true
|
|
458
|
+
*/
|
|
459
|
+
stripTenantFromPath?: boolean
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Transform tenant ID before routing (e.g., lowercase normalization)
|
|
463
|
+
*/
|
|
464
|
+
transformTenant?: TenantTransformer
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Ordered list of extraction strategies to try
|
|
468
|
+
* First successful extraction wins
|
|
469
|
+
*/
|
|
470
|
+
extractionOrder?: PresetExtractorType[]
|
|
471
|
+
|
|
472
|
+
/** Observability settings */
|
|
473
|
+
observability?: ObservabilityConfig
|
|
474
|
+
|
|
475
|
+
/** Rate limiting settings */
|
|
476
|
+
rateLimit?: RateLimitingConfig
|
|
477
|
+
|
|
478
|
+
/** Domain caching settings */
|
|
479
|
+
domainCache?: DomainCachingConfig
|
|
480
|
+
|
|
481
|
+
// =============================================================================
|
|
482
|
+
// Legacy flat config options (deprecated, use nested configs instead)
|
|
483
|
+
// =============================================================================
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @deprecated Use blockedTenantCallback instead
|
|
487
|
+
* Dynamic callback for checking if a tenant is blocked
|
|
488
|
+
*/
|
|
489
|
+
isBlocked?: BlockedTenantCallback
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @deprecated Use errorResponseFormatter instead
|
|
493
|
+
* Custom error response formatter
|
|
494
|
+
*/
|
|
495
|
+
formatError?: ErrorResponseFormatter
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @deprecated Use domainCache.ttlMs instead
|
|
499
|
+
* Cache TTL for custom domain mappings in milliseconds
|
|
500
|
+
* @default 300000 (5 minutes)
|
|
501
|
+
*/
|
|
502
|
+
domainCacheTtlMs?: number
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @deprecated Use rateLimit.maxRequests instead (with windowMs: 60000)
|
|
506
|
+
* Per-tenant rate limit (requests per minute)
|
|
507
|
+
* Set to 0 to disable rate limiting
|
|
508
|
+
* @default 0
|
|
509
|
+
*/
|
|
510
|
+
rateLimitPerMinute?: number
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @deprecated Use observability.metricsCollector instead
|
|
514
|
+
* Metrics collector for recording request metrics
|
|
515
|
+
*/
|
|
516
|
+
metricsCollector?: MetricsCollector
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @deprecated Use observability.logger instead
|
|
520
|
+
* Logger for structured logging
|
|
521
|
+
*/
|
|
522
|
+
logger?: TenantRouterLogger
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @deprecated Use observability.traceContextExtractor instead
|
|
526
|
+
* Function to extract trace context from incoming requests
|
|
527
|
+
*/
|
|
528
|
+
traceContextExtractor?: TraceContextExtractor
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @deprecated Use observability.propagateTraceContext instead
|
|
532
|
+
* Whether to propagate trace context to forwarded requests
|
|
533
|
+
* @default true
|
|
534
|
+
*/
|
|
535
|
+
propagateTraceContext?: boolean
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// =============================================================================
|
|
539
|
+
// Router Interface
|
|
540
|
+
// =============================================================================
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The TenantRouter interface for routing requests to tenant-specific DOs
|
|
544
|
+
*/
|
|
545
|
+
export interface TenantRouter {
|
|
546
|
+
/**
|
|
547
|
+
* Route a request to the appropriate tenant's Durable Object
|
|
548
|
+
* @param request The incoming request
|
|
549
|
+
* @returns Response from the tenant's DO
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
553
|
+
* export default {
|
|
554
|
+
* async fetch(request: Request, env: Env) {
|
|
555
|
+
* const router = createTenantRouter({
|
|
556
|
+
* doNamespace: env.TENANT_DO,
|
|
557
|
+
* extractTenant: 'subdomain',
|
|
558
|
+
* baseDomain: 'myapp.com',
|
|
559
|
+
* })
|
|
560
|
+
*
|
|
561
|
+
* // Route request to tenant's DO
|
|
562
|
+
* return router.route(request)
|
|
563
|
+
* }
|
|
564
|
+
* }
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
route(request: Request): Promise<Response>
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Route a request and return detailed routing result
|
|
571
|
+
* @param request The incoming request
|
|
572
|
+
* @returns Full routing result with response, metadata, timing, and context
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```typescript
|
|
576
|
+
* const result = await router.routeWithResult(request)
|
|
577
|
+
*
|
|
578
|
+
* console.log(`Tenant: ${result.tenantContext.tenantId}`)
|
|
579
|
+
* console.log(`Source: ${result.routingMetadata.extractorType}`)
|
|
580
|
+
* console.log(`Total time: ${result.timing.totalTimeMs}ms`)
|
|
581
|
+
*
|
|
582
|
+
* return result.response
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
routeWithResult(request: Request): Promise<TenantRoutingResult>
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Extract tenant ID from a request without routing
|
|
589
|
+
* @param request The incoming request
|
|
590
|
+
* @returns The tenant ID or null if not found
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* const tenantId = await router.getTenantId(request)
|
|
595
|
+
* if (!tenantId) {
|
|
596
|
+
* return new Response('Tenant not found', { status: 400 })
|
|
597
|
+
* }
|
|
598
|
+
* console.log(`Processing request for tenant: ${tenantId}`)
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
getTenantId(request: Request): Promise<string | null>
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get full extraction result from a request
|
|
605
|
+
* @param request The incoming request
|
|
606
|
+
* @returns The extraction result with metadata
|
|
607
|
+
*/
|
|
608
|
+
extractTenant(request: Request): Promise<TenantExtractionResult>
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Check if a tenant is blocked
|
|
612
|
+
* @param tenantId The tenant identifier
|
|
613
|
+
* @param request The original request (for context in dynamic checks)
|
|
614
|
+
* @returns True if the tenant is blocked
|
|
615
|
+
*/
|
|
616
|
+
isBlocked(tenantId: string, request: Request): Promise<boolean>
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Get the DO stub for a specific tenant
|
|
620
|
+
* @param tenantId The tenant identifier
|
|
621
|
+
* @returns The Durable Object stub
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```typescript
|
|
625
|
+
* const stub = router.getStub('acme-corp')
|
|
626
|
+
*
|
|
627
|
+
* // Make a custom request to the tenant's DO
|
|
628
|
+
* const response = await stub.fetch('https://internal/api/health')
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
getStub(tenantId: string): DurableObjectStub
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// =============================================================================
|
|
635
|
+
// Preset Extractors
|
|
636
|
+
// =============================================================================
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Preset tenant extraction functions
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```typescript
|
|
643
|
+
* // Use preset extractors with createTenantRouter
|
|
644
|
+
* const router = createTenantRouter({
|
|
645
|
+
* doNamespace: env.TENANT_DO,
|
|
646
|
+
* extractTenant: 'subdomain', // Uses extractors.subdomain internally
|
|
647
|
+
* baseDomain: 'myapp.com',
|
|
648
|
+
* })
|
|
649
|
+
*
|
|
650
|
+
* // Or use extractors directly
|
|
651
|
+
* const subdomainExtractor = extractors.subdomain('myapp.com')
|
|
652
|
+
* const tenantId = subdomainExtractor(request) // 'acme' from acme.myapp.com
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
export const extractors = {
|
|
656
|
+
/**
|
|
657
|
+
* Extract tenant from subdomain
|
|
658
|
+
* e.g., tenant1.app.com -> tenant1
|
|
659
|
+
*
|
|
660
|
+
* @param baseDomain Optional base domain for proper TLD handling
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```typescript
|
|
664
|
+
* const extractor = extractors.subdomain('myapp.com')
|
|
665
|
+
*
|
|
666
|
+
* // acme.myapp.com -> 'acme'
|
|
667
|
+
* // www.myapp.com -> null (apex domain)
|
|
668
|
+
* // myapp.com -> null (apex domain)
|
|
669
|
+
* ```
|
|
670
|
+
*/
|
|
671
|
+
subdomain:
|
|
672
|
+
(baseDomain?: string): SimpleTenantExtractor =>
|
|
673
|
+
(request: Request): string | null => {
|
|
674
|
+
const url = new URL(request.url)
|
|
675
|
+
let hostname = url.hostname.toLowerCase()
|
|
676
|
+
|
|
677
|
+
// Handle IP addresses - return null
|
|
678
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
|
|
679
|
+
return null
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Remove port if present (for localhost:8787 cases)
|
|
683
|
+
hostname = hostname.split(':')[0] ?? hostname
|
|
684
|
+
|
|
685
|
+
// If baseDomain is provided, use it to determine subdomain
|
|
686
|
+
if (baseDomain) {
|
|
687
|
+
const normalizedBaseDomain = baseDomain.toLowerCase()
|
|
688
|
+
// Check if hostname ends with the base domain
|
|
689
|
+
if (hostname === normalizedBaseDomain) {
|
|
690
|
+
// This is the apex domain
|
|
691
|
+
return null
|
|
692
|
+
}
|
|
693
|
+
if (hostname.endsWith('.' + normalizedBaseDomain)) {
|
|
694
|
+
// Extract subdomain part
|
|
695
|
+
const subdomainPart = hostname.slice(0, -(normalizedBaseDomain.length + 1))
|
|
696
|
+
const segments = subdomainPart.split('.')
|
|
697
|
+
// Handle www prefix
|
|
698
|
+
let firstSegment = segments[0]
|
|
699
|
+
if (firstSegment === 'www' && segments.length > 1) {
|
|
700
|
+
firstSegment = segments[1]
|
|
701
|
+
}
|
|
702
|
+
return firstSegment || null
|
|
703
|
+
}
|
|
704
|
+
// Hostname doesn't match base domain
|
|
705
|
+
return null
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Without baseDomain, parse generically
|
|
709
|
+
const parts = hostname.split('.')
|
|
710
|
+
|
|
711
|
+
// Handle localhost
|
|
712
|
+
if (parts.length === 1) {
|
|
713
|
+
return null // Just "localhost" or similar
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Handle common patterns
|
|
717
|
+
// For two-part domains like "app.com", return null (apex)
|
|
718
|
+
if (parts.length === 2) {
|
|
719
|
+
return null
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// For three or more parts, first segment is tenant (unless it's www)
|
|
723
|
+
let tenantSegment = parts[0]
|
|
724
|
+
if (tenantSegment === 'www' && parts.length > 3) {
|
|
725
|
+
tenantSegment = parts[1]
|
|
726
|
+
} else if (tenantSegment === 'www') {
|
|
727
|
+
return null // www.app.com is still apex
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return tenantSegment || null
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Extract tenant from URL path
|
|
735
|
+
* e.g., /tenant1/api/users -> tenant1
|
|
736
|
+
*
|
|
737
|
+
* @param prefix Path prefix before tenant segment
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```typescript
|
|
741
|
+
* const extractor = extractors.path('/orgs')
|
|
742
|
+
*
|
|
743
|
+
* // /orgs/acme/users -> 'acme'
|
|
744
|
+
* // /orgs/bigcorp/api/data -> 'bigcorp'
|
|
745
|
+
* // /api/users -> null (no match)
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
path:
|
|
749
|
+
(prefix: string = '/'): SimpleTenantExtractor =>
|
|
750
|
+
(request: Request): string | null => {
|
|
751
|
+
const url = new URL(request.url)
|
|
752
|
+
let pathname = url.pathname
|
|
753
|
+
|
|
754
|
+
const normalizedPrefix = normalizePathPrefix(prefix)
|
|
755
|
+
|
|
756
|
+
// Check if pathname starts with the prefix
|
|
757
|
+
if (normalizedPrefix !== '/' && !pathname.startsWith(normalizedPrefix)) {
|
|
758
|
+
return null
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Get the path after the prefix
|
|
762
|
+
let pathAfterPrefix: string
|
|
763
|
+
if (normalizedPrefix === '/') {
|
|
764
|
+
pathAfterPrefix = pathname
|
|
765
|
+
} else {
|
|
766
|
+
pathAfterPrefix = pathname.slice(normalizedPrefix.length)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// pathAfterPrefix should now start with / or be empty
|
|
770
|
+
if (!pathAfterPrefix || pathAfterPrefix === '/') {
|
|
771
|
+
return null
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Remove leading slash
|
|
775
|
+
if (pathAfterPrefix.startsWith('/')) {
|
|
776
|
+
pathAfterPrefix = pathAfterPrefix.slice(1)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Get the first segment (tenant)
|
|
780
|
+
const slashIndex = pathAfterPrefix.indexOf('/')
|
|
781
|
+
let tenantSegment: string
|
|
782
|
+
if (slashIndex === -1) {
|
|
783
|
+
tenantSegment = pathAfterPrefix
|
|
784
|
+
} else {
|
|
785
|
+
tenantSegment = pathAfterPrefix.slice(0, slashIndex)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Decode URL encoding
|
|
789
|
+
try {
|
|
790
|
+
tenantSegment = decodeURIComponent(tenantSegment)
|
|
791
|
+
} catch {
|
|
792
|
+
// If decoding fails, use as-is
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Return null for empty string
|
|
796
|
+
if (!tenantSegment) {
|
|
797
|
+
return null
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return tenantSegment
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Extract tenant from HTTP header
|
|
805
|
+
* e.g., X-Tenant-ID: tenant1 -> tenant1
|
|
806
|
+
*
|
|
807
|
+
* @param headerName The header to extract from
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* ```typescript
|
|
811
|
+
* const extractor = extractors.header('X-Org-ID')
|
|
812
|
+
*
|
|
813
|
+
* // Request with header 'X-Org-ID: acme' -> 'acme'
|
|
814
|
+
* // Request without header -> null
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
header:
|
|
818
|
+
(headerName: string = DEFAULT_HEADER_NAME): SimpleTenantExtractor =>
|
|
819
|
+
(request: Request): string | null => {
|
|
820
|
+
// Headers.get() is case-insensitive per HTTP spec
|
|
821
|
+
const value = request.headers.get(headerName)
|
|
822
|
+
|
|
823
|
+
if (value === null) {
|
|
824
|
+
return null
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Trim whitespace
|
|
828
|
+
const trimmed = value.trim()
|
|
829
|
+
|
|
830
|
+
// Return null for empty string
|
|
831
|
+
if (!trimmed) {
|
|
832
|
+
return null
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return trimmed
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Try multiple extractors in order
|
|
840
|
+
* Returns first successful extraction
|
|
841
|
+
*
|
|
842
|
+
* @param extractorFns Array of extractors to try
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```typescript
|
|
846
|
+
* // Try subdomain first, then fall back to header
|
|
847
|
+
* const extractor = extractors.combined([
|
|
848
|
+
* extractors.subdomain('myapp.com'),
|
|
849
|
+
* extractors.header('X-Tenant-ID'),
|
|
850
|
+
* ])
|
|
851
|
+
*
|
|
852
|
+
* // acme.myapp.com -> 'acme' (from subdomain)
|
|
853
|
+
* // myapp.com with X-Tenant-ID: bigcorp -> 'bigcorp' (from header)
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
combined:
|
|
857
|
+
(extractorFns: SimpleTenantExtractor[]): SimpleTenantExtractor =>
|
|
858
|
+
async (request: Request): Promise<string | null> => {
|
|
859
|
+
for (const extractor of extractorFns) {
|
|
860
|
+
const result = await extractor(request)
|
|
861
|
+
if (result !== null) {
|
|
862
|
+
return result
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null
|
|
866
|
+
},
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// =============================================================================
|
|
870
|
+
// Factory Function
|
|
871
|
+
// =============================================================================
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Create a new TenantRouter instance
|
|
875
|
+
*
|
|
876
|
+
* @param config Router configuration
|
|
877
|
+
* @returns A configured TenantRouter
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```typescript
|
|
881
|
+
* // Subdomain-based routing
|
|
882
|
+
* const router = createTenantRouter({
|
|
883
|
+
* doNamespace: env.TENANT_DO,
|
|
884
|
+
* extractTenant: 'subdomain',
|
|
885
|
+
* baseDomain: 'myapp.com',
|
|
886
|
+
* })
|
|
887
|
+
*
|
|
888
|
+
* // Path-based routing
|
|
889
|
+
* const router = createTenantRouter({
|
|
890
|
+
* doNamespace: env.TENANT_DO,
|
|
891
|
+
* extractTenant: 'path',
|
|
892
|
+
* pathPrefix: '/api/tenants',
|
|
893
|
+
* })
|
|
894
|
+
*
|
|
895
|
+
* // Header-based routing
|
|
896
|
+
* const router = createTenantRouter({
|
|
897
|
+
* doNamespace: env.TENANT_DO,
|
|
898
|
+
* extractTenant: 'header',
|
|
899
|
+
* headerName: 'X-Org-ID',
|
|
900
|
+
* })
|
|
901
|
+
*
|
|
902
|
+
* // Custom extractor
|
|
903
|
+
* const router = createTenantRouter({
|
|
904
|
+
* doNamespace: env.TENANT_DO,
|
|
905
|
+
* extractTenant: (req) => {
|
|
906
|
+
* const url = new URL(req.url)
|
|
907
|
+
* return url.searchParams.get('tenant')
|
|
908
|
+
* },
|
|
909
|
+
* })
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
export function createTenantRouter(config: TenantRouterConfig): TenantRouter {
|
|
913
|
+
const {
|
|
914
|
+
doNamespace,
|
|
915
|
+
extractTenant,
|
|
916
|
+
headerName = DEFAULT_HEADER_NAME,
|
|
917
|
+
pathPrefix = DEFAULT_PATH_PREFIX,
|
|
918
|
+
stripTenantFromPath = true,
|
|
919
|
+
blockedTenants = [],
|
|
920
|
+
blockedTenantCallback,
|
|
921
|
+
isBlocked: legacyIsBlockedCallback, // deprecated
|
|
922
|
+
transformTenant,
|
|
923
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
924
|
+
errorResponseFormatter,
|
|
925
|
+
formatError: legacyFormatError, // deprecated
|
|
926
|
+
baseDomain,
|
|
927
|
+
// Nested sub-configs
|
|
928
|
+
observability,
|
|
929
|
+
rateLimit: _rateLimit, // reserved for future use
|
|
930
|
+
domainCache: _domainCache, // reserved for future use
|
|
931
|
+
// Legacy flat observability options (deprecated)
|
|
932
|
+
metricsCollector: legacyMetricsCollector,
|
|
933
|
+
logger: legacyLogger,
|
|
934
|
+
traceContextExtractor: legacyTraceContextExtractor,
|
|
935
|
+
propagateTraceContext: legacyPropagateTraceContext,
|
|
936
|
+
} = config
|
|
937
|
+
|
|
938
|
+
// Resolve observability config: prefer nested config, fall back to legacy flat options
|
|
939
|
+
const metricsCollector = observability?.metricsCollector ?? legacyMetricsCollector
|
|
940
|
+
const logger = observability?.logger ?? legacyLogger
|
|
941
|
+
const traceContextExtractor = observability?.traceContextExtractor ?? legacyTraceContextExtractor
|
|
942
|
+
const propagateTraceContext = observability?.propagateTraceContext ?? legacyPropagateTraceContext ?? true
|
|
943
|
+
|
|
944
|
+
// Resolve blocked tenant callback: prefer new name, fall back to legacy
|
|
945
|
+
const isBlockedCallback = blockedTenantCallback ?? legacyIsBlockedCallback
|
|
946
|
+
|
|
947
|
+
// Resolve error formatter: prefer new name, fall back to legacy
|
|
948
|
+
const formatError = errorResponseFormatter ?? legacyFormatError
|
|
949
|
+
|
|
950
|
+
// Build the tenant extractor function
|
|
951
|
+
let tenantExtractor: TenantExtractor | SimpleTenantExtractor
|
|
952
|
+
|
|
953
|
+
if (typeof extractTenant === 'string') {
|
|
954
|
+
// Preset extractor type
|
|
955
|
+
switch (extractTenant) {
|
|
956
|
+
case 'subdomain':
|
|
957
|
+
tenantExtractor = extractors.subdomain(baseDomain)
|
|
958
|
+
break
|
|
959
|
+
case 'path':
|
|
960
|
+
tenantExtractor = extractors.path(pathPrefix)
|
|
961
|
+
break
|
|
962
|
+
case 'header':
|
|
963
|
+
tenantExtractor = extractors.header(headerName)
|
|
964
|
+
break
|
|
965
|
+
default:
|
|
966
|
+
throw new Error(`Invalid extractTenant value: ${extractTenant}`)
|
|
967
|
+
}
|
|
968
|
+
} else if (typeof extractTenant === 'function') {
|
|
969
|
+
tenantExtractor = extractTenant
|
|
970
|
+
} else {
|
|
971
|
+
throw new Error('extractTenant must be a string or function')
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Helper to generate correlation ID
|
|
975
|
+
function generateCorrelationId(): string {
|
|
976
|
+
return crypto.randomUUID()
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Helper to check if tenant matches a blocked pattern
|
|
980
|
+
function matchesBlockedPattern(tenantId: string, pattern: string): boolean {
|
|
981
|
+
if (pattern.endsWith('*')) {
|
|
982
|
+
const prefix = pattern.slice(0, -1)
|
|
983
|
+
return tenantId.startsWith(prefix)
|
|
984
|
+
}
|
|
985
|
+
return tenantId === pattern
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Default error formatter
|
|
989
|
+
function defaultFormatError(
|
|
990
|
+
status: number,
|
|
991
|
+
message: string,
|
|
992
|
+
correlationId: string,
|
|
993
|
+
_tenantId?: string
|
|
994
|
+
): Response {
|
|
995
|
+
return new Response(
|
|
996
|
+
JSON.stringify({
|
|
997
|
+
error: message,
|
|
998
|
+
correlationId,
|
|
999
|
+
}),
|
|
1000
|
+
{
|
|
1001
|
+
status,
|
|
1002
|
+
headers: {
|
|
1003
|
+
'Content-Type': 'application/json',
|
|
1004
|
+
[HEADERS.CORRELATION_ID]: correlationId,
|
|
1005
|
+
},
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const errorFormatter = formatError || defaultFormatError
|
|
1011
|
+
|
|
1012
|
+
// Implementation of the TenantRouter interface
|
|
1013
|
+
const router: TenantRouter = {
|
|
1014
|
+
async extractTenant(request: Request): Promise<TenantExtractionResult> {
|
|
1015
|
+
const url = new URL(request.url)
|
|
1016
|
+
const originalHostname = url.hostname
|
|
1017
|
+
|
|
1018
|
+
// Determine extraction source based on config
|
|
1019
|
+
let source: TenantExtractionResult['source'] = 'custom'
|
|
1020
|
+
if (typeof extractTenant === 'string') {
|
|
1021
|
+
source = extractTenant as TenantExtractionResult['source']
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Call the extractor
|
|
1025
|
+
let extractionResult = await tenantExtractor(request)
|
|
1026
|
+
|
|
1027
|
+
// Check if it's a full TenantExtractionResult or just a string/null
|
|
1028
|
+
if (
|
|
1029
|
+
extractionResult !== null &&
|
|
1030
|
+
typeof extractionResult === 'object' &&
|
|
1031
|
+
'tenantId' in extractionResult
|
|
1032
|
+
) {
|
|
1033
|
+
// It's a full TenantExtractionResult
|
|
1034
|
+
return extractionResult as TenantExtractionResult
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// It's a SimpleTenantExtractor result (string | null)
|
|
1038
|
+
let tenantId = extractionResult as string | null
|
|
1039
|
+
|
|
1040
|
+
// Apply transformation if configured
|
|
1041
|
+
if (tenantId !== null && transformTenant) {
|
|
1042
|
+
tenantId = transformTenant(tenantId)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Build modified path for path-based extraction
|
|
1046
|
+
let modifiedPath: string | undefined
|
|
1047
|
+
if (source === 'path' && tenantId !== null && stripTenantFromPath) {
|
|
1048
|
+
let pathname = url.pathname
|
|
1049
|
+
const normalizedPrefix = normalizePathPrefix(pathPrefix)
|
|
1050
|
+
|
|
1051
|
+
// Remove prefix and tenant segment from path
|
|
1052
|
+
let pathAfterPrefix: string
|
|
1053
|
+
if (normalizedPrefix === '/') {
|
|
1054
|
+
pathAfterPrefix = pathname
|
|
1055
|
+
} else {
|
|
1056
|
+
pathAfterPrefix = pathname.slice(normalizedPrefix.length)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Remove leading slash and tenant segment
|
|
1060
|
+
if (pathAfterPrefix.startsWith('/')) {
|
|
1061
|
+
pathAfterPrefix = pathAfterPrefix.slice(1)
|
|
1062
|
+
}
|
|
1063
|
+
const slashIndex = pathAfterPrefix.indexOf('/')
|
|
1064
|
+
if (slashIndex === -1) {
|
|
1065
|
+
modifiedPath = '/'
|
|
1066
|
+
} else {
|
|
1067
|
+
modifiedPath = pathAfterPrefix.slice(slashIndex)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
tenantId,
|
|
1073
|
+
source,
|
|
1074
|
+
originalHostname,
|
|
1075
|
+
modifiedPath,
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1079
|
+
async getTenantId(request: Request): Promise<string | null> {
|
|
1080
|
+
const result = await router.extractTenant(request)
|
|
1081
|
+
return result.tenantId
|
|
1082
|
+
},
|
|
1083
|
+
|
|
1084
|
+
async isBlocked(tenantId: string, request: Request): Promise<boolean> {
|
|
1085
|
+
// Check static blocked list
|
|
1086
|
+
for (const pattern of blockedTenants) {
|
|
1087
|
+
if (matchesBlockedPattern(tenantId, pattern)) {
|
|
1088
|
+
return true
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Check dynamic callback
|
|
1093
|
+
if (isBlockedCallback) {
|
|
1094
|
+
return await isBlockedCallback(tenantId, request)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return false
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
getStub(tenantId: string): DurableObjectStub {
|
|
1101
|
+
const doId = doNamespace.idFromName(tenantId)
|
|
1102
|
+
return doNamespace.get(doId)
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1105
|
+
async route(request: Request): Promise<Response> {
|
|
1106
|
+
const correlationId = generateCorrelationId()
|
|
1107
|
+
const startTime = Date.now()
|
|
1108
|
+
const requestUrl = new URL(request.url)
|
|
1109
|
+
|
|
1110
|
+
// Extract trace context from incoming request
|
|
1111
|
+
let traceContext: TraceContext | null = null
|
|
1112
|
+
if (traceContextExtractor) {
|
|
1113
|
+
traceContext = traceContextExtractor(request)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Log debug: routing decision starting
|
|
1117
|
+
if (logger) {
|
|
1118
|
+
logger.debug('Starting tenant routing', {
|
|
1119
|
+
correlationId,
|
|
1120
|
+
path: requestUrl.pathname,
|
|
1121
|
+
method: request.method,
|
|
1122
|
+
traceId: traceContext?.traceId,
|
|
1123
|
+
})
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
let tenantId: string | null = null
|
|
1127
|
+
let status = 500
|
|
1128
|
+
|
|
1129
|
+
try {
|
|
1130
|
+
// Extract tenant
|
|
1131
|
+
const extraction = await router.extractTenant(request)
|
|
1132
|
+
|
|
1133
|
+
if (extraction.tenantId === null) {
|
|
1134
|
+
// Log extraction failure
|
|
1135
|
+
if (logger) {
|
|
1136
|
+
logger.warn('Tenant extraction failed', {
|
|
1137
|
+
correlationId,
|
|
1138
|
+
path: requestUrl.pathname,
|
|
1139
|
+
method: request.method,
|
|
1140
|
+
source: extraction.source,
|
|
1141
|
+
hostname: extraction.originalHostname,
|
|
1142
|
+
})
|
|
1143
|
+
}
|
|
1144
|
+
status = 400
|
|
1145
|
+
return errorFormatter(400, 'Tenant identifier not found in request', correlationId)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
tenantId = extraction.tenantId
|
|
1149
|
+
|
|
1150
|
+
// Log debug: tenant extracted
|
|
1151
|
+
if (logger) {
|
|
1152
|
+
logger.debug('Tenant routing decision', {
|
|
1153
|
+
correlationId,
|
|
1154
|
+
tenantId,
|
|
1155
|
+
source: extraction.source,
|
|
1156
|
+
path: requestUrl.pathname,
|
|
1157
|
+
})
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Validate tenant ID format - comprehensive security validation
|
|
1161
|
+
if (!isValidTenantId(tenantId)) {
|
|
1162
|
+
status = 400
|
|
1163
|
+
return errorFormatter(400, 'Invalid tenant identifier format', correlationId)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Check if blocked
|
|
1167
|
+
if (await router.isBlocked(tenantId, request)) {
|
|
1168
|
+
status = 404
|
|
1169
|
+
return errorFormatter(404, 'Not Found', correlationId, tenantId)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Get DO stub
|
|
1173
|
+
const stub = router.getStub(tenantId)
|
|
1174
|
+
|
|
1175
|
+
// If path-based and stripping tenant, use modified path
|
|
1176
|
+
if (extraction.source === 'path' && stripTenantFromPath && extraction.modifiedPath) {
|
|
1177
|
+
requestUrl.pathname = extraction.modifiedPath
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Clone headers and add tenant ID
|
|
1181
|
+
const headers = new Headers(request.headers)
|
|
1182
|
+
headers.set(HEADERS.TENANT_ID, tenantId)
|
|
1183
|
+
headers.set(HEADERS.CORRELATION_ID, correlationId)
|
|
1184
|
+
|
|
1185
|
+
// Handle trace context propagation
|
|
1186
|
+
if (propagateTraceContext && traceContext) {
|
|
1187
|
+
// Propagate trace context headers
|
|
1188
|
+
headers.set(HEADERS.TRACE_ID, traceContext.traceId)
|
|
1189
|
+
headers.set(HEADERS.SPAN_ID, traceContext.spanId)
|
|
1190
|
+
if (traceContext.parentSpanId) {
|
|
1191
|
+
headers.set(HEADERS.PARENT_SPAN_ID, traceContext.parentSpanId)
|
|
1192
|
+
}
|
|
1193
|
+
} else if (!propagateTraceContext) {
|
|
1194
|
+
// Remove trace context headers when propagation is disabled
|
|
1195
|
+
headers.delete(HEADERS.TRACE_ID)
|
|
1196
|
+
headers.delete(HEADERS.SPAN_ID)
|
|
1197
|
+
headers.delete(HEADERS.PARENT_SPAN_ID)
|
|
1198
|
+
headers.delete(HEADERS.TRACE_SAMPLED)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Create new request with modified URL and headers
|
|
1202
|
+
const forwardedRequest = new Request(requestUrl.toString(), {
|
|
1203
|
+
method: request.method,
|
|
1204
|
+
headers,
|
|
1205
|
+
body: request.body,
|
|
1206
|
+
// @ts-expect-error - duplex is needed for streaming bodies
|
|
1207
|
+
duplex: request.body ? 'half' : undefined,
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
// Forward to DO with timeout
|
|
1211
|
+
const controller = new AbortController()
|
|
1212
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs)
|
|
1213
|
+
const doStartTime = Date.now()
|
|
1214
|
+
|
|
1215
|
+
try {
|
|
1216
|
+
const response = await stub.fetch(forwardedRequest, {
|
|
1217
|
+
signal: controller.signal,
|
|
1218
|
+
})
|
|
1219
|
+
clearTimeout(timeoutId)
|
|
1220
|
+
|
|
1221
|
+
const doResponseTimeMs = Date.now() - doStartTime
|
|
1222
|
+
const routingLatencyMs = Date.now() - startTime
|
|
1223
|
+
status = response.status
|
|
1224
|
+
|
|
1225
|
+
// Emit metrics
|
|
1226
|
+
if (metricsCollector) {
|
|
1227
|
+
metricsCollector.incrementRequestCount(tenantId)
|
|
1228
|
+
metricsCollector.recordLatency(tenantId, routingLatencyMs)
|
|
1229
|
+
metricsCollector.recordDoResponseTime(tenantId, doResponseTimeMs)
|
|
1230
|
+
metricsCollector.recordRequest({
|
|
1231
|
+
tenantId,
|
|
1232
|
+
method: request.method,
|
|
1233
|
+
path: requestUrl.pathname,
|
|
1234
|
+
status: response.status,
|
|
1235
|
+
routingLatencyMs,
|
|
1236
|
+
doResponseTimeMs,
|
|
1237
|
+
timestamp: startTime,
|
|
1238
|
+
correlationId,
|
|
1239
|
+
traceId: traceContext?.traceId,
|
|
1240
|
+
spanId: traceContext?.spanId,
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return response
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
clearTimeout(timeoutId)
|
|
1247
|
+
|
|
1248
|
+
if (error instanceof Error) {
|
|
1249
|
+
if (error.name === 'AbortError') {
|
|
1250
|
+
throw new TenantTimeoutError(tenantId, requestTimeoutMs)
|
|
1251
|
+
}
|
|
1252
|
+
throw new TenantUnavailableError(tenantId, error)
|
|
1253
|
+
}
|
|
1254
|
+
throw error
|
|
1255
|
+
}
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
const routingLatencyMs = Date.now() - startTime
|
|
1258
|
+
|
|
1259
|
+
// Emit error metrics
|
|
1260
|
+
if (metricsCollector && tenantId) {
|
|
1261
|
+
metricsCollector.incrementRequestCount(tenantId)
|
|
1262
|
+
metricsCollector.recordLatency(tenantId, routingLatencyMs)
|
|
1263
|
+
metricsCollector.recordRequest({
|
|
1264
|
+
tenantId,
|
|
1265
|
+
method: request.method,
|
|
1266
|
+
path: requestUrl.pathname,
|
|
1267
|
+
status,
|
|
1268
|
+
routingLatencyMs,
|
|
1269
|
+
doResponseTimeMs: 0,
|
|
1270
|
+
timestamp: startTime,
|
|
1271
|
+
correlationId,
|
|
1272
|
+
traceId: traceContext?.traceId,
|
|
1273
|
+
spanId: traceContext?.spanId,
|
|
1274
|
+
})
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (error instanceof TenantNotFoundError) {
|
|
1278
|
+
return errorFormatter(400, 'Tenant identifier not found in request', correlationId)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (error instanceof TenantBlockedError) {
|
|
1282
|
+
return errorFormatter(404, 'Not Found', correlationId, error.tenantId)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (error instanceof InvalidTenantIdError) {
|
|
1286
|
+
return errorFormatter(400, 'Invalid tenant identifier format', correlationId)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (error instanceof TenantTimeoutError) {
|
|
1290
|
+
return errorFormatter(504, 'Gateway Timeout', correlationId, error.tenantId)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (error instanceof TenantUnavailableError) {
|
|
1294
|
+
return errorFormatter(502, 'Bad Gateway', correlationId, error.tenantId)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Generic error - don't expose details
|
|
1298
|
+
return errorFormatter(500, 'Internal Server Error', correlationId)
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
async routeWithResult(request: Request): Promise<TenantRoutingResult> {
|
|
1303
|
+
const startTimestamp = Date.now()
|
|
1304
|
+
const correlationId = generateCorrelationId()
|
|
1305
|
+
|
|
1306
|
+
// Extract tenant with timing
|
|
1307
|
+
const extractionStartTime = Date.now()
|
|
1308
|
+
const extraction = await router.extractTenant(request)
|
|
1309
|
+
const extractionTimeMs = Date.now() - extractionStartTime
|
|
1310
|
+
|
|
1311
|
+
if (extraction.tenantId === null) {
|
|
1312
|
+
throw new TenantNotFoundError('Tenant identifier not found in request', request)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const tenantId = extraction.tenantId
|
|
1316
|
+
|
|
1317
|
+
// Validate tenant ID format - comprehensive security validation
|
|
1318
|
+
if (!isValidTenantId(tenantId)) {
|
|
1319
|
+
throw new InvalidTenantIdError(tenantId)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Check if blocked
|
|
1323
|
+
if (await router.isBlocked(tenantId, request)) {
|
|
1324
|
+
throw new TenantBlockedError(tenantId)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Get DO stub and ID
|
|
1328
|
+
const doId = doNamespace.idFromName(tenantId)
|
|
1329
|
+
const stub = doNamespace.get(doId)
|
|
1330
|
+
|
|
1331
|
+
// Build forwarded request
|
|
1332
|
+
const url = new URL(request.url)
|
|
1333
|
+
|
|
1334
|
+
// If path-based and stripping tenant, use modified path
|
|
1335
|
+
if (extraction.source === 'path' && stripTenantFromPath && extraction.modifiedPath) {
|
|
1336
|
+
url.pathname = extraction.modifiedPath
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Clone headers and add tenant ID
|
|
1340
|
+
const headers = new Headers(request.headers)
|
|
1341
|
+
headers.set(HEADERS.TENANT_ID, tenantId)
|
|
1342
|
+
headers.set(HEADERS.CORRELATION_ID, correlationId)
|
|
1343
|
+
|
|
1344
|
+
// Create new request with modified URL and headers
|
|
1345
|
+
const forwardedRequest = new Request(url.toString(), {
|
|
1346
|
+
method: request.method,
|
|
1347
|
+
headers,
|
|
1348
|
+
body: request.body,
|
|
1349
|
+
// @ts-expect-error - duplex is needed for streaming bodies
|
|
1350
|
+
duplex: request.body ? 'half' : undefined,
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
// Forward to DO with timeout
|
|
1354
|
+
const controller = new AbortController()
|
|
1355
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs)
|
|
1356
|
+
const routingStartTime = Date.now()
|
|
1357
|
+
|
|
1358
|
+
try {
|
|
1359
|
+
const response = await stub.fetch(forwardedRequest, {
|
|
1360
|
+
signal: controller.signal,
|
|
1361
|
+
})
|
|
1362
|
+
clearTimeout(timeoutId)
|
|
1363
|
+
|
|
1364
|
+
const routingTimeMs = Date.now() - routingStartTime
|
|
1365
|
+
const totalTimeMs = Date.now() - startTimestamp
|
|
1366
|
+
|
|
1367
|
+
return {
|
|
1368
|
+
response,
|
|
1369
|
+
routingMetadata: {
|
|
1370
|
+
extractorType: extraction.source,
|
|
1371
|
+
doId: doId.toString(),
|
|
1372
|
+
},
|
|
1373
|
+
timing: {
|
|
1374
|
+
extractionTimeMs,
|
|
1375
|
+
routingTimeMs,
|
|
1376
|
+
totalTimeMs,
|
|
1377
|
+
startTimestamp,
|
|
1378
|
+
},
|
|
1379
|
+
tenantContext: {
|
|
1380
|
+
tenantId,
|
|
1381
|
+
source: extraction.source,
|
|
1382
|
+
metadata: extraction.metadata,
|
|
1383
|
+
},
|
|
1384
|
+
}
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
clearTimeout(timeoutId)
|
|
1387
|
+
|
|
1388
|
+
if (error instanceof Error) {
|
|
1389
|
+
if (error.name === 'AbortError') {
|
|
1390
|
+
throw new TenantTimeoutError(tenantId, requestTimeoutMs)
|
|
1391
|
+
}
|
|
1392
|
+
throw new TenantUnavailableError(tenantId, error)
|
|
1393
|
+
}
|
|
1394
|
+
throw error
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
return router
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// =============================================================================
|
|
1403
|
+
// Helper Types
|
|
1404
|
+
// =============================================================================
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Request with tenant context attached
|
|
1408
|
+
*/
|
|
1409
|
+
export interface TenantRequest extends Request {
|
|
1410
|
+
tenantId: string
|
|
1411
|
+
tenantMetadata?: Record<string, unknown>
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Tenant routing metrics
|
|
1416
|
+
*/
|
|
1417
|
+
export interface TenantRoutingMetrics {
|
|
1418
|
+
requestCount: number
|
|
1419
|
+
successCount: number
|
|
1420
|
+
errorCount: number
|
|
1421
|
+
blockedCount: number
|
|
1422
|
+
avgRoutingLatencyMs: number
|
|
1423
|
+
avgDoResponseTimeMs: number
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Per-tenant metrics
|
|
1428
|
+
*/
|
|
1429
|
+
export interface PerTenantMetrics {
|
|
1430
|
+
tenantId: string
|
|
1431
|
+
requestCount: number
|
|
1432
|
+
lastRequestAt: number
|
|
1433
|
+
avgResponseTimeMs: number
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// =============================================================================
|
|
1437
|
+
// Error Types
|
|
1438
|
+
// =============================================================================
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Error thrown when tenant cannot be identified
|
|
1442
|
+
*/
|
|
1443
|
+
export class TenantNotFoundError extends Error {
|
|
1444
|
+
constructor(
|
|
1445
|
+
message: string = 'Tenant identifier not found in request',
|
|
1446
|
+
public readonly request?: Request
|
|
1447
|
+
) {
|
|
1448
|
+
super(message)
|
|
1449
|
+
this.name = 'TenantNotFoundError'
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Error thrown when tenant is blocked
|
|
1455
|
+
*/
|
|
1456
|
+
export class TenantBlockedError extends Error {
|
|
1457
|
+
constructor(
|
|
1458
|
+
public readonly tenantId: string,
|
|
1459
|
+
message: string = 'Tenant is blocked'
|
|
1460
|
+
) {
|
|
1461
|
+
super(message)
|
|
1462
|
+
this.name = 'TenantBlockedError'
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Error thrown when tenant ID format is invalid
|
|
1468
|
+
*/
|
|
1469
|
+
export class InvalidTenantIdError extends Error {
|
|
1470
|
+
constructor(
|
|
1471
|
+
public readonly tenantId: string,
|
|
1472
|
+
message: string = 'Invalid tenant identifier format'
|
|
1473
|
+
) {
|
|
1474
|
+
super(message)
|
|
1475
|
+
this.name = 'InvalidTenantIdError'
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Error thrown when DO request times out
|
|
1481
|
+
*/
|
|
1482
|
+
export class TenantTimeoutError extends Error {
|
|
1483
|
+
constructor(
|
|
1484
|
+
public readonly tenantId: string,
|
|
1485
|
+
public readonly timeoutMs: number,
|
|
1486
|
+
message: string = 'Tenant DO request timed out'
|
|
1487
|
+
) {
|
|
1488
|
+
super(message)
|
|
1489
|
+
this.name = 'TenantTimeoutError'
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Error thrown when DO is unreachable
|
|
1495
|
+
*/
|
|
1496
|
+
export class TenantUnavailableError extends Error {
|
|
1497
|
+
public readonly tenantId: string
|
|
1498
|
+
public override readonly cause?: Error | undefined
|
|
1499
|
+
|
|
1500
|
+
constructor(
|
|
1501
|
+
tenantId: string,
|
|
1502
|
+
cause?: Error,
|
|
1503
|
+
message: string = 'Tenant DO is unavailable'
|
|
1504
|
+
) {
|
|
1505
|
+
super(message)
|
|
1506
|
+
this.tenantId = tenantId
|
|
1507
|
+
this.cause = cause
|
|
1508
|
+
this.name = 'TenantUnavailableError'
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// =============================================================================
|
|
1513
|
+
// Rate Limiting
|
|
1514
|
+
// =============================================================================
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Rate limit configuration
|
|
1518
|
+
*/
|
|
1519
|
+
export interface RateLimitConfig {
|
|
1520
|
+
/**
|
|
1521
|
+
* Maximum requests allowed within the window
|
|
1522
|
+
*/
|
|
1523
|
+
maxRequests: number
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Time window in milliseconds
|
|
1527
|
+
* @default 60000 (1 minute)
|
|
1528
|
+
*/
|
|
1529
|
+
windowMs?: number
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* List of tenant IDs that bypass rate limiting
|
|
1533
|
+
*/
|
|
1534
|
+
bypassTenants?: string[]
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Custom function to determine if a tenant should bypass rate limiting
|
|
1538
|
+
*/
|
|
1539
|
+
bypassCallback?: (tenantId: string, request: Request) => boolean | Promise<boolean>
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Rate limit state for a single tenant
|
|
1544
|
+
*/
|
|
1545
|
+
export interface RateLimitState {
|
|
1546
|
+
/** Number of requests in the current window */
|
|
1547
|
+
requestCount: number
|
|
1548
|
+
/** Timestamp when the window started */
|
|
1549
|
+
windowStart: number
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Rate limit check result
|
|
1554
|
+
*/
|
|
1555
|
+
export interface RateLimitResult {
|
|
1556
|
+
/** Whether the request is allowed */
|
|
1557
|
+
allowed: boolean
|
|
1558
|
+
/** Current request count in the window */
|
|
1559
|
+
currentCount: number
|
|
1560
|
+
/** Maximum requests allowed */
|
|
1561
|
+
maxRequests: number
|
|
1562
|
+
/** Seconds until the rate limit resets */
|
|
1563
|
+
retryAfterSeconds: number
|
|
1564
|
+
/** Whether this tenant bypasses rate limiting */
|
|
1565
|
+
bypassed: boolean
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* In-memory rate limiter for per-tenant request tracking
|
|
1570
|
+
*/
|
|
1571
|
+
export class TenantRateLimiter {
|
|
1572
|
+
private state: Map<string, RateLimitState> = new Map()
|
|
1573
|
+
private readonly maxRequests: number
|
|
1574
|
+
private readonly windowMs: number
|
|
1575
|
+
private readonly bypassTenants: Set<string>
|
|
1576
|
+
private readonly bypassCallback?: ((tenantId: string, request: Request) => boolean | Promise<boolean>) | undefined
|
|
1577
|
+
|
|
1578
|
+
constructor(config: RateLimitConfig) {
|
|
1579
|
+
this.maxRequests = config.maxRequests
|
|
1580
|
+
this.windowMs = config.windowMs ?? 60_000
|
|
1581
|
+
this.bypassTenants = new Set(config.bypassTenants ?? [])
|
|
1582
|
+
this.bypassCallback = config.bypassCallback
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Check if a request should be allowed for the given tenant
|
|
1587
|
+
*/
|
|
1588
|
+
async check(tenantId: string, request: Request): Promise<RateLimitResult> {
|
|
1589
|
+
// Periodic cleanup of expired entries (1% chance per request)
|
|
1590
|
+
if (Math.random() < 0.01) {
|
|
1591
|
+
this.cleanupExpiredEntries()
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Check bypass list first
|
|
1595
|
+
if (this.bypassTenants.has(tenantId)) {
|
|
1596
|
+
return {
|
|
1597
|
+
allowed: true,
|
|
1598
|
+
currentCount: 0,
|
|
1599
|
+
maxRequests: this.maxRequests,
|
|
1600
|
+
retryAfterSeconds: 0,
|
|
1601
|
+
bypassed: true,
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// Check bypass callback
|
|
1606
|
+
if (this.bypassCallback) {
|
|
1607
|
+
const shouldBypass = await this.bypassCallback(tenantId, request)
|
|
1608
|
+
if (shouldBypass) {
|
|
1609
|
+
return {
|
|
1610
|
+
allowed: true,
|
|
1611
|
+
currentCount: 0,
|
|
1612
|
+
maxRequests: this.maxRequests,
|
|
1613
|
+
retryAfterSeconds: 0,
|
|
1614
|
+
bypassed: true,
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const now = Date.now()
|
|
1620
|
+
let tenantState = this.state.get(tenantId)
|
|
1621
|
+
|
|
1622
|
+
// Initialize or reset window if expired
|
|
1623
|
+
if (!tenantState || now - tenantState.windowStart >= this.windowMs) {
|
|
1624
|
+
tenantState = {
|
|
1625
|
+
requestCount: 0,
|
|
1626
|
+
windowStart: now,
|
|
1627
|
+
}
|
|
1628
|
+
this.state.set(tenantId, tenantState)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Calculate retry-after
|
|
1632
|
+
const windowEnd = tenantState.windowStart + this.windowMs
|
|
1633
|
+
const retryAfterSeconds = Math.ceil((windowEnd - now) / 1000)
|
|
1634
|
+
|
|
1635
|
+
// Check if limit exceeded
|
|
1636
|
+
if (tenantState.requestCount >= this.maxRequests) {
|
|
1637
|
+
return {
|
|
1638
|
+
allowed: false,
|
|
1639
|
+
currentCount: tenantState.requestCount,
|
|
1640
|
+
maxRequests: this.maxRequests,
|
|
1641
|
+
retryAfterSeconds,
|
|
1642
|
+
bypassed: false,
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Increment and allow
|
|
1647
|
+
tenantState.requestCount++
|
|
1648
|
+
|
|
1649
|
+
return {
|
|
1650
|
+
allowed: true,
|
|
1651
|
+
currentCount: tenantState.requestCount,
|
|
1652
|
+
maxRequests: this.maxRequests,
|
|
1653
|
+
retryAfterSeconds,
|
|
1654
|
+
bypassed: false,
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Get the current state for a tenant (for testing/debugging)
|
|
1660
|
+
*/
|
|
1661
|
+
getState(tenantId: string): RateLimitState | undefined {
|
|
1662
|
+
return this.state.get(tenantId)
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* Reset rate limit state for a tenant
|
|
1667
|
+
*/
|
|
1668
|
+
reset(tenantId: string): void {
|
|
1669
|
+
this.state.delete(tenantId)
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Reset all rate limit state
|
|
1674
|
+
*/
|
|
1675
|
+
resetAll(): void {
|
|
1676
|
+
this.state.clear()
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Clean up expired entries from the rate limit state map
|
|
1681
|
+
* Removes entries that are 2x past their window to prevent unbounded memory growth
|
|
1682
|
+
*/
|
|
1683
|
+
private cleanupExpiredEntries(): void {
|
|
1684
|
+
const now = Date.now()
|
|
1685
|
+
for (const [tenantId, state] of this.state.entries()) {
|
|
1686
|
+
// Remove entries that are 2x past their window
|
|
1687
|
+
if (now - state.windowStart >= this.windowMs * 2) {
|
|
1688
|
+
this.state.delete(tenantId)
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Get the current number of tracked tenants (for monitoring/debugging)
|
|
1695
|
+
*/
|
|
1696
|
+
getTrackedTenantCount(): number {
|
|
1697
|
+
return this.state.size
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Error thrown when rate limit is exceeded
|
|
1703
|
+
*/
|
|
1704
|
+
export class TenantRateLimitError extends Error {
|
|
1705
|
+
constructor(
|
|
1706
|
+
public readonly tenantId: string,
|
|
1707
|
+
public readonly retryAfterSeconds: number,
|
|
1708
|
+
message: string = 'Rate limit exceeded'
|
|
1709
|
+
) {
|
|
1710
|
+
super(message)
|
|
1711
|
+
this.name = 'TenantRateLimitError'
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// =============================================================================
|
|
1716
|
+
// Caching Types and Implementation
|
|
1717
|
+
// =============================================================================
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Cache entry for domain-to-tenant mappings
|
|
1721
|
+
*/
|
|
1722
|
+
export interface DomainCacheEntry {
|
|
1723
|
+
/** The tenant ID mapped to this domain */
|
|
1724
|
+
tenantId: string
|
|
1725
|
+
/** Timestamp when this entry was cached */
|
|
1726
|
+
cachedAt: number
|
|
1727
|
+
/** Additional metadata */
|
|
1728
|
+
metadata?: Record<string, unknown> | undefined
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Configuration for the domain mapping cache
|
|
1733
|
+
*/
|
|
1734
|
+
export interface DomainCacheConfig {
|
|
1735
|
+
/** TTL for cache entries in milliseconds */
|
|
1736
|
+
ttlMs: number
|
|
1737
|
+
/** Maximum number of entries to cache */
|
|
1738
|
+
maxEntries?: number
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Cache statistics
|
|
1743
|
+
*/
|
|
1744
|
+
export interface CacheStats {
|
|
1745
|
+
hits: number
|
|
1746
|
+
misses: number
|
|
1747
|
+
size: number
|
|
1748
|
+
invalidations: number
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Interface for domain-to-tenant mapping cache
|
|
1753
|
+
*/
|
|
1754
|
+
export interface DomainMappingCache {
|
|
1755
|
+
/** Get a cached tenant ID for a domain */
|
|
1756
|
+
get(domain: string): DomainCacheEntry | null
|
|
1757
|
+
/** Set a domain-to-tenant mapping in the cache */
|
|
1758
|
+
set(domain: string, tenantId: string, metadata?: Record<string, unknown>): void
|
|
1759
|
+
/** Invalidate a specific domain mapping */
|
|
1760
|
+
invalidate(domain: string): void
|
|
1761
|
+
/** Invalidate all cache entries */
|
|
1762
|
+
invalidateAll(): void
|
|
1763
|
+
/** Check if cache has a valid (non-expired) entry for a domain */
|
|
1764
|
+
has(domain: string): boolean
|
|
1765
|
+
/** Warm the cache with known domain-to-tenant mappings */
|
|
1766
|
+
warm(mappings: Record<string, string>): void
|
|
1767
|
+
/** Get the current cache size */
|
|
1768
|
+
size(): number
|
|
1769
|
+
/** Get cache statistics */
|
|
1770
|
+
stats(): CacheStats
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Create a new domain mapping cache
|
|
1775
|
+
*
|
|
1776
|
+
* @param config Cache configuration
|
|
1777
|
+
* @returns A DomainMappingCache instance
|
|
1778
|
+
*
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```typescript
|
|
1781
|
+
* const cache = createDomainMappingCache({ ttlMs: 300000 }) // 5 minute TTL
|
|
1782
|
+
*
|
|
1783
|
+
* // Set a mapping
|
|
1784
|
+
* cache.set('custom.example.com', 'tenant1')
|
|
1785
|
+
*
|
|
1786
|
+
* // Get a mapping (returns null if expired or not found)
|
|
1787
|
+
* const entry = cache.get('custom.example.com')
|
|
1788
|
+
* if (entry) {
|
|
1789
|
+
* console.log(entry.tenantId) // 'tenant1'
|
|
1790
|
+
* }
|
|
1791
|
+
*
|
|
1792
|
+
* // Warm cache on startup
|
|
1793
|
+
* cache.warm({
|
|
1794
|
+
* 'acme.com': 'acme-inc',
|
|
1795
|
+
* 'bigcorp.io': 'bigcorp',
|
|
1796
|
+
* })
|
|
1797
|
+
*
|
|
1798
|
+
* // Invalidate on config change
|
|
1799
|
+
* cache.invalidate('acme.com')
|
|
1800
|
+
* // or invalidate all
|
|
1801
|
+
* cache.invalidateAll()
|
|
1802
|
+
* ```
|
|
1803
|
+
*/
|
|
1804
|
+
export function createDomainMappingCache(config: DomainCacheConfig): DomainMappingCache {
|
|
1805
|
+
const { ttlMs, maxEntries = 10000 } = config
|
|
1806
|
+
const cache = new Map<string, DomainCacheEntry>()
|
|
1807
|
+
let hits = 0
|
|
1808
|
+
let misses = 0
|
|
1809
|
+
let invalidations = 0
|
|
1810
|
+
|
|
1811
|
+
// Helper to check if an entry is expired
|
|
1812
|
+
function isExpired(entry: DomainCacheEntry): boolean {
|
|
1813
|
+
return Date.now() - entry.cachedAt > ttlMs
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Helper to evict oldest entries if over capacity
|
|
1817
|
+
function evictIfNeeded(): void {
|
|
1818
|
+
if (cache.size <= maxEntries) return
|
|
1819
|
+
|
|
1820
|
+
// Find and remove oldest entries
|
|
1821
|
+
const entries = Array.from(cache.entries())
|
|
1822
|
+
entries.sort((a, b) => a[1].cachedAt - b[1].cachedAt)
|
|
1823
|
+
const toRemove = entries.slice(0, cache.size - maxEntries)
|
|
1824
|
+
for (const [key] of toRemove) {
|
|
1825
|
+
cache.delete(key)
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
return {
|
|
1830
|
+
get(domain: string): DomainCacheEntry | null {
|
|
1831
|
+
const normalizedDomain = domain.toLowerCase()
|
|
1832
|
+
const entry = cache.get(normalizedDomain)
|
|
1833
|
+
|
|
1834
|
+
if (!entry) {
|
|
1835
|
+
misses++
|
|
1836
|
+
return null
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (isExpired(entry)) {
|
|
1840
|
+
cache.delete(normalizedDomain)
|
|
1841
|
+
misses++
|
|
1842
|
+
return null
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
hits++
|
|
1846
|
+
return entry
|
|
1847
|
+
},
|
|
1848
|
+
|
|
1849
|
+
set(domain: string, tenantId: string, metadata?: Record<string, unknown>): void {
|
|
1850
|
+
const normalizedDomain = domain.toLowerCase()
|
|
1851
|
+
cache.set(normalizedDomain, {
|
|
1852
|
+
tenantId,
|
|
1853
|
+
cachedAt: Date.now(),
|
|
1854
|
+
metadata,
|
|
1855
|
+
})
|
|
1856
|
+
evictIfNeeded()
|
|
1857
|
+
},
|
|
1858
|
+
|
|
1859
|
+
invalidate(domain: string): void {
|
|
1860
|
+
const normalizedDomain = domain.toLowerCase()
|
|
1861
|
+
if (cache.delete(normalizedDomain)) {
|
|
1862
|
+
invalidations++
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
|
|
1866
|
+
invalidateAll(): void {
|
|
1867
|
+
const count = cache.size
|
|
1868
|
+
cache.clear()
|
|
1869
|
+
invalidations += count
|
|
1870
|
+
},
|
|
1871
|
+
|
|
1872
|
+
has(domain: string): boolean {
|
|
1873
|
+
const normalizedDomain = domain.toLowerCase()
|
|
1874
|
+
const entry = cache.get(normalizedDomain)
|
|
1875
|
+
if (!entry) return false
|
|
1876
|
+
if (isExpired(entry)) {
|
|
1877
|
+
cache.delete(normalizedDomain)
|
|
1878
|
+
return false
|
|
1879
|
+
}
|
|
1880
|
+
return true
|
|
1881
|
+
},
|
|
1882
|
+
|
|
1883
|
+
warm(mappings: Record<string, string>): void {
|
|
1884
|
+
const now = Date.now()
|
|
1885
|
+
for (const [domain, tenantId] of Object.entries(mappings)) {
|
|
1886
|
+
const normalizedDomain = domain.toLowerCase()
|
|
1887
|
+
cache.set(normalizedDomain, {
|
|
1888
|
+
tenantId,
|
|
1889
|
+
cachedAt: now,
|
|
1890
|
+
})
|
|
1891
|
+
}
|
|
1892
|
+
evictIfNeeded()
|
|
1893
|
+
},
|
|
1894
|
+
|
|
1895
|
+
size(): number {
|
|
1896
|
+
return cache.size
|
|
1897
|
+
},
|
|
1898
|
+
|
|
1899
|
+
stats(): CacheStats {
|
|
1900
|
+
return {
|
|
1901
|
+
hits,
|
|
1902
|
+
misses,
|
|
1903
|
+
size: cache.size,
|
|
1904
|
+
invalidations,
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
}
|
|
1908
|
+
}
|