@bivy/bivy 0.18.5-staging.2 → 0.18.5-staging.4
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/dist/github-app-repos.js +44 -0
- package/dist/server.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { createAppJwt } from "./github-app-auth.js";
|
|
4
|
+
/** App credentials cannot call /user/repos. Enumerate each installation instead. */
|
|
5
|
+
export async function listGithubAppRepos(apps, fetchImpl = fetch) {
|
|
6
|
+
const repos = new Map();
|
|
7
|
+
async function pages(path, token, field) {
|
|
8
|
+
const rows = [];
|
|
9
|
+
for (let page = 1;; page++) {
|
|
10
|
+
const res = await fetchImpl(`https://api.github.com${path}?per_page=100&page=${page}`, {
|
|
11
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "user-agent": "bivy" },
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
throw new Error(`GitHub responded ${res.status} while listing repositories`);
|
|
15
|
+
const body = await res.json();
|
|
16
|
+
const batch = field ? body[field] : body;
|
|
17
|
+
if (!Array.isArray(batch))
|
|
18
|
+
throw new Error("GitHub returned an invalid repository listing");
|
|
19
|
+
rows.push(...batch);
|
|
20
|
+
if (batch.length < 100)
|
|
21
|
+
return rows;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const app of apps) {
|
|
25
|
+
const jwt = createAppJwt(app.cfg.appId, app.cfg.privateKeyPem, Math.floor(Date.now() / 1000));
|
|
26
|
+
for (const installation of await pages("/app/installations", jwt)) {
|
|
27
|
+
if (installation.suspended_at)
|
|
28
|
+
continue;
|
|
29
|
+
const token = await app.cache.get(String(installation.id));
|
|
30
|
+
for (const repo of await pages("/installation/repositories", token, "repositories")) {
|
|
31
|
+
if (typeof repo.full_name !== "string" || !repo.full_name)
|
|
32
|
+
continue;
|
|
33
|
+
repos.set(repo.full_name.toLowerCase(), {
|
|
34
|
+
slug: repo.full_name,
|
|
35
|
+
description: typeof repo.description === "string" ? repo.description : "",
|
|
36
|
+
private: Boolean(repo.private),
|
|
37
|
+
pushedAt: typeof repo.pushed_at === "string" ? repo.pushed_at : undefined,
|
|
38
|
+
defaultBranch: typeof repo.default_branch === "string" ? repo.default_branch : undefined,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...repos.values()].sort((a, b) => (b.pushedAt ?? "").localeCompare(a.pushedAt ?? "") || a.slug.localeCompare(b.slug));
|
|
44
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -142,6 +142,7 @@ import { computeSessionContract } from "./session/session-contract.js";
|
|
|
142
142
|
import { SecretVault, resolveSecret } from "./secrets.js";
|
|
143
143
|
import { deviceFlowClientId, requestDeviceCode, pollAccessTokenOnce, REPO_CONNECT_SCOPE } from "./github-device-auth.js";
|
|
144
144
|
import { InstallationTokenCache, createAppJwt, resolveInstallationId } from "./github-app-auth.js";
|
|
145
|
+
import { listGithubAppRepos } from "./github-app-repos.js";
|
|
145
146
|
import { loadGitHubAppConfigs, orderAppsForOwner, listGitHubApps, removeGitHubApp, upsertGitHubApp, privateKeyIdFor, } from "./github-apps.js";
|
|
146
147
|
import { buildAppManifest, convertManifest, renderManifestForm } from "./github-app-manifest.js";
|
|
147
148
|
import { encryptGithubAppEnvelope, decryptGithubAppEnvelope, readLocalGithubAppVaultKey, writeLocalGithubAppVaultKey, forgetLocalGithubAppVaultKey, mintLocalGithubAppVaultKey, } from "./github-app-vault.js";
|
|
@@ -4843,6 +4844,7 @@ async function ensureGitHubApps() {
|
|
|
4843
4844
|
function invalidateGitHubApps() {
|
|
4844
4845
|
githubApps = undefined;
|
|
4845
4846
|
installationIdByRepo.clear();
|
|
4847
|
+
invalidateGithubListingCaches();
|
|
4846
4848
|
}
|
|
4847
4849
|
/**
|
|
4848
4850
|
* The node-side token seam. A GitHub App work item (carries an installationId,
|
|
@@ -10482,13 +10484,19 @@ async function createControlPlaneHook(kind = "github") {
|
|
|
10482
10484
|
return data;
|
|
10483
10485
|
}
|
|
10484
10486
|
// List the GitHub repos the node can reach with its own token (for picking a
|
|
10485
|
-
// git workspace).
|
|
10486
|
-
// machine (env or `gh`)
|
|
10487
|
+
// git workspace). Uses local App installations when available, otherwise the
|
|
10488
|
+
// token stored on this machine (env or `gh`). Returns `authed: false` with
|
|
10487
10489
|
// an empty list when there's no token so the UI falls back to manual owner/repo.
|
|
10488
10490
|
async function listAccessibleRepos() {
|
|
10489
10491
|
if (reposCache && Date.now() - reposCache.at < REPO_LIST_TTL_MS)
|
|
10490
10492
|
return reposCache.val;
|
|
10491
10493
|
try {
|
|
10494
|
+
const apps = await ensureGitHubApps();
|
|
10495
|
+
if (apps.length) {
|
|
10496
|
+
const val = { authed: true, repos: await listGithubAppRepos(apps) };
|
|
10497
|
+
reposCache = { at: Date.now(), val };
|
|
10498
|
+
return val;
|
|
10499
|
+
}
|
|
10492
10500
|
const token = await resolveGitHubToken();
|
|
10493
10501
|
if (!token)
|
|
10494
10502
|
return { authed: false, repos: [], reason: (await ghCliInstalled()) ? "gh-unauthed" : "no-token" };
|
package/package.json
CHANGED