diff --git a/packages/databricks-vscode/package.json b/packages/databricks-vscode/package.json index 982802ad4..5ad792786 100644 --- a/packages/databricks-vscode/package.json +++ b/packages/databricks-vscode/package.json @@ -1856,6 +1856,14 @@ "type": "number", "default": 5, "description": "The interval in minutes at which the remote state of the bundle is refreshed." + }, + "databricks.proxy.strictSSL": { + "type": "boolean", + "markdownDescription": "Controls whether TLS certificates are verified for the extension's Databricks SDK calls. When unset, the built-in `#http.proxyStrictSSL#` value is used. Set to `false` only when a corporate CA certificate cannot be resolved from the system trust store." + }, + "databricks.proxy.caCert": { + "type": "string", + "markdownDescription": "Absolute path to a PEM bundle of additional CA certificates to trust for the extension's Databricks SDK calls, merged with the OS trust store and Node's built-in roots. Use when a corporate CA cannot be read from the system store." } } } @@ -1875,7 +1883,7 @@ "version": "1.15.0" }, "scripts": { - "vscode:prepublish": "rm -rf out && yarn run package:compile && yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && yarn run package:copy-webview-toolkit && yarn run package:copy-markdown-it && yarn run package:copy-highlight && yarn run generate-telemetry", + "vscode:prepublish": "rm -rf out && yarn run package:compile && yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && yarn run package:copy-webview-toolkit && yarn run package:copy-markdown-it && yarn run package:copy-highlight && yarn run package:copy-windows-ca-certs && yarn run generate-telemetry", "package": "vsce package --baseContentUrl https://github.com/databricks/databricks-vscode/blob/${TAG:-main}/packages/databricks-vscode --baseImagesUrl https://raw.githubusercontent.com/databricks/databricks-vscode/${TAG:-main}/packages/databricks-vscode", "package:linux:x64": "./scripts/package-vsix.sh linux-x64", "package:linux:arm64": "./scripts/package-vsix.sh linux-arm64", @@ -1893,7 +1901,8 @@ "package:copy-webview-toolkit": "cp ./node_modules/@vscode/webview-ui-toolkit/dist/toolkit.js ./out/toolkit.js", "package:copy-markdown-it": "cp ./node_modules/markdown-it/dist/browser/markdown-it.umd.min.js ./out/markdown-it.min.js", "package:copy-highlight": "esbuild ./node_modules/highlight.js/lib/common.js --bundle --format=iife --global-name=hljs --outfile=out/highlight.min.js --minify", - "esbuild:base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node --sourcemap --target=es2019", + "package:copy-windows-ca-certs": "bash ./scripts/copy-windows-ca-certs.sh", + "esbuild:base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --external:@vscode/windows-ca-certs --format=cjs --platform=node --sourcemap --target=es2019", "build": "yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && tsc --build --force", "watch": "yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && yarn run package:copy-webview-toolkit && yarn run package:copy-markdown-it && yarn run package:copy-highlight && tsc --build --watch --verbose", "fix": "eslint src --fix && prettier . --write", @@ -1917,6 +1926,7 @@ "@types/shell-quote": "^1.7.5", "@vscode/debugadapter": "^1.64.0", "@vscode/extension-telemetry": "^1.5.2", + "@vscode/proxy-agent": "^0.44.0", "@vscode/webview-ui-toolkit": "^1.4.0", "add": "^2.0.6", "ansi-to-html": "^0.7.2", @@ -1931,6 +1941,9 @@ "winston": "^3.11.0", "yaml": "^2.8.3" }, + "optionalDependencies": { + "@vscode/windows-ca-certs": "^0.3.1" + }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@sinonjs/fake-timers": "^15.4.0", diff --git a/packages/databricks-vscode/scripts/copy-windows-ca-certs.sh b/packages/databricks-vscode/scripts/copy-windows-ca-certs.sh new file mode 100755 index 000000000..6f74f84a9 --- /dev/null +++ b/packages/databricks-vscode/scripts/copy-windows-ca-certs.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Copy the native @vscode/windows-ca-certs module into out/node_modules so the +# esbuild bundle's runtime `require("@vscode/windows-ca-certs")` resolves in the +# packaged extension. The bundle marks the module external (a native .node can't +# be bundled) and the VSIX excludes the top-level node_modules/**, so the module +# has to live under out/ to ship — same approach as the other package:copy-* +# steps. It runs from vscode:prepublish (before vsce zips out/), which is the +# only point at which we can add files to the packaged output. +# +# The module is a win32-only optional dependency, so this is a no-op unless we're +# packaging a win32 VSIX. package-vsix.sh signals that by exporting +# INCLUDE_WINDOWS_CA_CERTS=1 for the win32-* targets; for any other invocation +# (dev builds, mac/linux VSIXs) we skip. +# +# Even for win32 targets the module may be absent when the VSIX is cross-built on +# a non-Windows host (yarn honours the os=win32 install condition and skips it +# there). We warn rather than fail: at runtime a missing module degrades to +# Node's bundled roots plus databricks.proxy.caCert, so the build should still +# succeed. To actually ship the native reader, build the win32 VSIX on a Windows +# host (or install a prebuilt @vscode/windows-ca-certs) so the module is present. + +set -euo pipefail + +# get path of the repo (parent of scripts/) +cd "$(dirname "$(realpath "$0")")/.." + +if [ "${INCLUDE_WINDOWS_CA_CERTS:-}" != "1" ]; then + # Not a win32 packaging target — nothing to do. + exit 0 +fi + +SRC="./node_modules/@vscode/windows-ca-certs" +DEST="./out/node_modules/@vscode/windows-ca-certs" + +if [ ! -d "$SRC" ]; then + echo "WARNING: $SRC not found — packaging a win32 VSIX without the native" >&2 + echo "Windows CA reader. On older Node the SDK will fall back to Node's" >&2 + echo "bundled roots (+ databricks.proxy.caCert). Build on a Windows host to" >&2 + echo "include it." >&2 + exit 0 +fi + +rm -rf "$DEST" +mkdir -p "$(dirname "$DEST")" +# Copy the whole module directory (index.js, package.json, build/Release/*.node, +# etc.) so its own require() calls and the native addon all resolve at runtime. +cp -R "$SRC" "$DEST" + +echo "Copied @vscode/windows-ca-certs -> $DEST" diff --git a/packages/databricks-vscode/scripts/package-vsix.sh b/packages/databricks-vscode/scripts/package-vsix.sh index 5795f9dc8..088e35ea8 100755 --- a/packages/databricks-vscode/scripts/package-vsix.sh +++ b/packages/databricks-vscode/scripts/package-vsix.sh @@ -58,6 +58,14 @@ if [ $ARCH != "win32-arm64" ]; then fi yarn run prettier package.json --write + +# Ship the native Windows CA reader (@vscode/windows-ca-certs) only in the win32 +# VSIXs. vscode:prepublish reads this to decide whether to copy the module into +# out/ (see scripts/copy-windows-ca-certs.sh). +case $ARCH in + win32-*) export INCLUDE_WINDOWS_CA_CERTS=1 ;; +esac + TAG="release-v$(cat package.json | jq -r .version)" yarn run package -t $VSXI_ARCH git checkout -- package.json diff --git a/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts b/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts index 268d4b2af..4598a5860 100644 --- a/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts +++ b/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts @@ -1,22 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { - Config, - ProductVersion, - WorkspaceClient, - logging, -} from "@databricks/sdk-experimental"; +import {Config, WorkspaceClient, logging} from "@databricks/sdk-experimental"; import {CancellationToken, ProgressLocation, window} from "vscode"; import {normalizeHost} from "../../utils/urlUtils"; import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const extensionVersion = require("../../../package.json") - .version as ProductVersion; - import {AzureCliCheck} from "./AzureCliCheck"; import {DatabricksCliCheck} from "./DatabricksCliCheck"; import {Loggers} from "../../logger"; import {CliWrapper} from "../../cli/CliWrapper"; +import {createWorkspaceClient} from "../../utils/network/proxyAgent"; // TODO: Resolve this with SDK's AuthType. export type AuthType = @@ -52,11 +44,7 @@ export abstract class AuthProvider { async getWorkspaceClient(): Promise { const config = await this.getSdkConfig(); - - return new WorkspaceClient(config, { - product: "databricks-vscode", - productVersion: extensionVersion, - }); + return createWorkspaceClient(config, this.host); } /** diff --git a/packages/databricks-vscode/src/configuration/auth/AzureCliCheck.ts b/packages/databricks-vscode/src/configuration/auth/AzureCliCheck.ts index 3029b7905..c0f6837b7 100644 --- a/packages/databricks-vscode/src/configuration/auth/AzureCliCheck.ts +++ b/packages/databricks-vscode/src/configuration/auth/AzureCliCheck.ts @@ -1,9 +1,4 @@ -import { - Context, - ProductVersion, - WorkspaceClient, - logging, -} from "@databricks/sdk-experimental"; +import {Context, logging} from "@databricks/sdk-experimental"; import {CancellationToken, commands, Disposable, Uri, window} from "vscode"; import {Loggers} from "../../logger"; import {AzureCliAuthProvider} from "./AuthProvider"; @@ -14,14 +9,11 @@ import { FileNotFoundException, isFileNotFound, } from "@databricks/sdk-experimental/dist/config/execUtils"; +import {createWorkspaceClient} from "../../utils/network/proxyAgent"; // eslint-disable-next-line @typescript-eslint/naming-convention const {NamedLogger} = logging; -// eslint-disable-next-line @typescript-eslint/no-require-imports -const extensionVersion = require("../../../package.json") - .version as ProductVersion; - type AzureCloud = "AzureCloud" | "AzureChinaCloud" | "AzureUSGovernment"; type AzureStepName = @@ -212,16 +204,13 @@ export class AzureCliCheck implements Disposable { expired: boolean; error?: Error; }> { - const workspaceClient = new WorkspaceClient( + const workspaceClient = await createWorkspaceClient( { host: host.toString(), authType: "azure-cli", azureLoginAppId: this.azureLoginAppId, }, - { - product: "databricks-vscode", - productVersion: extensionVersion, - } + host ); try { await workspaceClient.currentUser.me( diff --git a/packages/databricks-vscode/src/configuration/auth/DatabricksCliCheck.ts b/packages/databricks-vscode/src/configuration/auth/DatabricksCliCheck.ts index 1d06988f7..eaea1bc67 100644 --- a/packages/databricks-vscode/src/configuration/auth/DatabricksCliCheck.ts +++ b/packages/databricks-vscode/src/configuration/auth/DatabricksCliCheck.ts @@ -1,8 +1,6 @@ import { CancellationToken, Context, - ProductVersion, - WorkspaceClient, logging, } from "@databricks/sdk-experimental"; import { @@ -14,10 +12,7 @@ import {DatabricksCliAuthProvider} from "./AuthProvider"; import {orchestrate, OrchestrationLoopError, Step} from "./orchestrate"; import {Loggers} from "../../logger"; import {execFile} from "../../cli/CliWrapper"; - -// eslint-disable-next-line @typescript-eslint/no-require-imports -const extensionVersion = require("../../../package.json") - .version as ProductVersion; +import {createWorkspaceClient} from "../../utils/network/proxyAgent"; type StepName = "tryLogin" | "login"; @@ -111,7 +106,7 @@ export class DatabricksCliCheck implements Disposable { private async tryLogin( cancellationToken?: CancellationToken ): Promise { - const workspaceClient = new WorkspaceClient( + const workspaceClient = await createWorkspaceClient( { host: this.authProvider.host.toString(), authType: "databricks-cli", @@ -121,10 +116,7 @@ export class DatabricksCliCheck implements Disposable { ? {workspaceId: this.authProvider.workspaceId} : {}), }, - { - product: "databricks-vscode", - productVersion: extensionVersion, - } + this.authProvider.host ); try { diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index b626341c5..6752f2676 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -108,6 +108,7 @@ import {BundleVariableModel} from "./bundle/models/BundleVariableModel"; import {BundleVariableTreeDataProvider} from "./ui/bundle-variables/BundleVariableTreeDataProvider"; import {ConfigurationTreeViewManager} from "./ui/configuration-view/ConfigurationTreeViewManager"; import {getCLIDependenciesEnvVars} from "./utils/envVarGenerators"; +import {applyProxyStrictSSLEnv} from "./utils/network/proxyAgent"; import {EnvironmentCommands} from "./language/EnvironmentCommands"; import {PackageManagerTelemetry} from "./language/PackageManagerTelemetry"; import {WorkspaceFolderManager} from "./vscode-objs/WorkspaceFolderManager"; @@ -674,20 +675,16 @@ export async function activate( customWhenContext.updateShowClusterView(); } - function updateStrictSSLEnv() { - const httpConfig = workspace.getConfiguration("http"); - const proxyStrictSSL = httpConfig.get("proxyStrictSSL"); - process.env["DATABRICKS_SDK_PROXY_STRICT_SSL"] = proxyStrictSSL - ? "true" - : "false"; - } - updateFeatureContexts(); - updateStrictSSLEnv(); + // Resolve TLS strict-SSL through the same layered setting the SDK client + // path uses (databricks.proxy.strictSSL -> http.proxyStrictSSL -> true), so + // both writers of DATABRICKS_SDK_PROXY_STRICT_SSL agree and verification + // stays on by default when nothing is configured. + applyProxyStrictSSLEnv(); context.subscriptions.push( workspace.onDidChangeConfiguration(() => { updateFeatureContexts(); - updateStrictSSLEnv(); + applyProxyStrictSSLEnv(); }) ); diff --git a/packages/databricks-vscode/src/utils/envVarGenerators.test.ts b/packages/databricks-vscode/src/utils/envVarGenerators.test.ts index 22ab6e72c..19ab0b4e3 100644 --- a/packages/databricks-vscode/src/utils/envVarGenerators.test.ts +++ b/packages/databricks-vscode/src/utils/envVarGenerators.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {anything, instance, mock, when} from "ts-mockito"; +import {anything, instance, mock, reset, spy, when} from "ts-mockito"; import {ConnectionManager} from "../configuration/ConnectionManager"; import {DatabricksWorkspace} from "../configuration/DatabricksWorkspace"; import {ApiClient, Config} from "@databricks/sdk-experimental"; @@ -13,6 +13,7 @@ import { import {Uri} from "vscode"; import assert from "assert"; import {AuthProvider} from "../configuration/auth/AuthProvider"; +import {workspaceConfigs} from "../vscode-objs/WorkspaceConfigs"; describe(__filename, () => { let mockConnectionManager: ConnectionManager; @@ -22,8 +23,14 @@ describe(__filename, () => { const mockHost = "http://example.com"; let mockApiClient: ApiClient; let existingEnv: any; + let configsSpy: typeof workspaceConfigs; beforeEach(() => { + configsSpy = spy(workspaceConfigs); + // Default: no proxy configured via VS Code settings, so getProxyEnvVars + // falls back to the process env vars the tests set explicitly. + when(configsSpy.httpProxy).thenReturn(undefined); + when(configsSpy.httpNoProxy).thenReturn([]); mockConnectionManager = mock(ConnectionManager); mockDatabricksWorkspace = mock(DatabricksWorkspace); mockCluster = mock(Cluster); @@ -45,6 +52,7 @@ describe(__filename, () => { }); afterEach(() => { + reset(configsSpy); process.env = existingEnv; }); @@ -86,6 +94,51 @@ describe(__filename, () => { }); }); + it("should use the http.proxy setting for both schemes when set", () => { + delete process.env.HTTP_PROXY; + delete process.env.HTTPS_PROXY; + delete process.env.http_proxy; + delete process.env.https_proxy; + when(configsSpy.httpProxy).thenReturn("http://proxy.local:8080"); + + const actual = getProxyEnvVars(); + assert.deepEqual(actual, { + HTTP_PROXY: "http://proxy.local:8080", + HTTPS_PROXY: "http://proxy.local:8080", + NO_PROXY: undefined, + }); + }); + + it("should let the http.proxy setting win over env vars", () => { + process.env.HTTP_PROXY = "http://env-proxy.local:1"; + process.env.HTTPS_PROXY = "http://env-proxy.local:1"; + when(configsSpy.httpProxy).thenReturn("http://setting-proxy.local:2"); + + const actual = getProxyEnvVars(); + assert.deepEqual(actual, { + HTTP_PROXY: "http://setting-proxy.local:2", + HTTPS_PROXY: "http://setting-proxy.local:2", + NO_PROXY: undefined, + }); + }); + + it("should merge http.noProxy setting with the NO_PROXY env var", () => { + delete process.env.no_proxy; + process.env.NO_PROXY = "shared.example.com,env.example.com"; + when(configsSpy.httpNoProxy).thenReturn([ + "setting.example.com", + "shared.example.com", + ]); + + const actual = getProxyEnvVars(); + assert.deepEqual(actual, { + HTTP_PROXY: undefined, + HTTPS_PROXY: undefined, + // Deduped: "shared.example.com" appears in both sources but once here. + NO_PROXY: "setting.example.com,shared.example.com,env.example.com", + }); + }); + it("should generate env vars with expected cluster id", () => { const actual = getCommonDatabricksEnvVars( instance(mockConnectionManager), diff --git a/packages/databricks-vscode/src/utils/envVarGenerators.ts b/packages/databricks-vscode/src/utils/envVarGenerators.ts index cf34e0139..f564fa711 100644 --- a/packages/databricks-vscode/src/utils/envVarGenerators.ts +++ b/packages/databricks-vscode/src/utils/envVarGenerators.ts @@ -4,6 +4,7 @@ import {ExtensionContext, Uri} from "vscode"; import {logging, Headers} from "@databricks/sdk-experimental"; import {ConnectionManager} from "../configuration/ConnectionManager"; import {TerraformMetadata} from "./terraformUtils"; +import {workspaceConfigs} from "../vscode-objs/WorkspaceConfigs"; // eslint-disable-next-line @typescript-eslint/no-require-imports const packageJson = require("../../package.json"); @@ -140,12 +141,47 @@ export async function getDbConnectEnvVars( /* eslint-enable @typescript-eslint/naming-convention */ } +/** + * Proxy env vars forwarded to the bundled Databricks CLI (and any other + * subprocess). The Go CLI does no proxy handling of its own — it relies on Go's + * `http.ProxyFromEnvironment`, which reads only these env vars — so this is the + * only way to route CLI traffic through a proxy. + * + * Precedence mirrors the in-process SDK path (`proxyAgent.ts` + * `getProxyAgentParams`): the VS Code `http.proxy` setting wins over the OS + * `http(s)_proxy` env vars. That keeps the CLI and the SDK resolving the same + * proxy from the same inputs, so a user who configures the proxy purely through + * the VS Code setting gets it applied to both. `no_proxy` is the exception — the + * `http.noProxy` setting and the env var are *unioned* (not overridden), since a + * bypass list is only ever safer when it's broader. + * + * Absent values stay `undefined` so `removeUndefinedKeys(...)` at the call sites + * strips them, leaving the CLI's own env untouched when nothing is configured. + */ export function getProxyEnvVars() { + // The `http.proxy` setting is a single URL used for both schemes. + const settingProxy = workspaceConfigs.httpProxy; + const httpProxy = + settingProxy || process.env.HTTP_PROXY || process.env.http_proxy; + const httpsProxy = + settingProxy || process.env.HTTPS_PROXY || process.env.https_proxy; + + // Merge the `http.noProxy` setting (an array) with the comma-separated + // NO_PROXY env var, deduping at host granularity. + const envNoProxy = process.env.NO_PROXY || process.env.no_proxy || ""; + const noProxyParts = [ + ...workspaceConfigs.httpNoProxy, + ...envNoProxy.split(","), + ] + .map((v) => v.trim()) + .filter((v) => v.length > 0); + const noProxy = [...new Set(noProxyParts)].join(",") || undefined; + return { /* eslint-disable @typescript-eslint/naming-convention */ - HTTP_PROXY: process.env.HTTP_PROXY || process.env.http_proxy, - HTTPS_PROXY: process.env.HTTPS_PROXY || process.env.https_proxy, - NO_PROXY: process.env.NO_PROXY || process.env.no_proxy, + HTTP_PROXY: httpProxy, + HTTPS_PROXY: httpsProxy, + NO_PROXY: noProxy, /* eslint-enable @typescript-eslint/naming-convention */ }; } diff --git a/packages/databricks-vscode/src/utils/network/proxyAgent.test.ts b/packages/databricks-vscode/src/utils/network/proxyAgent.test.ts new file mode 100644 index 000000000..cbb806667 --- /dev/null +++ b/packages/databricks-vscode/src/utils/network/proxyAgent.test.ts @@ -0,0 +1,330 @@ +import * as assert from "assert"; +import * as https from "node:https"; +import * as http from "node:http"; +import * as tls from "node:tls"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import {reset, spy, when} from "ts-mockito"; +import {HttpsProxyAgent} from "https-proxy-agent"; +import {HttpProxyAgent} from "http-proxy-agent"; +import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; +import {WorkspaceClient} from "@databricks/sdk-experimental"; +import { + applyProxyStrictSSLEnv, + createWorkspaceClient, + getDatabricksHttpAgent, + loadSystemCertificatesFromNode, + resetProxyAgentCaches, + strictSSL, +} from "./proxyAgent"; + +// A syntactically-valid but throwaway PEM used to assert it lands in the agent's +// `ca` list. It never has to verify anything — the tests only check membership. +const FAKE_CA_PEM = + "-----BEGIN CERTIFICATE-----\nMIIBFake\n-----END CERTIFICATE-----\n"; + +describe(__filename, () => { + let configsSpy: typeof workspaceConfigs; + let existingEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + existingEnv = Object.assign({}, process.env); + resetProxyAgentCaches(); + configsSpy = spy(workspaceConfigs); + // Defaults: strict SSL on, no proxy or custom CA configured. + when(configsSpy.proxyStrictSSL).thenReturn(true); + when(configsSpy.httpProxy).thenReturn(undefined); + when(configsSpy.httpNoProxy).thenReturn([]); + when(configsSpy.proxyCaCert).thenReturn(undefined); + }); + + afterEach(() => { + reset(configsSpy); + process.env = existingEnv; + }); + + describe("strictSSL / applyProxyStrictSSLEnv", () => { + it("reflects the effective setting", () => { + when(configsSpy.proxyStrictSSL).thenReturn(true); + assert.strictEqual(strictSSL(), true); + + when(configsSpy.proxyStrictSSL).thenReturn(false); + assert.strictEqual(strictSSL(), false); + }); + + it("sets DATABRICKS_SDK_PROXY_STRICT_SSL only when disabled", () => { + when(configsSpy.proxyStrictSSL).thenReturn(false); + applyProxyStrictSSLEnv(); + assert.strictEqual( + process.env.DATABRICKS_SDK_PROXY_STRICT_SSL, + "false" + ); + + when(configsSpy.proxyStrictSSL).thenReturn(true); + applyProxyStrictSSLEnv(); + assert.strictEqual( + process.env.DATABRICKS_SDK_PROXY_STRICT_SSL, + undefined + ); + }); + }); + + describe("getDatabricksHttpAgent", () => { + it("returns a plain https agent when no proxy is configured", async () => { + const agent = await getDatabricksHttpAgent( + new URL("https://example.com") + ); + assert.ok(agent instanceof https.Agent); + assert.ok(!(agent instanceof HttpsProxyAgent)); + }); + + it("returns an https proxy agent when http.proxy is set", async () => { + when(configsSpy.httpProxy).thenReturn("http://127.0.0.1:8080"); + const agent = await getDatabricksHttpAgent( + new URL("https://example.com") + ); + assert.ok(agent instanceof HttpsProxyAgent); + }); + + it("honours the http.proxy setting for http hosts", async () => { + when(configsSpy.httpProxy).thenReturn("http://127.0.0.1:8080"); + const agent = await getDatabricksHttpAgent( + new URL("http://example.com") + ); + assert.ok(agent instanceof HttpProxyAgent); + }); + + it("returns a plain agent when the host matches noProxy", async () => { + when(configsSpy.httpProxy).thenReturn("http://127.0.0.1:8080"); + when(configsSpy.httpNoProxy).thenReturn(["example.com"]); + const agent = await getDatabricksHttpAgent( + new URL("https://example.com") + ); + assert.ok(agent instanceof https.Agent); + assert.ok(!(agent instanceof HttpsProxyAgent)); + }); + + it("returns a plain agent when the host matches NO_PROXY and http.noProxy is also set", async () => { + process.env.NO_PROXY = "example.com"; + when(configsSpy.httpProxy).thenReturn("http://127.0.0.1:8080"); + when(configsSpy.httpNoProxy).thenReturn(["setting.example.com"]); + + const agent = await getDatabricksHttpAgent( + new URL("https://example.com") + ); + + assert.ok(agent instanceof https.Agent); + assert.ok(!(agent instanceof HttpsProxyAgent)); + }); + + it("falls back to a plain http agent for http hosts without a proxy", async () => { + const agent = await getDatabricksHttpAgent( + new URL("http://example.com") + ); + assert.ok(agent instanceof http.Agent); + assert.ok(!(agent instanceof HttpProxyAgent)); + }); + + it("sets the SDK default request timeout on plain agents", async () => { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + + assert.strictEqual( + (agent.options as https.AgentOptions).timeout, + 5000 + ); + }); + + it("preserves the configured SDK request timeout on plain agents", async () => { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com"), + 60 + )) as https.Agent; + + assert.strictEqual( + (agent.options as https.AgentOptions).timeout, + 60000 + ); + }); + + it("preserves the configured SDK request timeout on proxy agents", async () => { + when(configsSpy.httpProxy).thenReturn("http://127.0.0.1:8080"); + + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com"), + 60 + )) as HttpsProxyAgent; + + assert.strictEqual(agent.connectOpts.timeout, 60000); + }); + + it("merges the system trust store with Node's bundled roots (never replaces them)", async () => { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + const ca = (agent.options as https.AgentOptions).ca; + assert.ok(Array.isArray(ca)); + // tls.getCACertificates('system') returns only the OS store; setting + // `ca` to that alone would drop Node's bundled public roots. The + // agent must carry at least all of the bundled roots. + assert.ok( + (ca as unknown[]).length >= tls.rootCertificates.length, + `expected >= ${tls.rootCertificates.length} CAs, got ${ + (ca as unknown[]).length + }` + ); + // A known bundled root is still present. + assert.ok((ca as string[]).includes(tls.rootCertificates[0])); + // Deduped: the OS store often re-lists the bundled roots, but each + // cert appears at most once in the handed-off list. + assert.strictEqual( + new Set(ca as string[]).size, + (ca as string[]).length + ); + }); + + it("disables certificate verification when strict SSL is off", async () => { + when(configsSpy.proxyStrictSSL).thenReturn(false); + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + assert.strictEqual( + (agent.options as https.AgentOptions).rejectUnauthorized, + false + ); + }); + + it("falls back to Node's bundled CAs when the system store can't be read", async () => { + // @vscode/proxy-agent reads the store via tls.getCACertificates, + // which is absent/throws on older runtimes. Simulate that failure + // on the shared (required) tls module proxy-agent also sees. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const tls = require("node:tls"); + const original = tls.getCACertificates; + tls.getCACertificates = () => { + throw new Error("getCACertificates unavailable"); + }; + try { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + // `ca` must be omitted (not [] / undefined) so Node keeps its + // bundled roots instead of trusting nothing. + assert.ok(!("ca" in (agent.options as https.AgentOptions))); + assert.strictEqual( + (agent.options as https.AgentOptions).rejectUnauthorized, + true + ); + } finally { + tls.getCACertificates = original; + } + }); + + it("merges databricks.proxy.caCert onto the trust store", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dbx-ca-")); + const pemPath = path.join(dir, "corp-ca.pem"); + fs.writeFileSync(pemPath, FAKE_CA_PEM); + when(configsSpy.proxyCaCert).thenReturn(pemPath); + try { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + const ca = (agent.options as https.AgentOptions).ca as string[]; + assert.ok(Array.isArray(ca)); + assert.ok(ca.includes(FAKE_CA_PEM)); + // Still anchored on the bundled roots. + assert.ok(ca.includes(tls.rootCertificates[0])); + } finally { + fs.rmSync(dir, {recursive: true, force: true}); + } + }); + + it("applies databricks.proxy.caCert even when the system store can't be read", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dbx-ca-")); + const pemPath = path.join(dir, "corp-ca.pem"); + fs.writeFileSync(pemPath, FAKE_CA_PEM); + when(configsSpy.proxyCaCert).thenReturn(pemPath); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const tlsMod = require("node:tls"); + const original = tlsMod.getCACertificates; + tlsMod.getCACertificates = () => { + throw new Error("getCACertificates unavailable"); + }; + try { + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + const ca = (agent.options as https.AgentOptions).ca as string[]; + assert.ok(Array.isArray(ca)); + assert.ok(ca.includes(FAKE_CA_PEM)); + assert.ok(ca.includes(tls.rootCertificates[0])); + } finally { + tlsMod.getCACertificates = original; + fs.rmSync(dir, {recursive: true, force: true}); + } + }); + + it("ignores an unreadable databricks.proxy.caCert path", async () => { + when(configsSpy.proxyCaCert).thenReturn( + path.join(os.tmpdir(), "does-not-exist-xyz.pem") + ); + // Must not throw; falls back to the rest of the trust store. + const agent = (await getDatabricksHttpAgent( + new URL("https://example.com") + )) as https.Agent; + const ca = (agent.options as https.AgentOptions).ca as string[]; + assert.ok(!ca || !ca.includes(FAKE_CA_PEM)); + }); + }); + + describe("loadSystemCertificatesFromNode", () => { + it("is true when tls.getCACertificates exists, false when it doesn't", () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const tlsMod = require("node:tls"); + const original = tlsMod.getCACertificates; + try { + tlsMod.getCACertificates = () => []; + assert.strictEqual(loadSystemCertificatesFromNode(), true); + + tlsMod.getCACertificates = undefined; + assert.strictEqual(loadSystemCertificatesFromNode(), false); + } finally { + tlsMod.getCACertificates = original; + } + }); + }); + + describe("createWorkspaceClient", () => { + it("builds a client tagged with the extension product and a proxy-aware agent", async () => { + const client = await createWorkspaceClient( + {host: "https://example.com", authType: "pat", token: "t"}, + new URL("https://example.com") + ); + assert.ok(client instanceof WorkspaceClient); + assert.strictEqual(client.apiClient.product, "databricks-vscode"); + const agent = await client.apiClient.getAgent(); + assert.ok(agent instanceof https.Agent); + }); + + it("preserves the configured SDK request timeout on the injected agent", async () => { + const client = await createWorkspaceClient( + { + host: "https://example.com", + authType: "pat", + token: "t", + httpTimeoutSeconds: 60, + }, + new URL("https://example.com") + ); + + const agent = (await client.apiClient.getAgent()) as https.Agent; + + assert.strictEqual( + (agent.options as https.AgentOptions).timeout, + 60000 + ); + }); + }); +}); diff --git a/packages/databricks-vscode/src/utils/network/proxyAgent.ts b/packages/databricks-vscode/src/utils/network/proxyAgent.ts new file mode 100644 index 000000000..da9c43d29 --- /dev/null +++ b/packages/databricks-vscode/src/utils/network/proxyAgent.ts @@ -0,0 +1,297 @@ +import * as http from "node:http"; +import * as https from "node:https"; +import * as tls from "node:tls"; +import {readFile} from "node:fs/promises"; +import { + createProxyResolver, + loadSystemCertificates, + LogLevel, + type Log, + type ProxyAgentParams, +} from "@vscode/proxy-agent"; +import {HttpProxyAgent} from "http-proxy-agent"; +import {HttpsProxyAgent} from "https-proxy-agent"; +import { + logging, + ProductVersion, + WorkspaceClient, +} from "@databricks/sdk-experimental"; +import {Loggers} from "../../logger"; +import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const extensionVersion = require("../../../package.json") + .version as ProductVersion; + +// Mirror the SDK's own default agent tuning (see @databricks/sdk-experimental +// api-client.js ApiClient.getAgent), so behaviour is unchanged apart from the +// proxy + CA wiring we add on top. +const KEEP_ALIVE_MSECS = 15000; +const DEFAULT_HTTP_TIMEOUT_SECONDS = 5; + +/** + * Whether to let @vscode/proxy-agent read the OS trust store via Node's + * `tls.getCACertificates`. + * + * That API was added in Node 22.15. proxy-agent's "from Node" path calls it + * unconditionally, so on older runtimes (shipped by many supported VS Code + * builds) it throws and we lose the OS trust store. When it's missing we return + * `false` so proxy-agent instead uses its native readers (the + * `@vscode/windows-ca-certs` module on Windows, `security` on macOS, PEM bundle + * files on Linux), which work on every Node version. + * + * Evaluated per call (not cached) so tests can simulate an older runtime. + */ +export function loadSystemCertificatesFromNode(): boolean { + return ( + typeof (tls as {getCACertificates?: unknown}).getCACertificates === + "function" + ); +} + +function getLog(): Log { + const logger = logging.NamedLogger.getOrCreate(Loggers.Extension); + return { + trace: (message, ...args) => logger.debug(message, args), + debug: (message, ...args) => logger.debug(message, args), + info: (message, ...args) => logger.info(message, args), + warn: (message, ...args) => logger.warn(message, args), + error: (message, ...args) => + logger.error( + message instanceof Error ? message.message : message, + args + ), + }; +} + +/** + * Build the params @vscode/proxy-agent needs to resolve a proxy the same way + * VS Code core does: `http.proxy` setting first, then the `http(s)_proxy` env + * vars, honouring `http.noProxy` and `NO_PROXY`. System/PAC auto-detection is + * intentionally disabled (`isUseHostProxyEnabled: false`) — it needs Electron's + * proxy resolver, which the extension host doesn't expose. + */ +function getProxyAgentParams(): ProxyAgentParams { + const log = getLog(); + return { + resolveProxy: async () => undefined, + getProxyURL: () => workspaceConfigs.httpProxy, + getProxySupport: () => "on", + getNoProxyConfig: () => getNoProxyConfig(), + isAdditionalFetchSupportEnabled: () => false, + isWebSocketPatchEnabled: () => false, + addCertificatesV1: () => false, + addCertificatesV2: () => true, + loadSystemCertificatesFromNode, + loadAdditionalCertificates: async () => [], + log, + getLogLevel: () => LogLevel.Error, + proxyResolveTelemetry: () => {}, + isUseHostProxyEnabled: () => false, + env: process.env, + }; +} + +function getNoProxyConfig(): string[] { + const envNoProxy = process.env.NO_PROXY || process.env.no_proxy || ""; + const noProxyParts = [ + ...workspaceConfigs.httpNoProxy, + ...envNoProxy.split(","), + ] + .map((v) => v.trim()) + .filter((v) => v.length > 0); + + return [...new Set(noProxyParts)]; +} + +let systemCertificatesPromise: Promise | undefined; + +/** + * Load and cache the OS certificate trust store (Windows/macOS/Linux) plus + * Node's bundled CAs. Cached for the session; call {@link resetProxyAgentCaches} + * in tests. + * + * Returns `undefined` (never a rejected/empty promise) when the store can't be + * read. @vscode/proxy-agent reads it either via Node's `tls.getCACertificates` + * (Node >= 22.15) or, on older runtimes, its native readers (the + * `@vscode/windows-ca-certs` module on Windows, `security` on macOS, PEM files + * on Linux) — see {@link loadSystemCertificatesFromNode}. If that native module is + * absent (e.g. not shipped for this platform) the read can still fail; swallowing + * it here lets the caller fall back to Node's bundled roots instead of failing + * the whole SDK request. A missing custom CA is recoverable (users can point + * `databricks.proxy.caCert` at their PEM, or opt out via + * `databricks.proxy.strictSSL`), a broken agent is not. + */ +async function getSystemCertificates( + params: ProxyAgentParams +): Promise { + if (!systemCertificatesPromise) { + systemCertificatesPromise = loadSystemCertificates({ + loadSystemCertificatesFromNode: + params.loadSystemCertificatesFromNode, + log: params.log, + }).catch((e) => { + params.log.error( + "Failed to load system certificates; falling back to Node's " + + "bundled CAs. Custom/corporate CAs may not be trusted.", + e + ); + // Don't cache the rejection — leave the promise unset so a later + // call can retry. + systemCertificatesPromise = undefined; + return undefined; + }); + } + return systemCertificatesPromise; +} + +/** Reset the cached system certificates. Test-only. */ +export function resetProxyAgentCaches() { + systemCertificatesPromise = undefined; +} + +/** + * Whether the SDK must verify TLS certificates. `false` only when the user + * opted out via `databricks.proxy.strictSSL` or `http.proxyStrictSSL`. + */ +export function strictSSL(): boolean { + return workspaceConfigs.proxyStrictSSL; +} + +/** + * Keep the SDK's own fetch path (fetch.js reads DATABRICKS_SDK_PROXY_STRICT_SSL) + * and the bundled CLI subprocess consistent with the injected agent's + * `rejectUnauthorized`. + */ +export function applyProxyStrictSSLEnv() { + if (strictSSL()) { + delete process.env.DATABRICKS_SDK_PROXY_STRICT_SSL; + } else { + process.env.DATABRICKS_SDK_PROXY_STRICT_SSL = "false"; + } +} + +/** + * Read the user-configured `databricks.proxy.caCert` PEM bundle, if set. Returns + * `undefined` when the setting is empty or the file can't be read — a bad path + * shouldn't break every TLS handshake, so we log and fall back to the rest of + * the trust store. + */ +async function loadConfiguredCaCert(): Promise { + const caCertPath = workspaceConfigs.proxyCaCert; + if (!caCertPath) { + return undefined; + } + try { + return await readFile(caCertPath, "utf8"); + } catch (e) { + getLog().error( + `Failed to read databricks.proxy.caCert from "${caCertPath}"; ` + + "ignoring it. The certificate it points at will not be trusted.", + e + ); + return undefined; + } +} + +/** + * Assemble the CA trust list for the SDK's HTTPS agent, or `undefined` to leave + * Node's default store in place. + * + * Any list we build is anchored on `tls.rootCertificates` (Node's bundled public + * roots) and then extended with the OS trust store and the configured PEM. + * Setting `ca` *replaces* Node's defaults, so if we set it to only the extra + * certs, public-root TLS would break — hence the merge. When we have nothing to + * add (system store unreadable and no `caCert`), return `undefined` so the + * caller omits `ca` and Node keeps its defaults. + * + * Deduped because the OS store commonly re-lists the public roots already in + * `tls.rootCertificates`; a `Set` keeps the handed-off list minimal. + */ +function buildCaBundle( + systemCerts: string[] | undefined, + configuredCaCert: string | undefined +): string[] | undefined { + if (!systemCerts && !configuredCaCert) { + return undefined; + } + return [ + ...new Set([ + ...tls.rootCertificates, + ...(systemCerts ?? []), + ...(configuredCaCert ? [configuredCaCert] : []), + ]), + ]; +} + +/** + * Build the HTTP(S) agent the Databricks SDK should use, wiring in the proxy + * (VS Code `http.proxy` setting + `http(s)_proxy` env vars, honouring + * `NO_PROXY`) and the OS certificate trust store. This is what lets the + * in-process SDK calls work behind corporate proxies and internal-CA TLS + * interception, matching the bundled CLI's behaviour. + */ +export async function getDatabricksHttpAgent( + host: URL, + httpTimeoutSeconds?: number +): Promise { + const params = getProxyAgentParams(); + const isHttps = host.protocol === "https:"; + + const systemCerts = await getSystemCertificates(params); + const configuredCaCert = await loadConfiguredCaCert(); + const ca = buildCaBundle(systemCerts, configuredCaCert); + const rejectUnauthorized = strictSSL(); + + const resolver = createProxyResolver(params); + const proxyUrl = await resolver.resolveProxyURL(host.toString()); + + // Only set `ca` when we have certs to add on top of Node's bundled roots + // (which `buildCaBundle` already folds in). On the fallback path `ca` is + // `undefined`, so we omit it entirely and Node keeps its default store — + // passing `undefined`/`[]` would instead trust nothing. + const agentOptions: https.AgentOptions = { + keepAlive: true, + keepAliveMsecs: KEEP_ALIVE_MSECS, + timeout: (httpTimeoutSeconds || DEFAULT_HTTP_TIMEOUT_SECONDS) * 1000, + ...(isHttps ? {rejectUnauthorized, ...(ca ? {ca} : {})} : {}), + }; + + if (proxyUrl) { + return isHttps + ? new HttpsProxyAgent(proxyUrl, agentOptions) + : new HttpProxyAgent(proxyUrl, agentOptions); + } + + return isHttps + ? new https.Agent(agentOptions) + : new http.Agent(agentOptions); +} + +// The config shape the WorkspaceClient constructor accepts (ConfigOptions | Config), +// derived from the constructor so we don't depend on ConfigOptions — the SDK index +// doesn't re-export it. +type WorkspaceClientConfig = ConstructorParameters[0]; + +/** + * Construct a WorkspaceClient wired for the extension's network environment. + * + * The SDK pins its own agent per request, bypassing VS Code's global proxy/CA + * patching. This injects a proxy- and system-CA-aware agent (and keeps the SDK + * fetch path and bundled-CLI subprocess consistent via the strict-SSL env) so + * in-process SDK calls work behind corporate proxies and internal-CA TLS + * interception, matching the bundled CLI's behaviour. This is the only place + * the extension should build a WorkspaceClient. + */ +export async function createWorkspaceClient( + config: WorkspaceClientConfig, + host: URL +): Promise { + applyProxyStrictSSLEnv(); + const agent = await getDatabricksHttpAgent(host, config.httpTimeoutSeconds); + return new WorkspaceClient(config, { + product: "databricks-vscode", + productVersion: extensionVersion, + agent, + }); +} diff --git a/packages/databricks-vscode/src/vscode-objs/WorkspaceConfigs.ts b/packages/databricks-vscode/src/vscode-objs/WorkspaceConfigs.ts index ae54745eb..77f252242 100644 --- a/packages/databricks-vscode/src/vscode-objs/WorkspaceConfigs.ts +++ b/packages/databricks-vscode/src/vscode-objs/WorkspaceConfigs.ts @@ -181,6 +181,51 @@ export const workspaceConfigs = { return new Time(config, TimeUnits.minutes).toMillSeconds().value; }, + + /** + * Whether TLS certificates must be verified for the extension's SDK HTTP + * calls. `databricks.proxy.strictSSL`, when explicitly set, overrides the + * built-in `http.proxyStrictSSL`; both default to `true`. + */ + get proxyStrictSSL(): boolean { + const override = workspace + .getConfiguration("databricks") + .get("proxy.strictSSL"); + if (override !== undefined && override !== null) { + return override; + } + return ( + workspace.getConfiguration("http").get("proxyStrictSSL") ?? + true + ); + }, + + /** The `http.proxy` VS Code setting, mirrored for the SDK proxy resolver. */ + get httpProxy(): string | undefined { + return ( + workspace.getConfiguration("http").get("proxy") || undefined + ); + }, + + /** The `http.noProxy` VS Code setting, mirrored for the SDK proxy resolver. */ + get httpNoProxy(): string[] { + return workspace.getConfiguration("http").get("noProxy", []); + }, + + /** + * Absolute path to a PEM bundle of additional CA certificates to trust for + * the extension's SDK calls, merged with the OS trust store and Node's + * built-in roots. An escape hatch for corporate CAs that can't be read from + * the system store (e.g. older runtimes where the native reader is + * unavailable). + */ + get proxyCaCert(): string | undefined { + return ( + workspace + .getConfiguration("databricks") + .get("proxy.caCert") || undefined + ); + }, }; export type WorkspaceConfigs = typeof workspaceConfigs; diff --git a/yarn.lock b/yarn.lock index c45b0579b..1eaad58ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1528,6 +1528,13 @@ __metadata: languageName: node linkType: hard +"@tootallnate/once@npm:^3.0.0": + version: 3.0.1 + resolution: "@tootallnate/once@npm:3.0.1" + checksum: 1ec82a3ff0dd2270eb09631789bd63a34445f62dd3031f0d620f4b1c81c5916d5cdb312ae651c4e390ecf62115279165421024efa75b2bb708d0699dc80c4a42 + languageName: node + linkType: hard + "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -2066,6 +2073,25 @@ __metadata: languageName: node linkType: hard +"@vscode/proxy-agent@npm:^0.44.0": + version: 0.44.0 + resolution: "@vscode/proxy-agent@npm:0.44.0" + dependencies: + "@tootallnate/once": ^3.0.0 + "@vscode/windows-ca-certs": ^0.3.1 + agent-base: ^7.0.1 + debug: ^4.3.4 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.2 + socks-proxy-agent: ^8.0.1 + undici: ^7.2.0 + dependenciesMeta: + "@vscode/windows-ca-certs": + optional: true + checksum: 4ebf8fd3c1764c1bb94665e4c70ff7c43eedfebac3a0af29e2912349076a530b3956908ab91b860745f49f64707717187b1d21c14445d8966c07195c2ffb5b7d + languageName: node + linkType: hard + "@vscode/test-electron@npm:^2.4.1": version: 2.5.2 resolution: "@vscode/test-electron@npm:2.5.2" @@ -2106,6 +2132,16 @@ __metadata: languageName: node linkType: hard +"@vscode/windows-ca-certs@npm:^0.3.1": + version: 0.3.4 + resolution: "@vscode/windows-ca-certs@npm:0.3.4" + dependencies: + node-addon-api: ^8.2.0 + node-gyp: latest + conditions: os=win32 + languageName: node + linkType: hard + "@wdio/allure-reporter@npm:^9.1.3": version: 9.29.0 resolution: "@wdio/allure-reporter@npm:9.29.0" @@ -2530,6 +2566,13 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.0.1": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 86a7f542af277cfbd77dd61e7df8422f90bac512953709003a1c530171a9d019d072e2400eab2b59f84b49ab9dd237be44315ca663ac73e82b3922d10ea5eafa + languageName: node + linkType: hard + "agent-base@npm:^7.1.0": version: 7.1.0 resolution: "agent-base@npm:7.1.0" @@ -4023,8 +4066,10 @@ __metadata: "@typescript-eslint/utils": ^8.62.0 "@vscode/debugadapter": ^1.64.0 "@vscode/extension-telemetry": ^1.5.2 + "@vscode/proxy-agent": ^0.44.0 "@vscode/test-electron": ^3.1.0 "@vscode/webview-ui-toolkit": ^1.4.0 + "@vscode/windows-ca-certs": ^0.3.1 "@wdio/cli": ^9.29.0 "@wdio/local-runner": ^9.29.0 "@wdio/mocha-framework": ^9.29.0 @@ -4062,6 +4107,9 @@ __metadata: winston: ^3.11.0 yaml: ^2.8.3 yargs: ^17.7.2 + dependenciesMeta: + "@vscode/windows-ca-certs": + optional: true languageName: unknown linkType: soft @@ -6200,7 +6248,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.5, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -7855,6 +7903,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^8.2.0": + version: 8.9.2 + resolution: "node-addon-api@npm:8.9.2" + dependencies: + node-gyp: latest + checksum: 820b3099b7d27c555fd9d98c986feab52d7eaf2e4dac1bce927857368baa0f2a3f31db5bd5112acd45222fd33a453b3baf8bf78771a0643a4b712e09a33f0fdf + languageName: node + linkType: hard + "node-domexception@npm:^1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" @@ -9402,7 +9459,7 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.5": +"socks-proxy-agent@npm:^8.0.1, socks-proxy-agent@npm:^8.0.5": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" dependencies: @@ -10345,6 +10402,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.2.0": + version: 7.29.0 + resolution: "undici@npm:7.29.0" + checksum: b3327e05259e66c61d3e05242bf63899c68d277302208f4fc1c7c0b880a9ba82651b8b3215584ffd424d8c8410f0114b6c3a89e6af604c703dc33009daa99ed3 + languageName: node + linkType: hard + "unicode-properties@npm:^1.4.1": version: 1.4.1 resolution: "unicode-properties@npm:1.4.1"