Skip to content
Closed
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
8 changes: 8 additions & 0 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,21 @@ export type DenOrgLlmProviderModel = {
export type DenOrgLlmProvider = {
id: string;
source: "models_dev" | "custom" | "openwork";
credentialKind: "api_key" | "opencode_oauth";
providerId: string;
name: string;
providerConfig: Record<string, unknown>;
hasApiKey: boolean;
hasOpencodeAuth: boolean;
hasCredential: boolean;
models: DenOrgLlmProviderModel[];
createdAt: string | null;
updatedAt: string | null;
};

export type DenOrgLlmProviderConnection = DenOrgLlmProvider & {
apiKey: string | null;
opencodeAuth: string | null;
};

export type DenPluginConfigObjectType = "skill" | "agent" | "command" | "tool" | "mcp" | "hook" | "context" | "custom";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
};
}

Expand Down
44 changes: 40 additions & 4 deletions apps/app/src/react-app/domains/connections/provider-auth/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,12 @@ export function createProviderAuthStore(options: CreateProviderAuthStoreOptions)
a.length === b.length && a.every((value, index) => value === b[index]);

const getCloudManagedProviderId = (
provider: Pick<DenOrgLlmProvider, "id" | "providerId" | "source">,
) => provider.source === "openwork" ? "openwork" : provider.id.trim();
provider: Pick<DenOrgLlmProvider, "id" | "providerId" | "source" | "credentialKind">,
) => {
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";
Expand Down Expand Up @@ -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<string, unknown>;
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<typeof c.auth.set>[0]["auth"],
});
} else if (apiKey) {
await c.auth.set({
providerID: localProviderId,
auth: { type: "api", key: apiKey },
Expand Down Expand Up @@ -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}`;
Expand Down
Loading