@geohar/un-bien 0.7.0
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/LICENSE +21 -0
- package/README.md +726 -0
- package/dist/actions/handlers.d.ts +77 -0
- package/dist/actions/handlers.js +26 -0
- package/dist/actions/handlers.js.map +1 -0
- package/dist/actions/registry.d.ts +39 -0
- package/dist/actions/registry.js +62 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +86 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +84 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +131 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +176 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/cron_log.d.ts +45 -0
- package/dist/daemon/cron_log.js +71 -0
- package/dist/daemon/cron_log.js.map +1 -0
- package/dist/daemon/cron_registry.d.ts +80 -0
- package/dist/daemon/cron_registry.js +198 -0
- package/dist/daemon/cron_registry.js.map +1 -0
- package/dist/daemon/id.d.ts +24 -0
- package/dist/daemon/id.js +36 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +170 -0
- package/dist/daemon/install.js +603 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +63 -0
- package/dist/daemon/registry.js +153 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +194 -0
- package/dist/daemon/rpc_child.js +419 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +93 -0
- package/dist/daemon/supervisor.js +666 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/extension_ui_bridge.d.ts +26 -0
- package/dist/extension_ui_bridge.js +341 -0
- package/dist/extension_ui_bridge.js.map +1 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +5750 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/mesh_result.d.ts +10 -0
- package/dist/mcp/mesh_result.js +42 -0
- package/dist/mcp/mesh_result.js.map +1 -0
- package/dist/mcp/mesh_server.d.ts +16 -0
- package/dist/mcp/mesh_server.js +373 -0
- package/dist/mcp/mesh_server.js.map +1 -0
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +63 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +18 -0
- package/dist/mesh/client.js +127 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +38 -0
- package/dist/mesh/encoding.js +217 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +73 -0
- package/dist/mesh/self_revoke.js +407 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +49 -0
- package/dist/mesh/siblings.js +159 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +86 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/crypto.d.ts +8 -0
- package/dist/pairing/crypto.js +22 -0
- package/dist/pairing/crypto.js.map +1 -0
- package/dist/pairing/qr.d.ts +59 -0
- package/dist/pairing/qr.js +127 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +120 -0
- package/dist/pairing/storage.js +614 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/panel_bridge.d.ts +15 -0
- package/dist/panel_bridge.js +409 -0
- package/dist/panel_bridge.js.map +1 -0
- package/dist/paths.d.ts +39 -0
- package/dist/paths.js +51 -0
- package/dist/paths.js.map +1 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.js +48 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/types.d.ts +404 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/rooms.d.ts +42 -0
- package/dist/rooms.js +78 -0
- package/dist/rooms.js.map +1 -0
- package/dist/session/bridge.d.ts +37 -0
- package/dist/session/bridge.js +134 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/broker.d.ts +189 -0
- package/dist/session/broker.js +527 -0
- package/dist/session/broker.js.map +1 -0
- package/dist/session/broker_remote.d.ts +74 -0
- package/dist/session/broker_remote.js +581 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +33 -0
- package/dist/session/cwd_lock.js +127 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/debug_log.d.ts +3 -0
- package/dist/session/debug_log.js +32 -0
- package/dist/session/debug_log.js.map +1 -0
- package/dist/session/envelope.d.ts +33 -0
- package/dist/session/envelope.js +117 -0
- package/dist/session/envelope.js.map +1 -0
- package/dist/session/global_config.d.ts +33 -0
- package/dist/session/global_config.js +73 -0
- package/dist/session/global_config.js.map +1 -0
- package/dist/session/ipc.d.ts +27 -0
- package/dist/session/ipc.js +22 -0
- package/dist/session/ipc.js.map +1 -0
- package/dist/session/leader_election.d.ts +32 -0
- package/dist/session/leader_election.js +108 -0
- package/dist/session/leader_election.js.map +1 -0
- package/dist/session/local_config.d.ts +67 -0
- package/dist/session/local_config.js +193 -0
- package/dist/session/local_config.js.map +1 -0
- package/dist/session/mesh_node.d.ts +116 -0
- package/dist/session/mesh_node.js +596 -0
- package/dist/session/mesh_node.js.map +1 -0
- package/dist/session/peer.d.ts +139 -0
- package/dist/session/peer.js +474 -0
- package/dist/session/peer.js.map +1 -0
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/peer_limits.d.ts +9 -0
- package/dist/session/peer_limits.js +24 -0
- package/dist/session/peer_limits.js.map +1 -0
- package/dist/session/rpc_envelope.d.ts +108 -0
- package/dist/session/rpc_envelope.js +221 -0
- package/dist/session/rpc_envelope.js.map +1 -0
- package/dist/session/rpc_inbound.d.ts +55 -0
- package/dist/session/rpc_inbound.js +136 -0
- package/dist/session/rpc_inbound.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +39 -0
- package/dist/session/setup_wizard.js +63 -0
- package/dist/session/setup_wizard.js.map +1 -0
- package/dist/session/tools.d.ts +24 -0
- package/dist/session/tools.js +272 -0
- package/dist/session/tools.js.map +1 -0
- package/dist/session/wizard.d.ts +13 -0
- package/dist/session/wizard.js +20 -0
- package/dist/session/wizard.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +52 -0
- package/dist/transport/peer_channel.js +145 -0
- package/dist/transport/peer_channel.js.map +1 -0
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +68 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/transport/relay_client.d.ts +96 -0
- package/dist/transport/relay_client.js +210 -0
- package/dist/transport/relay_client.js.map +1 -0
- package/dist/ui/footer.d.ts +32 -0
- package/dist/ui/footer.js +34 -0
- package/dist/ui/footer.js.map +1 -0
- package/docs/daemon.md +289 -0
- package/package.json +92 -0
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/service-templates/task-launcher.vbs.template +10 -0
- package/service-templates/task-scheduler.xml.template +38 -0
- package/skills/agent-network/SKILL.md +310 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh-membership types — pi-extension side. Mirrors the wire shape defined
|
|
3
|
+
* in plan/24-mesh-membership.md and must stay bit-compatible with the Dart
|
|
4
|
+
* (app) and Rust (relay) implementations of the same protocol.
|
|
5
|
+
*
|
|
6
|
+
* On the wire, JSON field names are `snake_case` (per the plan); this module
|
|
7
|
+
* exposes `camelCase` for ergonomic TS use. Conversion happens at the
|
|
8
|
+
* (de)serialization boundary in `verify.ts` / `canonical.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export interface MeshMember {
|
|
11
|
+
/** Pi Ed25519 pubkey, canonical RFC 4648 standard base64 with padding. */
|
|
12
|
+
remoteEpk: string;
|
|
13
|
+
/** Relay URL where this member registers. */
|
|
14
|
+
relayUrl: string;
|
|
15
|
+
/** ISO-8601 timestamp of when the pairing happened. */
|
|
16
|
+
pairedAt: string;
|
|
17
|
+
/** Optional Owner-set label. */
|
|
18
|
+
nickname?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MeshHeader {
|
|
21
|
+
/** Owner-scoped monotonic counter. Higher = newer. */
|
|
22
|
+
version: number;
|
|
23
|
+
/** Issued-at, ms since epoch. */
|
|
24
|
+
issuedAt: number;
|
|
25
|
+
/** Owner's Ed25519 pubkey, raw 32 bytes. */
|
|
26
|
+
ownerPk: Uint8Array;
|
|
27
|
+
members: MeshMember[];
|
|
28
|
+
}
|
|
29
|
+
export interface MeshEnvelope {
|
|
30
|
+
/** Canonical JSON bytes of the header (snake_case keys, sorted, no whitespace). */
|
|
31
|
+
blob: Uint8Array;
|
|
32
|
+
/** Ed25519 signature of `blob`, 64 bytes. */
|
|
33
|
+
sig: Uint8Array;
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh-membership types — pi-extension side. Mirrors the wire shape defined
|
|
3
|
+
* in plan/24-mesh-membership.md and must stay bit-compatible with the Dart
|
|
4
|
+
* (app) and Rust (relay) implementations of the same protocol.
|
|
5
|
+
*
|
|
6
|
+
* On the wire, JSON field names are `snake_case` (per the plan); this module
|
|
7
|
+
* exposes `camelCase` for ergonomic TS use. Conversion happens at the
|
|
8
|
+
* (de)serialization boundary in `verify.ts` / `canonical.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/mesh/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MeshEnvelope, MeshHeader } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies the Ed25519 signature on a mesh envelope and decodes the blob
|
|
4
|
+
* into a typed `MeshHeader`.
|
|
5
|
+
*
|
|
6
|
+
* The verification key is extracted *from the blob* (`owner_pk` field) —
|
|
7
|
+
* the caller MUST then check that `sha256(header.ownerPk)` matches the
|
|
8
|
+
* URL hash they queried with. Otherwise a malicious relay could serve a
|
|
9
|
+
* valid-but-different-owner blob at our hash slot.
|
|
10
|
+
*
|
|
11
|
+
* Throws on:
|
|
12
|
+
* - JSON parse failure
|
|
13
|
+
* - Missing or wrong-type required fields
|
|
14
|
+
* - `owner_pk` not 32 bytes
|
|
15
|
+
* - Signature mismatch
|
|
16
|
+
*/
|
|
17
|
+
export declare function verifyEnvelope(env: MeshEnvelope): Promise<MeshHeader>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ed25519Verify } from "../pairing/crypto.js";
|
|
2
|
+
import { canonicalizeEd25519PublicKey, decodeEd25519PublicKey, } from "./encoding.js";
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the Ed25519 signature on a mesh envelope and decodes the blob
|
|
5
|
+
* into a typed `MeshHeader`.
|
|
6
|
+
*
|
|
7
|
+
* The verification key is extracted *from the blob* (`owner_pk` field) —
|
|
8
|
+
* the caller MUST then check that `sha256(header.ownerPk)` matches the
|
|
9
|
+
* URL hash they queried with. Otherwise a malicious relay could serve a
|
|
10
|
+
* valid-but-different-owner blob at our hash slot.
|
|
11
|
+
*
|
|
12
|
+
* Throws on:
|
|
13
|
+
* - JSON parse failure
|
|
14
|
+
* - Missing or wrong-type required fields
|
|
15
|
+
* - `owner_pk` not 32 bytes
|
|
16
|
+
* - Signature mismatch
|
|
17
|
+
*/
|
|
18
|
+
export async function verifyEnvelope(env) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(new TextDecoder().decode(env.blob));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("mesh: blob is not valid JSON");
|
|
25
|
+
}
|
|
26
|
+
if (!parsed || typeof parsed !== "object") {
|
|
27
|
+
throw new Error("mesh: blob is not a JSON object");
|
|
28
|
+
}
|
|
29
|
+
const o = parsed;
|
|
30
|
+
if (typeof o["owner_pk"] !== "string") {
|
|
31
|
+
throw new Error("mesh: owner_pk missing or not a string");
|
|
32
|
+
}
|
|
33
|
+
if (typeof o["version"] !== "number" || !Number.isInteger(o["version"])) {
|
|
34
|
+
throw new Error("mesh: version missing or not an integer");
|
|
35
|
+
}
|
|
36
|
+
if (typeof o["issued_at"] !== "number" || !Number.isInteger(o["issued_at"])) {
|
|
37
|
+
throw new Error("mesh: issued_at missing or not an integer");
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(o["members"])) {
|
|
40
|
+
throw new Error("mesh: members missing or not an array");
|
|
41
|
+
}
|
|
42
|
+
const rawMembers = o["members"].map((raw, index) => {
|
|
43
|
+
if (!raw || typeof raw !== "object") {
|
|
44
|
+
throw new Error(`mesh: members[${index}] is not an object`);
|
|
45
|
+
}
|
|
46
|
+
const member = raw;
|
|
47
|
+
if (typeof member["remote_epk"] !== "string") {
|
|
48
|
+
throw new Error(`mesh: members[${index}].remote_epk invalid`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof member["relay_url"] !== "string") {
|
|
51
|
+
throw new Error(`mesh: members[${index}].relay_url invalid`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof member["paired_at"] !== "string") {
|
|
54
|
+
throw new Error(`mesh: members[${index}].paired_at invalid`);
|
|
55
|
+
}
|
|
56
|
+
const nickname = member["nickname"];
|
|
57
|
+
if (nickname !== undefined &&
|
|
58
|
+
nickname !== null &&
|
|
59
|
+
typeof nickname !== "string") {
|
|
60
|
+
throw new Error(`mesh: members[${index}].nickname invalid`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
remoteEpk: member["remote_epk"],
|
|
64
|
+
relayUrl: member["relay_url"],
|
|
65
|
+
pairedAt: member["paired_at"],
|
|
66
|
+
...(typeof nickname === "string" ? { nickname } : {}),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
const ownerPk = decodeEd25519PublicKey(o["owner_pk"], "owner_pk");
|
|
70
|
+
if (!ed25519Verify(ownerPk, env.blob, env.sig)) {
|
|
71
|
+
throw new Error("mesh: signature verification failed");
|
|
72
|
+
}
|
|
73
|
+
const members = rawMembers.map((member, index) => ({
|
|
74
|
+
remoteEpk: canonicalizeEd25519PublicKey(member.remoteEpk, `members[${index}].remote_epk`),
|
|
75
|
+
relayUrl: member.relayUrl,
|
|
76
|
+
pairedAt: member.pairedAt,
|
|
77
|
+
...(member.nickname !== undefined ? { nickname: member.nickname } : {}),
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
version: o["version"],
|
|
81
|
+
issuedAt: o["issued_at"],
|
|
82
|
+
ownerPk,
|
|
83
|
+
members,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/mesh/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Ed25519Keypair {
|
|
2
|
+
publicKey: Uint8Array;
|
|
3
|
+
secretKey: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
/** Generates an Ed25519 keypair for relay challenge-response auth. */
|
|
6
|
+
export declare function generateEd25519Keypair(): Ed25519Keypair;
|
|
7
|
+
export declare function ed25519Sign(sk: Uint8Array, msg: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function ed25519Verify(pk: Uint8Array, msg: Uint8Array, sig: Uint8Array): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import * as ed from "@noble/ed25519";
|
|
3
|
+
// Configure @noble/ed25519 v3 to use Node.js built-in SHA-512
|
|
4
|
+
ed.hashes["sha512"] = (...msgs) => {
|
|
5
|
+
const h = createHash("sha512");
|
|
6
|
+
for (const m of msgs)
|
|
7
|
+
h.update(m);
|
|
8
|
+
return Uint8Array.from(h.digest());
|
|
9
|
+
};
|
|
10
|
+
/** Generates an Ed25519 keypair for relay challenge-response auth. */
|
|
11
|
+
export function generateEd25519Keypair() {
|
|
12
|
+
const secretKey = randomBytes(32);
|
|
13
|
+
const publicKey = ed.getPublicKey(secretKey);
|
|
14
|
+
return { secretKey, publicKey: Buffer.from(publicKey) };
|
|
15
|
+
}
|
|
16
|
+
export function ed25519Sign(sk, msg) {
|
|
17
|
+
return Buffer.from(ed.sign(msg, sk));
|
|
18
|
+
}
|
|
19
|
+
export function ed25519Verify(pk, msg, sig) {
|
|
20
|
+
return ed.verify(sig, msg, pk);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/pairing/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAErC,8DAA8D;AAC7D,EAAE,CAAC,MAAkC,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAkB,EAAE,EAAE;IAC3E,MAAM,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACrC,CAAC,CAAC;AAOF,sEAAsE;AACtE,MAAM,UAAU,sBAAsB;IACpC,MAAM,SAAS,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IAC7C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAc,EAAE,GAAe;IACzD,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,EAAc,EACd,GAAe,EACf,GAAe;IAEf,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Default ephemeral-token lifetime (also the QR rotation period). */
|
|
2
|
+
export declare const TOKEN_TTL_MS = 60000;
|
|
3
|
+
/** Bounds for a caller-supplied pairing TTL (e.g. `/unbien pair --ttl <s>`). */
|
|
4
|
+
export declare const PAIR_TTL_MIN_MS = 10000;
|
|
5
|
+
export declare const PAIR_TTL_MAX_MS = 600000;
|
|
6
|
+
/** Clamp an arbitrary ttl (ms) into the safe pairing range; NaN → default. */
|
|
7
|
+
export declare function clampPairTtlMs(ttlMs: number): number;
|
|
8
|
+
/** Encapsulates the single active QR token. One instance per Pi process. */
|
|
9
|
+
export declare class QRSession {
|
|
10
|
+
private active;
|
|
11
|
+
/** Generates a fresh 16-byte random token encoded as base64url. */
|
|
12
|
+
generateToken(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Issues a new active token, invalidating any previous one.
|
|
15
|
+
* Returns the token and its expiry timestamp.
|
|
16
|
+
*/
|
|
17
|
+
issueToken(ttlMs?: number): {
|
|
18
|
+
token: string;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
};
|
|
21
|
+
/** Validates and atomically consumes a token. */
|
|
22
|
+
consumeToken(token: string): "ok" | "expired" | "consumed" | "unknown";
|
|
23
|
+
clear(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare const qrSession: QRSession;
|
|
26
|
+
export declare function buildQRUri(token: string, longtermEdPk: Uint8Array, // Ed25519 — only peer ID after E2E rollback
|
|
27
|
+
sessionName: string,
|
|
28
|
+
/**
|
|
29
|
+
* Pi room id (12 chars, base64url) derived from cwd. App routes pair_request
|
|
30
|
+
* to this room so the relay delivers it to the right Pi instance among N
|
|
31
|
+
* paralelos com mesmo epk. Adicionado no fix do plano 17 (sem `rm` o app
|
|
32
|
+
* cai em room=main e o relay drops com "dest not found").
|
|
33
|
+
*/
|
|
34
|
+
roomId?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the QR ASCII as a string (pure Unicode block characters —
|
|
37
|
+
* `█ ▀ ▄` and space, NO ANSI escapes — qrcode-terminal v0.12 small mode
|
|
38
|
+
* is escape-free, see lib/main.js:48-53).
|
|
39
|
+
*
|
|
40
|
+
* The caller can either write the string to stderr (legacy path, breaks
|
|
41
|
+
* the Pi TUI layout) or inject it via `pi.sendMessage` (renders inside
|
|
42
|
+
* the chat panel as proper content).
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderQRAscii(uri: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Legacy stderr writer — kept for the standalone CLI mode
|
|
47
|
+
* (`pi-extension/src/index.ts` bottom block, which runs outside a Pi TUI).
|
|
48
|
+
* Inside the Pi TUI extension flow, use `renderQRAscii` + `pi.sendMessage`
|
|
49
|
+
* instead — direct stderr writes from inside an extension break the TUI's
|
|
50
|
+
* scrollable output widget (the QR overflows the panel and other writes
|
|
51
|
+
* collide with the prompt area).
|
|
52
|
+
*/
|
|
53
|
+
export declare function displayQR(uri: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Starts a rotating QR session: generates a new QR every 60s, printing it
|
|
56
|
+
* to stdout. Returns a `stop()` function that cancels the rotation and clears
|
|
57
|
+
* the active token.
|
|
58
|
+
*/
|
|
59
|
+
export declare function startQRRotation(longtermEdPk: Uint8Array, sessionName: string, roomId?: string): () => void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import qrTerminal from "qrcode-terminal";
|
|
3
|
+
/** Default ephemeral-token lifetime (also the QR rotation period). */
|
|
4
|
+
export const TOKEN_TTL_MS = 60_000;
|
|
5
|
+
/** Bounds for a caller-supplied pairing TTL (e.g. `/unbien pair --ttl <s>`). */
|
|
6
|
+
export const PAIR_TTL_MIN_MS = 10_000;
|
|
7
|
+
export const PAIR_TTL_MAX_MS = 600_000;
|
|
8
|
+
/** Clamp an arbitrary ttl (ms) into the safe pairing range; NaN → default. */
|
|
9
|
+
export function clampPairTtlMs(ttlMs) {
|
|
10
|
+
if (!Number.isFinite(ttlMs))
|
|
11
|
+
return TOKEN_TTL_MS;
|
|
12
|
+
return Math.min(PAIR_TTL_MAX_MS, Math.max(PAIR_TTL_MIN_MS, Math.floor(ttlMs)));
|
|
13
|
+
}
|
|
14
|
+
/** Encapsulates the single active QR token. One instance per Pi process. */
|
|
15
|
+
export class QRSession {
|
|
16
|
+
active = null;
|
|
17
|
+
/** Generates a fresh 16-byte random token encoded as base64url. */
|
|
18
|
+
generateToken() {
|
|
19
|
+
return randomBytes(16).toString("base64url");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Issues a new active token, invalidating any previous one.
|
|
23
|
+
* Returns the token and its expiry timestamp.
|
|
24
|
+
*/
|
|
25
|
+
issueToken(ttlMs = TOKEN_TTL_MS) {
|
|
26
|
+
const token = this.generateToken();
|
|
27
|
+
const expiresAt = Date.now() + ttlMs;
|
|
28
|
+
this.active = { token, expiresAt, consumed: false };
|
|
29
|
+
return { token, expiresAt };
|
|
30
|
+
}
|
|
31
|
+
/** Validates and atomically consumes a token. */
|
|
32
|
+
consumeToken(token) {
|
|
33
|
+
if (!this.active || this.active.token !== token)
|
|
34
|
+
return "unknown";
|
|
35
|
+
if (this.active.consumed)
|
|
36
|
+
return "consumed";
|
|
37
|
+
if (Date.now() > this.active.expiresAt)
|
|
38
|
+
return "expired";
|
|
39
|
+
this.active.consumed = true;
|
|
40
|
+
return "ok";
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.active = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const qrSession = new QRSession();
|
|
47
|
+
// ── URI + display ─────────────────────────────────────────────────────────────
|
|
48
|
+
export function buildQRUri(token, longtermEdPk, // Ed25519 — only peer ID after E2E rollback
|
|
49
|
+
sessionName,
|
|
50
|
+
/**
|
|
51
|
+
* Pi room id (12 chars, base64url) derived from cwd. App routes pair_request
|
|
52
|
+
* to this room so the relay delivers it to the right Pi instance among N
|
|
53
|
+
* paralelos com mesmo epk. Adicionado no fix do plano 17 (sem `rm` o app
|
|
54
|
+
* cai em room=main e o relay drops com "dest not found").
|
|
55
|
+
*/
|
|
56
|
+
roomId) {
|
|
57
|
+
// `r` (relay URL) removed in plano 14 — relay now comes from app config /
|
|
58
|
+
// pi-ext env|config|default chain. Keeps QR ~30-50 chars shorter.
|
|
59
|
+
// `n` (session name) is kept: the app uses it for the pre-pair_ok preview
|
|
60
|
+
// screen (showing the agent name immediately after scan, before the
|
|
61
|
+
// handshake completes). Dropping it briefly shrank the QR but the QR
|
|
62
|
+
// size no longer matters now that the copy-paste URI is rendered via
|
|
63
|
+
// `pi.sendMessage` into the chat panel (not the QR overflow area).
|
|
64
|
+
const epkB64 = Buffer.from(longtermEdPk).toString("base64url");
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
t: token,
|
|
67
|
+
epk: epkB64,
|
|
68
|
+
n: sessionName.slice(0, 80),
|
|
69
|
+
});
|
|
70
|
+
if (roomId)
|
|
71
|
+
params.set("rm", roomId);
|
|
72
|
+
return `unbien://pair?${params.toString()}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns the QR ASCII as a string (pure Unicode block characters —
|
|
76
|
+
* `█ ▀ ▄` and space, NO ANSI escapes — qrcode-terminal v0.12 small mode
|
|
77
|
+
* is escape-free, see lib/main.js:48-53).
|
|
78
|
+
*
|
|
79
|
+
* The caller can either write the string to stderr (legacy path, breaks
|
|
80
|
+
* the Pi TUI layout) or inject it via `pi.sendMessage` (renders inside
|
|
81
|
+
* the chat panel as proper content).
|
|
82
|
+
*/
|
|
83
|
+
export function renderQRAscii(uri) {
|
|
84
|
+
let out = "";
|
|
85
|
+
qrTerminal.generate(uri, { small: true }, (qrcode) => {
|
|
86
|
+
out = qrcode;
|
|
87
|
+
});
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Legacy stderr writer — kept for the standalone CLI mode
|
|
92
|
+
* (`pi-extension/src/index.ts` bottom block, which runs outside a Pi TUI).
|
|
93
|
+
* Inside the Pi TUI extension flow, use `renderQRAscii` + `pi.sendMessage`
|
|
94
|
+
* instead — direct stderr writes from inside an extension break the TUI's
|
|
95
|
+
* scrollable output widget (the QR overflows the panel and other writes
|
|
96
|
+
* collide with the prompt area).
|
|
97
|
+
*/
|
|
98
|
+
export function displayQR(uri) {
|
|
99
|
+
const qrcode = renderQRAscii(uri);
|
|
100
|
+
process.stderr.write(`\n📱 Scan to pair:\n\n${qrcode}\n`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Starts a rotating QR session: generates a new QR every 60s, printing it
|
|
104
|
+
* to stdout. Returns a `stop()` function that cancels the rotation and clears
|
|
105
|
+
* the active token.
|
|
106
|
+
*/
|
|
107
|
+
export function startQRRotation(longtermEdPk, sessionName, roomId) {
|
|
108
|
+
let timer = null;
|
|
109
|
+
let stopped = false;
|
|
110
|
+
const rotate = () => {
|
|
111
|
+
if (stopped)
|
|
112
|
+
return;
|
|
113
|
+
const { token, expiresAt } = qrSession.issueToken();
|
|
114
|
+
const uri = buildQRUri(token, longtermEdPk, sessionName, roomId);
|
|
115
|
+
displayQR(uri);
|
|
116
|
+
console.log(`⏱ Renews at ${new Date(expiresAt).toLocaleTimeString()} — waiting for scan…`);
|
|
117
|
+
timer = setTimeout(rotate, TOKEN_TTL_MS);
|
|
118
|
+
};
|
|
119
|
+
rotate();
|
|
120
|
+
return () => {
|
|
121
|
+
stopped = true;
|
|
122
|
+
if (timer !== null)
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
qrSession.clear();
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Ed25519Keypair } from "./crypto.js";
|
|
2
|
+
/** Raised when the keyring is unreadable on a platform where it's a core OS
|
|
3
|
+
* service (macOS Keychain, Windows Credential Manager) AND no prior file
|
|
4
|
+
* identity exists. We refuse to generate a NEW identity here because that
|
|
5
|
+
* would break existing pairing — the caller surfaces this so the user can
|
|
6
|
+
* unlock the keychain and retry instead of silently re-pairing. */
|
|
7
|
+
export declare class KeyringUnavailableError extends Error {
|
|
8
|
+
constructor(cause: unknown);
|
|
9
|
+
}
|
|
10
|
+
/** Raised when no identity can be resolved (keyring unreadable, no identity
|
|
11
|
+
* file) BUT `peers.json` already lists paired devices. Minting a fresh key
|
|
12
|
+
* here would make SelfRevoke wipe those pairings — see issues #95 / #69. */
|
|
13
|
+
export declare class PairedIdentityMissingError extends Error {
|
|
14
|
+
constructor(pairedCount: number, cause: unknown);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Minimal backend interface for credential reads/writes. Swappable so
|
|
18
|
+
* tests can inject a controlled in-memory store without touching the OS
|
|
19
|
+
* keyring (which is shared with the developer's own credentials).
|
|
20
|
+
*
|
|
21
|
+
* Errors thrown by `read`/`write`/`delete` signal "backend unavailable on
|
|
22
|
+
* this platform" — callers fall back to the file store on first failure.
|
|
23
|
+
* Returning `undefined` from `read` means "no such entry" (a normal,
|
|
24
|
+
* non-error condition).
|
|
25
|
+
*/
|
|
26
|
+
export interface KeyStoreBackend {
|
|
27
|
+
read(service: string, account: string): Promise<string | undefined>;
|
|
28
|
+
write(service: string, account: string, value: string): Promise<void>;
|
|
29
|
+
delete(service: string, account: string): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
/** Test-only: force (or clear with `null`) a memoized binding-load failure, so
|
|
32
|
+
* the Bun/no-native-binding branch is reachable without a Bun host. */
|
|
33
|
+
export declare function _setNativeBindingErrorForTest(err: unknown): void;
|
|
34
|
+
/** Test-only: swap (or clear with `null`) the keyring backend. */
|
|
35
|
+
export declare function _setKeyStoreBackendForTest(backend: KeyStoreBackend | null): void;
|
|
36
|
+
/** Test-only: force `_keyringExpectedAvailable()` (so a darwin test host can
|
|
37
|
+
* exercise the Linux/headless branch and vice-versa). `null` restores the
|
|
38
|
+
* real platform check. */
|
|
39
|
+
export declare function _setKeyringExpectedForTest(value: boolean | null): void;
|
|
40
|
+
/** Test-only: shrink retry attempts/delay so the persistent-failure path is
|
|
41
|
+
* fast. `null`/omitted restores defaults. */
|
|
42
|
+
export declare function _setKeyringRetryForTest(attempts: number | null, delayMs?: number): void;
|
|
43
|
+
/**
|
|
44
|
+
* Returns the Pi-secret Ed25519 keypair, generating + persisting one on
|
|
45
|
+
* first call. Resolution order:
|
|
46
|
+
* 1. Existing file `~/.pi/un-bien/identity.json`, if present — it WINS over
|
|
47
|
+
* the keyring. A file identity is only ever written by the headless/
|
|
48
|
+
* degraded fallback (step 4) or an explicit `UNBIEN_ALLOW_FILE_IDENTITY`
|
|
49
|
+
* opt-in, so its mere presence means this machine established its identity
|
|
50
|
+
* as a file and the mobile device paired against THAT pubkey. If the
|
|
51
|
+
* platform keyring later becomes readable (D-Bus/libsecret installed, a
|
|
52
|
+
* desktop session, or a stale/other entry from another install), reading
|
|
53
|
+
* it first would mask the file identity — returning a DIFFERENT key, or
|
|
54
|
+
* (when the keyring is empty) minting a fresh one and persisting it —
|
|
55
|
+
* silently breaking the existing pairing. So when both exist, file wins.
|
|
56
|
+
* 2. New keyring service `dev.unbien.pi` (read retried — a transiently
|
|
57
|
+
* locked Keychain throws; we don't treat that as "no key")
|
|
58
|
+
* 3. Old keyring service `dev.unbien.mac` (migrate → step 2, delete old)
|
|
59
|
+
* 4. Generate a fresh keypair, BUT only when it's safe to: either both
|
|
60
|
+
* keyring reads succeeded and returned nothing (genuine first run), or
|
|
61
|
+
* the keyring is genuinely unavailable on a platform without a core one
|
|
62
|
+
* (headless Linux → a file identity is minted here). On macOS/Windows a
|
|
63
|
+
* persistent read failure with no file identity throws
|
|
64
|
+
* `KeyringUnavailableError` instead of minting a new key — generating
|
|
65
|
+
* there silently breaks existing pairing (the "lost pairing after idle"
|
|
66
|
+
* bug). `UNBIEN_ALLOW_FILE_IDENTITY=1` opts back into a file identity
|
|
67
|
+
* for headless macOS/Windows hosts.
|
|
68
|
+
*
|
|
69
|
+
* Idempotent: subsequent calls return the same identity. The migration
|
|
70
|
+
* runs at most once per machine (the old entry is deleted after copy).
|
|
71
|
+
*/
|
|
72
|
+
export declare function getOrCreateEd25519Keypair(): Promise<Ed25519Keypair>;
|
|
73
|
+
export interface PeerRecord {
|
|
74
|
+
name: string;
|
|
75
|
+
remote_epk: string;
|
|
76
|
+
paired_at: string;
|
|
77
|
+
}
|
|
78
|
+
export declare function listPeers(): Promise<PeerRecord[]>;
|
|
79
|
+
declare const _ownerStorageTokenBrand: unique symbol;
|
|
80
|
+
/** Opaque, process-local provenance for one canonical Owner storage slot. */
|
|
81
|
+
export type OwnerStorageToken = {
|
|
82
|
+
readonly [_ownerStorageTokenBrand]: true;
|
|
83
|
+
};
|
|
84
|
+
export interface OwnerStorageSnapshotRecord {
|
|
85
|
+
readonly rawOwnerPubkey: unknown;
|
|
86
|
+
readonly token: OwnerStorageToken;
|
|
87
|
+
}
|
|
88
|
+
export type ConditionalPeerRemoval = {
|
|
89
|
+
readonly outcome: "removed";
|
|
90
|
+
readonly nextToken: OwnerStorageToken;
|
|
91
|
+
} | {
|
|
92
|
+
readonly outcome: "stale" | "not_found" | "no_authority";
|
|
93
|
+
};
|
|
94
|
+
export declare function addPeer(record: PeerRecord): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Returns the set of distinct `remote_epk` values in peers.json.
|
|
97
|
+
*
|
|
98
|
+
* In the current pairing model (plan/23 + plan/24), each `remote_epk` is the
|
|
99
|
+
* Owner's Ed25519 pubkey — and we treat each as a distinct Owner the Pi has
|
|
100
|
+
* been paired with. Used by the mesh self-revoke poller (plan/24 Wave 3) to
|
|
101
|
+
* know which Owners' mesh blobs to fetch.
|
|
102
|
+
*/
|
|
103
|
+
export declare function listOwnerPubkeys(): Promise<unknown[]>;
|
|
104
|
+
/**
|
|
105
|
+
* Atomically snapshots raw Owner handles and their canonical-slot provenance.
|
|
106
|
+
* The token is deliberately process-local and opaque to callers.
|
|
107
|
+
*/
|
|
108
|
+
export declare function snapshotOwnerPubkeys(): Promise<readonly OwnerStorageSnapshotRecord[]>;
|
|
109
|
+
/**
|
|
110
|
+
* Removes one exact raw handle only when its snapshot provenance still owns
|
|
111
|
+
* the target canonical Owner slot. The final authority/token checks and sync
|
|
112
|
+
* write share the existing serialized mutation lane.
|
|
113
|
+
*/
|
|
114
|
+
export declare function conditionalRemovePeer(remoteEpk: string, expectedToken: OwnerStorageToken, canCommit?: () => boolean): Promise<ConditionalPeerRemoval>;
|
|
115
|
+
export declare function removePeer(remoteEpk: string, canCommit?: () => boolean): Promise<boolean>;
|
|
116
|
+
/** Test-only: expose the identity-file path so tests can clean it. */
|
|
117
|
+
export declare const _IDENTITY_FILE_FOR_TEST: string;
|
|
118
|
+
/** Test-only: expose unlink for cleanup. */
|
|
119
|
+
export declare const _unlinkIdentityFileForTest: () => Promise<void>;
|
|
120
|
+
export {};
|