@gtc6244/immediacy-daemon 0.2.1 → 0.2.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/src/crypto.d.ts +18 -39
- package/dist/src/crypto.js +33 -152
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/devices.d.ts +46 -0
- package/dist/src/devices.js +114 -0
- package/dist/src/devices.js.map +1 -0
- package/dist/src/envelope.d.ts +3 -3
- package/dist/src/envelope.js +3 -3
- package/dist/src/forward.d.ts +4 -4
- package/dist/src/forward.js +1 -1
- package/dist/src/forward.js.map +1 -1
- package/dist/src/framing.d.ts +76 -0
- package/dist/src/framing.js +133 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/hub.d.ts +29 -9
- package/dist/src/hub.js +20 -5
- package/dist/src/hub.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +79 -17
- package/dist/src/index.js.map +1 -1
- package/dist/src/lan.d.ts +103 -0
- package/dist/src/lan.js +182 -0
- package/dist/src/lan.js.map +1 -0
- package/dist/src/pairing.d.ts +69 -0
- package/dist/src/pairing.js +125 -0
- package/dist/src/pairing.js.map +1 -0
- package/dist/src/protocol.d.ts +66 -23
- package/dist/src/protocol.js +4 -1
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/push.d.ts +104 -0
- package/dist/src/push.js +339 -0
- package/dist/src/push.js.map +1 -0
- package/dist/src/relay-transport.d.ts +62 -0
- package/dist/src/relay-transport.js +180 -0
- package/dist/src/relay-transport.js.map +1 -0
- package/dist/src/relay-wire.d.ts +25 -0
- package/dist/src/relay-wire.js +37 -0
- package/dist/src/relay-wire.js.map +1 -0
- package/dist/src/rendezvous.d.ts +57 -0
- package/dist/src/rendezvous.js +152 -0
- package/dist/src/rendezvous.js.map +1 -0
- package/dist/src/server.d.ts +19 -7
- package/dist/src/server.js +180 -63
- package/dist/src/server.js.map +1 -1
- package/dist/src/session-socket.d.ts +34 -0
- package/dist/src/session-socket.js +84 -0
- package/dist/src/session-socket.js.map +1 -0
- package/dist/src/session.d.ts +151 -0
- package/dist/src/session.js +241 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/tcp-transport.d.ts +37 -0
- package/dist/src/tcp-transport.js +106 -0
- package/dist/src/tcp-transport.js.map +1 -0
- package/dist/src/telemetry.d.ts +69 -0
- package/dist/src/telemetry.js +64 -0
- package/dist/src/telemetry.js.map +1 -0
- package/dist/src/transport.d.ts +75 -0
- package/dist/src/transport.js +136 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/workspace.d.ts +20 -5
- package/dist/src/workspace.js +19 -1
- package/dist/src/workspace.js.map +1 -1
- package/package.json +17 -4
package/dist/src/lan.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAN direct transport (docs/WEBRTC_MIGRATION.md Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Priority-2 path: when a client and daemon share a network segment they talk
|
|
5
|
+
* directly over TCP — no Cloudflare, no relay, works with the router's WAN link
|
|
6
|
+
* unplugged. The daemon:
|
|
7
|
+
* - advertises `_immediacy._tcp` over mDNS (TXT `rid=<rendezvous id>`) so a
|
|
8
|
+
* client can discover it without any typed address;
|
|
9
|
+
* - listens on TCP and, per connection, runs the §5.3 session handshake over
|
|
10
|
+
* the frame `Mux` (tcp-transport.ts + transport.ts), then hands the caller a
|
|
11
|
+
* `SecureChannel` for that peer.
|
|
12
|
+
*
|
|
13
|
+
* The handshake and frame codec are the SAME modules the loopback and (future)
|
|
14
|
+
* relay/WebRTC paths use — only the byte transport differs.
|
|
15
|
+
*/
|
|
16
|
+
import { createServer, connect as tcpConnect } from "node:net";
|
|
17
|
+
import { Bonjour } from "bonjour-service";
|
|
18
|
+
import { Mux } from "./transport.js";
|
|
19
|
+
import { TcpTransport } from "./tcp-transport.js";
|
|
20
|
+
import { Channel } from "./framing.js";
|
|
21
|
+
import { acceptHandshake, startHandshake, } from "./session.js";
|
|
22
|
+
/** bonjour "type" (it prepends `_` and appends `._tcp`) ⇒ `_immediacy._tcp`. */
|
|
23
|
+
export const LAN_SERVICE_TYPE = "immediacy";
|
|
24
|
+
/**
|
|
25
|
+
* Run the responder (daemon) side of the handshake over `mux`'s control channel:
|
|
26
|
+
* receive msg1, verify the device against `isPaired`, reply msg2, and resolve
|
|
27
|
+
* with the established `SecureChannel`. Rejects on an unknown/revoked device or a
|
|
28
|
+
* malformed/failed handshake.
|
|
29
|
+
*/
|
|
30
|
+
export function respondHandshakeOverMux(mux, opts, timeoutMs = 10_000) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timer = setTimeout(() => reject(new Error("handshake timeout")), timeoutMs);
|
|
33
|
+
let done = false;
|
|
34
|
+
mux.onMessage((channelId, message) => {
|
|
35
|
+
if (done || channelId !== Channel.control)
|
|
36
|
+
return;
|
|
37
|
+
done = true;
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
try {
|
|
40
|
+
const msg1 = JSON.parse(message.toString("utf8"));
|
|
41
|
+
const { msg2, channel, devicePub, paired, label } = acceptHandshake(msg1, opts);
|
|
42
|
+
void mux.send(Channel.control, Buffer.from(JSON.stringify(msg2), "utf8"));
|
|
43
|
+
resolve({ channel, devicePub, paired, label });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run the initiator (client) side over `mux`'s control channel: send msg1, wait
|
|
53
|
+
* for msg2, and resolve with the `SecureChannel`. Used by the LAN test and
|
|
54
|
+
* mirrored by the Flutter/macOS LAN clients.
|
|
55
|
+
*/
|
|
56
|
+
export function initiateHandshakeOverMux(mux, opts, timeoutMs = 10_000) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const { msg1, finish } = startHandshake(opts);
|
|
59
|
+
const timer = setTimeout(() => reject(new Error("handshake timeout")), timeoutMs);
|
|
60
|
+
mux.onMessage((channelId, message) => {
|
|
61
|
+
if (channelId !== Channel.control)
|
|
62
|
+
return;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
try {
|
|
65
|
+
const msg2 = JSON.parse(message.toString("utf8"));
|
|
66
|
+
resolve(finish(msg2));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
void mux.send(Channel.control, Buffer.from(JSON.stringify(msg1), "utf8"));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export class LanServer {
|
|
76
|
+
rendezvous;
|
|
77
|
+
server;
|
|
78
|
+
bonjour;
|
|
79
|
+
service;
|
|
80
|
+
constructor(rendezvous) {
|
|
81
|
+
this.rendezvous = rendezvous;
|
|
82
|
+
}
|
|
83
|
+
/** Called for each peer that completes the handshake over LAN. */
|
|
84
|
+
onSession;
|
|
85
|
+
/**
|
|
86
|
+
* Start the TCP listener and advertise it over mDNS. Returns the bound port.
|
|
87
|
+
* `advertise: false` skips mDNS (useful in tests / restricted networks).
|
|
88
|
+
*/
|
|
89
|
+
async start(port = 0, opts) {
|
|
90
|
+
const server = createServer((socket) => this.onConnection(socket));
|
|
91
|
+
this.server = server;
|
|
92
|
+
const boundPort = await new Promise((resolve, reject) => {
|
|
93
|
+
server.once("error", reject);
|
|
94
|
+
server.listen(port, opts?.host, () => {
|
|
95
|
+
const addr = server.address();
|
|
96
|
+
resolve(typeof addr === "object" && addr ? addr.port : port);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
if (opts?.advertise !== false)
|
|
100
|
+
this.advertise(boundPort);
|
|
101
|
+
return boundPort;
|
|
102
|
+
}
|
|
103
|
+
advertise(port) {
|
|
104
|
+
try {
|
|
105
|
+
this.bonjour = new Bonjour();
|
|
106
|
+
this.service = this.bonjour.publish({
|
|
107
|
+
name: `immediacy-${this.rendezvous.identity.rendezvousId().slice(0, 8)}`,
|
|
108
|
+
type: LAN_SERVICE_TYPE,
|
|
109
|
+
port,
|
|
110
|
+
txt: { rid: this.rendezvous.identity.rendezvousId() },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// mDNS can fail on locked-down networks; LAN still works if the client
|
|
115
|
+
// reaches the port another way. Best-effort.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
onConnection(socket) {
|
|
119
|
+
const transport = new TcpTransport(socket);
|
|
120
|
+
const mux = new Mux(transport);
|
|
121
|
+
const pairing = this.rendezvous.pairing;
|
|
122
|
+
respondHandshakeOverMux(mux, {
|
|
123
|
+
daemonSignSecret: this.rendezvous.identity.signSecretKey(),
|
|
124
|
+
rendezvousId: this.rendezvous.identity.rendezvousId(),
|
|
125
|
+
isPaired: (dev) => this.rendezvous.devices.has(dev),
|
|
126
|
+
redeemCode: pairing ? (_dev, code) => pairing.consume(code) : undefined,
|
|
127
|
+
})
|
|
128
|
+
.then(async ({ channel, devicePub, paired, label }) => {
|
|
129
|
+
// A first-pairing handshake authorized via a valid code — record the
|
|
130
|
+
// device now (the code is already consumed) before serving the session.
|
|
131
|
+
if (paired)
|
|
132
|
+
await this.rendezvous.devices.add(devicePub, label ?? "device");
|
|
133
|
+
void this.rendezvous.devices.touch(devicePub).catch(() => { });
|
|
134
|
+
this.onSession?.({
|
|
135
|
+
mux,
|
|
136
|
+
channel,
|
|
137
|
+
devicePub,
|
|
138
|
+
close: () => transport.close(),
|
|
139
|
+
onClose: (cb) => transport.onClose(cb),
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
.catch(() => transport.close());
|
|
143
|
+
}
|
|
144
|
+
async stop() {
|
|
145
|
+
this.service?.stop?.();
|
|
146
|
+
this.bonjour?.destroy();
|
|
147
|
+
await new Promise((resolve) => {
|
|
148
|
+
if (!this.server)
|
|
149
|
+
return resolve();
|
|
150
|
+
this.server.close(() => resolve());
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Browse the LAN for advertised daemons. Calls `onPeer` for each service that
|
|
156
|
+
* carries a rendezvous-id TXT record. Returns a stop function.
|
|
157
|
+
*/
|
|
158
|
+
export function browseLan(onPeer) {
|
|
159
|
+
const bonjour = new Bonjour();
|
|
160
|
+
const browser = bonjour.find({ type: LAN_SERVICE_TYPE }, (service) => {
|
|
161
|
+
const rid = service.txt?.rid;
|
|
162
|
+
const host = service.referer?.address ?? service.host;
|
|
163
|
+
if (rid && host) {
|
|
164
|
+
onPeer({ rendezvousId: rid, host, port: service.port, name: service.name });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return () => {
|
|
168
|
+
browser.stop();
|
|
169
|
+
bonjour.destroy();
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Open a LAN TCP connection to `host:port` and wrap it in a `Mux`. */
|
|
173
|
+
export function lanConnect(host, port) {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const socket = tcpConnect({ host, port }, () => {
|
|
176
|
+
const transport = new TcpTransport(socket);
|
|
177
|
+
resolve({ mux: new Mux(transport), close: () => transport.close() });
|
|
178
|
+
});
|
|
179
|
+
socket.once("error", reject);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=lan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lan.js","sourceRoot":"","sources":["../../src/lan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing — single-use pairing codes + the QR payload a client scans (§3.4).
|
|
3
|
+
*
|
|
4
|
+
* A pairing code is a 128-bit secret, **in-memory only**, with a 60 s TTL and
|
|
5
|
+
* single use: it is discarded the moment it is consumed or expires. It gates a
|
|
6
|
+
* *new* device joining the daemon's device table; once paired, a device
|
|
7
|
+
* reconnects with its own key and never needs a code again.
|
|
8
|
+
*
|
|
9
|
+
* The QR payload is small JSON: `{ v, r, k, c, s }` — protocol version,
|
|
10
|
+
* rendezvous id, daemon Ed25519 pubkey (base64), the pairing code (base64), and
|
|
11
|
+
* the signal-server URL. The client scans it, dials the rendezvous, presents the
|
|
12
|
+
* code + a fresh device key, and proceeds straight into the session handshake.
|
|
13
|
+
*/
|
|
14
|
+
/** Default signal server (§8: ownership/deploy pipeline still to be confirmed). */
|
|
15
|
+
export declare const DEFAULT_SIGNAL_URL = "wss://signal.ixiomsoftware.com";
|
|
16
|
+
/** Pairing-code lifetime. Short enough that a leaked QR photo goes stale fast. */
|
|
17
|
+
export declare const PAIRING_CODE_TTL_MS = 60000;
|
|
18
|
+
/** The QR payload a daemon renders and a client scans. Keep it tiny. */
|
|
19
|
+
export interface PairingPayload {
|
|
20
|
+
v: 1;
|
|
21
|
+
r: string;
|
|
22
|
+
k: string;
|
|
23
|
+
c: string;
|
|
24
|
+
s: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Issues and validates single-use pairing codes. All state is in memory, so a
|
|
28
|
+
* daemon restart invalidates every outstanding code — intentional.
|
|
29
|
+
*/
|
|
30
|
+
export declare class PairingCodes {
|
|
31
|
+
private pending;
|
|
32
|
+
private readonly ttlMs;
|
|
33
|
+
/** Injectable clock so tests don't sleep. */
|
|
34
|
+
private readonly now;
|
|
35
|
+
constructor(opts?: {
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
});
|
|
39
|
+
/** Mint a fresh 128-bit code, remember it until its TTL, and return it. */
|
|
40
|
+
issue(): {
|
|
41
|
+
code: string;
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Consume a code. Returns true exactly once for a live code, then discards it
|
|
46
|
+
* (single use). Returns false for an unknown, already-used, or expired code.
|
|
47
|
+
* Comparison is constant-time to avoid leaking which prefix matched.
|
|
48
|
+
*/
|
|
49
|
+
consume(code: string): boolean;
|
|
50
|
+
/** Number of live (unexpired) codes — for tests/telemetry. */
|
|
51
|
+
size(): number;
|
|
52
|
+
/** Drop expired codes so they never accumulate. */
|
|
53
|
+
private sweep;
|
|
54
|
+
}
|
|
55
|
+
/** Build the QR payload object a client scans. */
|
|
56
|
+
export declare function buildPairingPayload(opts: {
|
|
57
|
+
rendezvousId: string;
|
|
58
|
+
daemonPublicKeyB64: string;
|
|
59
|
+
code: string;
|
|
60
|
+
signalUrl?: string;
|
|
61
|
+
}): PairingPayload;
|
|
62
|
+
/** Serialize a pairing payload to the compact JSON string that goes in the QR. */
|
|
63
|
+
export declare function encodePairingPayload(payload: PairingPayload): string;
|
|
64
|
+
/**
|
|
65
|
+
* Render a pairing payload as a terminal QR code, with a plaintext fallback so
|
|
66
|
+
* the user can still pair on a terminal that can't draw the QR (or via copy /
|
|
67
|
+
* paste). Resolves to the string that was printed's source payload.
|
|
68
|
+
*/
|
|
69
|
+
export declare function renderPairingQr(payload: PairingPayload, out?: (line: string) => void): Promise<string>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing — single-use pairing codes + the QR payload a client scans (§3.4).
|
|
3
|
+
*
|
|
4
|
+
* A pairing code is a 128-bit secret, **in-memory only**, with a 60 s TTL and
|
|
5
|
+
* single use: it is discarded the moment it is consumed or expires. It gates a
|
|
6
|
+
* *new* device joining the daemon's device table; once paired, a device
|
|
7
|
+
* reconnects with its own key and never needs a code again.
|
|
8
|
+
*
|
|
9
|
+
* The QR payload is small JSON: `{ v, r, k, c, s }` — protocol version,
|
|
10
|
+
* rendezvous id, daemon Ed25519 pubkey (base64), the pairing code (base64), and
|
|
11
|
+
* the signal-server URL. The client scans it, dials the rendezvous, presents the
|
|
12
|
+
* code + a fresh device key, and proceeds straight into the session handshake.
|
|
13
|
+
*/
|
|
14
|
+
import sodium from "sodium-native";
|
|
15
|
+
import qrcode from "qrcode-terminal";
|
|
16
|
+
/** Default signal server (§8: ownership/deploy pipeline still to be confirmed). */
|
|
17
|
+
export const DEFAULT_SIGNAL_URL = "wss://signal.ixiomsoftware.com";
|
|
18
|
+
/** Pairing-code lifetime. Short enough that a leaked QR photo goes stale fast. */
|
|
19
|
+
export const PAIRING_CODE_TTL_MS = 60_000;
|
|
20
|
+
/** Pairing-code entropy: 128 bits, matching the rendezvous id. */
|
|
21
|
+
const PAIRING_CODE_BYTES = 16;
|
|
22
|
+
/**
|
|
23
|
+
* Issues and validates single-use pairing codes. All state is in memory, so a
|
|
24
|
+
* daemon restart invalidates every outstanding code — intentional.
|
|
25
|
+
*/
|
|
26
|
+
export class PairingCodes {
|
|
27
|
+
pending = new Map();
|
|
28
|
+
ttlMs;
|
|
29
|
+
/** Injectable clock so tests don't sleep. */
|
|
30
|
+
now;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.ttlMs = opts?.ttlMs ?? PAIRING_CODE_TTL_MS;
|
|
33
|
+
this.now = opts?.now ?? Date.now;
|
|
34
|
+
}
|
|
35
|
+
/** Mint a fresh 128-bit code, remember it until its TTL, and return it. */
|
|
36
|
+
issue() {
|
|
37
|
+
this.sweep();
|
|
38
|
+
const raw = Buffer.alloc(PAIRING_CODE_BYTES);
|
|
39
|
+
sodium.randombytes_buf(raw);
|
|
40
|
+
const code = raw.toString("base64");
|
|
41
|
+
const expiresAt = this.now() + this.ttlMs;
|
|
42
|
+
this.pending.set(code, { code, expiresAt });
|
|
43
|
+
return { code, expiresAt };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Consume a code. Returns true exactly once for a live code, then discards it
|
|
47
|
+
* (single use). Returns false for an unknown, already-used, or expired code.
|
|
48
|
+
* Comparison is constant-time to avoid leaking which prefix matched.
|
|
49
|
+
*/
|
|
50
|
+
consume(code) {
|
|
51
|
+
this.sweep();
|
|
52
|
+
if (typeof code !== "string")
|
|
53
|
+
return false;
|
|
54
|
+
let candidate;
|
|
55
|
+
try {
|
|
56
|
+
candidate = Buffer.from(code, "base64");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (candidate.length !== PAIRING_CODE_BYTES)
|
|
62
|
+
return false;
|
|
63
|
+
for (const [key, entry] of this.pending) {
|
|
64
|
+
const known = Buffer.from(entry.code, "base64");
|
|
65
|
+
if (known.length === candidate.length && sodium.sodium_memcmp(known, candidate)) {
|
|
66
|
+
this.pending.delete(key); // single use — gone even on the winning path
|
|
67
|
+
return entry.expiresAt > this.now();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** Number of live (unexpired) codes — for tests/telemetry. */
|
|
73
|
+
size() {
|
|
74
|
+
this.sweep();
|
|
75
|
+
return this.pending.size;
|
|
76
|
+
}
|
|
77
|
+
/** Drop expired codes so they never accumulate. */
|
|
78
|
+
sweep() {
|
|
79
|
+
const t = this.now();
|
|
80
|
+
for (const [key, entry] of this.pending) {
|
|
81
|
+
if (entry.expiresAt <= t)
|
|
82
|
+
this.pending.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Build the QR payload object a client scans. */
|
|
87
|
+
export function buildPairingPayload(opts) {
|
|
88
|
+
return {
|
|
89
|
+
v: 1,
|
|
90
|
+
r: opts.rendezvousId,
|
|
91
|
+
k: opts.daemonPublicKeyB64,
|
|
92
|
+
c: opts.code,
|
|
93
|
+
s: opts.signalUrl ?? DEFAULT_SIGNAL_URL,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Serialize a pairing payload to the compact JSON string that goes in the QR. */
|
|
97
|
+
export function encodePairingPayload(payload) {
|
|
98
|
+
return JSON.stringify(payload);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Render a pairing payload as a terminal QR code, with a plaintext fallback so
|
|
102
|
+
* the user can still pair on a terminal that can't draw the QR (or via copy /
|
|
103
|
+
* paste). Resolves to the string that was printed's source payload.
|
|
104
|
+
*/
|
|
105
|
+
export async function renderPairingQr(payload, out = (l) => console.log(l)) {
|
|
106
|
+
const encoded = encodePairingPayload(payload);
|
|
107
|
+
try {
|
|
108
|
+
const art = await new Promise((resolve, reject) => {
|
|
109
|
+
try {
|
|
110
|
+
qrcode.generate(encoded, { small: true }, (o) => resolve(o));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
reject(err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
out(art);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Fallback: some terminals/CI can't render — print the payload to scan/paste.
|
|
120
|
+
out("[pairing] QR unavailable — paste this into the app to pair:");
|
|
121
|
+
}
|
|
122
|
+
out(encoded);
|
|
123
|
+
return encoded;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|
package/dist/src/protocol.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `payload` is encrypted in the E2E layer (slice 5); until then it travels as
|
|
13
13
|
* plain JSON.
|
|
14
14
|
*/
|
|
15
|
-
export declare const PROTOCOL_VERSION:
|
|
15
|
+
export declare const PROTOCOL_VERSION: 4;
|
|
16
16
|
/** Workspace lifecycle status, surfaced as a badge in the client. */
|
|
17
17
|
export type WorkspaceStatus = "idle" | "running" | "awaiting_permission" | "done" | "error";
|
|
18
18
|
/** Generic envelope. `T` is the message `type`; `P` its payload shape. */
|
|
@@ -25,9 +25,9 @@ export interface Envelope<T extends MessageType = MessageType> {
|
|
|
25
25
|
payload: PayloadFor<T>;
|
|
26
26
|
}
|
|
27
27
|
/** Client → daemon. */
|
|
28
|
-
export type ClientMessageType = "project.list" | "project.create" | "project.delete" | "project.rename" | "project.reorder" | "project.branches" | "project.pulls" | "project.issues" | "issue.create" | "workspace.list" | "workspace.create" | "workspace.delete" | "workspace.archive" | "workspace.rename" | "workspace.action" | "workspace.setModel" | "delete.batch" | "prompt.send" | "agent.stop" | "permission.respond" | "question.respond" | "allowlist.list" | "allowlist.revoke" | "allowlist.clear" | "terminal.input" | "terminal.resize" | "terminal.snapshot" | "forward.list" | "forward.open" | "forward.data" | "forward.close" | "git.status" | "git.commit" | "git.push" | "git.resolve" | "git.merge" | "pr.create" | "pr.merge" | "workspace.open" | "diff.get" | "tree.get" | "file.get" | "file.set" | "media.get" | "chat.history" | "github.status" | "github.repos" | "models.list" | "job.list" | "job.create" | "job.update" | "job.delete" | "job.run" | "job.runs" | "settings.get" | "settings.set" | "config.set" | "log.tail" | "client.hello" | "
|
|
28
|
+
export type ClientMessageType = "project.list" | "project.create" | "project.delete" | "project.rename" | "project.reorder" | "project.branches" | "project.pulls" | "project.issues" | "issue.create" | "workspace.list" | "workspace.create" | "workspace.delete" | "workspace.archive" | "workspace.rename" | "workspace.action" | "workspace.setModel" | "delete.batch" | "prompt.send" | "agent.stop" | "permission.respond" | "question.respond" | "allowlist.list" | "allowlist.revoke" | "allowlist.clear" | "terminal.input" | "terminal.resize" | "terminal.snapshot" | "forward.list" | "forward.open" | "forward.data" | "forward.close" | "git.status" | "git.commit" | "git.push" | "git.resolve" | "git.merge" | "pr.create" | "pr.merge" | "workspace.open" | "diff.get" | "tree.get" | "file.get" | "file.set" | "media.get" | "chat.history" | "github.status" | "github.repos" | "models.list" | "job.list" | "job.create" | "job.update" | "job.delete" | "job.run" | "job.runs" | "settings.get" | "settings.set" | "config.set" | "log.tail" | "client.hello" | "pair.begin" | "session.begin" | "daemon.get" | "daemon.rename" | "daemon.update.check" | "daemon.update" | "push.register" | "push.unregister" | "ping" | "echo";
|
|
29
29
|
/** Daemon → client. */
|
|
30
|
-
export type DaemonMessageType = "project.list.result" | "project.branches.result" | "project.pulls.result" | "project.issues.result" | "workspace.list.result" | "workspace.created" | "workspace.renamed" | "delete.batch.result" | "event.assistant_message" | "event.assistant_image" | "event.tool_use" | "event.permission_request" | "event.permission_resolved" | "event.ask_question" | "event.question_resolved" | "event.session" | "event.agent_done" | "event.session_limit" | "event.system_notice" | "event.auth_failure" | "terminal.data" | "terminal.snapshot.result" | "forward.available" | "git.status.result" | "git.result" | "diff.result" | "tree.result" | "file.result" | "file.saved" | "media.result" | "chat.history.result" | "github.status.result" | "github.repos.result" | "models.list.result" | "job.list.result" | "job.runs.result" | "event.job_run" | "settings.result" | "allowlist.result" | "log.tail.result" | "log.event" | "
|
|
30
|
+
export type DaemonMessageType = "project.list.result" | "project.branches.result" | "project.pulls.result" | "project.issues.result" | "workspace.list.result" | "workspace.created" | "workspace.renamed" | "delete.batch.result" | "event.assistant_message" | "event.assistant_image" | "event.tool_use" | "event.permission_request" | "event.permission_resolved" | "event.ask_question" | "event.question_resolved" | "event.session" | "event.agent_done" | "event.session_limit" | "event.system_notice" | "event.auth_failure" | "terminal.data" | "terminal.snapshot.result" | "forward.available" | "git.status.result" | "git.result" | "diff.result" | "tree.result" | "file.result" | "file.saved" | "media.result" | "chat.history.result" | "github.status.result" | "github.repos.result" | "models.list.result" | "job.list.result" | "job.runs.result" | "event.job_run" | "settings.result" | "allowlist.result" | "log.tail.result" | "log.event" | "pair.begin.result" | "session.begin.result" | "daemon.info" | "daemon.update.result" | "pong" | "error" | "echo.result";
|
|
31
31
|
export type MessageType = ClientMessageType | DaemonMessageType;
|
|
32
32
|
export interface ProjectListPayload {
|
|
33
33
|
}
|
|
@@ -1154,26 +1154,51 @@ export interface GithubReposResultPayload {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
export interface PongPayload {
|
|
1156
1156
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1157
|
+
export type { HandshakeMsg1, HandshakeMsg2 } from "./session.js";
|
|
1158
|
+
import type { HandshakeMsg1, HandshakeMsg2 } from "./session.js";
|
|
1159
|
+
/**
|
|
1160
|
+
* Client → daemon: redeem a single-use pairing code (from the scanned QR) and
|
|
1161
|
+
* register this device's long-term Ed25519 public key. Sent cleartext (the code
|
|
1162
|
+
* is the pairing secret); the daemon adds the device to its table and replies
|
|
1163
|
+
* with its pinned identity so the client can persist a `KnownDaemon`.
|
|
1164
|
+
*/
|
|
1165
|
+
export interface PairBeginPayload {
|
|
1166
|
+
/** The single-use pairing code from the QR (`c` field), base64. */
|
|
1167
|
+
code: string;
|
|
1168
|
+
/** This device's long-term Ed25519 public key, base64. */
|
|
1169
|
+
devicePub: string;
|
|
1161
1170
|
/** Optional human label for the paired-devices record. */
|
|
1162
1171
|
label?: string;
|
|
1163
1172
|
}
|
|
1164
|
-
/** Daemon → client:
|
|
1165
|
-
export interface
|
|
1166
|
-
/**
|
|
1167
|
-
|
|
1168
|
-
/**
|
|
1169
|
-
|
|
1173
|
+
/** Daemon → client: the outcome of `pair.begin`. */
|
|
1174
|
+
export interface PairBeginResultPayload {
|
|
1175
|
+
/** True iff the code redeemed and the device was registered. */
|
|
1176
|
+
ok: boolean;
|
|
1177
|
+
/** The daemon's rendezvous id (base32) — present on success. */
|
|
1178
|
+
rendezvousId?: string;
|
|
1179
|
+
/** The daemon's long-term Ed25519 public key, base64 — present on success. */
|
|
1180
|
+
daemonPub?: string;
|
|
1181
|
+
/** The daemon's human name at pair time (for the client's `KnownDaemon`). */
|
|
1182
|
+
label?: string;
|
|
1183
|
+
/** Machine-readable failure reason (e.g. "bad_code", "bad_key") on !ok. */
|
|
1184
|
+
error?: string;
|
|
1170
1185
|
}
|
|
1171
|
-
/**
|
|
1172
|
-
|
|
1186
|
+
/**
|
|
1187
|
+
* Client → daemon: begin the §5.3 session handshake. Carries the signed
|
|
1188
|
+
* ephemeral-key message (msg1). Sent cleartext; the reply (msg2) is also
|
|
1189
|
+
* cleartext, after which BOTH sides seal every frame with the derived keys.
|
|
1190
|
+
*/
|
|
1191
|
+
export interface SessionBeginPayload {
|
|
1192
|
+
msg1: HandshakeMsg1;
|
|
1173
1193
|
}
|
|
1174
|
-
/** Daemon → client: the
|
|
1175
|
-
export interface
|
|
1176
|
-
|
|
1194
|
+
/** Daemon → client: the handshake reply, or a failure. */
|
|
1195
|
+
export interface SessionBeginResultPayload {
|
|
1196
|
+
/** True iff the daemon verified the device and produced its reply. */
|
|
1197
|
+
ok: boolean;
|
|
1198
|
+
/** The daemon's signed ephemeral-key message (msg2) — present on success. */
|
|
1199
|
+
msg2?: HandshakeMsg2;
|
|
1200
|
+
/** Machine-readable failure reason (e.g. "unknown_device", "bad_signature"). */
|
|
1201
|
+
error?: string;
|
|
1177
1202
|
}
|
|
1178
1203
|
/** Client → daemon: ask for this daemon's identity (name, id, host, version). */
|
|
1179
1204
|
export interface DaemonGetPayload {
|
|
@@ -1366,7 +1391,7 @@ export interface EventJobRunPayload {
|
|
|
1366
1391
|
/** Severity. `debug` is dropped unless LOG_DEBUG=1; warn/error always kept. */
|
|
1367
1392
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
1368
1393
|
/** Which subsystem an entry came from — drives the client-side filter. */
|
|
1369
|
-
export type LogCategory = "conn" | "model" | "agent" | "forward" | "error" | "system";
|
|
1394
|
+
export type LogCategory = "conn" | "model" | "agent" | "forward" | "push" | "error" | "system";
|
|
1370
1395
|
/**
|
|
1371
1396
|
* One structured log line. `seq` is a daemon-monotonic counter (resets to 0 on
|
|
1372
1397
|
* restart) and is the ordering + dedup key: clients render ascending `seq` and
|
|
@@ -1401,6 +1426,22 @@ export interface LogTailResultPayload {
|
|
|
1401
1426
|
export interface LogEventPayload {
|
|
1402
1427
|
entry: LogEntry;
|
|
1403
1428
|
}
|
|
1429
|
+
/** The platform a push token was minted on (drives the daemon's FCM payload). */
|
|
1430
|
+
export type PushPlatform = "android" | "ios" | "macos" | "web";
|
|
1431
|
+
/**
|
|
1432
|
+
* Client → daemon: register this device's FCM token so the daemon can remote-wake
|
|
1433
|
+
* it when no socket is live (a deferred permission, a finished turn, a usage
|
|
1434
|
+
* limit). Idempotent — re-registering the same token just refreshes it (FCM
|
|
1435
|
+
* rotates tokens, so the client re-sends on `onTokenRefresh`).
|
|
1436
|
+
*/
|
|
1437
|
+
export interface PushRegisterPayload {
|
|
1438
|
+
token: string;
|
|
1439
|
+
platform: PushPlatform;
|
|
1440
|
+
}
|
|
1441
|
+
/** Client → daemon: drop a previously-registered token (sign-out / disabled). */
|
|
1442
|
+
export interface PushUnregisterPayload {
|
|
1443
|
+
token: string;
|
|
1444
|
+
}
|
|
1404
1445
|
export interface PayloadMap {
|
|
1405
1446
|
"project.list": ProjectListPayload;
|
|
1406
1447
|
"project.create": ProjectCreatePayload;
|
|
@@ -1459,12 +1500,14 @@ export interface PayloadMap {
|
|
|
1459
1500
|
"config.set": ConfigSetPayload;
|
|
1460
1501
|
"log.tail": LogTailPayload;
|
|
1461
1502
|
"client.hello": ClientHelloPayload;
|
|
1462
|
-
|
|
1463
|
-
"
|
|
1503
|
+
"pair.begin": PairBeginPayload;
|
|
1504
|
+
"session.begin": SessionBeginPayload;
|
|
1464
1505
|
"daemon.get": DaemonGetPayload;
|
|
1465
1506
|
"daemon.rename": DaemonRenamePayload;
|
|
1466
1507
|
"daemon.update.check": DaemonUpdateCheckPayload;
|
|
1467
1508
|
"daemon.update": DaemonUpdatePayload;
|
|
1509
|
+
"push.register": PushRegisterPayload;
|
|
1510
|
+
"push.unregister": PushUnregisterPayload;
|
|
1468
1511
|
ping: PingPayload;
|
|
1469
1512
|
echo: EchoPayload;
|
|
1470
1513
|
"project.list.result": ProjectListResultPayload;
|
|
@@ -1508,8 +1551,8 @@ export interface PayloadMap {
|
|
|
1508
1551
|
"allowlist.result": AllowlistResultPayload;
|
|
1509
1552
|
"log.tail.result": LogTailResultPayload;
|
|
1510
1553
|
"log.event": LogEventPayload;
|
|
1511
|
-
"
|
|
1512
|
-
"
|
|
1554
|
+
"pair.begin.result": PairBeginResultPayload;
|
|
1555
|
+
"session.begin.result": SessionBeginResultPayload;
|
|
1513
1556
|
"daemon.info": DaemonInfoPayload;
|
|
1514
1557
|
"daemon.update.result": DaemonUpdateResultPayload;
|
|
1515
1558
|
pong: PongPayload;
|
package/dist/src/protocol.js
CHANGED
|
@@ -14,5 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
// v3: added `workspace.setModel` (change a workspace's model/effort before its
|
|
16
16
|
// first turn) and started populating `WorkspaceSummary.effort` on the wire.
|
|
17
|
-
|
|
17
|
+
// v4: added `push.register` / `push.unregister` (FCM device-token registration
|
|
18
|
+
// for daemon-driven remote wake). Additive + client→daemon only, so an older
|
|
19
|
+
// daemon just ignores it — the skew is a warning, never fatal.
|
|
20
|
+
export const PROTOCOL_VERSION = 4;
|
|
18
21
|
//# sourceMappingURL=protocol.js.map
|
package/dist/src/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+EAA+E;AAC/E,4EAA4E;AAC5E,+EAA+E;AAC/E,6EAA6E;AAC7E,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC"}
|