@malloy-publisher/server 0.0.235 → 0.0.237
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/dist/app/api-doc.yaml +83 -27
- package/dist/app/assets/{EnvironmentPage-BsAnavYN.js → EnvironmentPage-DaP6CpgK.js} +1 -1
- package/dist/app/assets/{HomePage-CADE138j.js → HomePage-C9yNw3iu.js} +1 -1
- package/dist/app/assets/{LightMode-Cfh7KzN8.js → LightMode-Be4-G-dX.js} +1 -1
- package/dist/app/assets/{MainPage-CO3pRlnV.js → MainPage-J_rU2SgI.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-p9YjkRXZ.js → MaterializationsPage-DHgPI4MO.js} +1 -1
- package/dist/app/assets/ModelPage-BzXfjTu5.js +1 -0
- package/dist/app/assets/{PackagePage-e4kN75YR.js → PackagePage-ByoSTLJ4.js} +1 -1
- package/dist/app/assets/RouteError-CrSeKN2r.js +1 -0
- package/dist/app/assets/{ThemeEditorPage-CciagFTq.js → ThemeEditorPage-Btxnmhtk.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DNWmkCXa.js → WorkbookPage-D6JLPnWO.js} +1 -1
- package/dist/app/assets/{core-Rj_4rRnA.es-BZyvITuO.js → core-BSRgt1mG.es-Bc_D2Lq4.js} +1 -1
- package/dist/app/assets/{index-CH2AcDzc.js → index-BRijWj6K.js} +15 -15
- package/dist/app/assets/{index-DQa463gC.js → index-BX4D94xw.js} +1 -1
- package/dist/app/assets/{index-VBbcc8s6.js → index-Cvy0xtb7.js} +1 -1
- package/dist/app/assets/{index-DQpV7MyA.js → index-DMAhIJNC.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +59 -20
- package/dist/server.mjs +1342 -616
- package/package.json +8 -2
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -97
- package/dist/app/assets/ModelPage-C1OSTv-x.js +0 -1
- package/dist/app/assets/RouteError-CzbfOkng.js +0 -1
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.example.bigquery.json +0 -33
- package/publisher.config.example.duckdb.json +0 -23
- package/publisher.config.json +0 -23
- package/scripts/bake-duckdb-extensions.js +0 -110
- package/src/config.spec.ts +0 -1535
- package/src/config.theme.spec.ts +0 -178
- package/src/config.ts +0 -1142
- package/src/constants.ts +0 -105
- package/src/controller/compile.controller.ts +0 -40
- package/src/controller/connection.controller.spec.ts +0 -849
- package/src/controller/connection.controller.ts +0 -901
- package/src/controller/database.controller.ts +0 -24
- package/src/controller/materialization.controller.spec.ts +0 -293
- package/src/controller/materialization.controller.ts +0 -319
- package/src/controller/model.controller.ts +0 -164
- package/src/controller/package.controller.spec.ts +0 -273
- package/src/controller/package.controller.ts +0 -306
- package/src/controller/query.controller.ts +0 -152
- package/src/controller/theme.controller.ts +0 -83
- package/src/controller/watch-mode.controller.ts +0 -243
- package/src/data_styles.ts +0 -71
- package/src/default-publisher.config.json +0 -23
- package/src/ducklake_version.spec.ts +0 -163
- package/src/ducklake_version.ts +0 -153
- package/src/errors.spec.ts +0 -119
- package/src/errors.ts +0 -237
- package/src/filter_deprecation.spec.ts +0 -64
- package/src/filter_deprecation.ts +0 -42
- package/src/health.spec.ts +0 -123
- package/src/health.ts +0 -233
- package/src/heap_check.spec.ts +0 -144
- package/src/heap_check.ts +0 -144
- package/src/instrumentation.ts +0 -214
- package/src/json_utils.spec.ts +0 -51
- package/src/json_utils.ts +0 -33
- package/src/logger.spec.ts +0 -210
- package/src/logger.ts +0 -251
- package/src/malloy_pin_prereqs.spec.ts +0 -25
- package/src/materialization_metrics.spec.ts +0 -169
- package/src/materialization_metrics.ts +0 -346
- package/src/mcp/error_messages.spec.ts +0 -35
- package/src/mcp/error_messages.ts +0 -183
- package/src/mcp/handler_utils.spec.ts +0 -108
- package/src/mcp/handler_utils.ts +0 -251
- package/src/mcp/mcp_constants.ts +0 -6
- package/src/mcp/query_envelope.spec.ts +0 -229
- package/src/mcp/query_envelope.ts +0 -240
- package/src/mcp/server.protocol.spec.ts +0 -250
- package/src/mcp/server.ts +0 -87
- package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
- package/src/mcp/skills/build_skills_bundle.ts +0 -176
- package/src/mcp/skills/skills_bundle.json +0 -1
- package/src/mcp/skills/skills_bundle.spec.ts +0 -171
- package/src/mcp/tool_response.spec.ts +0 -108
- package/src/mcp/tool_response.ts +0 -138
- package/src/mcp/tools/compile_tool.spec.ts +0 -315
- package/src/mcp/tools/compile_tool.ts +0 -208
- package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
- package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
- package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
- package/src/mcp/tools/docs_search_tool.ts +0 -128
- package/src/mcp/tools/embedding_index.spec.ts +0 -1236
- package/src/mcp/tools/embedding_index.ts +0 -808
- package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
- package/src/mcp/tools/execute_query_tool.ts +0 -316
- package/src/mcp/tools/get_context_eval.ts +0 -275
- package/src/mcp/tools/get_context_tool.spec.ts +0 -592
- package/src/mcp/tools/get_context_tool.ts +0 -585
- package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
- package/src/mcp/tools/reload_package_tool.ts +0 -132
- package/src/mcp_config.spec.ts +0 -919
- package/src/mcp_config.ts +0 -425
- package/src/oom_guards.integration.spec.ts +0 -269
- package/src/package_load/package_load_pool.spec.ts +0 -252
- package/src/package_load/package_load_pool.ts +0 -931
- package/src/package_load/package_load_worker.ts +0 -1034
- package/src/package_load/protocol.ts +0 -364
- package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
- package/src/package_load/rpc_wait_accountant.ts +0 -76
- package/src/package_load_metrics.spec.ts +0 -114
- package/src/package_load_metrics.ts +0 -127
- package/src/path_safety.spec.ts +0 -158
- package/src/path_safety.ts +0 -146
- package/src/pg_helpers.spec.ts +0 -223
- package/src/pg_helpers.ts +0 -52
- package/src/query_cap_metrics.spec.ts +0 -89
- package/src/query_cap_metrics.ts +0 -116
- package/src/query_concurrency.spec.ts +0 -247
- package/src/query_concurrency.ts +0 -237
- package/src/query_metadata_metrics.ts +0 -49
- package/src/query_param_utils.ts +0 -29
- package/src/query_timeout.spec.ts +0 -224
- package/src/query_timeout.ts +0 -179
- package/src/runtime/publisher.js +0 -328
- package/src/server-old.ts +0 -988
- package/src/server.ts +0 -2059
- package/src/service/annotations.spec.ts +0 -118
- package/src/service/annotations.ts +0 -91
- package/src/service/authorize.spec.ts +0 -154
- package/src/service/authorize.ts +0 -498
- package/src/service/authorize_integration.spec.ts +0 -2041
- package/src/service/build_plan.spec.ts +0 -685
- package/src/service/build_plan.ts +0 -797
- package/src/service/compile_authorize.spec.ts +0 -85
- package/src/service/compile_fragment_techniques.spec.ts +0 -156
- package/src/service/connection.spec.ts +0 -2360
- package/src/service/connection.ts +0 -2187
- package/src/service/connection_config.spec.ts +0 -734
- package/src/service/connection_config.ts +0 -899
- package/src/service/connection_federation.spec.ts +0 -184
- package/src/service/connection_fingerprint.spec.ts +0 -102
- package/src/service/connection_service.spec.ts +0 -711
- package/src/service/connection_service.ts +0 -291
- package/src/service/cron_evaluator.spec.ts +0 -79
- package/src/service/cron_evaluator.ts +0 -105
- package/src/service/db_utils.spec.ts +0 -1085
- package/src/service/db_utils.ts +0 -1502
- package/src/service/duckdb_instance_isolation.spec.ts +0 -137
- package/src/service/ducklake_lazy_attach.spec.ts +0 -110
- package/src/service/embedding_provider.spec.ts +0 -329
- package/src/service/embedding_provider.ts +0 -236
- package/src/service/environment.ts +0 -2191
- package/src/service/environment_admission.spec.ts +0 -344
- package/src/service/environment_compile.spec.ts +0 -197
- package/src/service/environment_store.spec.ts +0 -2274
- package/src/service/environment_store.ts +0 -2434
- package/src/service/environment_store_anchoring.spec.ts +0 -107
- package/src/service/environment_store_clone.spec.ts +0 -350
- package/src/service/explore_visibility.spec.ts +0 -434
- package/src/service/exports_probe.spec.ts +0 -107
- package/src/service/extension_fetch_policy.spec.ts +0 -256
- package/src/service/filter.spec.ts +0 -458
- package/src/service/filter.ts +0 -341
- package/src/service/filter_bypass.spec.ts +0 -418
- package/src/service/filter_integration.spec.ts +0 -1004
- package/src/service/freshness.spec.ts +0 -183
- package/src/service/freshness.ts +0 -112
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/given.ts +0 -105
- package/src/service/givens_integration.spec.ts +0 -219
- package/src/service/manifest_loader.spec.ts +0 -189
- package/src/service/manifest_loader.ts +0 -156
- package/src/service/materialization_build_session.spec.ts +0 -435
- package/src/service/materialization_build_session.ts +0 -681
- package/src/service/materialization_config_validation.spec.ts +0 -99
- package/src/service/materialization_config_validation.ts +0 -120
- package/src/service/materialization_eligibility.spec.ts +0 -158
- package/src/service/materialization_eligibility.ts +0 -305
- package/src/service/materialization_schedule_surface.spec.ts +0 -256
- package/src/service/materialization_scheduler.spec.ts +0 -289
- package/src/service/materialization_scheduler.ts +0 -304
- package/src/service/materialization_serve_transform.spec.ts +0 -1003
- package/src/service/materialization_serve_transform.ts +0 -779
- package/src/service/materialization_service.spec.ts +0 -2373
- package/src/service/materialization_service.ts +0 -2342
- package/src/service/materialization_test_fixtures.ts +0 -249
- package/src/service/model.spec.ts +0 -1157
- package/src/service/model.ts +0 -3370
- package/src/service/model_limits.spec.ts +0 -209
- package/src/service/model_limits.ts +0 -131
- package/src/service/model_storage_serve.spec.ts +0 -193
- package/src/service/model_storage_serve_joins.spec.ts +0 -193
- package/src/service/package.spec.ts +0 -612
- package/src/service/package.ts +0 -1799
- package/src/service/package_manifest.spec.ts +0 -230
- package/src/service/package_manifest.ts +0 -241
- package/src/service/package_memory_governor.spec.ts +0 -173
- package/src/service/package_memory_governor.ts +0 -233
- package/src/service/package_quote_bound_tables.spec.ts +0 -171
- package/src/service/package_race.spec.ts +0 -208
- package/src/service/package_reload_safety.spec.ts +0 -193
- package/src/service/package_rollback.spec.ts +0 -190
- package/src/service/package_worker_path.spec.ts +0 -645
- package/src/service/path_injection.spec.ts +0 -39
- package/src/service/persist_annotation_validation.spec.ts +0 -89
- package/src/service/persist_annotation_validation.ts +0 -59
- package/src/service/persistence_policy.spec.ts +0 -374
- package/src/service/proxy.spec.ts +0 -414
- package/src/service/proxy.ts +0 -248
- package/src/service/query_boundary.spec.ts +0 -470
- package/src/service/query_metadata.spec.ts +0 -408
- package/src/service/query_metadata.ts +0 -492
- package/src/service/query_metadata_identity.spec.ts +0 -149
- package/src/service/query_text.spec.ts +0 -79
- package/src/service/query_text.ts +0 -43
- package/src/service/quoting.spec.ts +0 -135
- package/src/service/quoting.ts +0 -78
- package/src/service/resolve_environment.ts +0 -15
- package/src/service/restricted_mode.spec.ts +0 -299
- package/src/service/source_extraction.ts +0 -225
- package/src/service/theme_store.ts +0 -199
- package/src/storage/DatabaseInterface.ts +0 -198
- package/src/storage/StorageManager.ts +0 -113
- package/src/storage/duckdb/ConnectionRepository.ts +0 -143
- package/src/storage/duckdb/DuckDBConnection.ts +0 -123
- package/src/storage/duckdb/DuckDBRepository.ts +0 -208
- package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
- package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
- package/src/storage/duckdb/MaterializationRepository.ts +0 -336
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/schema.ts +0 -229
- package/src/stream_helpers.spec.ts +0 -280
- package/src/stream_helpers.ts +0 -162
- package/src/telemetry.ts +0 -20
- package/src/test_helpers/metrics_harness.ts +0 -166
- package/src/theme_key_parity.spec.ts +0 -57
- package/src/utils.ts +0 -29
- package/tests/fixtures/authorize-compile/model.malloy +0 -9
- package/tests/fixtures/authorize-compile/publisher.json +0 -4
- package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
- package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/data.csv +0 -3
- package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
- package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
- package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
- package/tests/fixtures/html-pages-test/public/data.json +0 -1
- package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
- package/tests/fixtures/html-pages-test/public/index.html +0 -9
- package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
- package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
- package/tests/fixtures/html-pages-test/public/slides.html +0 -12
- package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
- package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
- package/tests/fixtures/html-pages-test/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/report.malloy +0 -1
- package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
- package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
- package/tests/fixtures/persist-multi-level/publisher.json +0 -5
- package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
- package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/fixtures/query-givens/data/orders.csv +0 -7
- package/tests/fixtures/query-givens/model.malloy +0 -34
- package/tests/fixtures/query-givens/publisher.json +0 -5
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/harness/mcp_test_setup.ts +0 -332
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
- package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
- package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
- package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
- package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
- package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
- package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
- package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
- package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
- package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
- package/tests/unit/controller/theme.controller.spec.ts +0 -141
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
- package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
- package/tests/unit/duckdb/repositories.test.ts +0 -208
- package/tests/unit/ducklake/ducklake.test.ts +0 -952
- package/tests/unit/service/theme_store.spec.ts +0 -139
- package/tsconfig.json +0 -32
|
@@ -1,734 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { generateKeyPairSync } from "crypto";
|
|
3
|
-
import { components } from "../api";
|
|
4
|
-
import {
|
|
5
|
-
assembleEnvironmentConnections,
|
|
6
|
-
normalizeSnowflakePrivateKey,
|
|
7
|
-
} from "./connection_config";
|
|
8
|
-
|
|
9
|
-
type ApiConnection = components["schemas"]["Connection"];
|
|
10
|
-
|
|
11
|
-
describe("assembleEnvironmentConnections — databricks", () => {
|
|
12
|
-
const validBase: ApiConnection = {
|
|
13
|
-
name: "dbx",
|
|
14
|
-
type: "databricks",
|
|
15
|
-
databricksConnection: {
|
|
16
|
-
host: "dbc.cloud.databricks.com",
|
|
17
|
-
path: "/sql/1.0/warehouses/abc",
|
|
18
|
-
token: "dapiXXXX",
|
|
19
|
-
defaultCatalog: "main",
|
|
20
|
-
defaultSchema: "default",
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
it("emits a databricks core entry with all known fields preserved", () => {
|
|
25
|
-
const { pojo, apiConnections } = assembleEnvironmentConnections([
|
|
26
|
-
validBase,
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const entry = pojo.connections["dbx"];
|
|
30
|
-
expect(entry.is).toBe("databricks");
|
|
31
|
-
expect(entry.host).toBe("dbc.cloud.databricks.com");
|
|
32
|
-
expect(entry.path).toBe("/sql/1.0/warehouses/abc");
|
|
33
|
-
expect(entry.token).toBe("dapiXXXX");
|
|
34
|
-
expect(entry.defaultCatalog).toBe("main");
|
|
35
|
-
expect(entry.defaultSchema).toBe("default");
|
|
36
|
-
|
|
37
|
-
expect(apiConnections).toHaveLength(1);
|
|
38
|
-
expect(apiConnections[0].attributes?.dialectName).toBe("databricks");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("accepts OAuth M2M auth (clientId + secret) without a token", () => {
|
|
42
|
-
const conn: ApiConnection = {
|
|
43
|
-
name: "dbx-oauth",
|
|
44
|
-
type: "databricks",
|
|
45
|
-
databricksConnection: {
|
|
46
|
-
host: "dbc.cloud.databricks.com",
|
|
47
|
-
path: "/sql/1.0/warehouses/abc",
|
|
48
|
-
oauthClientId: "client-id",
|
|
49
|
-
oauthClientSecret: "client-secret",
|
|
50
|
-
defaultCatalog: "main",
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
const { pojo } = assembleEnvironmentConnections([conn]);
|
|
54
|
-
const entry = pojo.connections["dbx-oauth"];
|
|
55
|
-
expect(entry.is).toBe("databricks");
|
|
56
|
-
expect(entry.oauthClientId).toBe("client-id");
|
|
57
|
-
expect(entry.oauthClientSecret).toBe("client-secret");
|
|
58
|
-
expect(entry.token).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("rejects connections missing the databricksConnection block", () => {
|
|
62
|
-
const conn: ApiConnection = {
|
|
63
|
-
name: "dbx",
|
|
64
|
-
type: "databricks",
|
|
65
|
-
};
|
|
66
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
67
|
-
"Databricks connection configuration is missing.",
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("rejects connections with a missing host", () => {
|
|
72
|
-
const conn: ApiConnection = {
|
|
73
|
-
...validBase,
|
|
74
|
-
databricksConnection: {
|
|
75
|
-
...validBase.databricksConnection!,
|
|
76
|
-
host: undefined,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
80
|
-
"Databricks host is required",
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("rejects connections with a missing path", () => {
|
|
85
|
-
const conn: ApiConnection = {
|
|
86
|
-
...validBase,
|
|
87
|
-
databricksConnection: {
|
|
88
|
-
...validBase.databricksConnection!,
|
|
89
|
-
path: undefined,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
93
|
-
"Databricks SQL warehouse HTTP path is required",
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("rejects when defaultCatalog is missing", () => {
|
|
98
|
-
const conn: ApiConnection = {
|
|
99
|
-
name: "dbx",
|
|
100
|
-
type: "databricks",
|
|
101
|
-
databricksConnection: {
|
|
102
|
-
host: "dbc.cloud.databricks.com",
|
|
103
|
-
path: "/sql/1.0/warehouses/abc",
|
|
104
|
-
token: "dapiXXXX",
|
|
105
|
-
// defaultCatalog deliberately omitted
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
109
|
-
"Databricks default catalog is required",
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("rejects when neither token nor full OAuth credentials are provided", () => {
|
|
114
|
-
const conn: ApiConnection = {
|
|
115
|
-
name: "dbx",
|
|
116
|
-
type: "databricks",
|
|
117
|
-
databricksConnection: {
|
|
118
|
-
host: "dbc.cloud.databricks.com",
|
|
119
|
-
path: "/sql/1.0/warehouses/abc",
|
|
120
|
-
// Only oauthClientId, missing secret → rejected.
|
|
121
|
-
oauthClientId: "client-id",
|
|
122
|
-
defaultCatalog: "main",
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
126
|
-
"Databricks requires",
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("normalizeSnowflakePrivateKey", () => {
|
|
132
|
-
const { privateKey: pkcs8Pem } = generateKeyPairSync("rsa", {
|
|
133
|
-
modulusLength: 2048,
|
|
134
|
-
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
135
|
-
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
136
|
-
});
|
|
137
|
-
const { privateKey: pkcs1Pem } = generateKeyPairSync("rsa", {
|
|
138
|
-
modulusLength: 2048,
|
|
139
|
-
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
140
|
-
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("passes a multi-line PKCS#8 key through and adds a trailing newline", () => {
|
|
144
|
-
const trimmed = (pkcs8Pem as string).trimEnd();
|
|
145
|
-
const result = normalizeSnowflakePrivateKey(trimmed);
|
|
146
|
-
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
147
|
-
expect(result.endsWith("\n")).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("converts a multi-line PKCS#1 RSA key to PKCS#8", () => {
|
|
151
|
-
const result = normalizeSnowflakePrivateKey(pkcs1Pem as string);
|
|
152
|
-
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
153
|
-
expect(result).not.toContain("BEGIN RSA PRIVATE KEY");
|
|
154
|
-
expect(result.endsWith("\n")).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("converts a single-line PKCS#1 RSA key to PKCS#8", () => {
|
|
158
|
-
const singleLine = (pkcs1Pem as string).replace(/\n/g, "");
|
|
159
|
-
const result = normalizeSnowflakePrivateKey(singleLine);
|
|
160
|
-
expect(result).toContain("-----BEGIN PRIVATE KEY-----");
|
|
161
|
-
expect(result).not.toContain("BEGIN RSA PRIVATE KEY");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("reconstructs a single-line PKCS#8 key without conversion", () => {
|
|
165
|
-
const singleLine = (pkcs8Pem as string).replace(/\n/g, "");
|
|
166
|
-
const result = normalizeSnowflakePrivateKey(singleLine);
|
|
167
|
-
expect(result.startsWith("-----BEGIN PRIVATE KEY-----\n")).toBe(true);
|
|
168
|
-
expect(result.endsWith("-----END PRIVATE KEY-----\n")).toBe(true);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("assembleEnvironmentConnections — publisher", () => {
|
|
173
|
-
const validBase: ApiConnection = {
|
|
174
|
-
name: "analytics",
|
|
175
|
-
type: "publisher",
|
|
176
|
-
publisherConnection: {
|
|
177
|
-
connectionUri:
|
|
178
|
-
"https://org.data.example.com/api/v0/environments/proj/connections/analytics",
|
|
179
|
-
accessToken: "jwt-token",
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// publisher connections are default-deny (SSRF gate); these assembly tests
|
|
184
|
-
// exercise the enabled path, so opt in for the block and restore after.
|
|
185
|
-
const priorFlag = process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
186
|
-
beforeEach(() => {
|
|
187
|
-
process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS = "true";
|
|
188
|
-
});
|
|
189
|
-
afterEach(() => {
|
|
190
|
-
if (priorFlag === undefined) {
|
|
191
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
192
|
-
} else {
|
|
193
|
-
process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS = priorFlag;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("emits a publisher core entry proxying to the remote dataplane", () => {
|
|
198
|
-
const { pojo } = assembleEnvironmentConnections([validBase]);
|
|
199
|
-
|
|
200
|
-
const entry = pojo.connections["analytics"];
|
|
201
|
-
expect(entry.is).toBe("publisher");
|
|
202
|
-
expect(entry.connectionUri).toBe(
|
|
203
|
-
"https://org.data.example.com/api/v0/environments/proj/connections/analytics",
|
|
204
|
-
);
|
|
205
|
-
expect(entry.accessToken).toBe("jwt-token");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("does not populate static connection attributes (dialect is resolved at runtime)", () => {
|
|
209
|
-
const { apiConnections } = assembleEnvironmentConnections([validBase]);
|
|
210
|
-
expect(apiConnections).toHaveLength(1);
|
|
211
|
-
expect(apiConnections[0].attributes).toBeUndefined();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("assembles without an accessToken (optional)", () => {
|
|
215
|
-
const conn: ApiConnection = {
|
|
216
|
-
name: "analytics",
|
|
217
|
-
type: "publisher",
|
|
218
|
-
publisherConnection: {
|
|
219
|
-
connectionUri:
|
|
220
|
-
"https://org.data.example.com/api/v0/environments/proj/connections/analytics",
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
const { pojo } = assembleEnvironmentConnections([conn]);
|
|
224
|
-
const entry = pojo.connections["analytics"];
|
|
225
|
-
expect(entry.is).toBe("publisher");
|
|
226
|
-
expect(entry.accessToken).toBeUndefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("rejects a publisher connection missing connectionUri with an actionable error", () => {
|
|
230
|
-
const conn: ApiConnection = {
|
|
231
|
-
name: "analytics",
|
|
232
|
-
type: "publisher",
|
|
233
|
-
publisherConnection:
|
|
234
|
-
{} as components["schemas"]["PublisherConnection"],
|
|
235
|
-
};
|
|
236
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
237
|
-
"Invalid publisher connection 'analytics': missing connectionUri.",
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("rejects a publisher connection missing the publisherConnection block", () => {
|
|
242
|
-
const conn: ApiConnection = {
|
|
243
|
-
name: "analytics",
|
|
244
|
-
type: "publisher",
|
|
245
|
-
};
|
|
246
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
247
|
-
"Invalid publisher connection 'analytics': missing connectionUri.",
|
|
248
|
-
);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("rejects a publisher connection whose connectionUri is not a valid URL", () => {
|
|
252
|
-
const conn: ApiConnection = {
|
|
253
|
-
name: "analytics",
|
|
254
|
-
type: "publisher",
|
|
255
|
-
publisherConnection: { connectionUri: "not a url" },
|
|
256
|
-
};
|
|
257
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
258
|
-
"Invalid publisher connection 'analytics': connectionUri is not a valid URL.",
|
|
259
|
-
);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("rejects a publisher connection whose connectionUri uses a non-http(s) scheme", () => {
|
|
263
|
-
const conn: ApiConnection = {
|
|
264
|
-
name: "analytics",
|
|
265
|
-
type: "publisher",
|
|
266
|
-
publisherConnection: { connectionUri: "file:///etc/passwd" },
|
|
267
|
-
};
|
|
268
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
269
|
-
"connectionUri must use http or https (got 'file:')",
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("does not echo a credential-bearing connectionUri in the validation error", () => {
|
|
274
|
-
const conn: ApiConnection = {
|
|
275
|
-
name: "analytics",
|
|
276
|
-
type: "publisher",
|
|
277
|
-
// Userinfo present but the URI is otherwise malformed (space in host)
|
|
278
|
-
// so it fails to parse and must not be reflected back verbatim.
|
|
279
|
-
publisherConnection: {
|
|
280
|
-
connectionUri: "https://user:s3cret@bad host/connections/x",
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
284
|
-
"connectionUri is not a valid URL",
|
|
285
|
-
);
|
|
286
|
-
expect(() => assembleEnvironmentConnections([conn])).not.toThrow(
|
|
287
|
-
/s3cret/,
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("still rejects the reserved 'duckdb' name for a publisher connection", () => {
|
|
292
|
-
const conn: ApiConnection = {
|
|
293
|
-
name: "duckdb",
|
|
294
|
-
type: "publisher",
|
|
295
|
-
publisherConnection: { connectionUri: "https://x/connections/duckdb" },
|
|
296
|
-
};
|
|
297
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
298
|
-
"Connection name 'duckdb' is reserved",
|
|
299
|
-
);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("still rejects a publisher connection with no name", () => {
|
|
303
|
-
const conn = {
|
|
304
|
-
type: "publisher",
|
|
305
|
-
publisherConnection: { connectionUri: "https://x/connections/y" },
|
|
306
|
-
} as ApiConnection;
|
|
307
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
308
|
-
"Invalid connection configuration. No name.",
|
|
309
|
-
);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
describe("SSRF gate (PUBLISHER_ALLOW_PROXY_CONNECTIONS)", () => {
|
|
313
|
-
it("denies a valid publisher connection when the flag is unset (default-deny)", () => {
|
|
314
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
315
|
-
expect(() => assembleEnvironmentConnections([validBase])).toThrow(
|
|
316
|
-
"Publisher proxy connection 'analytics' is disabled in this deployment",
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("error names the env var to flip", () => {
|
|
321
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
322
|
-
expect(() => assembleEnvironmentConnections([validBase])).toThrow(
|
|
323
|
-
"Fix: set the environment variable PUBLISHER_ALLOW_PROXY_CONNECTIONS=true",
|
|
324
|
-
);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("denies for any non-'true' value (fail-closed)", () => {
|
|
328
|
-
process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS = "1";
|
|
329
|
-
expect(() => assembleEnvironmentConnections([validBase])).toThrow(
|
|
330
|
-
"is disabled in this deployment",
|
|
331
|
-
);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("gate fires before the connectionUri shape check", () => {
|
|
335
|
-
// A publisher connection missing connectionUri still surfaces the
|
|
336
|
-
// disabled error first when the gate is closed — the type is refused
|
|
337
|
-
// outright, not validated.
|
|
338
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
339
|
-
const conn: ApiConnection = { name: "analytics", type: "publisher" };
|
|
340
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
341
|
-
"is disabled in this deployment",
|
|
342
|
-
);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("allows a valid publisher connection when the flag is 'true'", () => {
|
|
346
|
-
process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS = "true";
|
|
347
|
-
const { pojo } = assembleEnvironmentConnections([validBase]);
|
|
348
|
-
expect(pojo.connections["analytics"].is).toBe("publisher");
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
describe("SSH proxy validation", () => {
|
|
354
|
-
const validSshProxy: ApiConnection = {
|
|
355
|
-
name: "pg-via-bastion",
|
|
356
|
-
type: "postgres",
|
|
357
|
-
postgresConnection: {
|
|
358
|
-
host: "127.0.0.1",
|
|
359
|
-
port: 5432,
|
|
360
|
-
databaseName: "mydb",
|
|
361
|
-
userName: "user",
|
|
362
|
-
password: "pass",
|
|
363
|
-
},
|
|
364
|
-
proxy: {
|
|
365
|
-
type: "ssh",
|
|
366
|
-
ssh: {
|
|
367
|
-
host: "bastion.example.com",
|
|
368
|
-
username: "ec2-user",
|
|
369
|
-
privateKey:
|
|
370
|
-
"-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----",
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
it("allows a postgres+ssh-proxy connection with no env flag required", () => {
|
|
376
|
-
// The SSH proxy is a normal connection capability — deliberately NOT gated
|
|
377
|
-
// by PUBLISHER_ALLOW_PROXY_CONNECTIONS (that flag is for the `publisher`
|
|
378
|
-
// HTTP multi-hop type). Prove it assembles with the flag unset.
|
|
379
|
-
const priorFlag = process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
380
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
381
|
-
try {
|
|
382
|
-
const { pojo } = assembleEnvironmentConnections([validSshProxy]);
|
|
383
|
-
expect(pojo.connections["pg-via-bastion"].is).toBe("postgres");
|
|
384
|
-
} finally {
|
|
385
|
-
if (priorFlag === undefined) {
|
|
386
|
-
delete process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS;
|
|
387
|
-
} else {
|
|
388
|
-
process.env.PUBLISHER_ALLOW_PROXY_CONNECTIONS = priorFlag;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("rejects a non-postgres (bigquery) connection with a proxy", () => {
|
|
394
|
-
const conn: ApiConnection = {
|
|
395
|
-
name: "bq-via-bastion",
|
|
396
|
-
type: "bigquery",
|
|
397
|
-
bigqueryConnection: {
|
|
398
|
-
serviceAccountKeyJson: JSON.stringify({
|
|
399
|
-
type: "service_account",
|
|
400
|
-
project_id: "proj",
|
|
401
|
-
private_key: "key",
|
|
402
|
-
client_email: "sa@proj.iam.gserviceaccount.com",
|
|
403
|
-
}),
|
|
404
|
-
},
|
|
405
|
-
proxy: {
|
|
406
|
-
type: "ssh",
|
|
407
|
-
ssh: {
|
|
408
|
-
host: "bastion.example.com",
|
|
409
|
-
username: "ec2-user",
|
|
410
|
-
privateKey: "key",
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
415
|
-
"Connection proxy is not supported for type 'bigquery' (only 'postgres' today)",
|
|
416
|
-
);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it("rejects an ssh proxy with no ssh config object", () => {
|
|
420
|
-
const conn: ApiConnection = {
|
|
421
|
-
name: "pg-no-ssh-config",
|
|
422
|
-
type: "postgres",
|
|
423
|
-
postgresConnection: {
|
|
424
|
-
host: "127.0.0.1",
|
|
425
|
-
databaseName: "mydb",
|
|
426
|
-
},
|
|
427
|
-
proxy: {
|
|
428
|
-
type: "ssh",
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
432
|
-
"Connection proxy on 'pg-no-ssh-config' has type 'ssh' but no 'ssh' config object",
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it("rejects a proxied postgres connection that also carries a connectionString", () => {
|
|
437
|
-
const conn: ApiConnection = {
|
|
438
|
-
...validSshProxy,
|
|
439
|
-
name: "pg-with-connstring",
|
|
440
|
-
postgresConnection: {
|
|
441
|
-
connectionString: "postgresql://real-db.example.com:5432/mydb",
|
|
442
|
-
host: "127.0.0.1",
|
|
443
|
-
port: 5432,
|
|
444
|
-
databaseName: "mydb",
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
// The tunnel forwards to host/port; a connectionString would be silently
|
|
448
|
-
// ignored and could point at a different database, so it's rejected.
|
|
449
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
450
|
-
"does not support the connectionString form",
|
|
451
|
-
);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it("rejects a proxied postgres connection missing explicit host/port", () => {
|
|
455
|
-
const conn: ApiConnection = {
|
|
456
|
-
...validSshProxy,
|
|
457
|
-
name: "pg-no-host-port",
|
|
458
|
-
postgresConnection: {
|
|
459
|
-
databaseName: "mydb",
|
|
460
|
-
userName: "user",
|
|
461
|
-
},
|
|
462
|
-
};
|
|
463
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
464
|
-
"requires explicit host and port",
|
|
465
|
-
);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("accepts a multi-line hostKey and sslmode=no-verify", () => {
|
|
469
|
-
const conn: ApiConnection = {
|
|
470
|
-
...validSshProxy,
|
|
471
|
-
postgresConnection: {
|
|
472
|
-
...validSshProxy.postgresConnection!,
|
|
473
|
-
sslmode: "no-verify",
|
|
474
|
-
},
|
|
475
|
-
proxy: {
|
|
476
|
-
type: "ssh",
|
|
477
|
-
ssh: {
|
|
478
|
-
...validSshProxy.proxy!.ssh!,
|
|
479
|
-
hostKey:
|
|
480
|
-
"bastion ssh-ed25519 AAAAdecoy\nbastion ssh-rsa AAAAreal",
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
expect(() => assembleEnvironmentConnections([conn])).not.toThrow();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it("rejects a hostKey that parses to zero keys (comment/blank only) — fail-closed at config load", () => {
|
|
488
|
-
const conn: ApiConnection = {
|
|
489
|
-
...validSshProxy,
|
|
490
|
-
proxy: {
|
|
491
|
-
type: "ssh",
|
|
492
|
-
ssh: {
|
|
493
|
-
...validSshProxy.proxy!.ssh!,
|
|
494
|
-
hostKey: "# ssh-keyscan bastion timed out\n \n",
|
|
495
|
-
},
|
|
496
|
-
},
|
|
497
|
-
};
|
|
498
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
499
|
-
"no usable host-key line",
|
|
500
|
-
);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("rejects a whitespace-only hostKey (must not silently connect unpinned)", () => {
|
|
504
|
-
const conn: ApiConnection = {
|
|
505
|
-
...validSshProxy,
|
|
506
|
-
proxy: {
|
|
507
|
-
type: "ssh",
|
|
508
|
-
ssh: {
|
|
509
|
-
...validSshProxy.proxy!.ssh!,
|
|
510
|
-
hostKey: " ",
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
|
-
};
|
|
514
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
515
|
-
"no usable host-key line",
|
|
516
|
-
);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it("treats an empty-string hostKey as unpinned (omission-equivalent)", () => {
|
|
520
|
-
const conn: ApiConnection = {
|
|
521
|
-
...validSshProxy,
|
|
522
|
-
proxy: {
|
|
523
|
-
type: "ssh",
|
|
524
|
-
ssh: { ...validSshProxy.proxy!.ssh!, hostKey: "" },
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
expect(() => assembleEnvironmentConnections([conn])).not.toThrow();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("rejects an unsupported sslmode", () => {
|
|
531
|
-
const conn = {
|
|
532
|
-
...validSshProxy,
|
|
533
|
-
postgresConnection: {
|
|
534
|
-
...validSshProxy.postgresConnection!,
|
|
535
|
-
sslmode: "require",
|
|
536
|
-
},
|
|
537
|
-
} as unknown as ApiConnection;
|
|
538
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
539
|
-
"unsupported sslmode 'require'",
|
|
540
|
-
);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it("rejects an empty-string sslmode at config load (not deferred to connect)", () => {
|
|
544
|
-
const conn = {
|
|
545
|
-
...validSshProxy,
|
|
546
|
-
postgresConnection: {
|
|
547
|
-
...validSshProxy.postgresConnection!,
|
|
548
|
-
sslmode: "",
|
|
549
|
-
},
|
|
550
|
-
} as unknown as ApiConnection;
|
|
551
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
552
|
-
"unsupported sslmode",
|
|
553
|
-
);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it("rejects sslmode=verify-ca when no CA bundle is available", () => {
|
|
557
|
-
const prior = process.env.NODE_EXTRA_CA_CERTS;
|
|
558
|
-
delete process.env.NODE_EXTRA_CA_CERTS;
|
|
559
|
-
try {
|
|
560
|
-
const conn: ApiConnection = {
|
|
561
|
-
...validSshProxy,
|
|
562
|
-
postgresConnection: {
|
|
563
|
-
...validSshProxy.postgresConnection!,
|
|
564
|
-
sslmode: "verify-ca",
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
568
|
-
"verify-ca",
|
|
569
|
-
);
|
|
570
|
-
} finally {
|
|
571
|
-
if (prior !== undefined) process.env.NODE_EXTRA_CA_CERTS = prior;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("accepts sslmode=verify-full on a proxied connection with no CA bundle (verifies against Node's bundled CA roots + NODE_EXTRA_CA_CERTS)", () => {
|
|
576
|
-
// Unlike verify-ca (which passes an explicit sslrootcert path and so
|
|
577
|
-
// fails fast without NODE_EXTRA_CA_CERTS), verify-full trusts Node's
|
|
578
|
-
// bundled Mozilla CA roots + NODE_EXTRA_CA_CERTS — so it must NOT require
|
|
579
|
-
// a bundle at load.
|
|
580
|
-
const prior = process.env.NODE_EXTRA_CA_CERTS;
|
|
581
|
-
delete process.env.NODE_EXTRA_CA_CERTS;
|
|
582
|
-
try {
|
|
583
|
-
const conn: ApiConnection = {
|
|
584
|
-
...validSshProxy,
|
|
585
|
-
postgresConnection: {
|
|
586
|
-
...validSshProxy.postgresConnection!,
|
|
587
|
-
sslmode: "verify-full",
|
|
588
|
-
},
|
|
589
|
-
};
|
|
590
|
-
expect(() => assembleEnvironmentConnections([conn])).not.toThrow();
|
|
591
|
-
} finally {
|
|
592
|
-
if (prior !== undefined) process.env.NODE_EXTRA_CA_CERTS = prior;
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it("rejects sslmode set on a non-proxied connection (silent-ignore footgun)", () => {
|
|
597
|
-
const conn: ApiConnection = {
|
|
598
|
-
name: "pg-direct",
|
|
599
|
-
type: "postgres",
|
|
600
|
-
postgresConnection: {
|
|
601
|
-
host: "db.example.com",
|
|
602
|
-
port: 5432,
|
|
603
|
-
databaseName: "mydb",
|
|
604
|
-
userName: "user",
|
|
605
|
-
password: "pass",
|
|
606
|
-
sslmode: "verify-ca",
|
|
607
|
-
},
|
|
608
|
-
};
|
|
609
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
610
|
-
"only supported for proxied connections",
|
|
611
|
-
);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it("rejects a proxied database name with URI-reserved characters", () => {
|
|
615
|
-
const conn: ApiConnection = {
|
|
616
|
-
...validSshProxy,
|
|
617
|
-
postgresConnection: {
|
|
618
|
-
...validSshProxy.postgresConnection!,
|
|
619
|
-
databaseName: "sales@prod",
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
|
-
expect(() => assembleEnvironmentConnections([conn])).toThrow(
|
|
623
|
-
"URI-reserved characters",
|
|
624
|
-
);
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
describe("ducklake shape validation", () => {
|
|
629
|
-
// A DuckLake destination is only attached at its first BUILD, so without an
|
|
630
|
-
// eager check a missing catalog or bucketUrl surfaced hours after the config
|
|
631
|
-
// change that caused it. Validated at load like its duckdb-family siblings.
|
|
632
|
-
const valid: ApiConnection = {
|
|
633
|
-
name: "lake",
|
|
634
|
-
type: "ducklake",
|
|
635
|
-
ducklakeConnection: {
|
|
636
|
-
catalog: {
|
|
637
|
-
postgresConnection: {
|
|
638
|
-
host: "127.0.0.1",
|
|
639
|
-
port: 5432,
|
|
640
|
-
databaseName: "catalog",
|
|
641
|
-
userName: "u",
|
|
642
|
-
password: "p",
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
storage: { bucketUrl: "/tmp/lake" },
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
it("accepts a fully configured destination", () => {
|
|
650
|
-
expect(() =>
|
|
651
|
-
assembleEnvironmentConnections([valid], "/tmp/env"),
|
|
652
|
-
).not.toThrow();
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("rejects a missing catalog connection at load", () => {
|
|
656
|
-
const c = {
|
|
657
|
-
...valid,
|
|
658
|
-
ducklakeConnection: { storage: { bucketUrl: "/tmp/lake" } },
|
|
659
|
-
} as ApiConnection;
|
|
660
|
-
expect(() => assembleEnvironmentConnections([c], "/tmp/env")).toThrow(
|
|
661
|
-
/PostgreSQL connection configuration is required for DuckLake catalog/i,
|
|
662
|
-
);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it("rejects a missing bucketUrl at load", () => {
|
|
666
|
-
const c = {
|
|
667
|
-
...valid,
|
|
668
|
-
ducklakeConnection: { catalog: valid.ducklakeConnection!.catalog },
|
|
669
|
-
} as ApiConnection;
|
|
670
|
-
expect(() => assembleEnvironmentConnections([c], "/tmp/env")).toThrow(
|
|
671
|
-
/Storage bucketUrl is required for DuckLake/i,
|
|
672
|
-
);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
// metadataSchema reaches TWO grammars: a quoted string literal in the ATTACH,
|
|
676
|
-
// and a quoted identifier in the catalog-format preflight's table reference.
|
|
677
|
-
// Restricting it to a plain identifier at load is what keeps one value valid in
|
|
678
|
-
// both, so these pin the accept/reject boundary rather than trusting escaping.
|
|
679
|
-
const withSchema = (metadataSchema: unknown): ApiConnection =>
|
|
680
|
-
({
|
|
681
|
-
...valid,
|
|
682
|
-
ducklakeConnection: {
|
|
683
|
-
...valid.ducklakeConnection,
|
|
684
|
-
catalog: {
|
|
685
|
-
...valid.ducklakeConnection!.catalog,
|
|
686
|
-
metadataSchema,
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
}) as ApiConnection;
|
|
690
|
-
|
|
691
|
-
it("accepts an absent metadataSchema", () => {
|
|
692
|
-
// Optional: absence keeps DuckLake's default schema, the prior behavior.
|
|
693
|
-
expect(() =>
|
|
694
|
-
assembleEnvironmentConnections([valid], "/tmp/env"),
|
|
695
|
-
).not.toThrow();
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
it("accepts a plain identifier metadataSchema", () => {
|
|
699
|
-
for (const ok of ["org_a", "_private", "Lake1", "a"]) {
|
|
700
|
-
expect(() =>
|
|
701
|
-
assembleEnvironmentConnections([withSchema(ok)], "/tmp/env"),
|
|
702
|
-
).not.toThrow();
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it("rejects a metadataSchema that is not a plain identifier", () => {
|
|
707
|
-
for (const bad of [
|
|
708
|
-
"foo'; DROP TABLE x; --",
|
|
709
|
-
"has space",
|
|
710
|
-
"dotted.name",
|
|
711
|
-
"1leading_digit",
|
|
712
|
-
"",
|
|
713
|
-
'"quoted"',
|
|
714
|
-
]) {
|
|
715
|
-
expect(() =>
|
|
716
|
-
assembleEnvironmentConnections([withSchema(bad)], "/tmp/env"),
|
|
717
|
-
).toThrow(/metadataSchema must be a plain identifier/i);
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
it("rejects a non-string metadataSchema rather than coercing it", () => {
|
|
722
|
-
// The value comes from untyped JSON and RegExp.test() coerces, so `true` and
|
|
723
|
-
// `null` match the identifier pattern as "true"/"null" and would pass a
|
|
724
|
-
// pattern-only check — then reach escapeSQL's String.replace as a non-string
|
|
725
|
-
// and throw TypeError at the connection's first attach. That runtime failure is
|
|
726
|
-
// the thing this load-time check exists to prevent, so the type is part of the
|
|
727
|
-
// contract, not a formality.
|
|
728
|
-
for (const bad of [true, false, 0, 1, null, {}, [], ["org_a"]]) {
|
|
729
|
-
expect(() =>
|
|
730
|
-
assembleEnvironmentConnections([withSchema(bad)], "/tmp/env"),
|
|
731
|
-
).toThrow(/metadataSchema must be a plain identifier/i);
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
});
|