credential.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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. /** Replaces any credential for an integration and returns the new record. */
  43. readonly create: (input: {
  44. readonly integrationID: IntegrationSchema.ID
  45. readonly value: Info
  46. readonly label?: string
  47. }) => Effect.Effect<Stored>
  48. /** Updates the label or secret value of a stored credential. */
  49. readonly update: (id: ID, updates: Partial<Pick<Stored, "label" | "value">>) => Effect.Effect<void>
  50. /** Removes a stored credential. */
  51. readonly remove: (id: ID) => Effect.Effect<void>
  52. }
  53. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
  54. export const layer = Layer.effect(
  55. Service,
  56. Effect.gen(function* () {
  57. const { db } = yield* Database.Service
  58. const decode = Schema.decodeUnknownSync(Info)
  59. const stored = (row: typeof CredentialTable.$inferSelect) => {
  60. if (!row.integration_id) return
  61. return new Stored({
  62. id: row.id,
  63. integrationID: row.integration_id,
  64. label: row.label,
  65. value: decode(row.value),
  66. })
  67. }
  68. return Service.of({
  69. all: Effect.fn("Credential.all")(function* () {
  70. return (yield* db
  71. .select()
  72. .from(CredentialTable)
  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. list: Effect.fn("Credential.list")(function* (integrationID) {
  81. return (yield* db
  82. .select()
  83. .from(CredentialTable)
  84. .where(eq(CredentialTable.integration_id, integrationID))
  85. .orderBy(asc(CredentialTable.time_created))
  86. .all()
  87. .pipe(Effect.orDie)).flatMap((row) => {
  88. const credential = stored(row)
  89. return credential ? [credential] : []
  90. })
  91. }),
  92. create: Effect.fn("Credential.create")(function* (input) {
  93. const credential = new Stored({
  94. id: ID.create(),
  95. integrationID: input.integrationID,
  96. label: input.label ?? "default",
  97. value: input.value,
  98. })
  99. yield* db
  100. .transaction((tx) =>
  101. Effect.gen(function* () {
  102. yield* tx
  103. .delete(CredentialTable)
  104. .where(eq(CredentialTable.integration_id, credential.integrationID))
  105. .run()
  106. yield* tx
  107. .insert(CredentialTable)
  108. .values({
  109. id: credential.id,
  110. integration_id: credential.integrationID,
  111. label: credential.label,
  112. value: credential.value,
  113. })
  114. .run()
  115. }),
  116. )
  117. .pipe(Effect.orDie)
  118. return credential
  119. }),
  120. update: Effect.fn("Credential.update")(function* (id, updates) {
  121. if (!updates.label && !updates.value) return
  122. yield* db
  123. .update(CredentialTable)
  124. .set({ label: updates.label, value: updates.value })
  125. .where(eq(CredentialTable.id, id))
  126. .run()
  127. .pipe(Effect.orDie)
  128. }),
  129. remove: Effect.fn("Credential.remove")(function* (id) {
  130. yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie)
  131. }),
  132. })
  133. }),
  134. )
  135. export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))