@bivy/bivy 0.18.5-staging.1 → 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.
- package/dist/github-app-repos.js +44 -0
- package/dist/model-auth-key-recovery.js +22 -0
- package/dist/server.js +32 -9
- 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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
/** Validate keys against the vault before caching them or sharing them with peers.
|
|
4
|
+
* A stale local key must not hide a fresh peer wrap; an unusable wrap must never
|
|
5
|
+
* be persisted as though key recovery succeeded. No credential data is changed.
|
|
6
|
+
*/
|
|
7
|
+
export function recoverModelAuthKey(options) {
|
|
8
|
+
if (options.localKey) {
|
|
9
|
+
try {
|
|
10
|
+
return { key: options.localKey, value: options.decrypt(options.localKey) };
|
|
11
|
+
}
|
|
12
|
+
catch { /* Try the peer's replacement before requesting another wrap. */ }
|
|
13
|
+
}
|
|
14
|
+
if (options.unwrap) {
|
|
15
|
+
try {
|
|
16
|
+
const key = options.unwrap();
|
|
17
|
+
return { key, value: options.decrypt(key) };
|
|
18
|
+
}
|
|
19
|
+
catch { /* The caller must reject this exact wrap and request a new one. */ }
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
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";
|
|
@@ -149,6 +150,7 @@ import { redactSecrets } from "./redact.js";
|
|
|
149
150
|
import { getSttConfig, isSttProvider, removeSttKey, setSttKey, setSttProvider, transcribeAudio, MAX_AUDIO_BYTES, } from "./stt.js";
|
|
150
151
|
import { synthesizeOpenAiSpeech } from "./tts.js";
|
|
151
152
|
import { seal, open } from "./e2e.js";
|
|
153
|
+
import { recoverModelAuthKey } from "./model-auth-key-recovery.js";
|
|
152
154
|
import { RunDelegationService, parseDelegationSource } from "./run-tools.js";
|
|
153
155
|
import { ControlPlaneTaskPoller, resolveControlPlaneTaskConfig, } from "./control-plane-tasks.js";
|
|
154
156
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -3365,20 +3367,31 @@ async function syncModelAuthFromControlPlane() {
|
|
|
3365
3367
|
}
|
|
3366
3368
|
const targetVault = hostedCustody ? hostedData.hostedVault : data.vault;
|
|
3367
3369
|
let vaultKeyB64 = hostedCustody ? readHostedModelAuthVaultKey() : readLocalModelAuthVaultKey();
|
|
3368
|
-
if (!hostedCustody && !vaultKeyB64 && data.wrappedKey?.wrappedKey) {
|
|
3369
|
-
vaultKeyB64 = pairingStore.unwrapFromNodePublicKey(data.wrappedKey.wrappedByPublicKey, data.wrappedKey.wrappedKey);
|
|
3370
|
-
writeLocalModelAuthVaultKey(vaultKeyB64);
|
|
3371
|
-
}
|
|
3372
3370
|
// Hosted runners may decrypt ONLY the separately encrypted snapshot of
|
|
3373
3371
|
// records that the user explicitly granted to unattended execution.
|
|
3374
3372
|
if (hostedCustody && !vaultKeyB64 && hostedData.hostedKey && hostedData.hostedVault && Buffer.from(hostedData.hostedKey, "base64").length === 32) {
|
|
3375
3373
|
vaultKeyB64 = hostedData.hostedKey;
|
|
3376
3374
|
writeHostedModelAuthVaultKey(vaultKeyB64);
|
|
3377
3375
|
}
|
|
3378
|
-
if (targetVault?.ciphertext && vaultKeyB64) {
|
|
3376
|
+
if (targetVault?.ciphertext && (vaultKeyB64 || (!hostedCustody && data.wrappedKey))) {
|
|
3379
3377
|
let decrypted;
|
|
3380
3378
|
try {
|
|
3381
|
-
|
|
3379
|
+
if (hostedCustody) {
|
|
3380
|
+
decrypted = decryptModelAuthEnvelope(targetVault.ciphertext, vaultKeyB64);
|
|
3381
|
+
}
|
|
3382
|
+
else {
|
|
3383
|
+
const wrapped = data.wrappedKey;
|
|
3384
|
+
const recovered = recoverModelAuthKey({
|
|
3385
|
+
localKey: vaultKeyB64,
|
|
3386
|
+
unwrap: wrapped ? () => pairingStore.unwrapFromNodePublicKey(wrapped.wrappedByPublicKey, wrapped.wrappedKey) : undefined,
|
|
3387
|
+
decrypt: (key) => decryptModelAuthEnvelope(targetVault.ciphertext, key),
|
|
3388
|
+
});
|
|
3389
|
+
if (!recovered)
|
|
3390
|
+
throw new Error("No available key can decrypt the account vault");
|
|
3391
|
+
vaultKeyB64 = recovered.key;
|
|
3392
|
+
decrypted = recovered.value;
|
|
3393
|
+
writeLocalModelAuthVaultKey(vaultKeyB64);
|
|
3394
|
+
}
|
|
3382
3395
|
}
|
|
3383
3396
|
catch (error) {
|
|
3384
3397
|
// Most commonly this node cached the previous generation while another
|
|
@@ -3394,7 +3407,10 @@ async function syncModelAuthFromControlPlane() {
|
|
|
3394
3407
|
else {
|
|
3395
3408
|
forgetLocalModelAuthVaultKey();
|
|
3396
3409
|
lastPushedModelAuthCiphertext = "";
|
|
3397
|
-
await modelAuthFetch("/node/model-auth-key/request", {
|
|
3410
|
+
await modelAuthFetch("/node/model-auth-key/request", {
|
|
3411
|
+
method: "POST",
|
|
3412
|
+
body: JSON.stringify({ publicKey: pairingStore.nodePublicKeyB64(), rejectedWrappedKey: data.wrappedKey?.wrappedKey }),
|
|
3413
|
+
});
|
|
3398
3414
|
ensureModelAuthColdStart();
|
|
3399
3415
|
}
|
|
3400
3416
|
console.warn("[auth-sync] cached vault key is stale; requested the rotated key:", error.message);
|
|
@@ -4828,6 +4844,7 @@ async function ensureGitHubApps() {
|
|
|
4828
4844
|
function invalidateGitHubApps() {
|
|
4829
4845
|
githubApps = undefined;
|
|
4830
4846
|
installationIdByRepo.clear();
|
|
4847
|
+
invalidateGithubListingCaches();
|
|
4831
4848
|
}
|
|
4832
4849
|
/**
|
|
4833
4850
|
* The node-side token seam. A GitHub App work item (carries an installationId,
|
|
@@ -10467,13 +10484,19 @@ async function createControlPlaneHook(kind = "github") {
|
|
|
10467
10484
|
return data;
|
|
10468
10485
|
}
|
|
10469
10486
|
// List the GitHub repos the node can reach with its own token (for picking a
|
|
10470
|
-
// git workspace).
|
|
10471
|
-
// 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
|
|
10472
10489
|
// an empty list when there's no token so the UI falls back to manual owner/repo.
|
|
10473
10490
|
async function listAccessibleRepos() {
|
|
10474
10491
|
if (reposCache && Date.now() - reposCache.at < REPO_LIST_TTL_MS)
|
|
10475
10492
|
return reposCache.val;
|
|
10476
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
|
+
}
|
|
10477
10500
|
const token = await resolveGitHubToken();
|
|
10478
10501
|
if (!token)
|
|
10479
10502
|
return { authed: false, repos: [], reason: (await ghCliInstalled()) ? "gh-unauthed" : "no-token" };
|
package/package.json
CHANGED