credential.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. export * as Credential from "./credential"
  2. import { asc, eq } from "drizzle-orm"
  3. import { Context, Effect, Layer, Schema } from "effect"
  4. import { Credential } from "@opencode-ai/schema/credential"
  5. import { Integration } from "@opencode-ai/schema/integration"
  6. import { Database } from "./database/database"
  7. import { CredentialTable } from "./credential/sql"
  8. export const ID = Credential.ID
  9. export type ID = Credential.ID
  10. export const OAuth = Credential.OAuth
  11. export type OAuth = Credential.OAuth
  12. export const Key = Credential.Key
  13. export type Key = Credential.Key
  14. export const Value = Credential.Value
  15. export type Value = Credential.Value
  16. export class Info extends Schema.Class<Info>("Credential.Info")({
  17. id: ID,
  18. integrationID: Integration.ID,
  19. label: Schema.String,
  20. value: Value,
  21. }) {}
  22. export interface Interface {
  23. /** Returns every stored credential. */
  24. readonly all: () => Effect.Effect<Info[]>
  25. /** Returns stored credentials belonging to one integration. */
  26. readonly list: (integrationID: Integration.ID) => Effect.Effect<Info[]>
  27. /** Returns one stored credential by ID. */
  28. readonly get: (id: ID) => Effect.Effect<Info | undefined>
  29. /** Replaces any credential for an integration and returns the new record. */
  30. readonly create: (input: {
  31. readonly integrationID: Integration.ID
  32. readonly value: Value
  33. readonly label?: string
  34. }) => Effect.Effect<Info>
  35. /** Updates the label or secret value of a stored credential. */
  36. readonly update: (id: ID, updates: Partial<Pick<Info, "label" | "value">>) => Effect.Effect<void>
  37. /** Removes a stored credential. */
  38. readonly remove: (id: ID) => Effect.Effect<void>
  39. }
  40. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
  41. export const layer = Layer.effect(
  42. Service,
  43. Effect.gen(function* () {
  44. const { db } = yield* Database.Service
  45. const decode = Schema.decodeUnknownSync(Value)
  46. const stored = (row: typeof CredentialTable.$inferSelect) => {
  47. if (!row.integration_id) return
  48. return new Info({
  49. id: row.id,
  50. integrationID: row.integration_id,
  51. label: row.label,
  52. value: decode(row.value),
  53. })
  54. }
  55. return Service.of({
  56. all: Effect.fn("Credential.all")(function* () {
  57. return (yield* db
  58. .select()
  59. .from(CredentialTable)
  60. .orderBy(asc(CredentialTable.time_created))
  61. .all()
  62. .pipe(Effect.orDie)).flatMap((row) => {
  63. const credential = stored(row)
  64. return credential ? [credential] : []
  65. })
  66. }),
  67. list: Effect.fn("Credential.list")(function* (integrationID) {
  68. return (yield* db
  69. .select()
  70. .from(CredentialTable)
  71. .where(eq(CredentialTable.integration_id, integrationID))
  72. .orderBy(asc(CredentialTable.time_created))
  73. .all()
  74. .pipe(Effect.orDie)).flatMap((row) => {
  75. const credential = stored(row)
  76. return credential ? [credential] : []
  77. })
  78. }),
  79. get: Effect.fn("Credential.get")(function* (id) {
  80. const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
  81. return row ? stored(row) : undefined
  82. }),
  83. create: Effect.fn("Credential.create")(function* (input) {
  84. const credential = new Info({
  85. id: ID.create(),
  86. integrationID: input.integrationID,
  87. label: input.label ?? "default",
  88. value: input.value,
  89. })
  90. yield* db
  91. .transaction((tx) =>
  92. Effect.gen(function* () {
  93. yield* tx
  94. .delete(CredentialTable)
  95. .where(eq(CredentialTable.integration_id, credential.integrationID))
  96. .run()
  97. yield* tx
  98. .insert(CredentialTable)
  99. .values({
  100. id: credential.id,
  101. integration_id: credential.integrationID,
  102. label: credential.label,
  103. value: credential.value,
  104. })
  105. .run()
  106. }),
  107. )
  108. .pipe(Effect.orDie)
  109. return credential
  110. }),
  111. update: Effect.fn("Credential.update")(function* (id, updates) {
  112. if (!updates.label && !updates.value) return
  113. yield* db
  114. .update(CredentialTable)
  115. .set({ label: updates.label, value: updates.value })
  116. .where(eq(CredentialTable.id, id))
  117. .run()
  118. .pipe(Effect.orDie)
  119. }),
  120. remove: Effect.fn("Credential.remove")(function* (id) {
  121. yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie)
  122. }),
  123. })
  124. }),
  125. )
  126. export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))