"use node"; import { env } from "@code/env/convex"; import { decodeGitConnectionInput } from "@code/primitives/execution-runtime"; import { ConvexError, v } from "convex/values"; import { Effect } from "effect"; import { internal } from "./_generated/api"; import type { Id } from "./_generated/dataModel"; import { action } from "./_generated/server"; import { authComponent, createAuth } from "./auth"; const encryptionKey = async (): Promise => { if (!env.GIT_CREDENTIAL_ENCRYPTION_KEY) { throw new ConvexError("Git credential encryption is not configured"); } const bytes = Buffer.from(env.GIT_CREDENTIAL_ENCRYPTION_KEY, "base64url"); if (bytes.byteLength !== 32) { throw new ConvexError("Git credential encryption key must be 32 bytes"); } return await crypto.subtle.importKey("raw", bytes, "AES-GCM", false, [ "encrypt", "decrypt", ]); }; const encryptCredential = async (credential: string) => { const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { iv, name: "AES-GCM" }, await encryptionKey(), new TextEncoder().encode(credential) ); return { credentialCiphertext: Buffer.from(encrypted).toString("base64url"), credentialIv: Buffer.from(iv).toString("base64url"), }; }; export const decryptCredential = async ( credentialCiphertext: string, credentialIv: string ): Promise => { const decrypted = await crypto.subtle.decrypt( { iv: Buffer.from(credentialIv, "base64url"), name: "AES-GCM", }, await encryptionKey(), Buffer.from(credentialCiphertext, "base64url") ); return new TextDecoder().decode(decrypted); }; export const connectGitea = action({ args: { serverUrl: v.string(), token: v.string(), username: v.optional(v.string()), }, handler: async ( ctx, args ): Promise<{ connectionId: Id<"gitConnections"> }> => { const userId = await ctx.auth.getUserIdentity().then((identity) => { if (!identity) { throw new ConvexError("Authentication required"); } return identity.tokenIdentifier; }); const connection = await Effect.runPromise( decodeGitConnectionInput({ credential: args.token, credentialKind: "token", provider: "gitea", serverUrl: args.serverUrl, username: args.username, }) ); const encrypted = await encryptCredential(connection.credential); const connectionId = await ctx.runMutation( internal.gitConnectionData.persist, { ...encrypted, credentialKind: connection.credentialKind, provider: connection.provider, serverUrl: connection.serverUrl, userId, username: connection.username, } ); return { connectionId }; }, }); export const connectGithub = action({ args: {}, handler: async (ctx): Promise<{ connectionId: Id<"gitConnections"> }> => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new ConvexError("Authentication required"); } const { auth, headers } = await authComponent.getAuth(createAuth, ctx); const token = await auth.api.getAccessToken({ body: { providerId: "github" }, headers, }); if (!token.accessToken) { throw new ConvexError("GitHub account is not connected"); } const encrypted = await encryptCredential(token.accessToken); const connectionId = await ctx.runMutation( internal.gitConnectionData.persist, { ...encrypted, credentialKind: "oauth", provider: "github", serverUrl: "https://github.com", userId: identity.tokenIdentifier, } ); return { connectionId }; }, });