opencode/packages/core/src/credential.ts
Kit Langton 41283933ff
chore: merge dev into v2 (#34317)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
2026-06-28 11:30:38 -04:00

140 lines
4.8 KiB
TypeScript

export * as Credential from "./credential"
import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Credential } from "@opencode-ai/schema/credential"
import { Integration } from "@opencode-ai/schema/integration"
import { Database } from "./database/database"
import { makeGlobalNode } from "./effect/app-node"
import { CredentialTable } from "./credential/sql"
export const ID = Credential.ID
export type ID = Credential.ID
export const OAuth = Credential.OAuth
export type OAuth = Credential.OAuth
export const Key = Credential.Key
export type Key = Credential.Key
export const Value = Credential.Value
export type Value = Credential.Value
export class Info extends Schema.Class<Info>("Credential.Info")({
id: ID,
integrationID: Integration.ID,
label: Schema.String,
value: Value,
}) {}
export interface Interface {
/** Returns every stored credential. */
readonly all: () => Effect.Effect<Info[]>
/** Returns stored credentials belonging to one integration. */
readonly list: (integrationID: Integration.ID) => Effect.Effect<Info[]>
/** Returns one stored credential by ID. */
readonly get: (id: ID) => Effect.Effect<Info | undefined>
/** Replaces any credential for an integration and returns the new record. */
readonly create: (input: {
readonly integrationID: Integration.ID
readonly value: Value
readonly label?: string
}) => Effect.Effect<Info>
/** Updates the label or secret value of a stored credential. */
readonly update: (id: ID, updates: Partial<Pick<Info, "label" | "value">>) => Effect.Effect<void>
/** Removes a stored credential. */
readonly remove: (id: ID) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
const decode = Schema.decodeUnknownSync(Value)
const stored = (row: typeof CredentialTable.$inferSelect) => {
if (!row.integration_id) return
return new Info({
id: row.id,
integrationID: row.integration_id,
label: row.label,
value: decode(row.value),
})
}
return Service.of({
all: Effect.fn("Credential.all")(function* () {
return (yield* db
.select()
.from(CredentialTable)
.orderBy(asc(CredentialTable.time_created))
.all()
.pipe(Effect.orDie)).flatMap((row) => {
const credential = stored(row)
return credential ? [credential] : []
})
}),
list: Effect.fn("Credential.list")(function* (integrationID) {
return (yield* db
.select()
.from(CredentialTable)
.where(eq(CredentialTable.integration_id, integrationID))
.orderBy(asc(CredentialTable.time_created))
.all()
.pipe(Effect.orDie)).flatMap((row) => {
const credential = stored(row)
return credential ? [credential] : []
})
}),
get: Effect.fn("Credential.get")(function* (id) {
const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
return row ? stored(row) : undefined
}),
create: Effect.fn("Credential.create")(function* (input) {
const credential = new Info({
id: ID.create(),
integrationID: input.integrationID,
label: input.label ?? "default",
value: input.value,
})
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.delete(CredentialTable)
.where(eq(CredentialTable.integration_id, credential.integrationID))
.run()
yield* tx
.insert(CredentialTable)
.values({
id: credential.id,
integration_id: credential.integrationID,
label: credential.label,
value: credential.value,
})
.run()
}),
)
.pipe(Effect.orDie)
return credential
}),
update: Effect.fn("Credential.update")(function* (id, updates) {
if (!updates.label && !updates.value) return
yield* db
.update(CredentialTable)
.set({ label: updates.label, value: updates.value })
.where(eq(CredentialTable.id, id))
.run()
.pipe(Effect.orDie)
}),
remove: Effect.fn("Credential.remove")(function* (id) {
yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie)
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })