Skip to content
Merged
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
31 changes: 29 additions & 2 deletions packages/client/src/promise/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ export type CredentialOAuth = {
metadata?: { [x: string]: JsonValue }
}

export type CredentialExternal = { type: "external"; methodID: string; metadata?: { [x: string]: JsonValue } }

export type ProjectVcs = string

export type ProjectIcon = { url?: string; override?: string; color?: string }
Expand Down Expand Up @@ -1492,7 +1494,7 @@ export type ConnectionCredentialInfo = {
type: "credential"
id: string
label: string
method: "key" | "oauth"
method: "key" | "oauth" | "external"
status?: ConnectionStatus
}

Expand Down Expand Up @@ -2256,7 +2258,7 @@ export type SessionMessageAssistantTool1 = {

export type FormFields = [FormField, ...Array<FormField>]

export type CredentialValue = CredentialOAuth | CredentialKey
export type CredentialValue = CredentialOAuth | CredentialKey | CredentialExternal

export type FormFields2 = [FormField1, ...Array<FormField1>]

Expand Down Expand Up @@ -5854,6 +5856,11 @@ export type CredentialCreateInput = {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| {
readonly type: "external"
readonly methodID: string
readonly metadata?: { readonly [x: string]: JsonValue }
}
readonly activate?: boolean
}["id"]
readonly integrationID: {
Expand All @@ -5877,6 +5884,11 @@ export type CredentialCreateInput = {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| {
readonly type: "external"
readonly methodID: string
readonly metadata?: { readonly [x: string]: JsonValue }
}
readonly activate?: boolean
}["integrationID"]
readonly label?: {
Expand All @@ -5900,6 +5912,11 @@ export type CredentialCreateInput = {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| {
readonly type: "external"
readonly methodID: string
readonly metadata?: { readonly [x: string]: JsonValue }
}
readonly activate?: boolean
}["label"]
readonly value: {
Expand All @@ -5923,6 +5940,11 @@ export type CredentialCreateInput = {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| {
readonly type: "external"
readonly methodID: string
readonly metadata?: { readonly [x: string]: JsonValue }
}
readonly activate?: boolean
}["value"]
readonly activate?: {
Expand All @@ -5946,6 +5968,11 @@ export type CredentialCreateInput = {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| {
readonly type: "external"
readonly methodID: string
readonly metadata?: { readonly [x: string]: JsonValue }
}
readonly activate?: boolean
}["activate"]
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type OAuth = Credential.OAuth
export const Key = Credential.Key
export type Key = Credential.Key

export const External = Credential.External
export type External = Credential.External

export const Value = Credential.Value
export type Value = Credential.Value

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export interface Interface extends State.Transformable<Editor> {
readonly connection: {
/** Returns the active connection for one integration. */
readonly active: (id: ID) => Effect.Effect<IntegrationConnection.Info | undefined>
/** Resolves a connection into usable credential material. */
/** Resolves a connection into credential material or an external credential-source reference. */
readonly resolve: (
connection: IntegrationConnection.Info,
) => Effect.Effect<Credential.Value | undefined, AuthorizationError>
Expand Down Expand Up @@ -700,7 +700,7 @@ const layer = Layer.effect(
}
const credential = yield* credentials.get(connection.id)
if (!credential) return undefined
if (credential.value.type === "key") return credential.value
if (credential.value.type !== "oauth") return credential.value
const implementation = state
.get()
.integrations.get(credential.integrationID)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ function unresolvedProviderVariables(model: RuntimeInfo, baseURL: string) {
}

const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => {
if (!credential) return {}
if (!credential || credential.type === "external") return {}
if (credential.type === "key") return { apiKey: credential.key }
if (specifier === "@opencode/ai/providers/anthropic" || specifier === "@opencode/ai/providers/anthropic-compatible")
return { authToken: credential.access }
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/plugin/provider/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function make(

const resourceDeployments = Effect.fn("AzurePlugin.resourceDeployments")(function* (
url: string,
credential: Credential.Value,
credential: Credential.Key | Credential.OAuth,
) {
return yield* http
.execute(
Expand Down Expand Up @@ -266,7 +266,7 @@ export function make(
// data-plane version 2022-12-01 has it; later versions dropped `/deployments` and keep `/models`, which lists
// models the resource can deploy rather than its deployments.
// https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/data-plane/OpenAIAuthoring/stable/2022-12-01/azureopenai.json
const deployments = (url: string, resource: string, credential: Credential.Value) =>
const deployments = (url: string, resource: string, credential: Credential.Key | Credential.OAuth) =>
credential.type === "oauth"
? managementDeployments(resource).pipe(Effect.catch(() => resourceDeployments(url, credential)))
: resourceDeployments(url, credential)
Expand Down Expand Up @@ -308,7 +308,12 @@ export function make(
const credential = yield* ctx.integration.connection
.resolve(connection)
.pipe(Effect.orElseSucceed(() => undefined))
if (!credential || (credential.type === "oauth" && credential.methodID !== methodID)) return
if (
!credential ||
credential.type === "external" ||
(credential.type === "oauth" && credential.methodID !== methodID)
)
return
const found = yield* deployments(url, name, credential).pipe(
// Azure promises no order; normalize it so a reordered response does not rebuild the model list.
Effect.map((list) => list.toSorted((a, b) => a.name.localeCompare(b.name))),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/plugin/provider/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const GitLabPlugin = define({
const credential = stored
? yield* ctx.integration.connection.resolve(stored).pipe(Effect.orElseSucceed(() => undefined))
: undefined
if (!stored || !credential) {
if (!stored || !credential || credential.type === "external") {
loaded.models = undefined
loaded.connection = undefined
return
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/plugin/provider/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
}
return yield* ctx.integration.connection.resolve(connection).pipe(
Effect.flatMap((credential) => {
if (!credential)
if (!credential || credential.type === "external")
return Effect.succeed({ config: undefined, connection, organization: undefined, mcp: undefined })
return fetchConfig(http, credential).pipe(
Effect.map((config) => ({
Expand Down Expand Up @@ -360,7 +360,8 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
return yield* Effect.fail(new Error("OpenCode Console connection changed"))
}
const credential = yield* ctx.integration.connection.resolve(active)
if (!credential) return yield* Effect.fail(new Error("OpenCode Console is not connected"))
if (!credential || credential.type === "external")
return yield* Effect.fail(new Error("OpenCode Console is not connected"))
const metadata = credential.metadata
const orgID = typeof metadata?.orgID === "string" ? metadata.orgID : undefined
const token = credential.type === "oauth" ? credential.access : credential.key
Expand Down Expand Up @@ -469,7 +470,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
}),
})

function fetchConfig(http: HttpClient.HttpClient, value: Credential.Value) {
function fetchConfig(http: HttpClient.HttpClient, value: Credential.Key | Credential.OAuth) {
// Scoped so responses whose body is never read (404, errors) are released here instead of by a GC-time abort.
return HttpClient.withScope(http)
.execute(
Expand Down Expand Up @@ -532,7 +533,7 @@ function organizationName(credential: Credential.Value) {
return typeof credential.metadata?.orgName === "string" ? credential.metadata.orgName : undefined
}

function credentialHeaders(value: Credential.Value): Record<string, string> {
function credentialHeaders(value: Credential.Key | Credential.OAuth): Record<string, string> {
const orgID = value.metadata?.orgID
return {
authorization: `Bearer ${value.type === "oauth" ? value.access : value.key}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/schema/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export const CredentialInfo = Schema.Struct({
type: Schema.Literal("credential"),
id: Credential.ID,
label: Schema.String,
/** How the credential was obtained: a stored key or an OAuth grant. */
method: Schema.Literals(["key", "oauth"]),
/** Whether the connection stores a key, an OAuth grant, or an external credential-source reference. */
method: Schema.Literals(["key", "oauth", "external"]),
status: optional(Status),
}).annotate({ identifier: "Connection.CredentialInfo" })

Expand Down
10 changes: 9 additions & 1 deletion packages/schema/src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ export const Key = Schema.Struct({
configuration: optional(Form.Answer),
}).annotate({ identifier: "Credential.Key" })

export const Value = Schema.Union([OAuth, Key])
/** References a credential source whose secrets and renewal are managed outside the credential store. */
export interface External extends Schema.Schema.Type<typeof External> {}
export const External = Schema.Struct({
type: Schema.Literal("external"),
methodID: IntegrationMethodID,
metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
}).annotate({ identifier: "Credential.External" })

export const Value = Schema.Union([OAuth, Key, External])
.pipe(Schema.toTaggedUnion("type"))
.annotate({ identifier: "Credential.Value" })
export type Value = Schema.Schema.Type<typeof Value>
Expand Down
Loading