@bivy/bivy 0.3.0-staging.43 → 0.3.0-staging.44
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/device-registry.js +34 -2
- package/dist/relay-client.js +2 -1
- package/dist/server.js +18 -2
- package/package.json +1 -1
package/dist/device-registry.js
CHANGED
|
@@ -6,6 +6,26 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import { generatePairingKeypair, generateRoomKey, generatePairSecret, deriveWrapKey, verifyPairingProof, wrapRoomKey, } from "./pairing-crypto.js";
|
|
7
7
|
import { seal, open } from "./e2e.js";
|
|
8
8
|
const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
|
|
9
|
+
const ROOM_KEY_BYTES = 32;
|
|
10
|
+
/**
|
|
11
|
+
* Validate + canonicalize a pre-shared room-key seed. Returns the canonical
|
|
12
|
+
* base64 of a well-formed 32-byte key, or undefined for anything malformed
|
|
13
|
+
* (missing, wrong length, non-base64) so the caller falls back to a fresh key
|
|
14
|
+
* rather than adopting a bad seed that would make every device unable to decrypt.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeSeedRoomKey(seedRoomKeyB64) {
|
|
17
|
+
if (!seedRoomKeyB64)
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const buf = Buffer.from(seedRoomKeyB64, "base64");
|
|
21
|
+
if (buf.length !== ROOM_KEY_BYTES)
|
|
22
|
+
return undefined;
|
|
23
|
+
return buf.toString("base64");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
export class PairingStore {
|
|
10
30
|
filePath;
|
|
11
31
|
data;
|
|
@@ -18,8 +38,17 @@ export class PairingStore {
|
|
|
18
38
|
* Load (or create) the pairing state. A fresh state gets a randomly generated
|
|
19
39
|
* room key; devices receive it (and every later rotation) over the X25519
|
|
20
40
|
* pairing handshake, so there is no static seed to carry forward.
|
|
41
|
+
*
|
|
42
|
+
* `seedRoomKeyB64` (base64 32-byte key) is the ONE exception: an ephemeral
|
|
43
|
+
* REBUILD reuses a torn-down session's node id and must decrypt a snapshot
|
|
44
|
+
* sealed under that session's original room key. The launching device/CP bakes
|
|
45
|
+
* that key into relay.json (`e2eKey`) and the daemon passes it here, so a
|
|
46
|
+
* brand-new pairing state adopts it instead of minting a fresh one. It is used
|
|
47
|
+
* ONLY when there is no existing pairing.json — an already-paired node never
|
|
48
|
+
* has its room key overwritten. An absent/malformed seed falls back to a fresh
|
|
49
|
+
* random key (the ordinary first-run path).
|
|
21
50
|
*/
|
|
22
|
-
static load(appDir) {
|
|
51
|
+
static load(appDir, seedRoomKeyB64) {
|
|
23
52
|
const filePath = path.join(appDir, "pairing.json");
|
|
24
53
|
let raw;
|
|
25
54
|
try {
|
|
@@ -55,9 +84,12 @@ export class PairingStore {
|
|
|
55
84
|
devices: Array.isArray(parsed.devices) ? parsed.devices : [],
|
|
56
85
|
});
|
|
57
86
|
}
|
|
87
|
+
// No pairing.json yet — first run. Adopt a valid pre-shared seed (ephemeral
|
|
88
|
+
// rebuild) if one was supplied; otherwise mint a fresh random room key.
|
|
89
|
+
const seededRoomKey = normalizeSeedRoomKey(seedRoomKeyB64);
|
|
58
90
|
const data = {
|
|
59
91
|
nodeKeypair: generatePairingKeypair(),
|
|
60
|
-
roomKeyB64: generateRoomKey().toString("base64"),
|
|
92
|
+
roomKeyB64: seededRoomKey ?? generateRoomKey().toString("base64"),
|
|
61
93
|
devices: [],
|
|
62
94
|
};
|
|
63
95
|
const store = new PairingStore(filePath, data);
|
package/dist/relay-client.js
CHANGED
|
@@ -26,7 +26,8 @@ export function loadRelayConfig(appDir) {
|
|
|
26
26
|
return null;
|
|
27
27
|
const controlPlaneUrl = process.env.BIVY_CONTROL_PLANE_URL ?? raw.controlPlaneUrl;
|
|
28
28
|
const clientBaseUrl = process.env.BIVY_CLIENT_BASE_URL ?? raw.clientBaseUrl ?? controlPlaneUrl;
|
|
29
|
-
|
|
29
|
+
const e2eKey = process.env.BIVY_ROOM_KEY ?? raw.e2eKey;
|
|
30
|
+
return { url, enrollmentToken, controlPlaneUrl, clientBaseUrl, e2eKey };
|
|
30
31
|
}
|
|
31
32
|
// Application-level keepalive. On flaky/mobile links the node→relay TCP socket
|
|
32
33
|
// can half-open silently: the node still thinks it is connected and buffers
|
package/dist/server.js
CHANGED
|
@@ -740,8 +740,11 @@ function getRuntime(requested, sandbox) {
|
|
|
740
740
|
}
|
|
741
741
|
// Holds the node's X25519 identity key, the rotating room key, and the linked
|
|
742
742
|
// device registry. The room key is generated fresh on first load and delivered
|
|
743
|
-
// to devices via the X25519 pairing handshake — there is no static seed
|
|
744
|
-
|
|
743
|
+
// to devices via the X25519 pairing handshake — there is no static seed, EXCEPT
|
|
744
|
+
// on an ephemeral rebuild: relay.json carries the reused session's room key
|
|
745
|
+
// (`e2eKey`) so a brand-new pairing state adopts it and can decrypt the restored
|
|
746
|
+
// snapshot. Only seeds a first-run node; an existing pairing.json always wins.
|
|
747
|
+
const pairingStore = PairingStore.load(appDir, loadRelayConfig(appDir)?.e2eKey);
|
|
745
748
|
function syncPairingMetadata() {
|
|
746
749
|
for (const device of pairingStore.listDevices()) {
|
|
747
750
|
metadata.upsertDevice({ id: device.id, label: device.label, publicKeyB64: device.publicKeyB64, firstSeenAt: device.createdAt, lastSeenAt: device.lastSeenAt ?? undefined });
|
|
@@ -4797,6 +4800,19 @@ async function runWorkItem(item, report) {
|
|
|
4797
4800
|
if (item.url)
|
|
4798
4801
|
issue.url = item.url;
|
|
4799
4802
|
}
|
|
4803
|
+
// Case B: the control plane asked us to CONTINUE an existing session for this
|
|
4804
|
+
// issue (an inbound comment/issue on a thread that already has one). If that
|
|
4805
|
+
// session isn't live on this node — e.g. its ephemeral machine was torn down —
|
|
4806
|
+
// best-effort restore its snapshot first so its transcript + branch state are
|
|
4807
|
+
// rebuilt and the work continues the thread instead of starting cold. On
|
|
4808
|
+
// failure we fall through to the normal idempotent, remote-branch-adopting
|
|
4809
|
+
// pickup, so this can only help, never break.
|
|
4810
|
+
const issueSource = `issue:${parsed.owner}/${parsed.repo}#${item.issueNumber}`;
|
|
4811
|
+
if (item.targetKind === "existing_session" && item.targetSessionId && !findIssueSession(issueSource)) {
|
|
4812
|
+
await restoreSessionFromSnapshot(item.targetSessionId).catch((e) => {
|
|
4813
|
+
console.warn(`[case-b] snapshot restore for ${item.targetSessionId} failed:`, e.message);
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4800
4816
|
await runIssueTask(cfg, issue, { runtimeId: item.runtimeId, model: item.model, onEvidence: report });
|
|
4801
4817
|
return;
|
|
4802
4818
|
}
|
package/package.json
CHANGED