Skip to content

Commit 2c94182

Browse files
anandgupta42claude
andauthored
fix: [AI-266] Snowflake auth — support all auth methods, fix field name mismatches (#268)
* 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) <noreply@anthropic.com> * fix: validate required fields for all explicit Snowflake authenticators 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) <noreply@anthropic.com> * fix: JWT/PAT must alias to OAUTH, reject invalid `private_key` values 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8fd0c7b commit 2c94182

8 files changed

Lines changed: 245 additions & 26 deletions

File tree

packages/drivers/src/snowflake.ts

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,43 +48,165 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
4848
role: config.role,
4949
}
5050

51-
// Key-pair auth
52-
if (config.private_key_path) {
53-
const keyPath = config.private_key_path as string
54-
if (!fs.existsSync(keyPath)) {
55-
throw new Error(`Snowflake private key file not found: ${keyPath}`)
51+
// ---------------------------------------------------------------
52+
// Normalize field names: accept snake_case (dbt), camelCase (SDK),
53+
// and common LLM-generated variants so auth "just works".
54+
// ---------------------------------------------------------------
55+
const keyPath = (config.private_key_path ?? config.privateKeyPath) as string | undefined
56+
const inlineKey = (config.private_key ?? config.privateKey) as string | undefined
57+
const keyPassphrase = (config.private_key_passphrase ?? config.privateKeyPassphrase ?? config.privateKeyPass) as string | undefined
58+
const oauthToken = (config.token ?? config.access_token) as string | undefined
59+
const oauthClientId = (config.oauth_client_id ?? config.oauthClientId) as string | undefined
60+
const oauthClientSecret = (config.oauth_client_secret ?? config.oauthClientSecret) as string | undefined
61+
const authenticator = (config.authenticator as string | undefined)?.trim()
62+
const authUpper = authenticator?.toUpperCase()
63+
const passcode = config.passcode as string | undefined
64+
65+
// ---------------------------------------------------------------
66+
// 1. Key-pair auth (SNOWFLAKE_JWT)
67+
// Accepts: private_key_path (file), private_key (inline PEM or
68+
// file path auto-detected), privateKey, privateKeyPath.
69+
// ---------------------------------------------------------------
70+
// Resolve private_key: could be a file path or PEM content
71+
let resolvedKeyPath = keyPath
72+
let resolvedInlineKey = inlineKey
73+
if (!resolvedKeyPath && resolvedInlineKey && !resolvedInlineKey.includes("-----BEGIN")) {
74+
// Looks like a file path, not PEM content
75+
if (fs.existsSync(resolvedInlineKey)) {
76+
resolvedKeyPath = resolvedInlineKey
77+
resolvedInlineKey = undefined
78+
} else {
79+
throw new Error(
80+
`Snowflake private key: '${resolvedInlineKey}' is not a valid file path or PEM content. ` +
81+
`Use 'private_key_path' for file paths or provide PEM content starting with '-----BEGIN PRIVATE KEY-----'.`,
82+
)
83+
}
84+
}
85+
86+
if (resolvedKeyPath || resolvedInlineKey) {
87+
let keyContent: string
88+
if (resolvedKeyPath) {
89+
if (!fs.existsSync(resolvedKeyPath)) {
90+
throw new Error(`Snowflake private key file not found: ${resolvedKeyPath}`)
91+
}
92+
keyContent = fs.readFileSync(resolvedKeyPath, "utf-8")
93+
} else {
94+
keyContent = resolvedInlineKey!
95+
// Normalize escaped newlines from env vars / JSON configs
96+
if (keyContent.includes("\\n")) {
97+
keyContent = keyContent.replace(/\\n/g, "\n")
98+
}
5699
}
57-
const keyContent = fs.readFileSync(keyPath, "utf-8")
58100

59-
// If key is encrypted (has ENCRYPTED in header or passphrase provided),
60-
// decrypt it using Node crypto — snowflake-sdk expects unencrypted PEM.
101+
// If key is encrypted, decrypt using Node crypto —
102+
// snowflake-sdk expects unencrypted PKCS#8 PEM.
61103
let privateKey: string
62-
if (config.private_key_passphrase || keyContent.includes("ENCRYPTED")) {
104+
if (keyPassphrase || keyContent.includes("ENCRYPTED")) {
63105
const crypto = await import("crypto")
64-
const keyObject = crypto.createPrivateKey({
65-
key: keyContent,
66-
format: "pem",
67-
passphrase: (config.private_key_passphrase as string) || undefined,
68-
})
69-
privateKey = keyObject
70-
.export({ type: "pkcs8", format: "pem" })
71-
.toString()
106+
try {
107+
const keyObject = crypto.createPrivateKey({
108+
key: keyContent,
109+
format: "pem",
110+
passphrase: keyPassphrase || undefined,
111+
})
112+
privateKey = keyObject
113+
.export({ type: "pkcs8", format: "pem" })
114+
.toString()
115+
} catch (e) {
116+
const msg = e instanceof Error ? e.message : String(e)
117+
throw new Error(
118+
`Snowflake: Failed to decrypt private key. Verify the passphrase and key format (must be PEM/PKCS#8). ${msg}`,
119+
)
120+
}
72121
} else {
73122
privateKey = keyContent
74123
}
75124

76125
options.authenticator = "SNOWFLAKE_JWT"
77126
options.privateKey = privateKey
127+
128+
// ---------------------------------------------------------------
129+
// 2. External browser SSO
130+
// Interactive — opens user's browser for IdP login. Requires
131+
// connectAsync() instead of connect().
132+
// ---------------------------------------------------------------
133+
} else if (authUpper === "EXTERNALBROWSER") {
134+
options.authenticator = "EXTERNALBROWSER"
135+
136+
// ---------------------------------------------------------------
137+
// 3. Okta native SSO (authenticator is an Okta URL)
138+
// ---------------------------------------------------------------
139+
} else if (authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator)) {
140+
options.authenticator = authenticator
141+
if (config.password) options.password = config.password
142+
143+
// ---------------------------------------------------------------
144+
// 4. OAuth token auth
145+
// Triggered by: authenticator="oauth", OR token/access_token
146+
// present without a password.
147+
// ---------------------------------------------------------------
148+
} else if (authUpper === "OAUTH" || (oauthToken && !config.password)) {
149+
if (!oauthToken) {
150+
throw new Error(
151+
"Snowflake OAuth authenticator specified but no token provided (expected 'token' or 'access_token')",
152+
)
153+
}
154+
options.authenticator = "OAUTH"
155+
options.token = oauthToken
156+
157+
// ---------------------------------------------------------------
158+
// 5. JWT / Programmatic access token (pre-generated)
159+
// The Node.js snowflake-sdk only accepts pre-generated tokens
160+
// via the OAUTH authenticator. SNOWFLAKE_JWT expects a privateKey
161+
// for self-signing, and PROGRAMMATIC_ACCESS_TOKEN is not recognized.
162+
// Alias both to OAUTH so the token is passed correctly.
163+
// ---------------------------------------------------------------
164+
} else if (authUpper === "JWT" || authUpper === "PROGRAMMATIC_ACCESS_TOKEN") {
165+
if (!oauthToken) {
166+
throw new Error(`Snowflake ${authenticator} authenticator specified but no token provided (expected 'token' or 'access_token')`)
167+
}
168+
options.authenticator = "OAUTH"
169+
options.token = oauthToken
170+
171+
// ---------------------------------------------------------------
172+
// 7. Username + password + MFA
173+
// ---------------------------------------------------------------
174+
} else if (authUpper === "USERNAME_PASSWORD_MFA") {
175+
if (!config.password) {
176+
throw new Error("Snowflake USERNAME_PASSWORD_MFA authenticator requires 'password'")
177+
}
178+
options.authenticator = "USERNAME_PASSWORD_MFA"
179+
options.password = config.password
180+
if (passcode) options.passcode = passcode
181+
182+
// ---------------------------------------------------------------
183+
// 8. Plain password auth (default)
184+
// ---------------------------------------------------------------
78185
} else if (config.password) {
79186
options.password = config.password
80187
}
81188

189+
// Use connectAsync for browser-based auth (SSO/Okta), connect for everything else
190+
const isOktaUrl = authenticator && /^https?:\/\/.+\.okta\.com/i.test(authenticator)
191+
const useBrowserAuth = authUpper === "EXTERNALBROWSER" || isOktaUrl
192+
82193
connection = await new Promise<any>((resolve, reject) => {
83194
const conn = snowflake.createConnection(options)
84-
conn.connect((err: Error | null) => {
85-
if (err) reject(err)
86-
else resolve(conn)
87-
})
195+
if (useBrowserAuth) {
196+
if (typeof conn.connectAsync !== "function") {
197+
reject(new Error("Snowflake browser/SSO auth requires snowflake-sdk with connectAsync support. Upgrade snowflake-sdk."))
198+
return
199+
}
200+
conn.connectAsync((err: Error | null) => {
201+
if (err) reject(err)
202+
else resolve(conn)
203+
}).catch(reject)
204+
} else {
205+
conn.connect((err: Error | null) => {
206+
if (err) reject(err)
207+
else resolve(conn)
208+
})
209+
}
88210
})
89211
},
90212

packages/opencode/src/altimate/native/connections/credential-store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@ const SERVICE_NAME = "altimate-code"
1414

1515
const SENSITIVE_FIELDS = new Set([
1616
"password",
17+
"private_key",
18+
"privateKey",
1719
"private_key_passphrase",
20+
"privateKeyPassphrase",
21+
"privateKeyPass",
1822
"access_token",
23+
"token",
24+
"oauth_client_secret",
25+
"oauthClientSecret",
26+
"passcode",
1927
"ssh_password",
2028
"connection_string",
2129
])

packages/opencode/src/altimate/native/connections/dbt-profiles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ const KEY_MAP: Record<string, string> = {
3636
server_hostname: "server_hostname",
3737
http_path: "http_path",
3838
token: "access_token",
39+
private_key: "private_key",
3940
private_key_path: "private_key_path",
4041
private_key_passphrase: "private_key_passphrase",
42+
authenticator: "authenticator",
43+
oauth_client_id: "oauth_client_id",
44+
oauth_client_secret: "oauth_client_secret",
4145
keyfile: "credentials_path",
4246
keyfile_json: "credentials_json",
4347
project: "project",

packages/opencode/src/altimate/native/connections/registry.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ async function createConnector(
209209
export function detectAuthMethod(config: ConnectionConfig | null | undefined): string {
210210
if (!config || typeof config !== "object") return "unknown"
211211
if (config.connection_string) return "connection_string"
212-
if (config.private_key_path) return "key_pair"
212+
if (config.private_key_path || config.privateKeyPath || config.private_key || config.privateKey) return "key_pair"
213+
const auth = typeof config.authenticator === "string" ? config.authenticator.toUpperCase() : ""
214+
if (auth === "EXTERNALBROWSER" || (typeof config.authenticator === "string" && /^https?:\/\/.+\.okta\.com/i.test(config.authenticator))) return "sso"
215+
if (auth === "OAUTH") return "oauth"
213216
if (config.access_token || config.token) return "token"
214217
if (config.password) return "password"
215218
const t = typeof config.type === "string" ? config.type.toLowerCase() : ""
@@ -374,8 +377,10 @@ export async function add(
374377
existing[name] = sanitized
375378
fs.writeFileSync(globalPath, JSON.stringify(existing, null, 2), "utf-8")
376379

377-
// Update in-memory with sanitized config (no plaintext credentials)
378-
configs.set(name, sanitized)
380+
// In-memory: keep original config (with credentials) so the current
381+
// session can connect even when keytar is unavailable. Only the disk
382+
// file uses the sanitized version (credentials stripped).
383+
configs.set(name, config)
379384

380385
// Clear cached connector
381386
const cached = connectors.get(name)

packages/opencode/src/altimate/tools/dbt-profiles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import z from "zod"
22
import { Tool } from "../../tool/tool"
33
import { Dispatcher } from "../native"
4+
import { isSensitiveField } from "../native/connections/credential-store"
45

56
export const DbtProfilesTool = Tool.define("dbt_profiles", {
67
description:
@@ -52,7 +53,7 @@ function formatConnections(connections: Array<{ name: string; type: string; conf
5253
for (const conn of connections) {
5354
lines.push(`${conn.name} (${conn.type})`)
5455
for (const [key, val] of Object.entries(conn.config)) {
55-
if (key === "password" || key === "private_key_passphrase" || key === "access_token") {
56+
if (isSensitiveField(key)) {
5657
lines.push(` ${key}: ****`)
5758
} else {
5859
lines.push(` ${key}: ${val}`)

packages/opencode/src/altimate/tools/warehouse-add.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ export const WarehouseAddTool = Tool.define("warehouse_add", {
1010
config: z
1111
.record(z.string(), z.unknown())
1212
.describe(
13-
'Connection configuration. Must include "type" (postgres, snowflake, duckdb, etc). Example: {"type": "postgres", "host": "localhost", "port": 5432, "database": "mydb", "user": "admin", "password": "secret"}',
13+
'Connection configuration. Must include "type" (postgres, snowflake, duckdb, etc). ' +
14+
'Snowflake auth methods: ' +
15+
'(1) Password: {"type":"snowflake","account":"xy12345","user":"admin","password":"secret","warehouse":"WH","database":"db","schema":"public","role":"ROLE"}. ' +
16+
'(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"}. ' +
17+
'(3) Key-pair (inline): use "private_key" instead of "private_key_path" with PEM content. ' +
18+
'(4) OAuth: {"type":"snowflake","account":"xy12345","authenticator":"oauth","token":"<access_token>","warehouse":"WH","database":"db","schema":"public"}. ' +
19+
'(5) SSO: {"type":"snowflake","account":"xy12345","user":"admin","authenticator":"externalbrowser","warehouse":"WH","database":"db","schema":"public","role":"ROLE"}. ' +
20+
'IMPORTANT: For private key file paths, always use "private_key_path" (not "private_key"). ' +
21+
'Postgres: {"type":"postgres","host":"localhost","port":5432,"database":"mydb","user":"admin","password":"secret"}.',
1422
),
1523
}),
1624
async execute(args, ctx) {

packages/opencode/test/altimate/connections.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,35 @@ describe("CredentialStore", () => {
105105

106106
test("isSensitiveField identifies sensitive fields", () => {
107107
expect(CredentialStore.isSensitiveField("password")).toBe(true)
108+
expect(CredentialStore.isSensitiveField("private_key")).toBe(true)
109+
expect(CredentialStore.isSensitiveField("privateKey")).toBe(true)
110+
expect(CredentialStore.isSensitiveField("private_key_passphrase")).toBe(true)
111+
expect(CredentialStore.isSensitiveField("privateKeyPassphrase")).toBe(true)
112+
expect(CredentialStore.isSensitiveField("privateKeyPass")).toBe(true)
108113
expect(CredentialStore.isSensitiveField("access_token")).toBe(true)
114+
expect(CredentialStore.isSensitiveField("token")).toBe(true)
115+
expect(CredentialStore.isSensitiveField("oauth_client_secret")).toBe(true)
116+
expect(CredentialStore.isSensitiveField("oauthClientSecret")).toBe(true)
117+
expect(CredentialStore.isSensitiveField("passcode")).toBe(true)
109118
expect(CredentialStore.isSensitiveField("connection_string")).toBe(true)
110119
expect(CredentialStore.isSensitiveField("host")).toBe(false)
111120
expect(CredentialStore.isSensitiveField("port")).toBe(false)
121+
expect(CredentialStore.isSensitiveField("authenticator")).toBe(false)
122+
})
123+
124+
test("saveConnection strips inline private_key as sensitive", async () => {
125+
const config = { type: "snowflake", private_key: "-----BEGIN PRIVATE KEY-----\nMIIE..." } as any
126+
const { sanitized, warnings } = await CredentialStore.saveConnection("sf_keypair", config)
127+
expect(sanitized.private_key).toBeUndefined()
128+
expect(warnings.length).toBeGreaterThan(0)
129+
})
130+
131+
test("saveConnection strips OAuth credentials as sensitive", async () => {
132+
const config = { type: "snowflake", authenticator: "oauth", token: "access-token-123", oauth_client_secret: "secret" } as any
133+
const { sanitized } = await CredentialStore.saveConnection("sf_oauth", config)
134+
expect(sanitized.token).toBeUndefined()
135+
expect(sanitized.oauth_client_secret).toBeUndefined()
136+
expect(sanitized.authenticator).toBe("oauth")
112137
})
113138
})
114139

@@ -165,6 +190,44 @@ myproject:
165190
}
166191
})
167192

193+
test("parses Snowflake private_key from dbt profile", async () => {
194+
const fs = await import("fs")
195+
const os = await import("os")
196+
const path = await import("path")
197+
198+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dbt-test-"))
199+
const profilesPath = path.join(tmpDir, "profiles.yml")
200+
201+
fs.writeFileSync(
202+
profilesPath,
203+
`
204+
snowflake_keypair:
205+
outputs:
206+
prod:
207+
type: snowflake
208+
account: abc123
209+
user: svc_user
210+
private_key: "-----BEGIN PRIVATE KEY-----\\nMIIEvQ..."
211+
private_key_passphrase: "my-passphrase"
212+
database: ANALYTICS
213+
warehouse: COMPUTE_WH
214+
schema: PUBLIC
215+
role: TRANSFORMER
216+
`,
217+
)
218+
219+
try {
220+
const connections = await parseDbtProfiles(profilesPath)
221+
expect(connections).toHaveLength(1)
222+
expect(connections[0].type).toBe("snowflake")
223+
expect(connections[0].config.private_key).toBe("-----BEGIN PRIVATE KEY-----\nMIIEvQ...")
224+
expect(connections[0].config.private_key_passphrase).toBe("my-passphrase")
225+
expect(connections[0].config.password).toBeUndefined()
226+
} finally {
227+
fs.rmSync(tmpDir, { recursive: true })
228+
}
229+
})
230+
168231
test("maps dbt adapter types correctly", async () => {
169232
const fs = await import("fs")
170233
const os = await import("os")

packages/opencode/test/altimate/telemetry-safety.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,15 @@ describe("Telemetry Safety: Helper functions never throw", () => {
4646
test("detectAuthMethod handles all config shapes", () => {
4747
expect(detectAuthMethod({ type: "postgres", connection_string: "pg://..." })).toBe("connection_string")
4848
expect(detectAuthMethod({ type: "snowflake", private_key_path: "/key.p8" })).toBe("key_pair")
49+
expect(detectAuthMethod({ type: "snowflake", private_key: "-----BEGIN PRIVATE KEY-----\n..." })).toBe("key_pair")
50+
expect(detectAuthMethod({ type: "snowflake", privateKey: "-----BEGIN PRIVATE KEY-----\n..." })).toBe("key_pair")
51+
expect(detectAuthMethod({ type: "snowflake", privateKeyPath: "/key.p8" })).toBe("key_pair")
52+
expect(detectAuthMethod({ type: "snowflake", authenticator: "externalbrowser" })).toBe("sso")
53+
expect(detectAuthMethod({ type: "snowflake", authenticator: "https://myorg.okta.com" })).toBe("sso")
54+
expect(detectAuthMethod({ type: "snowflake", authenticator: "oauth" })).toBe("oauth")
55+
expect(detectAuthMethod({ type: "snowflake", authenticator: "OAUTH" })).toBe("oauth")
4956
expect(detectAuthMethod({ type: "databricks", access_token: "dapi..." })).toBe("token")
57+
expect(detectAuthMethod({ type: "snowflake", token: "jwt-token" })).toBe("token")
5058
expect(detectAuthMethod({ type: "postgres", password: "secret" })).toBe("password")
5159
expect(detectAuthMethod({ type: "duckdb" })).toBe("file")
5260
expect(detectAuthMethod({ type: "sqlite" })).toBe("file")

0 commit comments

Comments
 (0)