@h-rig/github-lib 0.0.6-alpha.158
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 +1 -0
- package/dist/src/auth-store.d.ts +42 -0
- package/dist/src/auth-store.js +227 -0
- package/dist/src/credentials.d.ts +20 -0
- package/dist/src/credentials.js +118 -0
- package/dist/src/github-api.d.ts +107 -0
- package/dist/src/github-api.js +452 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +713 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +148 -0
- package/package.json +31 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/github-lib/src/github-api.ts
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
|
|
5
|
+
// packages/github-lib/src/auth-store.ts
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
import { resolveRigStatePaths } from "@rig/core/server-paths";
|
|
10
|
+
function cleanString(value) {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
function cleanScopes(value) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [];
|
|
16
|
+
return value.flatMap((entry) => {
|
|
17
|
+
const clean = cleanString(entry);
|
|
18
|
+
return clean ? [clean] : [];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function parseApiSessions(value) {
|
|
22
|
+
if (!Array.isArray(value))
|
|
23
|
+
return [];
|
|
24
|
+
return value.flatMap((entry) => {
|
|
25
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
26
|
+
return [];
|
|
27
|
+
const record = entry;
|
|
28
|
+
const token = cleanString(record.token);
|
|
29
|
+
if (!token)
|
|
30
|
+
return [];
|
|
31
|
+
const createdAt = cleanString(record.createdAt);
|
|
32
|
+
return [{
|
|
33
|
+
token,
|
|
34
|
+
login: cleanString(record.login),
|
|
35
|
+
userId: cleanString(record.userId),
|
|
36
|
+
...createdAt ? { createdAt } : {}
|
|
37
|
+
}];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function parsePendingDevice(value) {
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
return null;
|
|
43
|
+
const record = value;
|
|
44
|
+
const pollId = cleanString(record.pollId);
|
|
45
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
46
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
47
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
48
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
49
|
+
return null;
|
|
50
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
51
|
+
}
|
|
52
|
+
function parsePendingDevices(value) {
|
|
53
|
+
if (!Array.isArray(value))
|
|
54
|
+
return [];
|
|
55
|
+
return value.flatMap((entry) => {
|
|
56
|
+
const pending = parsePendingDevice(entry);
|
|
57
|
+
return pending ? [pending] : [];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function readStoredAuth(stateFile) {
|
|
61
|
+
if (!existsSync(stateFile))
|
|
62
|
+
return {};
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
65
|
+
return {
|
|
66
|
+
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
67
|
+
login: cleanString(parsed.login),
|
|
68
|
+
userId: cleanString(parsed.userId),
|
|
69
|
+
scopes: cleanScopes(parsed.scopes),
|
|
70
|
+
selectedRepo: cleanString(parsed.selectedRepo),
|
|
71
|
+
...parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? { tokenSource: parsed.tokenSource } : {},
|
|
72
|
+
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
73
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
74
|
+
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
75
|
+
...cleanString(parsed.updatedAt) ? { updatedAt: cleanString(parsed.updatedAt) } : {}
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function newApiSessionToken() {
|
|
82
|
+
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
83
|
+
}
|
|
84
|
+
function writeStoredAuth(stateFile, payload) {
|
|
85
|
+
mkdirSync(dirname(stateFile), { recursive: true });
|
|
86
|
+
writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
87
|
+
`, { encoding: "utf8", mode: 384 });
|
|
88
|
+
try {
|
|
89
|
+
chmodSync(stateFile, 384);
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
93
|
+
return resolve(projectRoot, ".rig", "state", "github-auth.json");
|
|
94
|
+
}
|
|
95
|
+
function resolveGitHubAuthStateFile(projectRoot) {
|
|
96
|
+
return resolve(resolveRigStatePaths(projectRoot).stateDir, "github-auth.json");
|
|
97
|
+
}
|
|
98
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
99
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
100
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
101
|
+
if (existsSync(stateFile)) {
|
|
102
|
+
copyFileSync(stateFile, targetFile);
|
|
103
|
+
try {
|
|
104
|
+
chmodSync(targetFile, 384);
|
|
105
|
+
} catch {}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
writeStoredAuth(targetFile, {});
|
|
109
|
+
}
|
|
110
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
111
|
+
return {
|
|
112
|
+
stateFile,
|
|
113
|
+
status(options) {
|
|
114
|
+
const stored = readStoredAuth(stateFile);
|
|
115
|
+
const token = cleanString(stored.token);
|
|
116
|
+
return {
|
|
117
|
+
signedIn: Boolean(token),
|
|
118
|
+
login: cleanString(stored.login),
|
|
119
|
+
userId: cleanString(stored.userId),
|
|
120
|
+
scopes: cleanScopes(stored.scopes),
|
|
121
|
+
selectedRepo: cleanString(stored.selectedRepo),
|
|
122
|
+
oauthConfigured: options?.oauthConfigured === true,
|
|
123
|
+
tokenSource: token ? stored.tokenSource ?? "manual-token" : null
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
readToken() {
|
|
127
|
+
return cleanString(readStoredAuth(stateFile).token);
|
|
128
|
+
},
|
|
129
|
+
saveToken(input) {
|
|
130
|
+
const previous = readStoredAuth(stateFile);
|
|
131
|
+
writeStoredAuth(stateFile, {
|
|
132
|
+
...previous,
|
|
133
|
+
token: input.token,
|
|
134
|
+
tokenSource: input.tokenSource,
|
|
135
|
+
login: input.login ?? null,
|
|
136
|
+
userId: input.userId ?? null,
|
|
137
|
+
scopes: input.scopes ?? [],
|
|
138
|
+
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
139
|
+
pendingDevice: null,
|
|
140
|
+
pendingDevices: [],
|
|
141
|
+
apiSessions: previous.apiSessions ?? [],
|
|
142
|
+
updatedAt: new Date().toISOString()
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
createApiSession() {
|
|
146
|
+
const previous = readStoredAuth(stateFile);
|
|
147
|
+
const token = newApiSessionToken();
|
|
148
|
+
const session = {
|
|
149
|
+
token,
|
|
150
|
+
login: cleanString(previous.login),
|
|
151
|
+
userId: cleanString(previous.userId),
|
|
152
|
+
createdAt: new Date().toISOString()
|
|
153
|
+
};
|
|
154
|
+
writeStoredAuth(stateFile, {
|
|
155
|
+
...previous,
|
|
156
|
+
apiSessions: [...(previous.apiSessions ?? []).slice(-9), session],
|
|
157
|
+
updatedAt: new Date().toISOString()
|
|
158
|
+
});
|
|
159
|
+
return { token, login: session.login ?? null, userId: session.userId ?? null };
|
|
160
|
+
},
|
|
161
|
+
readApiSession(token) {
|
|
162
|
+
const clean = cleanString(token);
|
|
163
|
+
if (!clean)
|
|
164
|
+
return null;
|
|
165
|
+
const previous = readStoredAuth(stateFile);
|
|
166
|
+
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
167
|
+
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
168
|
+
},
|
|
169
|
+
copyToProjectRoot(projectRoot) {
|
|
170
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
171
|
+
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
172
|
+
},
|
|
173
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
174
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
175
|
+
},
|
|
176
|
+
savePendingDevice(input) {
|
|
177
|
+
const previous = readStoredAuth(stateFile);
|
|
178
|
+
const pendingDevices = [
|
|
179
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
180
|
+
...previous.pendingDevices ?? [],
|
|
181
|
+
input
|
|
182
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
183
|
+
writeStoredAuth(stateFile, {
|
|
184
|
+
...previous,
|
|
185
|
+
pendingDevice: null,
|
|
186
|
+
pendingDevices,
|
|
187
|
+
updatedAt: new Date().toISOString()
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
saveSelectedRepo(selectedRepo) {
|
|
191
|
+
const previous = readStoredAuth(stateFile);
|
|
192
|
+
writeStoredAuth(stateFile, {
|
|
193
|
+
...previous,
|
|
194
|
+
selectedRepo: selectedRepo ?? null,
|
|
195
|
+
updatedAt: new Date().toISOString()
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
readPendingDevice(pollId) {
|
|
199
|
+
const previous = readStoredAuth(stateFile);
|
|
200
|
+
const pending = [
|
|
201
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
202
|
+
...previous.pendingDevices ?? []
|
|
203
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
204
|
+
if (!pending)
|
|
205
|
+
return null;
|
|
206
|
+
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
207
|
+
return null;
|
|
208
|
+
return pending;
|
|
209
|
+
},
|
|
210
|
+
clearPendingDevice(pollId) {
|
|
211
|
+
const previous = readStoredAuth(stateFile);
|
|
212
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
213
|
+
writeStoredAuth(stateFile, {
|
|
214
|
+
...previous,
|
|
215
|
+
pendingDevice: null,
|
|
216
|
+
pendingDevices: remaining,
|
|
217
|
+
updatedAt: new Date().toISOString()
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function createGitHubAuthStore(projectRoot) {
|
|
223
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// packages/github-lib/src/github-api.ts
|
|
227
|
+
function normalizeString(value) {
|
|
228
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
229
|
+
}
|
|
230
|
+
function normalizeScopes(value) {
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
return value.flatMap((entry) => {
|
|
233
|
+
const normalized = normalizeString(entry);
|
|
234
|
+
return normalized ? [normalized] : [];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === "string") {
|
|
238
|
+
return value.split(/[ ,]+/).map((entry) => entry.trim()).filter(Boolean);
|
|
239
|
+
}
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
async function defaultPostGitHubForm(endpoint, body) {
|
|
243
|
+
const response = await fetch(endpoint, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: {
|
|
246
|
+
accept: "application/json",
|
|
247
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
248
|
+
"user-agent": "rig"
|
|
249
|
+
},
|
|
250
|
+
body: new URLSearchParams(body)
|
|
251
|
+
});
|
|
252
|
+
const payload = await response.json().catch(() => ({}));
|
|
253
|
+
return { status: response.status, payload };
|
|
254
|
+
}
|
|
255
|
+
async function fetchGitHubUserInfo(token) {
|
|
256
|
+
const response = await fetch("https://api.github.com/user", {
|
|
257
|
+
headers: {
|
|
258
|
+
accept: "application/vnd.github+json",
|
|
259
|
+
authorization: `Bearer ${token}`,
|
|
260
|
+
"user-agent": "rig"
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const payload = await response.json().catch(() => ({}));
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(typeof payload.message === "string" ? payload.message : `GitHub user lookup failed (${response.status}).`);
|
|
266
|
+
}
|
|
267
|
+
const login = normalizeString(payload.login);
|
|
268
|
+
const id = typeof payload.id === "number" ? String(payload.id) : normalizeString(payload.id);
|
|
269
|
+
if (!login || !id) {
|
|
270
|
+
throw new Error("GitHub user lookup did not return login/id.");
|
|
271
|
+
}
|
|
272
|
+
return { login, id, scopes: normalizeScopes(response.headers.get("x-oauth-scopes")) };
|
|
273
|
+
}
|
|
274
|
+
function resolveGitHubAuthStatus(input) {
|
|
275
|
+
return createGitHubAuthStore(input.projectRoot).status(input.oauthConfigured !== undefined ? { oauthConfigured: input.oauthConfigured } : {});
|
|
276
|
+
}
|
|
277
|
+
async function saveGitHubTokenForProject(input) {
|
|
278
|
+
const token = normalizeString(input.token);
|
|
279
|
+
if (!token) {
|
|
280
|
+
throw new Error("token is required");
|
|
281
|
+
}
|
|
282
|
+
const user = await (input.fetchUser ?? fetchGitHubUserInfo)(token);
|
|
283
|
+
const store = createGitHubAuthStore(input.projectRoot);
|
|
284
|
+
store.saveToken({
|
|
285
|
+
token,
|
|
286
|
+
tokenSource: input.tokenSource ?? "manual-token",
|
|
287
|
+
login: user.login,
|
|
288
|
+
userId: user.id,
|
|
289
|
+
scopes: user.scopes ?? [],
|
|
290
|
+
selectedRepo: input.selectedRepo ?? null
|
|
291
|
+
});
|
|
292
|
+
return { ok: true, ...store.status({ oauthConfigured: true }) };
|
|
293
|
+
}
|
|
294
|
+
async function beginGitHubDeviceFlow(input) {
|
|
295
|
+
const clientId = normalizeString(input.clientId);
|
|
296
|
+
if (!clientId) {
|
|
297
|
+
throw new Error("clientId is required");
|
|
298
|
+
}
|
|
299
|
+
const postForm = input.postForm ?? defaultPostGitHubForm;
|
|
300
|
+
const result = await postForm("https://github.com/login/device/code", {
|
|
301
|
+
client_id: clientId,
|
|
302
|
+
scope: normalizeString(input.scope) ?? "repo read:project user:email"
|
|
303
|
+
});
|
|
304
|
+
const deviceCode = normalizeString(result.payload.device_code);
|
|
305
|
+
if (result.status < 200 || result.status >= 300 || !deviceCode) {
|
|
306
|
+
throw new Error(normalizeString(result.payload.error_description) ?? "GitHub device flow start failed.");
|
|
307
|
+
}
|
|
308
|
+
const expiresIn = typeof result.payload.expires_in === "number" ? result.payload.expires_in : 900;
|
|
309
|
+
const intervalSeconds = typeof result.payload.interval === "number" ? result.payload.interval : 5;
|
|
310
|
+
const pollId = randomUUID();
|
|
311
|
+
createGitHubAuthStore(input.projectRoot).savePendingDevice({
|
|
312
|
+
pollId,
|
|
313
|
+
deviceCode,
|
|
314
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
315
|
+
intervalSeconds
|
|
316
|
+
});
|
|
317
|
+
if (input.selectedRepo) {
|
|
318
|
+
createGitHubAuthStore(input.projectRoot).saveSelectedRepo(input.selectedRepo);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
pollId,
|
|
323
|
+
userCode: normalizeString(result.payload.user_code),
|
|
324
|
+
verificationUri: normalizeString(result.payload.verification_uri),
|
|
325
|
+
expiresIn,
|
|
326
|
+
intervalSeconds
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function pollGitHubDeviceFlow(input) {
|
|
330
|
+
const clientId = normalizeString(input.clientId);
|
|
331
|
+
if (!clientId) {
|
|
332
|
+
throw new Error("clientId is required");
|
|
333
|
+
}
|
|
334
|
+
const store = createGitHubAuthStore(input.projectRoot);
|
|
335
|
+
const pending = store.readPendingDevice(input.pollId);
|
|
336
|
+
if (!pending) {
|
|
337
|
+
return { ok: false, status: "expired", error: "GitHub device flow expired or unknown." };
|
|
338
|
+
}
|
|
339
|
+
const result = await (input.postForm ?? defaultPostGitHubForm)("https://github.com/login/oauth/access_token", {
|
|
340
|
+
client_id: clientId,
|
|
341
|
+
device_code: pending.deviceCode,
|
|
342
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
343
|
+
});
|
|
344
|
+
const error = normalizeString(result.payload.error);
|
|
345
|
+
if (error === "authorization_pending" || error === "slow_down") {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
status: error === "slow_down" ? "slow-down" : "pending",
|
|
349
|
+
intervalSeconds: error === "slow_down" ? pending.intervalSeconds + 5 : pending.intervalSeconds
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (error || typeof result.payload.access_token !== "string") {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
status: "error",
|
|
356
|
+
error: normalizeString(result.payload.error_description) ?? "GitHub device authorization failed."
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const token = result.payload.access_token;
|
|
360
|
+
const user = await (input.fetchUser ?? fetchGitHubUserInfo)(token);
|
|
361
|
+
store.saveToken({
|
|
362
|
+
token,
|
|
363
|
+
tokenSource: "oauth-device",
|
|
364
|
+
login: user.login,
|
|
365
|
+
userId: user.id,
|
|
366
|
+
scopes: user.scopes ?? normalizeScopes(result.payload.scope),
|
|
367
|
+
selectedRepo: input.selectedRepo ?? null
|
|
368
|
+
});
|
|
369
|
+
store.clearPendingDevice(input.pollId);
|
|
370
|
+
return { ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) };
|
|
371
|
+
}
|
|
372
|
+
function checkGitHubRepoPermissions(input) {
|
|
373
|
+
const store = createGitHubAuthStore(input.projectRoot);
|
|
374
|
+
const auth = store.status(input.oauthConfigured !== undefined ? { oauthConfigured: input.oauthConfigured } : {});
|
|
375
|
+
if (!auth.signedIn) {
|
|
376
|
+
return { ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" };
|
|
377
|
+
}
|
|
378
|
+
const normalizedScopes = auth.scopes.map((scope) => scope.toLowerCase());
|
|
379
|
+
const broadEnough = normalizedScopes.includes("repo") || normalizedScopes.includes("public_repo");
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
signedIn: true,
|
|
383
|
+
login: auth.login,
|
|
384
|
+
scopes: auth.scopes,
|
|
385
|
+
canOpenPullRequest: broadEnough,
|
|
386
|
+
pullRequests: broadEnough,
|
|
387
|
+
push: broadEnough,
|
|
388
|
+
reason: broadEnough ? "stored-token" : "token-scope-unverified"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function probeGitHubRepository(input) {
|
|
392
|
+
const headers = {
|
|
393
|
+
accept: "application/vnd.github+json",
|
|
394
|
+
"user-agent": "rig"
|
|
395
|
+
};
|
|
396
|
+
if (input.token) {
|
|
397
|
+
headers.authorization = `Bearer ${input.token}`;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const response = await (input.fetchRepository ?? fetch)(`https://api.github.com/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}`, { headers });
|
|
401
|
+
const payload = await response.json().catch(() => ({}));
|
|
402
|
+
if (response.ok) {
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
owner: input.owner,
|
|
406
|
+
repo: input.repo,
|
|
407
|
+
status: response.status,
|
|
408
|
+
authenticated: Boolean(input.token),
|
|
409
|
+
authenticationRequired: payload.private === true && !input.token,
|
|
410
|
+
fullName: typeof payload.full_name === "string" ? payload.full_name : `${input.owner}/${input.repo}`,
|
|
411
|
+
private: typeof payload.private === "boolean" ? payload.private : null,
|
|
412
|
+
message: input.token ? "Repository access verified with signed-in GitHub credentials." : "Public repository access verified without credentials.",
|
|
413
|
+
scopes: input.scopes
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const authenticationRequired = !input.token && (response.status === 401 || response.status === 403 || response.status === 404);
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
owner: input.owner,
|
|
420
|
+
repo: input.repo,
|
|
421
|
+
status: response.status,
|
|
422
|
+
authenticated: Boolean(input.token),
|
|
423
|
+
authenticationRequired,
|
|
424
|
+
fullName: null,
|
|
425
|
+
private: null,
|
|
426
|
+
message: authenticationRequired ? "Repository is private, missing, or inaccessible without GitHub sign-in. Sign in before saving this config." : typeof payload.message === "string" ? payload.message : `GitHub repository probe failed (${response.status}).`,
|
|
427
|
+
scopes: input.scopes
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
owner: input.owner,
|
|
433
|
+
repo: input.repo,
|
|
434
|
+
status: 0,
|
|
435
|
+
authenticated: Boolean(input.token),
|
|
436
|
+
authenticationRequired: !input.token,
|
|
437
|
+
fullName: null,
|
|
438
|
+
private: null,
|
|
439
|
+
message: error instanceof Error ? error.message : "GitHub repository probe failed.",
|
|
440
|
+
scopes: input.scopes
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
export {
|
|
445
|
+
saveGitHubTokenForProject,
|
|
446
|
+
resolveGitHubAuthStatus,
|
|
447
|
+
probeGitHubRepository,
|
|
448
|
+
pollGitHubDeviceFlow,
|
|
449
|
+
fetchGitHubUserInfo,
|
|
450
|
+
checkGitHubRepoPermissions,
|
|
451
|
+
beginGitHubDeviceFlow
|
|
452
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rig/github-lib — the GitHub SCM IO library (auth-store, credential providers,
|
|
3
|
+
* GitHub REST/GraphQL API: device flow, repo probe, auth status, Projects v2).
|
|
4
|
+
*
|
|
5
|
+
* This is a NON-PLUGIN domain library extracted out of `@rig/github-provider-plugin`
|
|
6
|
+
* so that above-runtime GitHub surfaces (cli-surface auth UI, init setup, doctor
|
|
7
|
+
* diagnostics, task-sources credential providers) consume the GitHub API WITHOUT
|
|
8
|
+
* importing a sibling plugin's impl (SEAM-ONLY §6.4). The plugin re-exports this
|
|
9
|
+
* library and wraps the genuinely-swappable credential resolution as the
|
|
10
|
+
* GITHUB_PROVIDER capability; issue-analysis/triage stay plugin-internal behind
|
|
11
|
+
* the ISSUE_TRIAGE capability. Deps are floor only (@rig/contracts, @rig/core).
|
|
12
|
+
*/
|
|
13
|
+
export * from "./auth-store";
|
|
14
|
+
export * from "./credentials";
|
|
15
|
+
export * from "./projects";
|
|
16
|
+
export * from "./github-api";
|