credential.ts 4.7 KB

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