fix(providers): secure Zed import route and add dashboard UI component
This commit is contained in:
Generated
+26
@@ -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
@@ -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}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user