Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,413 changes: 1,187 additions & 2,226 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
"@ai-sdk/openai": "^3.0.49",
"@ai-sdk/openai-compatible": "^2.0.49",
"@anthropic-ai/sdk": "^0.60.0",
"@aws-sdk/client-s3": "^3.846.0",
"@aws-sdk/s3-request-presigner": "^3.846.0",
"@aws-sdk/client-s3": "3.1138.0",
"@aws-sdk/s3-request-presigner": "3.1138.0",
"@azure/storage-blob": "^12.27.0",
"@electron/notarize": "^2.3.2",
"@emotion/react": "^11.14.0",
Expand Down Expand Up @@ -171,7 +171,7 @@
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"simple-git": "^3.27.0",
"snowflake-sdk": "^2.0.3",
"snowflake-sdk": "^3.3.0",
"split-pane-react": "^0.1.3",
"tar": "^7.4.3",
"uuid": "^11.1.0",
Expand Down
42 changes: 40 additions & 2 deletions src/main/extractor/snowflake.extractor.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop, consistent-return */
import snowflake from 'snowflake-sdk';
import { Column, Table } from '../../types/backend';
import {
SNOWFLAKE_REAUTH_MESSAGE,
SnowflakeAuthManager,
} from '../utils/snowflakeAuth';

export default class SnowflakeExtractor {
private connection: snowflake.Connection;

private authMethod: 'password' | 'oauth_browser';

constructor(config: {
account: string;
username: string;
password: string;
password?: string;
warehouse: string;
database: string;
schema: string;
role?: string;
authMethod?: 'password' | 'oauth_browser';
}) {
this.connection = snowflake.createConnection(config);
this.authMethod = config.authMethod ?? 'password';
const baseConfig = {
account: config.account,
username: config.username,
warehouse: config.warehouse,
database: config.database,
schema: config.schema,
role: config.role,
};

if (this.authMethod === 'oauth_browser') {
this.connection = snowflake.createConnection({
...baseConfig,
authenticator: 'OAUTH_AUTHORIZATION_CODE',
browserActionTimeout: 120000,
clientStoreTemporaryCredential: true,
openExternalBrowserCallback: () => {
throw new Error(SNOWFLAKE_REAUTH_MESSAGE);
},
});
} else {
this.connection = snowflake.createConnection({
...baseConfig,
password: config.password,
});
}
}

async connect(): Promise<void> {
if (this.authMethod === 'oauth_browser') {
// No browser flows outside the Connections screen: a cold session
// fails fast with guidance instead of opening an ungated popup.
// A warm SDK cache connects silently without user interaction.
SnowflakeAuthManager.assertCachedSession();
}
return new Promise((resolve, reject) => {
this.connection.connect((err) => {
if (err) return reject(err);
Expand Down
64 changes: 63 additions & 1 deletion src/main/ipcHandlers/connectors.ipcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import type {
QueryResponseType,
ExecuteStatementType,
} from '../../types/backend';
import { ConfigureConnectionBody, UpdateConnectionBody } from '../../types/ipc';
import {
ConfigureConnectionBody,
UpdateConnectionBody,
StartSnowflakeAuthRequest,
} from '../../types/ipc';
import { CloudConnection, RecentItem } from '../../types/frontend';
import { SnowflakeAuthManager } from '../utils/snowflakeAuth';

const handlerChannels = [
'connector:configure',
Expand All @@ -21,6 +26,10 @@ const handlerChannels = [
'connector:updateQuery',
'connector:getQuery',
'connector:executeQuery',
'connector:snowflake:auth:start',
'connector:snowflake:auth:cancel',
'connector:snowflake:auth:revoke',
'connector:snowflake:auth:materialize',
];

const removeConnectorsIpcHandlers = () => {
Expand Down Expand Up @@ -189,6 +198,59 @@ const registerConnectorsHandlers = () => {
}
},
);

ipcMain.handle(
'connector:snowflake:auth:start',
async (event, request: StartSnowflakeAuthRequest) => {
// Narrow Snowflake-only pre-validation: never invoke the SDK with
// missing identity fields. Detailed validation also lives in
// SnowflakeAuthManager; this keeps a clear IPC boundary.
if (
!request ||
typeof request.correlationId !== 'string' ||
!request.correlationId.trim() ||
typeof request.account !== 'string' ||
!request.account.trim() ||
typeof request.username !== 'string' ||
!request.username.trim()
) {
throw new Error(
'Snowflake account identifier and username are required before starting browser authentication.',
);
}
return SnowflakeAuthManager.startAuth(request, (payload) => {
event.sender.send('connector:snowflake:auth:event', payload);
});
},
);

ipcMain.handle(
'connector:snowflake:auth:cancel',
async (_event, correlationId: string) => {
SnowflakeAuthManager.cancelAuth(correlationId);
return { success: true };
},
);
ipcMain.handle('connector:snowflake:auth:hasToken', async () => {
return SnowflakeAuthManager.hasSnowflakeToken();
});

ipcMain.handle(
'connector:snowflake:auth:revoke',
async (_event, body?: { connectionName?: string } | string) => {
const connectionName =
typeof body === 'string' ? body : (body?.connectionName ?? '');
return ConnectorsService.revokeSnowflakeSession(connectionName);
},
);
ipcMain.handle(
'connector:snowflake:auth:materialize',
async (_event, body?: { connectionName?: string } | string) => {
const connectionName =
typeof body === 'string' ? body : (body?.connectionName ?? '');
return ConnectorsService.materializeSnowflakeOAuthEnv(connectionName);
},
);
};

export default registerConnectorsHandlers;
Loading
Loading