Skip to content

Commit c10e1fb

Browse files
ARHAEEMclaude
andcommitted
feat: auth security upgrade — manual login, storage panel, backup/restore, browser choice
Addresses Airtable team concerns about credential safety: - Manual/Auto login mode toggle (manual = zero credentials stored) - Manual login runner opens visible browser for user to authenticate - Profile path canonicalized to ~/.airtable-user-mcp/.chrome-profile/ - Credential forwarding gated on loginMode in MCP registration - Logout clears both OS keychain and browser profile - Session expiry notification with re-login button (manual mode) - Browser selection dropdown with custom path support - Browser choice propagated to external IDE configs - Storage & Data panel with paths, sizes, open-in-explorer - Encrypted backup/restore (AES-256-GCM) for session data - OS-level file permission hardening (chmod/icacls) - 27 tests passing, VSIX packages cleanly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 6f5d7ea + 10a46d8 commit c10e1fb

24 files changed

+1570
-144
lines changed

packages/extension/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,40 @@
229229
"maximum": 168,
230230
"description": "Hours between automatic session health checks (1–168)."
231231
},
232+
"airtableFormula.auth.loginMode": {
233+
"type": "string",
234+
"enum": [
235+
"manual",
236+
"auto"
237+
],
238+
"default": "manual",
239+
"description": "Login mode: 'manual' opens a browser for you to log in, 'auto' uses stored credentials for automated login."
240+
},
241+
"airtableFormula.auth.browserChoice": {
242+
"type": "object",
243+
"default": {
244+
"mode": "auto"
245+
},
246+
"description": "Browser selection for authentication. Set via the dashboard UI.",
247+
"properties": {
248+
"mode": {
249+
"type": "string",
250+
"enum": [
251+
"auto",
252+
"custom"
253+
]
254+
},
255+
"channel": {
256+
"type": "string"
257+
},
258+
"executablePath": {
259+
"type": "string"
260+
},
261+
"label": {
262+
"type": "string"
263+
}
264+
}
265+
},
232266
"airtableFormula.mcp.toolProfile": {
233267
"type": "string",
234268
"default": "safe-write",
@@ -327,11 +361,15 @@
327361
"@airtable-formula/shared": "workspace:*"
328362
},
329363
"devDependencies": {
364+
"@types/adm-zip": "^0.5.8",
365+
"@types/archiver": "^7.0.0",
330366
"@types/node": "^20.0.0",
331367
"@types/vscode": "^1.100.0",
332368
"@vscode/test-electron": "^2.4.0",
333369
"@vscode/vsce": "^3.0.0",
370+
"adm-zip": "^0.5.17",
334371
"airtable-user-mcp": "workspace:*",
372+
"archiver": "^7.0.1",
335373
"tsup": "^8.0.0",
336374
"typescript": "^5.4.0",
337375
"vitest": "^1.6.0"

packages/extension/src/auto-config/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from 'path';
22
import * as os from 'os';
33
import * as fs from 'fs';
44
import type { IdeId, IdeStatus, AiFiles } from '@airtable-formula/shared';
5+
import { getSettings } from '../settings.js';
56
import { IDE_CONFIGS } from './ide-configs.js';
67
import { detectInstalledIdes, isIdeInstalled, readConfigFile, writeConfigAtomic, mergeServerEntry, removeServerEntry } from './ide-detection.js';
78

@@ -53,21 +54,38 @@ export async function ensureLauncher(serverPath: string): Promise<void> {
5354
}
5455

5556
export function buildServerEntry(_serverPath: string): Record<string, unknown> {
56-
// Point to the stable launcher — no version number in this path
57+
const env: Record<string, string> = {
58+
AIRTABLE_HEADLESS_ONLY: '1',
59+
AIRTABLE_PROFILE_DIR: path.join(os.homedir(), '.airtable-user-mcp', '.chrome-profile'),
60+
};
61+
62+
const settings = getSettings();
63+
const choice = settings.auth.browserChoice;
64+
if (choice?.channel) env.AIRTABLE_BROWSER_CHANNEL = choice.channel;
65+
if (choice?.executablePath) env.AIRTABLE_BROWSER_PATH = choice.executablePath;
66+
5767
return {
5868
command: 'node',
5969
args: [LAUNCHER_SCRIPT],
60-
env: {
61-
AIRTABLE_HEADLESS_ONLY: '1',
62-
},
70+
env,
6371
};
6472
}
6573

6674
export function buildNpxServerEntry(): Record<string, unknown> {
75+
const env: Record<string, string> = {
76+
AIRTABLE_HEADLESS_ONLY: '1',
77+
AIRTABLE_PROFILE_DIR: path.join(os.homedir(), '.airtable-user-mcp', '.chrome-profile'),
78+
};
79+
80+
const settings = getSettings();
81+
const choice = settings.auth.browserChoice;
82+
if (choice?.channel) env.AIRTABLE_BROWSER_CHANNEL = choice.channel;
83+
if (choice?.executablePath) env.AIRTABLE_BROWSER_PATH = choice.executablePath;
84+
6785
return {
6886
command: 'npx',
6987
args: ['-y', 'airtable-user-mcp'],
70-
env: { AIRTABLE_HEADLESS_ONLY: '1' },
88+
env,
7189
};
7290
}
7391

packages/extension/src/extension.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -433,32 +433,33 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
433433
// ── Auth commands ────────────────────────────────────────────────────
434434
context.subscriptions.push(
435435
vscode.commands.registerCommand('airtable-formula.login', async () => {
436-
const hasCreds = await authManager.hasCredentials();
437-
if (!hasCreds) {
438-
vscode.window.showWarningMessage('Airtable Formula: No credentials stored. Open Settings tab in the dashboard to save your Airtable credentials.');
439-
return;
436+
const settings = getSettings();
437+
if (settings.auth.loginMode === 'manual') {
438+
await vscode.window.withProgress(
439+
{ location: vscode.ProgressLocation.Notification, title: 'Airtable: Opening browser for login...' },
440+
() => authManager.manualLogin(),
441+
);
442+
} else {
443+
if (!(await authManager.hasCredentials())) {
444+
vscode.window.showWarningMessage('Airtable Formula: No credentials stored. Open Settings tab in the dashboard to save your Airtable credentials.');
445+
return;
440446
}
441-
vscode.window.withProgress(
442-
{ location: vscode.ProgressLocation.Notification, title: 'Airtable Formula: Logging in...', cancellable: false },
443-
async () => {
444-
debugCollector.trace('ext', 'auth', 'auth:login_start', { method: 'programmatic' });
445-
const state = await authManager.login();
446-
debugCollector.trace('ext', 'auth', 'auth:login_result', {
447-
success: state.status === 'valid',
448-
}, state.status !== 'valid' ? state.error : undefined);
449-
if (state.status === 'valid') {
450-
vscode.window.showInformationMessage(`Airtable Formula: Logged in successfully (${state.userId || 'unknown user'}).`);
451-
} else {
452-
vscode.window.showErrorMessage(`Airtable Formula: Login failed — ${state.error || 'unknown error'}.`);
453-
}
454-
dashboardProvider.refresh();
455-
}
447+
await vscode.window.withProgress(
448+
{ location: vscode.ProgressLocation.Notification, title: 'Airtable: Logging in...' },
449+
() => authManager.login(),
456450
);
451+
}
457452
}),
458453
vscode.commands.registerCommand('airtable-formula.logout', async () => {
459-
await authManager.clearCredentials();
460-
vscode.window.showInformationMessage('Airtable Formula: Credentials cleared.');
461-
dashboardProvider.refresh();
454+
const confirm = await vscode.window.showWarningMessage(
455+
'This will clear your Airtable browser session and any stored credentials. You\'ll need to log in again.',
456+
{ modal: true },
457+
'Logout',
458+
);
459+
if (confirm !== 'Logout') return;
460+
await authManager.logout();
461+
dashboardProvider.refresh();
462+
vscode.window.showInformationMessage('Airtable: Logged out and session cleared.');
462463
}),
463464
vscode.commands.registerCommand('airtable-formula.status', async () => {
464465
vscode.window.withProgress(

0 commit comments

Comments
 (0)