feat: add well-known auth service

This commit is contained in:
Dax Raad 2026-05-17 16:27:50 -04:00
commit c25128a6f5
11 changed files with 658 additions and 357 deletions

View file

@ -0,0 +1,237 @@
export * as AuthWellKnown from "./auth-well-known"
import path from "path"
import { Context, Effect, Layer, Option, Schema, SynchronizedRef } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { AppFileSystem } from "./filesystem"
import { Global } from "./global"
import { Substitution } from "./substitution"
export class Entry extends Schema.Class<Entry>("AuthWellKnown.Entry")({
key: Schema.String,
token: Schema.String,
}) {}
export class FileWriteError extends Schema.TaggedErrorClass<FileWriteError>()("AuthWellKnown.FileWriteError", {
operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]),
cause: Schema.Defect,
}) {}
export class RemoteConfigError extends Schema.TaggedErrorClass<RemoteConfigError>()("AuthWellKnown.RemoteConfigError", {
url: Schema.String,
status: Schema.Number.pipe(Schema.optional),
cause: Schema.Defect.pipe(Schema.optional),
}) {}
export type Error = FileWriteError | RemoteConfigError
const RemoteConfig = Schema.Struct({
url: Schema.String,
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
})
export class Metadata extends Schema.Class<Metadata>("AuthWellKnown.Metadata")({
auth: Schema.Struct({
command: Schema.Array(Schema.String),
env: Schema.String,
}).pipe(Schema.optional),
config: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
remote_config: RemoteConfig.pipe(Schema.optional),
}) {}
export type ConfigDocument = {
url: string
source: string
dir: string
content: unknown
}
export interface Interface {
readonly all: () => Effect.Effect<Record<string, Entry>, Error>
readonly get: (url: string) => Effect.Effect<Entry | undefined, Error>
readonly set: (url: string, entry: Entry) => Effect.Effect<void, Error>
readonly remove: (url: string) => Effect.Effect<void, Error>
readonly configs: () => Effect.Effect<ConfigDocument[], Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AuthWellKnown") {}
const decodeMetadata = Schema.decodeUnknownEffect(Metadata)
const decodeRemoteConfig = Schema.decodeUnknownEffect(RemoteConfig)
function loadLegacyAuth(input: {
fsys: AppFileSystem.Interface
dataDir: string
write: (data: Record<string, Entry>) => Effect.Effect<void, Error>
}) {
return Effect.gen(function* () {
const decodeLegacy = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))
const decodeLegacyCredential = Schema.decodeUnknownOption(
Schema.Struct({
type: Schema.Literal("wellknown"),
key: Schema.String,
token: Schema.String,
}),
)
const legacy = Object.fromEntries(
Object.entries(
Option.getOrElse(
decodeLegacy(
yield* input.fsys.readJson(path.join(input.dataDir, "auth.json")).pipe(Effect.orElseSucceed(() => null)),
),
() => ({}),
),
).flatMap(([url, value]) => {
const decoded = Option.getOrUndefined(decodeLegacyCredential(value))
return decoded ? [[url.replace(/\/+$/, ""), new Entry({ key: decoded.key, token: decoded.token })]] : []
}),
)
if (Object.keys(legacy).length > 0) yield* input.write(legacy).pipe(Effect.ignore)
return legacy
})
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service
const global = yield* Global.Service
const http = yield* HttpClient.HttpClient
const substitution = yield* Substitution.Service
const file = path.join(global.data, "well-known.json")
const decodeEntries = Schema.decodeUnknownOption(Schema.Record(Schema.String, Entry))
const normalizeUrl = (url: string) => url.replace(/\/+$/, "")
const write = (operation: "migrate" | "write", data: Record<string, Entry>) =>
fsys.writeJson(file, data, 0o600).pipe(Effect.mapError((cause) => new FileWriteError({ operation, cause })))
const load: () => Effect.Effect<Record<string, Entry>> = Effect.fnUntraced(function* () {
const current = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
if (current && typeof current === "object")
return Option.getOrElse(decodeEntries(current), () => ({}) as Record<string, Entry>)
return yield* loadLegacyAuth({ fsys, dataDir: global.data, write: (data) => write("migrate", data) })
})
const state = SynchronizedRef.makeUnsafe<Record<string, Entry>>(yield* load())
const metadata = Effect.fn("AuthWellKnown.metadata")(function* (url: string) {
const normalized = normalizeUrl(url)
const source = `${normalized}/.well-known/opencode`
const response = yield* HttpClientRequest.get(source).pipe(
HttpClientRequest.acceptJson,
http.execute,
Effect.mapError((cause) => new RemoteConfigError({ url: source, cause })),
)
if (response.status < 200 || response.status >= 300) {
return yield* new RemoteConfigError({ url: source, status: response.status })
}
const metadata = yield* response.json.pipe(
Effect.flatMap(decodeMetadata),
Effect.mapError((cause) => new RemoteConfigError({ url: source, cause })),
)
return { url: normalized, source, dir: path.dirname(source), metadata }
})
const remote = Effect.fn("AuthWellKnown.remote")(function* (input: { url: string; headers?: Record<string, string> }) {
const response = yield* HttpClientRequest.get(input.url).pipe(
HttpClientRequest.acceptJson,
input.headers ? HttpClientRequest.setHeaders(input.headers) : (request) => request,
http.execute,
Effect.mapError((cause) => new RemoteConfigError({ url: input.url, cause })),
)
if (response.status < 200 || response.status >= 300) {
return yield* new RemoteConfigError({ url: input.url, status: response.status })
}
return yield* response.json.pipe(Effect.mapError((cause) => new RemoteConfigError({ url: input.url, cause })))
})
return Service.of({
all: Effect.fn("AuthWellKnown.all")(function* () {
return yield* SynchronizedRef.get(state)
}),
get: Effect.fn("AuthWellKnown.get")(function* (url) {
return (yield* SynchronizedRef.get(state))[normalizeUrl(url)]
}),
set: Effect.fn("AuthWellKnown.set")(function* (url, entry) {
yield* SynchronizedRef.updateEffect(
state,
Effect.fnUntraced(function* (data) {
const next = { ...data, [normalizeUrl(url)]: entry }
yield* write("write", next)
return next
}),
)
}),
remove: Effect.fn("AuthWellKnown.remove")(function* (url) {
yield* SynchronizedRef.updateEffect(
state,
Effect.fnUntraced(function* (data) {
const next = { ...data }
delete next[url]
delete next[normalizeUrl(url)]
yield* write("write", next)
return next
}),
)
}),
configs: Effect.fn("AuthWellKnown.configs")(function* () {
const documents = yield* Effect.all(
Object.entries(yield* SynchronizedRef.get(state)).map(([url, entry]) =>
Effect.gen(function* () {
const configs: ConfigDocument[] = []
const response = yield* metadata(url)
const env = { [entry.key]: entry.token }
if (response.metadata.config) {
configs.push({
url: response.url,
source: response.source,
dir: response.dir,
content: response.metadata.config,
})
}
if (response.metadata.remote_config) {
const remoteConfig = yield* substitution
.substitute({
text: JSON.stringify(response.metadata.remote_config),
type: "virtual",
dir: response.url,
source: response.source,
env,
})
.pipe(
Effect.flatMap((text) =>
Effect.try({
try: () => JSON.parse(text) as unknown,
catch: (cause) => new RemoteConfigError({ url: response.source, cause }),
}),
),
Effect.flatMap(decodeRemoteConfig),
Effect.mapError((cause) => new RemoteConfigError({ url: response.source, cause })),
)
configs.push({
url: remoteConfig.url,
source: remoteConfig.url,
dir: path.dirname(remoteConfig.url),
content: yield* remote({ url: remoteConfig.url, headers: remoteConfig.headers }),
})
}
return configs
}),
),
{ concurrency: "unbounded" },
)
return documents.flat()
}),
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Global.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Substitution.defaultLayer),
)

