From 465676736de1f309284fc76d21f3109420d69a24 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 09:43:20 -0700 Subject: [PATCH 1/6] fix: [AI-266] Snowflake auth fails with `MissingParameterError` for private key and non-password methods Root cause: the `warehouse_add` tool description only showed a Postgres password example, so the Builder LLM used `private_key` (not `private_key_path`) for file paths. The driver only checked `private_key_path`, so key-pair auth was never triggered. Changes: - Support all 8 Snowflake auth methods: password, key-pair (file + inline PEM), OAuth, external browser SSO, Okta SSO, JWT, programmatic access token, MFA - Auto-detect whether `private_key` contains a file path or PEM content - Normalize escaped `\n` in inline PEM from env vars / JSON configs - Accept both snake_case (dbt) and camelCase (SDK) field name variants - Add `private_key`, `privateKey`, `token`, `oauth_client_secret`, `passcode` and camelCase variants to `SENSITIVE_FIELDS` for secure keychain storage - Use `isSensitiveField()` in `formatConnections` instead of hardcoded list - Update `warehouse_add` tool description with Snowflake-specific examples for all auth methods so the Builder LLM picks correct field names - Add `private_key`, `authenticator`, `oauth_client_id`, `oauth_client_secret` to dbt-profiles key mapper - `detectAuthMethod` now returns `sso`, `oauth`, `key_pair` for all variants - Better error messages for key decryption failures and missing OAuth tokens - Use `connectAsync()` for browser-based SSO (external browser / Okta) Closes #266 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/drivers/src/snowflake.ts | 154 +++++++++++++++--- .../native/connections/credential-store.ts | 7 + .../native/connections/dbt-profiles.ts | 4 + .../altimate/native/connections/registry.ts | 5 +- .../src/altimate/tools/dbt-profiles.ts | 3 +- .../src/altimate/tools/warehouse-add.ts | 10 +- .../test/altimate/connections.test.ts | 62 +++++++ .../test/altimate/telemetry-safety.test.ts | 8 + 8 files changed, 229 insertions(+), 24 deletions(-) diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index aa9b381bff..3aab40fd53 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -48,43 +48,155 @@ export async function connect(config: ConnectionConfig): Promise { role: config.role, } - // Key-pair auth - if (config.private_key_path) { - const keyPath = config.private_key_path as string - if (!fs.existsSync(keyPath)) { - throw new Error(`Snowflake private key file not found: ${keyPath}`) + // --------------------------------------------------------------- + // Normalize field names: accept snake_case (dbt), camelCase (SDK), + // and common LLM-generated variants so auth "just works". + // --------------------------------------------------------------- + const keyPath = (config.private_key_path ?? config.privateKeyPath) as string | undefined + const inlineKey = (config.private_key ?? config.privateKey) as string | undefined + const keyPassphrase = (config.private_key_passphrase ?? config.privateKeyPassphrase ?? config.privateKeyPass) as string | undefined + const oauthToken = (config.token ?? config.access_token) as string | undefined + const oauthClientId = (config.oauth_client_id ?? config.oauthClientId) as string | undefined + const oauthClientSecret = (config.oauth_client_secret ?? config.oauthClientSecret) as string | undefined + const authenticator = (config.authenticator as string | undefined)?.trim() + const authUpper = authenticator?.toUpperCase() + const passcode = config.passcode as string | undefined + + // --------------------------------------------------------------- + // 1. Key-pair auth (SNOWFLAKE_JWT) + // Accepts: private_key_path (file), private_key (inline PEM or + // file path auto-detected), privateKey, privateKeyPath. + // --------------------------------------------------------------- + // Resolve private_key: could be a file path or PEM content + let resolvedKeyPath = keyPath + let resolvedInlineKey = inlineKey + if (!resolvedKeyPath && resolvedInlineKey && !resolvedInlineKey.includes("-----BEGIN")) { + // Looks like a file path, not PEM content + if (fs.existsSync(resolvedInlineKey)) { + resolvedKeyPath = resolvedInlineKey + resolvedInlineKey = undefined + } + } + + if (resolvedKeyPath || resolvedInlineKey) { + let keyContent: string + if (resolvedKeyPath) { + if (!fs.existsSync(resolvedKeyPath)) { + throw new Error(`Snowflake private key file not found: ${resolvedKeyPath}`) + } + keyContent = fs.readFileSync(resolvedKeyPath, "utf-8") + } else { + keyContent = resolvedInlineKey! + // Normalize escaped newlines from env vars / JSON configs + if (keyContent.includes("\\n")) { + keyContent = keyContent.replace(/\\n/g, "\n") + } } - const keyContent = fs.readFileSync(keyPath, "utf-8") - // If key is encrypted (has ENCRYPTED in header or passphrase provided), - // decrypt it using Node crypto — snowflake-sdk expects unencrypted PEM. + // If key is encrypted, decrypt using Node crypto — + // snowflake-sdk expects unencrypted PKCS#8 PEM. let privateKey: string - if (config.private_key_passphrase || keyContent.includes("ENCRYPTED")) { + if (keyPassphrase || keyContent.includes("ENCRYPTED")) { const crypto = await import("crypto") - const keyObject = crypto.createPrivateKey({ - key: keyContent, - format: "pem", - passphrase: (config.private_key_passphrase as string) || undefined, - }) - privateKey = keyObject - .export({ type: "pkcs8", format: "pem" }) - .toString() + try { + const keyObject = crypto.createPrivateKey({ + key: keyContent, + format: "pem", + passphrase: keyPassphrase || undefined, + }) + privateKey = keyObject + .export({ type: "pkcs8", format: "pem" }) + .toString() + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + throw new Error( + `Snowflake: Failed to decrypt private key. Verify the passphrase and key format (must be PEM/PKCS#8). ${msg}`, + ) + } } else { privateKey = keyContent } options.authenticator = "SNOWFLAKE_JWT" options.privateKey = privateKey + + // --------------------------------------------------------------- + // 2. External browser SSO + // User's browser opens for IdP login. Non-interactive — requires + // connectAsync() instead of connect(). + // --------------------------------------------------------------- + } else if (authUpper === "EXTERNALBROWSER") { + options.authenticator = "EXTERNALBROWSER" + + // --------------------------------------------------------------- + // 3. Okta native SSO (authenticator is an Okta URL) + // --------------------------------------------------------------- + } else if (authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator)) { + options.authenticator = authenticator + if (config.password) options.password = config.password + + // --------------------------------------------------------------- + // 4. OAuth token auth + // Triggered by: authenticator="oauth", OR token/access_token + // present without a password. + // --------------------------------------------------------------- + } else if (authUpper === "OAUTH" || (oauthToken && !config.password)) { + if (!oauthToken) { + throw new Error( + "Snowflake OAuth authenticator specified but no token provided (expected 'token' or 'access_token')", + ) + } + options.authenticator = "OAUTH" + options.token = oauthToken + if (oauthClientId) options.oauthClientId = oauthClientId + if (oauthClientSecret) options.oauthClientSecret = oauthClientSecret + + // --------------------------------------------------------------- + // 5. JWT token auth (non-key-pair JWT) + // --------------------------------------------------------------- + } else if (authUpper === "JWT" && oauthToken) { + options.authenticator = "SNOWFLAKE_JWT" + options.token = oauthToken + + // --------------------------------------------------------------- + // 6. Programmatic access token + // --------------------------------------------------------------- + } else if (authUpper === "PROGRAMMATIC_ACCESS_TOKEN" && oauthToken) { + options.authenticator = "PROGRAMMATIC_ACCESS_TOKEN" + options.token = oauthToken + + // --------------------------------------------------------------- + // 7. Username + password + MFA + // --------------------------------------------------------------- + } else if (authUpper === "USERNAME_PASSWORD_MFA") { + options.authenticator = "USERNAME_PASSWORD_MFA" + options.password = config.password + if (passcode) options.passcode = passcode + + // --------------------------------------------------------------- + // 8. Plain password auth (default) + // --------------------------------------------------------------- } else if (config.password) { options.password = config.password } + // Use connectAsync for browser-based auth (SSO/Okta), connect for everything else + const useBrowserAuth = authUpper === "EXTERNALBROWSER" || + (authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator)) + connection = await new Promise((resolve, reject) => { const conn = snowflake.createConnection(options) - conn.connect((err: Error | null) => { - if (err) reject(err) - else resolve(conn) - }) + if (useBrowserAuth && typeof conn.connectAsync === "function") { + conn.connectAsync((err: Error | null) => { + if (err) reject(err) + else resolve(conn) + }) + } else { + conn.connect((err: Error | null) => { + if (err) reject(err) + else resolve(conn) + }) + } }) }, diff --git a/packages/opencode/src/altimate/native/connections/credential-store.ts b/packages/opencode/src/altimate/native/connections/credential-store.ts index 724455860e..1baf3ec1bc 100644 --- a/packages/opencode/src/altimate/native/connections/credential-store.ts +++ b/packages/opencode/src/altimate/native/connections/credential-store.ts @@ -14,8 +14,15 @@ const SERVICE_NAME = "altimate-code" const SENSITIVE_FIELDS = new Set([ "password", + "private_key", + "privateKey", "private_key_passphrase", + "privateKeyPassphrase", "access_token", + "token", + "oauth_client_secret", + "oauthClientSecret", + "passcode", "ssh_password", "connection_string", ]) diff --git a/packages/opencode/src/altimate/native/connections/dbt-profiles.ts b/packages/opencode/src/altimate/native/connections/dbt-profiles.ts index a384729b7a..e08892e128 100644 --- a/packages/opencode/src/altimate/native/connections/dbt-profiles.ts +++ b/packages/opencode/src/altimate/native/connections/dbt-profiles.ts @@ -36,8 +36,12 @@ const KEY_MAP: Record = { server_hostname: "server_hostname", http_path: "http_path", token: "access_token", + private_key: "private_key", private_key_path: "private_key_path", private_key_passphrase: "private_key_passphrase", + authenticator: "authenticator", + oauth_client_id: "oauth_client_id", + oauth_client_secret: "oauth_client_secret", keyfile: "credentials_path", keyfile_json: "credentials_json", project: "project", diff --git a/packages/opencode/src/altimate/native/connections/registry.ts b/packages/opencode/src/altimate/native/connections/registry.ts index c0fbf6b522..1122cbaf46 100644 --- a/packages/opencode/src/altimate/native/connections/registry.ts +++ b/packages/opencode/src/altimate/native/connections/registry.ts @@ -209,7 +209,10 @@ async function createConnector( export function detectAuthMethod(config: ConnectionConfig | null | undefined): string { if (!config || typeof config !== "object") return "unknown" if (config.connection_string) return "connection_string" - if (config.private_key_path) return "key_pair" + if (config.private_key_path || config.privateKeyPath || config.private_key || config.privateKey) return "key_pair" + const auth = typeof config.authenticator === "string" ? config.authenticator.toUpperCase() : "" + if (auth === "EXTERNALBROWSER" || (typeof config.authenticator === "string" && /^https?:\/\/.+\.okta\.com/i.test(config.authenticator))) return "sso" + if (auth === "OAUTH") return "oauth" if (config.access_token || config.token) return "token" if (config.password) return "password" const t = typeof config.type === "string" ? config.type.toLowerCase() : "" diff --git a/packages/opencode/src/altimate/tools/dbt-profiles.ts b/packages/opencode/src/altimate/tools/dbt-profiles.ts index 2642880bc0..1573ad26bc 100644 --- a/packages/opencode/src/altimate/tools/dbt-profiles.ts +++ b/packages/opencode/src/altimate/tools/dbt-profiles.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import { isSensitiveField } from "../native/connections/credential-store" export const DbtProfilesTool = Tool.define("dbt_profiles", { description: @@ -52,7 +53,7 @@ function formatConnections(connections: Array<{ name: string; type: string; conf for (const conn of connections) { lines.push(`${conn.name} (${conn.type})`) for (const [key, val] of Object.entries(conn.config)) { - if (key === "password" || key === "private_key_passphrase" || key === "access_token") { + if (isSensitiveField(key)) { lines.push(` ${key}: ****`) } else { lines.push(` ${key}: ${val}`) diff --git a/packages/opencode/src/altimate/tools/warehouse-add.ts b/packages/opencode/src/altimate/tools/warehouse-add.ts index d396715387..7127027215 100644 --- a/packages/opencode/src/altimate/tools/warehouse-add.ts +++ b/packages/opencode/src/altimate/tools/warehouse-add.ts @@ -10,7 +10,15 @@ export const WarehouseAddTool = Tool.define("warehouse_add", { config: z .record(z.string(), z.unknown()) .describe( - 'Connection configuration. Must include "type" (postgres, snowflake, duckdb, etc). Example: {"type": "postgres", "host": "localhost", "port": 5432, "database": "mydb", "user": "admin", "password": "secret"}', + 'Connection configuration. Must include "type" (postgres, snowflake, duckdb, etc). ' + + 'Snowflake auth methods: ' + + '(1) Password: {"type":"snowflake","account":"xy12345","user":"admin","password":"secret","warehouse":"WH","database":"db","schema":"public","role":"ROLE"}. ' + + '(2) Key-pair (file): {"type":"snowflake","account":"xy12345","user":"admin","private_key_path":"/path/to/rsa_key.p8","private_key_passphrase":"optional","warehouse":"WH","database":"db","schema":"public","role":"ROLE"}. ' + + '(3) Key-pair (inline): use "private_key" instead of "private_key_path" with PEM content. ' + + '(4) OAuth: {"type":"snowflake","account":"xy12345","authenticator":"oauth","token":"","warehouse":"WH","database":"db","schema":"public"}. ' + + '(5) SSO: {"type":"snowflake","account":"xy12345","user":"admin","authenticator":"externalbrowser","warehouse":"WH","database":"db","schema":"public","role":"ROLE"}. ' + + 'IMPORTANT: For private key file paths, always use "private_key_path" (not "private_key"). ' + + 'Postgres: {"type":"postgres","host":"localhost","port":5432,"database":"mydb","user":"admin","password":"secret"}.', ), }), async execute(args, ctx) { diff --git a/packages/opencode/test/altimate/connections.test.ts b/packages/opencode/test/altimate/connections.test.ts index f81378784c..31864dd5a2 100644 --- a/packages/opencode/test/altimate/connections.test.ts +++ b/packages/opencode/test/altimate/connections.test.ts @@ -105,10 +105,34 @@ describe("CredentialStore", () => { test("isSensitiveField identifies sensitive fields", () => { expect(CredentialStore.isSensitiveField("password")).toBe(true) + expect(CredentialStore.isSensitiveField("private_key")).toBe(true) + expect(CredentialStore.isSensitiveField("privateKey")).toBe(true) + expect(CredentialStore.isSensitiveField("private_key_passphrase")).toBe(true) + expect(CredentialStore.isSensitiveField("privateKeyPassphrase")).toBe(true) expect(CredentialStore.isSensitiveField("access_token")).toBe(true) + expect(CredentialStore.isSensitiveField("token")).toBe(true) + expect(CredentialStore.isSensitiveField("oauth_client_secret")).toBe(true) + expect(CredentialStore.isSensitiveField("oauthClientSecret")).toBe(true) + expect(CredentialStore.isSensitiveField("passcode")).toBe(true) expect(CredentialStore.isSensitiveField("connection_string")).toBe(true) expect(CredentialStore.isSensitiveField("host")).toBe(false) expect(CredentialStore.isSensitiveField("port")).toBe(false) + expect(CredentialStore.isSensitiveField("authenticator")).toBe(false) + }) + + test("saveConnection strips inline private_key as sensitive", async () => { + const config = { type: "snowflake", private_key: "-----BEGIN PRIVATE KEY-----\nMIIE..." } as any + const { sanitized, warnings } = await CredentialStore.saveConnection("sf_keypair", config) + expect(sanitized.private_key).toBeUndefined() + expect(warnings.length).toBeGreaterThan(0) + }) + + test("saveConnection strips OAuth credentials as sensitive", async () => { + const config = { type: "snowflake", authenticator: "oauth", token: "access-token-123", oauth_client_secret: "secret" } as any + const { sanitized } = await CredentialStore.saveConnection("sf_oauth", config) + expect(sanitized.token).toBeUndefined() + expect(sanitized.oauth_client_secret).toBeUndefined() + expect(sanitized.authenticator).toBe("oauth") }) }) @@ -165,6 +189,44 @@ myproject: } }) + test("parses Snowflake private_key from dbt profile", async () => { + const fs = await import("fs") + const os = await import("os") + const path = await import("path") + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dbt-test-")) + const profilesPath = path.join(tmpDir, "profiles.yml") + + fs.writeFileSync( + profilesPath, + ` +snowflake_keypair: + outputs: + prod: + type: snowflake + account: abc123 + user: svc_user + private_key: "-----BEGIN PRIVATE KEY-----\\nMIIEvQ..." + private_key_passphrase: "my-passphrase" + database: ANALYTICS + warehouse: COMPUTE_WH + schema: PUBLIC + role: TRANSFORMER +`, + ) + + try { + const connections = await parseDbtProfiles(profilesPath) + expect(connections).toHaveLength(1) + expect(connections[0].type).toBe("snowflake") + expect(connections[0].config.private_key).toBe("-----BEGIN PRIVATE KEY-----\nMIIEvQ...") + expect(connections[0].config.private_key_passphrase).toBe("my-passphrase") + expect(connections[0].config.password).toBeUndefined() + } finally { + fs.rmSync(tmpDir, { recursive: true }) + } + }) + test("maps dbt adapter types correctly", async () => { const fs = await import("fs") const os = await import("os") diff --git a/packages/opencode/test/altimate/telemetry-safety.test.ts b/packages/opencode/test/altimate/telemetry-safety.test.ts index 10bf2ee1e6..c268047ed0 100644 --- a/packages/opencode/test/altimate/telemetry-safety.test.ts +++ b/packages/opencode/test/altimate/telemetry-safety.test.ts @@ -46,7 +46,15 @@ describe("Telemetry Safety: Helper functions never throw", () => { test("detectAuthMethod handles all config shapes", () => { expect(detectAuthMethod({ type: "postgres", connection_string: "pg://..." })).toBe("connection_string") expect(detectAuthMethod({ type: "snowflake", private_key_path: "/key.p8" })).toBe("key_pair") + expect(detectAuthMethod({ type: "snowflake", private_key: "-----BEGIN PRIVATE KEY-----\n..." })).toBe("key_pair") + expect(detectAuthMethod({ type: "snowflake", privateKey: "-----BEGIN PRIVATE KEY-----\n..." })).toBe("key_pair") + expect(detectAuthMethod({ type: "snowflake", privateKeyPath: "/key.p8" })).toBe("key_pair") + expect(detectAuthMethod({ type: "snowflake", authenticator: "externalbrowser" })).toBe("sso") + expect(detectAuthMethod({ type: "snowflake", authenticator: "https://myorg.okta.com" })).toBe("sso") + expect(detectAuthMethod({ type: "snowflake", authenticator: "oauth" })).toBe("oauth") + expect(detectAuthMethod({ type: "snowflake", authenticator: "OAUTH" })).toBe("oauth") expect(detectAuthMethod({ type: "databricks", access_token: "dapi..." })).toBe("token") + expect(detectAuthMethod({ type: "snowflake", token: "jwt-token" })).toBe("token") expect(detectAuthMethod({ type: "postgres", password: "secret" })).toBe("password") expect(detectAuthMethod({ type: "duckdb" })).toBe("file") expect(detectAuthMethod({ type: "sqlite" })).toBe("file") From 7e5b3e4918a763baa80cff5a645efccf685513aa Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 10:16:59 -0700 Subject: [PATCH 2/6] fix: validate required fields for all explicit Snowflake authenticators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses code review findings from 6-model consensus review: - JWT, PROGRAMMATIC_ACCESS_TOKEN: error if token missing (was silent fallthrough) - USERNAME_PASSWORD_MFA: error if password missing (was undefined) - Browser SSO: error if `connectAsync` unavailable instead of silent fallback - Extract Okta URL regex to avoid duplication - Fix comment: "Non-interactive" → "Interactive" for browser SSO Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/drivers/src/snowflake.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index 3aab40fd53..f6d74807c6 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -122,7 +122,7 @@ export async function connect(config: ConnectionConfig): Promise { // --------------------------------------------------------------- // 2. External browser SSO - // User's browser opens for IdP login. Non-interactive — requires + // Interactive — opens user's browser for IdP login. Requires // connectAsync() instead of connect(). // --------------------------------------------------------------- } else if (authUpper === "EXTERNALBROWSER") { @@ -154,14 +154,20 @@ export async function connect(config: ConnectionConfig): Promise { // --------------------------------------------------------------- // 5. JWT token auth (non-key-pair JWT) // --------------------------------------------------------------- - } else if (authUpper === "JWT" && oauthToken) { + } else if (authUpper === "JWT") { + if (!oauthToken) { + throw new Error("Snowflake JWT authenticator specified but no token provided (expected 'token' or 'access_token')") + } options.authenticator = "SNOWFLAKE_JWT" options.token = oauthToken // --------------------------------------------------------------- // 6. Programmatic access token // --------------------------------------------------------------- - } else if (authUpper === "PROGRAMMATIC_ACCESS_TOKEN" && oauthToken) { + } else if (authUpper === "PROGRAMMATIC_ACCESS_TOKEN") { + if (!oauthToken) { + throw new Error("Snowflake PROGRAMMATIC_ACCESS_TOKEN authenticator specified but no token provided (expected 'token' or 'access_token')") + } options.authenticator = "PROGRAMMATIC_ACCESS_TOKEN" options.token = oauthToken @@ -169,6 +175,9 @@ export async function connect(config: ConnectionConfig): Promise { // 7. Username + password + MFA // --------------------------------------------------------------- } else if (authUpper === "USERNAME_PASSWORD_MFA") { + if (!config.password) { + throw new Error("Snowflake USERNAME_PASSWORD_MFA authenticator requires 'password'") + } options.authenticator = "USERNAME_PASSWORD_MFA" options.password = config.password if (passcode) options.passcode = passcode @@ -181,12 +190,16 @@ export async function connect(config: ConnectionConfig): Promise { } // Use connectAsync for browser-based auth (SSO/Okta), connect for everything else - const useBrowserAuth = authUpper === "EXTERNALBROWSER" || - (authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator)) + const isOktaUrl = authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator) + const useBrowserAuth = authUpper === "EXTERNALBROWSER" || isOktaUrl connection = await new Promise((resolve, reject) => { const conn = snowflake.createConnection(options) - if (useBrowserAuth && typeof conn.connectAsync === "function") { + if (useBrowserAuth) { + if (typeof conn.connectAsync !== "function") { + reject(new Error("Snowflake browser/SSO auth requires snowflake-sdk with connectAsync support. Upgrade snowflake-sdk.")) + return + } conn.connectAsync((err: Error | null) => { if (err) reject(err) else resolve(conn) From f9bf8ddc1429a6fae649794438d99e38faf7b574 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 10:20:12 -0700 Subject: [PATCH 3/6] fix: JWT/PAT must alias to OAUTH, reject invalid `private_key` values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini 3.1 Pro code review findings: - JWT and PROGRAMMATIC_ACCESS_TOKEN: snowflake-sdk Node.js only accepts pre-generated tokens via OAUTH authenticator. SNOWFLAKE_JWT expects a `privateKey` for self-signing and would crash with TypeError. - `private_key` containing a non-existent file path now throws a clear error instead of passing the path string to `crypto.createPrivateKey` which produces a cryptic OpenSSL error. - Remove `oauthClientId`/`oauthClientSecret` passthrough — the SDK's `AuthOauth` class ignores them entirely. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/drivers/src/snowflake.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index f6d74807c6..e4133d42b7 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -75,6 +75,11 @@ export async function connect(config: ConnectionConfig): Promise { if (fs.existsSync(resolvedInlineKey)) { resolvedKeyPath = resolvedInlineKey resolvedInlineKey = undefined + } else { + throw new Error( + `Snowflake private key: '${resolvedInlineKey}' is not a valid file path or PEM content. ` + + `Use 'private_key_path' for file paths or provide PEM content starting with '-----BEGIN PRIVATE KEY-----'.`, + ) } } @@ -148,27 +153,19 @@ export async function connect(config: ConnectionConfig): Promise { } options.authenticator = "OAUTH" options.token = oauthToken - if (oauthClientId) options.oauthClientId = oauthClientId - if (oauthClientSecret) options.oauthClientSecret = oauthClientSecret // --------------------------------------------------------------- - // 5. JWT token auth (non-key-pair JWT) + // 5. JWT / Programmatic access token (pre-generated) + // The Node.js snowflake-sdk only accepts pre-generated tokens + // via the OAUTH authenticator. SNOWFLAKE_JWT expects a privateKey + // for self-signing, and PROGRAMMATIC_ACCESS_TOKEN is not recognized. + // Alias both to OAUTH so the token is passed correctly. // --------------------------------------------------------------- - } else if (authUpper === "JWT") { + } else if (authUpper === "JWT" || authUpper === "PROGRAMMATIC_ACCESS_TOKEN") { if (!oauthToken) { - throw new Error("Snowflake JWT authenticator specified but no token provided (expected 'token' or 'access_token')") + throw new Error(`Snowflake ${authenticator} authenticator specified but no token provided (expected 'token' or 'access_token')`) } - options.authenticator = "SNOWFLAKE_JWT" - options.token = oauthToken - - // --------------------------------------------------------------- - // 6. Programmatic access token - // --------------------------------------------------------------- - } else if (authUpper === "PROGRAMMATIC_ACCESS_TOKEN") { - if (!oauthToken) { - throw new Error("Snowflake PROGRAMMATIC_ACCESS_TOKEN authenticator specified but no token provided (expected 'token' or 'access_token')") - } - options.authenticator = "PROGRAMMATIC_ACCESS_TOKEN" + options.authenticator = "OAUTH" options.token = oauthToken // --------------------------------------------------------------- From d2dcecb31643118fbad8e92fec2185da86dae8a9 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 10:29:13 -0700 Subject: [PATCH 4/6] fix: keep credentials in memory when keytar unavailable Sentry flagged: when keytar is unavailable (CI/headless), `saveConnection` strips sensitive fields from both the disk config AND the in-memory config. This causes subsequent `warehouse_test` calls in the same session to fail because the credentials are permanently lost. Fix: store the original config (with credentials) in memory so the current session can connect. Only the disk file uses the sanitized version. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../opencode/src/altimate/native/connections/registry.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/native/connections/registry.ts b/packages/opencode/src/altimate/native/connections/registry.ts index 1122cbaf46..7af182e956 100644 --- a/packages/opencode/src/altimate/native/connections/registry.ts +++ b/packages/opencode/src/altimate/native/connections/registry.ts @@ -377,8 +377,10 @@ export async function add( existing[name] = sanitized fs.writeFileSync(globalPath, JSON.stringify(existing, null, 2), "utf-8") - // Update in-memory with sanitized config (no plaintext credentials) - configs.set(name, sanitized) + // In-memory: keep original config (with credentials) so the current + // session can connect even when keytar is unavailable. Only the disk + // file uses the sanitized version (credentials stripped). + configs.set(name, config) // Clear cached connector const cached = connectors.get(name) From 06e8bb214ffb352c99b8c696ed7d9ad73b12a12a Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 10:35:26 -0700 Subject: [PATCH 5/6] fix: handle `connectAsync` Promise rejection to prevent unhandled errors Sentry flagged: `connectAsync()` returns a Promise in addition to accepting a callback. If the SDK rejects the Promise (instead of calling the callback with an error), the rejection was unhandled, potentially causing the connection to hang silently. Fix: chain `.catch(reject)` to forward Promise-based errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/drivers/src/snowflake.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drivers/src/snowflake.ts b/packages/drivers/src/snowflake.ts index e4133d42b7..240beb55aa 100644 --- a/packages/drivers/src/snowflake.ts +++ b/packages/drivers/src/snowflake.ts @@ -200,7 +200,7 @@ export async function connect(config: ConnectionConfig): Promise { conn.connectAsync((err: Error | null) => { if (err) reject(err) else resolve(conn) - }) + }).catch(reject) } else { conn.connect((err: Error | null) => { if (err) reject(err) From 1fef6e6662ae319697ad0d97f10fc8c4b93c81b5 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Wed, 18 Mar 2026 10:40:18 -0700 Subject: [PATCH 6/6] fix: add `privateKeyPass` to SENSITIVE_FIELDS Sentry flagged: the driver accepts `privateKeyPass` as a passphrase alias but it was missing from SENSITIVE_FIELDS, causing plaintext storage in connections.json. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../opencode/src/altimate/native/connections/credential-store.ts | 1 + packages/opencode/test/altimate/connections.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/opencode/src/altimate/native/connections/credential-store.ts b/packages/opencode/src/altimate/native/connections/credential-store.ts index 1baf3ec1bc..10be89e480 100644 --- a/packages/opencode/src/altimate/native/connections/credential-store.ts +++ b/packages/opencode/src/altimate/native/connections/credential-store.ts @@ -18,6 +18,7 @@ const SENSITIVE_FIELDS = new Set([ "privateKey", "private_key_passphrase", "privateKeyPassphrase", + "privateKeyPass", "access_token", "token", "oauth_client_secret", diff --git a/packages/opencode/test/altimate/connections.test.ts b/packages/opencode/test/altimate/connections.test.ts index 31864dd5a2..9558a79565 100644 --- a/packages/opencode/test/altimate/connections.test.ts +++ b/packages/opencode/test/altimate/connections.test.ts @@ -109,6 +109,7 @@ describe("CredentialStore", () => { expect(CredentialStore.isSensitiveField("privateKey")).toBe(true) expect(CredentialStore.isSensitiveField("private_key_passphrase")).toBe(true) expect(CredentialStore.isSensitiveField("privateKeyPassphrase")).toBe(true) + expect(CredentialStore.isSensitiveField("privateKeyPass")).toBe(true) expect(CredentialStore.isSensitiveField("access_token")).toBe(true) expect(CredentialStore.isSensitiveField("token")).toBe(true) expect(CredentialStore.isSensitiveField("oauth_client_secret")).toBe(true)