From c07783881002668af9dc8ea2fb57fba14fb9fdd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 23 May 2026 22:56:28 +0200 Subject: [PATCH] feat(app): import OAuth-backed Den providers Allow desktop cloud-provider import to consume OpenCode OAuth-backed organization providers from the Den credential import endpoint. --- apps/app/src/app/lib/den.ts | 8 ++++ .../connections/provider-auth/store.ts | 44 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/apps/app/src/app/lib/den.ts b/apps/app/src/app/lib/den.ts index d5aebaf2be..0f425a9473 100644 --- a/apps/app/src/app/lib/den.ts +++ b/apps/app/src/app/lib/den.ts @@ -111,10 +111,13 @@ export type DenOrgLlmProviderModel = { export type DenOrgLlmProvider = { id: string; source: "models_dev" | "custom" | "openwork"; + credentialKind: "api_key" | "opencode_oauth"; providerId: string; name: string; providerConfig: Record; hasApiKey: boolean; + hasOpencodeAuth: boolean; + hasCredential: boolean; models: DenOrgLlmProviderModel[]; createdAt: string | null; updatedAt: string | null; @@ -122,6 +125,7 @@ export type DenOrgLlmProvider = { export type DenOrgLlmProviderConnection = DenOrgLlmProvider & { apiKey: string | null; + opencodeAuth: string | null; }; export type DenPluginConfigObjectType = "skill" | "agent" | "command" | "tool" | "mcp" | "hook" | "context" | "custom"; @@ -897,10 +901,13 @@ function parseDenOrgLlmProvider(value: unknown): DenOrgLlmProvider | null { return { id: value.id, source: value.source, + credentialKind: value.credentialKind === "opencode_oauth" ? "opencode_oauth" : "api_key", providerId: value.providerId, name: value.name, providerConfig: isRecord(value.providerConfig) ? value.providerConfig : {}, hasApiKey: value.hasApiKey === true, + hasOpencodeAuth: value.hasOpencodeAuth === true, + hasCredential: value.hasCredential === true || value.hasApiKey === true || value.hasOpencodeAuth === true, models: Array.isArray(value.models) ? value.models.flatMap((model) => { const parsed = parseDenOrgLlmProviderModel(model); @@ -936,6 +943,7 @@ function getDenOrgLlmProviderConnection(payload: unknown): DenOrgLlmProviderConn return { ...provider, apiKey: typeof payload.llmProvider.apiKey === "string" ? payload.llmProvider.apiKey : null, + opencodeAuth: typeof payload.llmProvider.opencodeAuth === "string" ? payload.llmProvider.opencodeAuth : null, }; } diff --git a/apps/app/src/react-app/domains/connections/provider-auth/store.ts b/apps/app/src/react-app/domains/connections/provider-auth/store.ts index b0550da8f8..192c403d36 100644 --- a/apps/app/src/react-app/domains/connections/provider-auth/store.ts +++ b/apps/app/src/react-app/domains/connections/provider-auth/store.ts @@ -163,8 +163,12 @@ export function createProviderAuthStore(options: CreateProviderAuthStoreOptions) a.length === b.length && a.every((value, index) => value === b[index]); const getCloudManagedProviderId = ( - provider: Pick, - ) => provider.source === "openwork" ? "openwork" : provider.id.trim(); + provider: Pick, + ) => { + if (provider.source === "openwork") return "openwork"; + if (provider.credentialKind === "opencode_oauth") return provider.providerId.trim(); + return provider.id.trim(); + }; const getProviderAuthWorkerType = (): "local" | "remote" => options.selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local"; @@ -1327,14 +1331,45 @@ export function createProviderAuthStore(options: CreateProviderAuthStoreOptions) const existingImported = state.importedCloudProviders[cloudProviderId] ?? null; const localProviderId = getCloudManagedProviderId(provider); const apiKey = provider.apiKey?.trim() ?? ""; + const opencodeAuth = provider.opencodeAuth?.trim() ?? ""; const env = getCloudProviderEnv(provider.providerConfig); - if (!apiKey && env.length > 0) { + if (provider.credentialKind === "opencode_oauth" && !opencodeAuth) { + throw new Error(`${provider.name} does not have a stored OpenCode OAuth credential yet.`); + } + if (provider.credentialKind === "api_key" && !apiKey && env.length > 0) { throw new Error(`${provider.name} does not have a stored organization credential yet.`); } await assertCloudProviderImportSafe(provider); - if (apiKey) { + if (provider.credentialKind === "opencode_oauth" && opencodeAuth) { + let parsedAuth: unknown; + try { + parsedAuth = JSON.parse(opencodeAuth); + } catch { + throw new Error(`${provider.name} has invalid OpenCode OAuth JSON.`); + } + if (!parsedAuth || typeof parsedAuth !== "object" || Array.isArray(parsedAuth)) { + throw new Error(`${provider.name} OpenCode OAuth auth must be a JSON object.`); + } + const authRecord = parsedAuth as Record; + if (authRecord.type !== "oauth") { + throw new Error(`${provider.name} OpenCode OAuth auth must include type "oauth".`); + } + if (typeof authRecord.access !== "string" || !authRecord.access.trim()) { + throw new Error(`${provider.name} OpenCode OAuth auth must include an access token.`); + } + if (typeof authRecord.refresh !== "string" || !authRecord.refresh.trim()) { + throw new Error(`${provider.name} OpenCode OAuth auth must include a refresh token.`); + } + if (typeof authRecord.expires !== "number" || !Number.isFinite(authRecord.expires) || authRecord.expires < 0) { + throw new Error(`${provider.name} OpenCode OAuth auth must include a non-negative numeric expires value.`); + } + await c.auth.set({ + providerID: localProviderId, + auth: parsedAuth as Parameters[0]["auth"], + }); + } else if (apiKey) { await c.auth.set({ providerID: localProviderId, auth: { type: "api", key: apiKey }, @@ -1380,6 +1415,7 @@ export function createProviderAuthStore(options: CreateProviderAuthStoreOptions) .filter((id) => id !== localProviderId && id !== existingImported?.providerId); options.setDisabledProviders(nextDisabledProviders); options.markOpencodeConfigReloadRequired(); + await refreshProviders({ dispose: true }).catch(() => null); refreshSnapshot(); emitChange(); return `${t("status.connected")} ${provider.name}`;