125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
"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<CryptoKey> => {
|
|
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<string> => {
|
|
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 };
|
|
},
|
|
});
|