146 lines
5 KiB
TypeScript
146 lines
5 KiB
TypeScript
export * as Credential from "./credential"
|
|
|
|
import { asc, eq } from "drizzle-orm"
|
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
import { Database } from "./database/database"
|
|
import { IntegrationSchema } from "./integration/schema"
|
|
import { NonNegativeInt, withStatics } from "./schema"
|
|
import { Identifier } from "./util/identifier"
|
|
import { CredentialTable } from "./credential/sql"
|
|
|
|
export const ID = Schema.String.pipe(
|
|
Schema.brand("Credential.ID"),
|
|
withStatics((schema) => ({ create: () => schema.make("cred_" + Identifier.ascending()) })),
|
|
)
|
|
export type ID = typeof ID.Type
|
|
|
|
export class OAuth extends Schema.Class<OAuth>("Credential.OAuth")({
|
|
type: Schema.Literal("oauth"),
|
|
methodID: IntegrationSchema.MethodID,
|
|
refresh: Schema.String,
|
|
access: Schema.String,
|
|
expires: NonNegativeInt,
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
}) {}
|
|
|
|
export class Key extends Schema.Class<Key>("Credential.Key")({
|
|
type: Schema.Literal("key"),
|
|
key: Schema.String,
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
}) {}
|
|
|
|
export const Info = Schema.Union([OAuth, Key])
|
|
.pipe(Schema.toTaggedUnion("type"))
|
|
.annotate({ identifier: "Credential.Info" })
|
|
export type Info = Schema.Schema.Type<typeof Info>
|
|
|
|
export class Stored extends Schema.Class<Stored>("Credential.Stored")({
|
|
id: ID,
|
|
integrationID: IntegrationSchema.ID,
|
|
label: Schema.String,
|
|
value: Info,
|
|
}) {}
|
|
|
|
export interface Interface {
|
|
/** Returns every stored credential. */
|
|
readonly all: () => Effect.Effect<Stored[]>
|
|
/** Returns stored credentials belonging to one integration. */
|
|
readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Stored[]>
|
|
/** Replaces any credential for an integration and returns the new record. */
|
|
readonly create: (input: {
|
|
readonly integrationID: IntegrationSchema.ID
|
|
readonly value: Info
|
|
readonly label?: string
|
|
}) => Effect.Effect<Stored>
|
|
/** Updates the label or secret value of a stored credential. */
|
|
readonly update: (id: ID, updates: Partial<Pick<Stored, "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(Info)
|
|
const stored = (row: typeof CredentialTable.$inferSelect) => {
|
|
if (!row.integration_id) return
|
|
return new Stored({
|
|
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] : []
|
|
})
|
|
}),
|
|
create: Effect.fn("Credential.create")(function* (input) {
|
|
const credential = new Stored({
|
|
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))
|