@bivy/bivy 0.18.5-staging.2 → 0.18.5-staging.3

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,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). Privacy-preserving: uses only the token stored on this
10486
- // machine (env or `gh`), never the control plane. Returns `authed: false` with
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.5-staging.2",
3
+ "version": "0.18.5-staging.3",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",