fix(providers): secure Zed import route and add dashboard UI component

This commit is contained in:
diegosouzapw
2026-03-23 15:55:58 -03:00
parent 5fa97841b2
commit acb94216c8
4 changed files with 147 additions and 72 deletions
+26
View File
@@ -25,6 +25,7 @@
"http-proxy-middleware": "^3.0.5",
"https-proxy-agent": "^8.0.0",
"jose": "^6.1.3",
"keytar": "^7.9.0",
"lowdb": "^7.0.1",
"monaco-editor": "^0.55.1",
"next": "^16.1.6",
@@ -54,6 +55,7 @@
"@tailwindcss/postcss": "^4.1.18",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/keytar": "^4.4.0",
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -3431,6 +3433,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/keytar": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@types/keytar/-/keytar-4.4.0.tgz",
"integrity": "sha512-cq/NkUUy6rpWD8n7PweNQQBpw2o0cf5v6fbkUVEpOB9VzzIvyPvSEId1/goIj+MciW2v1Lw5mRimKO01XgE9EA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
@@ -8106,6 +8115,23 @@
"node": ">=4.0"
}
},
"node_modules/keytar": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.0.1"
}
},
"node_modules/keytar/node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+4 -2
View File
@@ -83,6 +83,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"@monaco-editor/react": "^4.7.0",
"@swc/helpers": "0.5.19",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.2",
"bottleneck": "^2.19.5",
@@ -92,6 +93,7 @@
"http-proxy-middleware": "^3.0.5",
"https-proxy-agent": "^8.0.0",
"jose": "^6.1.3",
"keytar": "^7.9.0",
"lowdb": "^7.0.1",
"monaco-editor": "^0.55.1",
"next": "^16.1.6",
@@ -110,14 +112,14 @@
"uuid": "^13.0.0",
"wreq-js": "^2.0.1",
"zod": "^4.3.6",
"zustand": "^5.0.10",
"@swc/helpers": "0.5.19"
"zustand": "^5.0.10"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.1.18",
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/keytar": "^4.4.0",
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -99,6 +99,7 @@ export default function ProvidersPage() {
const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] = useState(false);
const [testingMode, setTestingMode] = useState<string | null>(null);
const [testResults, setTestResults] = useState<any>(null);
const [importingZed, setImportingZed] = useState(false);
const notify = useNotificationStore();
const t = useTranslations("providers");
const tc = useTranslations("common");
@@ -123,6 +124,33 @@ export default function ProvidersPage() {
fetchData();
}, []);
const handleZedImport = async () => {
setImportingZed(true);
try {
const res = await fetch("/api/providers/zed/import", { method: "POST" });
const data = await res.json();
if (res.ok && data.success) {
if (data.count > 0) {
notify.success(
`Imported ${data.count} credentials from Zed IDE (${data.providers.join(", ")}).`
);
// Refresh connections silently
const connectionsRes = await fetch("/api/providers");
const connectionsData = await connectionsRes.json();
if (connectionsRes.ok) setConnections(connectionsData.connections || []);
} else {
notify.info("No supported OAuth credentials found in Zed IDE.");
}
} else {
notify.error(data.error || "Failed to import from Zed IDE.");
}
} catch (error) {
notify.error("Network error while trying to import from Zed.");
} finally {
setImportingZed(false);
}
};
const getProviderStats = (providerId, authType) => {
const providerConnections = connections.filter(
(c) => c.provider === providerId && c.authType === authType
@@ -269,6 +297,19 @@ export default function ProvidersPage() {
</h2>
<div className="flex items-center gap-2">
<ModelAvailabilityBadge />
<button
onClick={handleZedImport}
disabled={importingZed}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40`}
title="Import credentials from Zed IDE"
>
<span
className={`material-symbols-outlined text-[14px] ${importingZed ? "animate-spin" : ""}`}
>
{importingZed ? "sync" : "download"}
</span>
{importingZed ? "Importing..." : "Import from Zed"}
</button>
<button
onClick={() => handleBatchTest("oauth")}
disabled={!!testingMode}
+76 -70
View File
@@ -1,23 +1,18 @@
/**
* API endpoint for importing Zed IDE OAuth credentials
*
*
* POST /api/providers/zed/import
*
*
* Discovers and imports OAuth credentials from Zed IDE's keychain storage.
* Supports all major Zed providers: OpenAI, Anthropic, Google, Mistral, xAI, etc.
*
* Security: Requires authentication. First-time keychain access will prompt user for OS permission.
*
* FIX #4: Added actual credential storage integration.
*
* NOTE: This implementation provides the credential discovery logic.
* Integration with OmniRoute's provider registration system should be completed
* by the maintainer who has full context of the internal provider schema.
*
* Security: protected by requireManagementAuth.
*/
import { NextResponse } from 'next/server';
import { discoverZedCredentials, isZedInstalled } from '@/lib/zed-oauth/keychain-reader';
import type { ZedCredential } from '@/lib/zed-oauth/keychain-reader';
import { NextResponse } from "next/server";
import { discoverZedCredentials, isZedInstalled } from "@/lib/zed-oauth/keychain-reader";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { createProviderConnection } from "@/lib/db/providers";
interface ImportResponse {
success: boolean;
@@ -33,21 +28,28 @@ interface ImportResponse {
zedInstalled?: boolean;
}
export async function POST(request: Request): Promise<NextResponse<ImportResponse>> {
export async function POST(request: Request): Promise<NextResponse<ImportResponse> | Response> {
// Security verification
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
// Check if Zed is installed
const zedInstalled = await isZedInstalled();
if (!zedInstalled) {
return NextResponse.json({
success: false,
error: 'Zed IDE does not appear to be installed on this system.',
zedInstalled: false
}, { status: 404 });
return NextResponse.json(
{
success: false,
error: "Zed IDE does not appear to be installed on this system.",
zedInstalled: false,
},
{ status: 404 }
);
}
// Discover credentials from keychain
console.log('[Zed Import] Discovering Zed credentials from keychain...');
console.log("[Zed Import] Discovering Zed credentials from keychain...");
const credentials = await discoverZedCredentials();
if (credentials.length === 0) {
@@ -56,76 +58,80 @@ export async function POST(request: Request): Promise<NextResponse<ImportRespons
count: 0,
providers: [],
credentials: [],
zedInstalled: true
zedInstalled: true,
});
}
// FIX #4: Process and return credentials for integration
//
// MAINTAINER TODO: Integrate with OmniRoute's provider system here.
//
// Suggested integration points:
// 1. Save to database using OmniRoute's provider schema
// 2. Encrypt tokens using existing AES-256-GCM encryption
// 3. Trigger provider registration hooks
// 4. Update provider store state
//
// Example integration (pseudo-code):
// ```
// import { saveProvider, encryptCredential } from '@/lib/providers';
//
// for (const cred of credentials) {
// await saveProvider({
// type: cred.provider,
// apiKey: await encryptCredential(cred.token),
// source: 'zed-import',
// enabled: true
// });
// }
// ```
// Save to database using OmniRoute's provider schema
let savedCount = 0;
for (const cred of credentials) {
if (!cred.token) continue;
// For now, return credential metadata (not actual tokens) for manual review
const credentialSummary = credentials.map(cred => ({
try {
await createProviderConnection({
provider: cred.provider,
authType: "apikey",
apiKey: cred.token,
name: `Zed Import (${cred.account || cred.service})`,
isActive: true,
});
savedCount++;
} catch (err) {
console.error(`[Zed Import] Failed to save credential for ${cred.provider}:`, err);
}
}
const credentialSummary = credentials.map((cred) => ({
provider: cred.provider,
service: cred.service,
account: cred.account,
hasToken: Boolean(cred.token)
hasToken: Boolean(cred.token),
}));
const importedProviders = credentials.map(c => c.provider);
const importedProviders = credentials.map((c) => c.provider);
const uniqueProviders = [...new Set(importedProviders)];
console.log(`[Zed Import] Discovered ${credentials.length} credentials for ${uniqueProviders.length} providers`);
console.log(
`[Zed Import] Discovered ${credentials.length} credentials and successfully saved ${savedCount} for ${uniqueProviders.length} providers`
);
return NextResponse.json({
success: true,
count: credentials.length,
count: savedCount,
providers: uniqueProviders,
credentials: credentialSummary,
zedInstalled: true
zedInstalled: true,
});
} catch (error: any) {
console.error('[Zed Import] Error importing credentials:', error);
// Check for common keychain access errors
if (error?.message?.includes('User canceled') || error?.message?.includes('denied')) {
return NextResponse.json({
success: false,
error: 'Keychain access denied. Please grant permission when prompted by your OS.'
}, { status: 403 });
console.error("[Zed Import] Error importing credentials:", error);
if (error?.message?.includes("User canceled") || error?.message?.includes("denied")) {
return NextResponse.json(
{
success: false,
error: "Keychain access denied. Please grant permission when prompted by your OS.",
},
{ status: 403 }
);
}
if (error?.message?.includes('not found') || error?.message?.includes('ENOENT')) {
return NextResponse.json({
success: false,
error: 'Keychain service not available on this system. On Linux, install libsecret-1-dev.'
}, { status: 404 });
if (error?.message?.includes("not found") || error?.message?.includes("ENOENT")) {
return NextResponse.json(
{
success: false,
error:
"Keychain service not available on this system. On Linux, install libsecret-1-dev.",
},
{ status: 404 }
);
}
return NextResponse.json({
success: false,
error: `Failed to import credentials: ${error?.message || 'Unknown error'}`
}, { status: 500 });
return NextResponse.json(
{
success: false,
error: `Failed to import credentials: ${error?.message || "Unknown error"}`,
},
{ status: 500 }
);
}
}