@h-rig/server 0.0.6-alpha.10 → 0.0.6-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/src/index.js +620 -241
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +68 -24
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +538 -159
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-mutations.js +68 -26
- package/dist/src/server.js +620 -241
- package/package.json +4 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
7
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
8
|
+
function cleanString(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
12
|
+
const explicitRoot = cleanString(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
13
|
+
if (explicitRoot)
|
|
14
|
+
return resolve(explicitRoot);
|
|
15
|
+
const stateDir = cleanString(process.env.RIG_STATE_DIR);
|
|
16
|
+
if (stateDir)
|
|
17
|
+
return resolve(dirname(resolve(stateDir)), "users");
|
|
18
|
+
return resolve(projectRoot, ".rig", "users");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/server/src/server-helpers/github-api-session-index.ts
|
|
22
|
+
function cleanString2(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
function resolveGitHubApiSessionIndexFile(projectRoot) {
|
|
26
|
+
return resolve2(resolveRemoteUserNamespacesRoot(projectRoot), ".api-sessions.json");
|
|
27
|
+
}
|
|
28
|
+
function parseEntry(value) {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return null;
|
|
31
|
+
const record = value;
|
|
32
|
+
const token = cleanString2(record.token);
|
|
33
|
+
const namespaceKey = cleanString2(record.namespaceKey);
|
|
34
|
+
const namespaceRoot = cleanString2(record.namespaceRoot);
|
|
35
|
+
const authStateFile = cleanString2(record.authStateFile);
|
|
36
|
+
const checkoutBaseDir = cleanString2(record.checkoutBaseDir);
|
|
37
|
+
const snapshotBaseDir = cleanString2(record.snapshotBaseDir);
|
|
38
|
+
const createdAt = cleanString2(record.createdAt);
|
|
39
|
+
if (!token || !namespaceKey || !namespaceRoot || !authStateFile || !checkoutBaseDir || !snapshotBaseDir || !createdAt)
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
token,
|
|
43
|
+
namespaceKey,
|
|
44
|
+
namespaceRoot,
|
|
45
|
+
authStateFile,
|
|
46
|
+
checkoutBaseDir,
|
|
47
|
+
snapshotBaseDir,
|
|
48
|
+
createdAt,
|
|
49
|
+
login: cleanString2(record.login),
|
|
50
|
+
userId: cleanString2(record.userId),
|
|
51
|
+
selectedRepo: cleanString2(record.selectedRepo)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readIndex(indexFile) {
|
|
55
|
+
if (!existsSync(indexFile))
|
|
56
|
+
return [];
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(readFileSync(indexFile, "utf8"));
|
|
59
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.flatMap((entry) => {
|
|
60
|
+
const parsedEntry = parseEntry(entry);
|
|
61
|
+
return parsedEntry ? [parsedEntry] : [];
|
|
62
|
+
}) : [];
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeIndex(indexFile, sessions) {
|
|
68
|
+
mkdirSync(dirname2(indexFile), { recursive: true });
|
|
69
|
+
writeFileSync(indexFile, `${JSON.stringify({ sessions }, null, 2)}
|
|
70
|
+
`, { encoding: "utf8", mode: 384 });
|
|
71
|
+
try {
|
|
72
|
+
chmodSync(indexFile, 384);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
function registerGitHubApiSession(input) {
|
|
76
|
+
const cleanToken = cleanString2(input.token);
|
|
77
|
+
if (!cleanToken)
|
|
78
|
+
throw new Error("GitHub API session token is required");
|
|
79
|
+
const indexFile = resolveGitHubApiSessionIndexFile(input.projectRoot);
|
|
80
|
+
const createdAt = new Date().toISOString();
|
|
81
|
+
const entry = {
|
|
82
|
+
token: cleanToken,
|
|
83
|
+
login: input.namespace.login,
|
|
84
|
+
userId: input.namespace.userId,
|
|
85
|
+
namespaceKey: input.namespace.key,
|
|
86
|
+
namespaceRoot: input.namespace.root,
|
|
87
|
+
authStateFile: input.namespace.authStateFile,
|
|
88
|
+
checkoutBaseDir: input.namespace.checkoutBaseDir,
|
|
89
|
+
snapshotBaseDir: input.namespace.snapshotBaseDir,
|
|
90
|
+
selectedRepo: cleanString2(input.selectedRepo),
|
|
91
|
+
createdAt
|
|
92
|
+
};
|
|
93
|
+
const previous = readIndex(indexFile).filter((session) => session.token !== cleanToken);
|
|
94
|
+
writeIndex(indexFile, [...previous.slice(-199), entry]);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
function readGitHubApiSession(input) {
|
|
98
|
+
const cleanToken = cleanString2(input.token);
|
|
99
|
+
if (!cleanToken)
|
|
100
|
+
return null;
|
|
101
|
+
return readIndex(resolveGitHubApiSessionIndexFile(input.projectRoot)).find((entry) => entry.token === cleanToken) ?? null;
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
resolveGitHubApiSessionIndexFile,
|
|
105
|
+
registerGitHubApiSession,
|
|
106
|
+
readGitHubApiSession
|
|
107
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
-
import { resolve as resolve2 } from "path";
|
|
4
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
6
6
|
|
|
7
7
|
// packages/server/src/server-helpers/server-paths.ts
|
|
8
8
|
import { dirname, resolve } from "path";
|
|
@@ -58,6 +58,26 @@ function parseApiSessions(value) {
|
|
|
58
58
|
}];
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
function parsePendingDevice(value) {
|
|
62
|
+
if (!value || typeof value !== "object")
|
|
63
|
+
return null;
|
|
64
|
+
const record = value;
|
|
65
|
+
const pollId = cleanString(record.pollId);
|
|
66
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
67
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
68
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
69
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
70
|
+
return null;
|
|
71
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
72
|
+
}
|
|
73
|
+
function parsePendingDevices(value) {
|
|
74
|
+
if (!Array.isArray(value))
|
|
75
|
+
return [];
|
|
76
|
+
return value.flatMap((entry) => {
|
|
77
|
+
const pending = parsePendingDevice(entry);
|
|
78
|
+
return pending ? [pending] : [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
61
81
|
function readStoredAuth(stateFile) {
|
|
62
82
|
if (!existsSync(stateFile))
|
|
63
83
|
return {};
|
|
@@ -71,6 +91,7 @@ function readStoredAuth(stateFile) {
|
|
|
71
91
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
72
92
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
73
93
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
94
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
74
95
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
75
96
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
76
97
|
};
|
|
@@ -78,34 +99,36 @@ function readStoredAuth(stateFile) {
|
|
|
78
99
|
return {};
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
|
-
function parsePendingDevice(value) {
|
|
82
|
-
if (!value || typeof value !== "object")
|
|
83
|
-
return null;
|
|
84
|
-
const record = value;
|
|
85
|
-
const pollId = cleanString(record.pollId);
|
|
86
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
87
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
88
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
89
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
90
|
-
return null;
|
|
91
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
92
|
-
}
|
|
93
102
|
function newApiSessionToken() {
|
|
94
103
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
95
104
|
}
|
|
96
105
|
function writeStoredAuth(stateFile, payload) {
|
|
97
|
-
mkdirSync(
|
|
106
|
+
mkdirSync(dirname2(stateFile), { recursive: true });
|
|
98
107
|
writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
99
108
|
`, { encoding: "utf8", mode: 384 });
|
|
100
109
|
try {
|
|
101
110
|
chmodSync(stateFile, 384);
|
|
102
111
|
} catch {}
|
|
103
112
|
}
|
|
113
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
114
|
+
return resolve2(projectRoot, ".rig", "state", "github-auth.json");
|
|
115
|
+
}
|
|
104
116
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
105
117
|
return resolve2(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
106
118
|
}
|
|
107
|
-
function
|
|
108
|
-
const
|
|
119
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
120
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
121
|
+
mkdirSync(dirname2(targetFile), { recursive: true });
|
|
122
|
+
if (existsSync(stateFile)) {
|
|
123
|
+
copyFileSync(stateFile, targetFile);
|
|
124
|
+
try {
|
|
125
|
+
chmodSync(targetFile, 384);
|
|
126
|
+
} catch {}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
writeStoredAuth(targetFile, {});
|
|
130
|
+
}
|
|
131
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
109
132
|
return {
|
|
110
133
|
stateFile,
|
|
111
134
|
status(options) {
|
|
@@ -135,6 +158,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
135
158
|
scopes: input.scopes ?? [],
|
|
136
159
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
137
160
|
pendingDevice: null,
|
|
161
|
+
pendingDevices: [],
|
|
138
162
|
apiSessions: previous.apiSessions ?? [],
|
|
139
163
|
updatedAt: new Date().toISOString()
|
|
140
164
|
});
|
|
@@ -163,15 +187,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
163
187
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
164
188
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
165
189
|
},
|
|
166
|
-
copyToProjectRoot(
|
|
167
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
190
|
+
copyToProjectRoot(projectRoot) {
|
|
191
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
168
192
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
169
193
|
},
|
|
194
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
195
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
196
|
+
},
|
|
170
197
|
savePendingDevice(input) {
|
|
171
198
|
const previous = readStoredAuth(stateFile);
|
|
199
|
+
const pendingDevices = [
|
|
200
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
201
|
+
...previous.pendingDevices ?? [],
|
|
202
|
+
input
|
|
203
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
172
204
|
writeStoredAuth(stateFile, {
|
|
173
205
|
...previous,
|
|
174
|
-
pendingDevice:
|
|
206
|
+
pendingDevice: null,
|
|
207
|
+
pendingDevices,
|
|
175
208
|
updatedAt: new Date().toISOString()
|
|
176
209
|
});
|
|
177
210
|
},
|
|
@@ -184,24 +217,35 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
184
217
|
});
|
|
185
218
|
},
|
|
186
219
|
readPendingDevice(pollId) {
|
|
187
|
-
const
|
|
188
|
-
|
|
220
|
+
const previous = readStoredAuth(stateFile);
|
|
221
|
+
const pending = [
|
|
222
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
223
|
+
...previous.pendingDevices ?? []
|
|
224
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
225
|
+
if (!pending)
|
|
189
226
|
return null;
|
|
190
227
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
191
228
|
return null;
|
|
192
229
|
return pending;
|
|
193
230
|
},
|
|
194
|
-
clearPendingDevice() {
|
|
231
|
+
clearPendingDevice(pollId) {
|
|
195
232
|
const previous = readStoredAuth(stateFile);
|
|
233
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
196
234
|
writeStoredAuth(stateFile, {
|
|
197
235
|
...previous,
|
|
198
236
|
pendingDevice: null,
|
|
237
|
+
pendingDevices: remaining,
|
|
199
238
|
updatedAt: new Date().toISOString()
|
|
200
239
|
});
|
|
201
240
|
}
|
|
202
241
|
};
|
|
203
242
|
}
|
|
243
|
+
function createGitHubAuthStore(projectRoot) {
|
|
244
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
245
|
+
}
|
|
204
246
|
export {
|
|
205
247
|
resolveGitHubAuthStateFile,
|
|
206
|
-
|
|
248
|
+
createGitHubAuthStoreFromStateFile,
|
|
249
|
+
createGitHubAuthStore,
|
|
250
|
+
copyGitHubAuthStateToLocalProjectRoot
|
|
207
251
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/github-user-namespace.ts
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
5
|
+
function cleanString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
7
|
+
}
|
|
8
|
+
function sanitizePathSegment(value) {
|
|
9
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
|
|
10
|
+
}
|
|
11
|
+
function deriveGitHubUserNamespaceKey(identity) {
|
|
12
|
+
const userId = cleanString(identity.userId);
|
|
13
|
+
if (userId) {
|
|
14
|
+
const safeId = sanitizePathSegment(userId);
|
|
15
|
+
if (safeId)
|
|
16
|
+
return `ghu-${safeId}`;
|
|
17
|
+
}
|
|
18
|
+
const login = cleanString(identity.login);
|
|
19
|
+
if (login) {
|
|
20
|
+
const safeLogin = sanitizePathSegment(login);
|
|
21
|
+
if (safeLogin)
|
|
22
|
+
return `ghu-login-${safeLogin}`;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("GitHub user namespace requires a user id or login");
|
|
25
|
+
}
|
|
26
|
+
function resolveRemoteUserNamespacesRoot(projectRoot) {
|
|
27
|
+
const explicitRoot = cleanString(process.env.RIG_REMOTE_USER_NAMESPACE_ROOT);
|
|
28
|
+
if (explicitRoot)
|
|
29
|
+
return resolve(explicitRoot);
|
|
30
|
+
const stateDir = cleanString(process.env.RIG_STATE_DIR);
|
|
31
|
+
if (stateDir)
|
|
32
|
+
return resolve(dirname(resolve(stateDir)), "users");
|
|
33
|
+
return resolve(projectRoot, ".rig", "users");
|
|
34
|
+
}
|
|
35
|
+
function resolveRemoteUserNamespace(projectRoot, identity) {
|
|
36
|
+
const key = deriveGitHubUserNamespaceKey(identity);
|
|
37
|
+
const root = resolve(resolveRemoteUserNamespacesRoot(projectRoot), key);
|
|
38
|
+
const stateDir = resolve(root, ".rig", "state");
|
|
39
|
+
return {
|
|
40
|
+
key,
|
|
41
|
+
userId: cleanString(identity.userId),
|
|
42
|
+
login: cleanString(identity.login),
|
|
43
|
+
root,
|
|
44
|
+
stateDir,
|
|
45
|
+
authStateFile: resolve(stateDir, "github-auth.json"),
|
|
46
|
+
metadataFile: resolve(stateDir, "user-namespace.json"),
|
|
47
|
+
checkoutBaseDir: resolve(root, "remote-checkouts"),
|
|
48
|
+
snapshotBaseDir: resolve(root, "remote-snapshots")
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function serializeRemoteUserNamespace(namespace) {
|
|
52
|
+
return {
|
|
53
|
+
key: namespace.key,
|
|
54
|
+
userId: namespace.userId,
|
|
55
|
+
login: namespace.login,
|
|
56
|
+
root: namespace.root,
|
|
57
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
58
|
+
snapshotBaseDir: namespace.snapshotBaseDir
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function isPathInsideNamespace(namespaceRoot, candidatePath) {
|
|
62
|
+
const root = resolve(namespaceRoot);
|
|
63
|
+
const candidate = resolve(candidatePath);
|
|
64
|
+
const rel = relative(root, candidate);
|
|
65
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
66
|
+
}
|
|
67
|
+
function writeRemoteUserNamespaceMetadata(namespace) {
|
|
68
|
+
mkdirSync(namespace.stateDir, { recursive: true });
|
|
69
|
+
const previous = (() => {
|
|
70
|
+
if (!existsSync(namespace.metadataFile))
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(readFileSync(namespace.metadataFile, "utf8"));
|
|
74
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
writeFileSync(namespace.metadataFile, `${JSON.stringify({
|
|
81
|
+
key: namespace.key,
|
|
82
|
+
userId: namespace.userId,
|
|
83
|
+
login: namespace.login,
|
|
84
|
+
root: namespace.root,
|
|
85
|
+
checkoutBaseDir: namespace.checkoutBaseDir,
|
|
86
|
+
snapshotBaseDir: namespace.snapshotBaseDir,
|
|
87
|
+
createdAt: typeof previous?.createdAt === "string" ? previous.createdAt : now,
|
|
88
|
+
updatedAt: now
|
|
89
|
+
}, null, 2)}
|
|
90
|
+
`, { encoding: "utf8", mode: 384 });
|
|
91
|
+
try {
|
|
92
|
+
chmodSync(namespace.metadataFile, 384);
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
writeRemoteUserNamespaceMetadata,
|
|
97
|
+
serializeRemoteUserNamespace,
|
|
98
|
+
resolveRemoteUserNamespacesRoot,
|
|
99
|
+
resolveRemoteUserNamespace,
|
|
100
|
+
isPathInsideNamespace,
|
|
101
|
+
deriveGitHubUserNamespaceKey
|
|
102
|
+
};
|