Compare commits

...
Author SHA1 Message Date
Aiden Cline 03578a12cd chore: merge v2 into external credentials 2026-10-06 18:25:28 -05:00
Aiden Cline cd6cd7d4b9 feat(core): add external credential references 2026-10-06 17:38:24 -05:00
9 changed files with 40 additions and 16 deletions

No files matched your search

@@ -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 }
@@ -1492,7 +1494,7 @@ export type ConnectionCredentialInfo = {
type: "credential"
id: string
label: string
method: "key" | "oauth"
method: "key" | "oauth" | "external"
status?: ConnectionStatus
}
@@ -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>]
@@ -5854,6 +5856,7 @@ 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: {
@@ -5877,6 +5880,7 @@ 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?: {
@@ -5900,6 +5904,7 @@ 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: {
@@ -5923,6 +5928,7 @@ 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?: {
@@ -5946,6 +5952,7 @@ 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"]
}
+3
View File
@@ -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
+2 -2
View File
@@ -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>
@@ -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)
+1 -1
View File
@@ -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 }
+8 -3
View File
@@ -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(
@@ -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)
@@ -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))),
+1 -1
View File
@@ -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
@@ -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) => ({
@@ -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
@@ -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(
@@ -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}`,
+2 -2
View File
@@ -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" })
+9 -1
View File
@@ -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: 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>