/ opencodehub / scripts / ssh-server.ts
swadhinbiswas # Changelog · 1/21/2026
0c9b918
129 lines · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

import { getDatabase, schema } from "@/db";
import { canReadRepo, canWriteRepo } from "@/lib/permissions";
import { startSSHServer } from "@/lib/ssh";
import { and, eq } from "drizzle-orm";
import { join } from "path";
import { uploadRepoToStorage, getStorageRepoPath, isCloudStorage } from "@/lib/git-storage";

const DATA_DIR = join(process.cwd(), "data");
const REPOS_DIR = join(DATA_DIR, "repos");
const SSH_DIR = join(DATA_DIR, "ssh");
const HOST_KEY_PATH = join(SSH_DIR, "ssh_host_rsa_key");

async function main() {
    console.log("Starting OpenCodeHub SSH Server...");

    await startSSHServer({
        port: 2222,
        hostKeyPath: HOST_KEY_PATH,
        reposPath: REPOS_DIR,
        authenticateUser: async (_, key) => {
            // We ignore the username (usually 'git') and authenticate by key
            const db = getDatabase();

            // Iterate over all keys to find a match
            // In a production app, we would index by fingerprint or key type to optimize
            const allKeys = await db.query.sshKeys.findMany();

            for (const dbKey of allKeys) {
                try {
                    // Parse DB key
                    const parts = dbKey.publicKey.trim().split(" ");
                    if (parts.length < 2) continue;

                    const dbKeyData = Buffer.from(parts[1], 'base64');

                    // Compare buffers
                    if (dbKeyData.equals(key)) {
                        return {
                            valid: true,
                            userId: dbKey.userId,
                            canRead: true, // Specific repo auth is handled in authorizeRepo
                            canWrite: true
                        };
                    }
                } catch (e) {
                    console.error("Error checking key:", e);
                }
            }

            return { valid: false };
        },
        authorizeRepo: async (userId, repoPath, operation) => {
            // repoPath is "owner/repo.git"
            const [ownerName, repoNameWithGit] = repoPath.split("/");
            const repoName = repoNameWithGit.replace(".git", "");

            if (!ownerName || !repoName) return false;

            const db = getDatabase();

            // Find Repo
            const owner = await db.query.users.findFirst({
                where: eq(schema.users.username, ownerName)
            });

            if (!owner) return false;

            const repo = await db.query.repositories.findFirst({
                where: and(
                    eq(schema.repositories.ownerId, owner.id),
                    eq(schema.repositories.name, repoName)
                )
            });

            if (!repo) return false;

            if (operation === "read") {
                return canReadRepo(userId, repo);
            } else {
                return canWriteRepo(userId, repo);
            }
        },
        onPush: async (userId, repoPath, refs) => {
            console.log(`Push received to ${repoPath} from ${userId}`);

            // Resolve Repo ID
            const [ownerName, repoNameWithGit] = repoPath.split("/");
            const repoName = repoNameWithGit.replace(".git", "");

            const db = getDatabase();
            const owner = await db.query.users.findFirst({ where: eq(schema.users.username, ownerName) });
            if (!owner) return;

            const repo = await db.query.repositories.findFirst({
                where: and(eq(schema.repositories.ownerId, owner.id), eq(schema.repositories.name, repoName))
            });

            if (repo) {
                // 1. Run Analysis
                import("@/lib/analysis").then(({ analyzeRepository }) => {
                    analyzeRepository(repo.id, userId).catch(console.error);
                });

                // 2. Trigger Workflows
                import("@/lib/workflows").then(({ triggerRepoWorkflows }) => {
                    refs.forEach(refLine => {
                        const [, newSha, refName] = refLine.split(" ");
                        // Only trigger on branch updates (not deletions or tags for now unless supported)
                        if (newSha !== "0000000000000000000000000000000000000000" && refName.startsWith("refs/heads/")) {
                            triggerRepoWorkflows(repo.id, newSha, refName, userId).catch(console.error);
                        }
                    });
                });
                // 3. Sync to Cloud Storage (if enabled)
                if (await isCloudStorage()) {
                    console.log(`[SSH] Syncing ${repoPath} to storage...`);
                    const storagePath = getStorageRepoPath(owner.username, repo.name);
                    const localRepoPath = join(REPOS_DIR, owner.username, `${repo.name}.git`);
                    await uploadRepoToStorage(localRepoPath, storagePath);
                    console.log(`[SSH] Sync complete.`);
                }
            }
        }
    });
}

main().catch(console.error);