@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.21

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.
@@ -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(resolve2(stateFile, ".."), { recursive: true });
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 createGitHubAuthStore(projectRoot) {
108
- const stateFile = resolveGitHubAuthStateFile(projectRoot);
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(projectRoot2) {
167
- const targetFile = resolveGitHubAuthStateFile(projectRoot2);
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: input,
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 pending = readStoredAuth(stateFile).pendingDevice ?? null;
188
- if (!pending || pending.pollId !== pollId)
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
- createGitHubAuthStore
248
+ createGitHubAuthStoreFromStateFile,
249
+ createGitHubAuthStore,
250
+ copyGitHubAuthStateToLocalProjectRoot
207
251
  };
@@ -116,6 +116,7 @@ var DEFAULT_PROJECT_STATUSES = {
116
116
  running: "In Progress",
117
117
  prOpen: "In Review",
118
118
  ciFixing: "In Review",
119
+ merging: "Merging",
119
120
  done: "Done",
120
121
  needsAttention: "Needs Attention"
121
122
  };
@@ -129,6 +130,8 @@ function lifecycleStatusForTaskStatus(status) {
129
130
  return "prOpen";
130
131
  if (normalized === "ci_fixing" || normalized === "fixing")
131
132
  return "ciFixing";
133
+ if (normalized === "merging" || normalized === "merge")
134
+ return "merging";
132
135
  if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
133
136
  return "needsAttention";
134
137
  if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
@@ -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
+ };