credential.ts 5.3 KB

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