View file

@ -0,0 +1,94 @@
export * as Substitution from "./substitution"
import os from "os"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { AppFileSystem } from "./filesystem"
type Source =
| {
type: "path"
path: string
}
| {
type: "virtual"
source: string
dir: string
}
export type Input = Source & {
text: string
missing?: "error" | "empty"
env?: Record<string, string | undefined>
}
export class FileReferenceError extends Schema.TaggedErrorClass<FileReferenceError>()("Substitution.FileReferenceError", {
source: Schema.String,
token: Schema.String,
resolved: Schema.String,
cause: Schema.Defect,
}) {}
export type Error = FileReferenceError
export interface Interface {
readonly substitute: (input: Input) => Effect.Effect<string, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Substitution") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
return Service.of({
substitute: Effect.fn("Substitution.substitute")(function* (input) {
const missing = input.missing ?? "error"
const text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return input.env?.[varName] ?? process.env[varName] ?? ""
})
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
if (!fileMatches.length) return text
const configDir = input.type === "path" ? path.dirname(input.path) : input.dir
const configSource = input.type === "path" ? input.path : input.source
let out = ""
let cursor = 0
for (const match of fileMatches) {
const token = match[0]
const index = match.index!
out += text.slice(cursor, index)
const lineStart = text.lastIndexOf("\n", index - 1) + 1
const prefix = text.slice(lineStart, index).trimStart()
if (prefix.startsWith("//")) {
out += token
cursor = index + token.length
continue
}
const reference = token.replace(/^\{file:/, "").replace(/\}$/, "")
const filepath = reference.startsWith("~/") ? path.join(os.homedir(), reference.slice(2)) : reference
const resolved = path.isAbsolute(filepath) ? filepath : path.resolve(configDir, filepath)
const content = yield* fs.readFileString(resolved).pipe(
Effect.catch((cause) => {
if (missing === "empty") return Effect.succeed("")
return Effect.fail(new FileReferenceError({ source: configSource, token, resolved, cause }))
}),
)
out += JSON.stringify(content.trim()).slice(1, -1)
cursor = index + token.length
}
out += text.slice(cursor)
return out
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))

View file

@ -0,0 +1,169 @@
import { describe, expect } from "bun:test"
import path from "path"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Global } from "@opencode-ai/core/global"
import { Substitution } from "@opencode-ai/core/substitution"
import { AuthWellKnown } from "@opencode-ai/core/auth-well-known"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(Layer.empty)
const withAuthWellKnown = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R | AuthWellKnown.Service>) =>
effect.pipe(
Effect.provide(AuthWellKnown.layer),
Effect.provide(AppFileSystem.defaultLayer),
Effect.provide(Global.layerWith({ data: dir })),
Effect.provide(FetchHttpClient.layer),
Effect.provide(Substitution.defaultLayer),
)
describe("AuthWellKnown", () => {
it.live("stores well-known credentials", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
yield* withAuthWellKnown(
tmp.path,
Effect.gen(function* () {
const auth = yield* AuthWellKnown.Service
yield* auth.set("https://example.com/", new AuthWellKnown.Entry({ key: "TEST_TOKEN", token: "secret" }))
}),
)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "well-known.json")).json())).toEqual({
"https://example.com": {
key: "TEST_TOKEN",
token: "secret",
},
})
}),
)
it.live("migrates legacy well-known auth records", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
yield* Effect.promise(() =>
Bun.write(
path.join(tmp.path, "auth.json"),
JSON.stringify({
"https://example.com": {
type: "wellknown",
key: "TEST_TOKEN",
token: "secret",
},
}),
),
)
const entry = yield* withAuthWellKnown(
tmp.path,
Effect.gen(function* () {
const auth = yield* AuthWellKnown.Service
return yield* auth.get("https://example.com/")
}),
)
expect(entry).toEqual({
key: "TEST_TOKEN",
token: "secret",
})
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "well-known.json")).json())).toEqual({
"https://example.com": {
key: "TEST_TOKEN",
token: "secret",
},
})
}),
)
it.live("loads config documents", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
yield* Effect.promise(() =>
Bun.write(
path.join(tmp.path, "well-known.json"),
JSON.stringify({
"https://example.com": {
key: "TEST_TOKEN",
token: "secret",
},
}),
),
)
const originalFetch = Object.getOwnPropertyDescriptor(globalThis, "fetch")?.value as typeof fetch
const originalToken = process.env.TEST_TOKEN
yield* Effect.acquireRelease(
Effect.sync(() => {
const fakeFetch = Object.assign(
(input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
if (url === "https://example.com/.well-known/opencode") {
return Promise.resolve(
Response.json({
config: { instructions: ["local"] },
remote_config: {
url: "https://remote.example.com/config",
headers: {
authorization: "Bearer {env:TEST_TOKEN}",
},
},
}),
)
}
if (url === "https://remote.example.com/config") {
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer secret")
return Promise.resolve(Response.json({ model: "remote/model" }))
}
return Promise.resolve(new Response(null, { status: 404 }))
},
{ preconnect: originalFetch.preconnect },
)
Object.defineProperty(globalThis, "fetch", { value: fakeFetch, configurable: true, writable: true })
}),
() =>
Effect.sync(() => {
Object.defineProperty(globalThis, "fetch", { value: originalFetch, configurable: true, writable: true })
if (originalToken === undefined) delete process.env.TEST_TOKEN
else process.env.TEST_TOKEN = originalToken
}),
)
const result = yield* withAuthWellKnown(
tmp.path,
Effect.gen(function* () {
const auth = yield* AuthWellKnown.Service
return yield* auth.configs()
}),
)
expect(process.env.TEST_TOKEN).toBeUndefined()
expect(result).toEqual([
{
url: "https://example.com",
source: "https://example.com/.well-known/opencode",
dir: "https://example.com/.well-known",
content: { instructions: ["local"] },
},
{
url: "https://remote.example.com/config",
source: "https://remote.example.com/config",
dir: "https://remote.example.com",
content: { model: "remote/model" },
},
])
}),
)
})

View file

@ -0,0 +1,49 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Global } from "@opencode-ai/core/global"
import { AuthV2 } from "@opencode-ai/core/auth"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(Layer.empty)
const withAuth = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R | AuthV2.Service>) =>
effect.pipe(
Effect.provide(AuthV2.layer),
Effect.provide(AppFileSystem.defaultLayer),
Effect.provide(Global.layerWith({ data: dir })),
)
describe("AuthV2", () => {
it.live("stores api credentials", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const account = yield* withAuth(
tmp.path,
Effect.gen(function* () {
const auth = yield* AuthV2.Service
return yield* auth.create({
serviceID: AuthV2.ServiceID.make("anthropic"),
credential: new AuthV2.ApiKeyCredential({ type: "api", key: "sk-test" }),
})
}),
)
const active = yield* withAuth(
tmp.path,
Effect.gen(function* () {
const auth = yield* AuthV2.Service
return yield* auth.active(AuthV2.ServiceID.make("anthropic"))
}),
)
expect(active?.id).toBe(account.id)
expect(active?.credential).toEqual({ type: "api", key: "sk-test" })
}),
)
})