@agentdeck/bridge 1.0.15 → 1.0.17
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/auth.d.ts +30 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +162 -29
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +190 -27
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/esp32-serial.d.ts +23 -1
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +50 -1
- package/dist/esp32-serial.js.map +1 -1
- package/dist/http-auth-gate.d.ts +22 -5
- package/dist/http-auth-gate.d.ts.map +1 -1
- package/dist/http-auth-gate.js +14 -2
- package/dist/http-auth-gate.js.map +1 -1
- package/dist/pairing-window.d.ts +95 -0
- package/dist/pairing-window.d.ts.map +1 -0
- package/dist/pairing-window.js +268 -0
- package/dist/pairing-window.js.map +1 -0
- package/dist/passive-observer.d.ts +1 -0
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +13 -4
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +17 -9
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/session-registry.d.ts +1 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +23 -4
- package/dist/usage-event.js.map +1 -1
- package/dist/ws-server.d.ts +36 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +105 -2
- package/dist/ws-server.js.map +1 -1
- package/package.json +3 -3
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
/** Read existing token or generate a new one. */
|
|
2
2
|
export declare function getOrCreateToken(): string;
|
|
3
|
+
/** Tokens accepted for authentication but never handed out. */
|
|
4
|
+
export declare function getAcceptedTokens(): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Adopt the pairing token of a daemon that is already serving this machine.
|
|
7
|
+
*
|
|
8
|
+
* A machine has ONE fleet of paired devices but can be served by either the
|
|
9
|
+
* Node CLI daemon or the macOS app's in-process Swift daemon, and the two keep
|
|
10
|
+
* their token in different files — the sandboxed app cannot read
|
|
11
|
+
* `~/.agentdeck/auth-token` and this process must not poke at the app's
|
|
12
|
+
* container. So the credential travels over the one channel both can always
|
|
13
|
+
* use: the incumbent's loopback `/health`, which carries `pairingToken` to
|
|
14
|
+
* same-machine callers only. Whoever starts second adopts what the incumbent
|
|
15
|
+
* is already serving, so a later handover in EITHER direction is
|
|
16
|
+
* credential-neutral and start order stops mattering.
|
|
17
|
+
*
|
|
18
|
+
* The token we were holding moves to the accepted list rather than being
|
|
19
|
+
* dropped: devices provisioned under it must keep working until they are
|
|
20
|
+
* re-armed.
|
|
21
|
+
*
|
|
22
|
+
* No new trust is granted — `probeDaemonHealth` dials 127.0.0.1, and
|
|
23
|
+
* same-machine peers are already fully trusted by this daemon's access model.
|
|
24
|
+
*
|
|
25
|
+
* Returns true when the served token actually changed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function adoptPeerToken(peerToken: unknown): boolean;
|
|
3
28
|
/** Check if a connection originates from this machine (localhost or own LAN IPs). */
|
|
4
29
|
export declare function isLocalConnection(ip: string): boolean;
|
|
5
30
|
/**
|
|
@@ -10,7 +35,11 @@ export declare function isLocalConnection(ip: string): boolean;
|
|
|
10
35
|
* retired without hand-editing the file.
|
|
11
36
|
*/
|
|
12
37
|
export declare function rotateToken(): string;
|
|
13
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Validate a token against the one we serve, then against the ones we still
|
|
40
|
+
* accept (a token superseded by `adoptPeerToken`). Every candidate is compared
|
|
41
|
+
* — no early exit — so the answer costs the same whichever key matched.
|
|
42
|
+
*/
|
|
14
43
|
export declare function validateToken(token: string): boolean;
|
|
15
44
|
/** Build a ws:// URL with auth token for QR code pairing. */
|
|
16
45
|
export declare function getWsUrl(port: number): string;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAiDA,iDAAiD;AACjD,wBAAgB,gBAAgB,IAAI,MAAM,CA8BzC;AAOD,+DAA+D;AAC/D,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAiB5C;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAmB1D;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAYrD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAWpC;AAYD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7C"}
|
package/dist/auth.js
CHANGED
|
@@ -4,38 +4,157 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir, networkInterfaces } from 'os';
|
|
5
5
|
import { getLanIp } from '@agentdeck/shared';
|
|
6
6
|
import { debug } from './logger.js';
|
|
7
|
-
const
|
|
8
|
-
const TOKEN_FILE = join(AGENTDECK_DIR, 'auth-token');
|
|
7
|
+
const LEGACY_DIR = join(homedir(), '.agentdeck');
|
|
9
8
|
const TOKEN_LENGTH = 32; // 32 hex chars = 16 bytes
|
|
9
|
+
/** Bounded so a long-lived machine cannot accumulate an unbounded key ring. */
|
|
10
|
+
const MAX_ACCEPTED_TOKENS = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Every other module resolves its state through
|
|
13
|
+
* `AGENTDECK_DATA_DIR || ~/.agentdeck`; this one used to hardcode the home
|
|
14
|
+
* path, so a daemon started with a custom data dir kept its pairing token
|
|
15
|
+
* somewhere else than the rest of its state.
|
|
16
|
+
*/
|
|
17
|
+
function dataDir() {
|
|
18
|
+
return process.env.AGENTDECK_DATA_DIR || LEGACY_DIR;
|
|
19
|
+
}
|
|
20
|
+
function tokenFile(dir = dataDir()) {
|
|
21
|
+
return join(dir, 'auth-token');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Tokens this daemon still ACCEPTS but no longer hands out — the credential it
|
|
25
|
+
* held before adopting a peer daemon's one (see `adoptPeerToken`). Without it,
|
|
26
|
+
* convergence itself would lock out every device that was provisioned a moment
|
|
27
|
+
* earlier, which is the failure this whole path exists to prevent.
|
|
28
|
+
*/
|
|
29
|
+
function acceptedFile(dir = dataDir()) {
|
|
30
|
+
return join(dir, 'auth-token-accepted');
|
|
31
|
+
}
|
|
32
|
+
// Cached per resolved directory, so pointing AGENTDECK_DATA_DIR somewhere else
|
|
33
|
+
// re-reads instead of serving the previous directory's credential.
|
|
10
34
|
let cachedToken = null;
|
|
35
|
+
let cachedAccepted = null;
|
|
36
|
+
function readTokenFile(path) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(path))
|
|
39
|
+
return null;
|
|
40
|
+
const token = readFileSync(path, 'utf-8').trim();
|
|
41
|
+
return token.length >= TOKEN_LENGTH ? token : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
11
47
|
/** Read existing token or generate a new one. */
|
|
12
48
|
export function getOrCreateToken() {
|
|
13
|
-
|
|
14
|
-
|
|
49
|
+
const dir = dataDir();
|
|
50
|
+
if (cachedToken?.dir === dir)
|
|
51
|
+
return cachedToken.token;
|
|
52
|
+
let token = readTokenFile(tokenFile(dir));
|
|
53
|
+
// A custom data dir that has no token yet inherits the legacy one rather than
|
|
54
|
+
// minting a fresh one: generating here would silently un-pair every device
|
|
55
|
+
// the machine already has.
|
|
56
|
+
const inherited = token === null && dir !== LEGACY_DIR
|
|
57
|
+
? readTokenFile(tokenFile(LEGACY_DIR))
|
|
58
|
+
: null;
|
|
59
|
+
if (inherited)
|
|
60
|
+
token = inherited;
|
|
61
|
+
const generated = token === null;
|
|
62
|
+
if (token === null)
|
|
63
|
+
token = randomBytes(TOKEN_LENGTH / 2).toString('hex');
|
|
64
|
+
if (generated || inherited) {
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
writeFileSync(tokenFile(dir), token + '\n', { mode: 0o600 });
|
|
68
|
+
debug('auth', generated
|
|
69
|
+
? `Generated new auth token → ${tokenFile(dir)}`
|
|
70
|
+
: `Carried the existing auth token into ${tokenFile(dir)}`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
debug('auth', `Failed to write token file: ${err}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
cachedToken = { dir, token };
|
|
77
|
+
return token;
|
|
78
|
+
}
|
|
79
|
+
/** Shape check for a token handed to us by someone else. */
|
|
80
|
+
function isWellFormedToken(token) {
|
|
81
|
+
return typeof token === 'string' && token.trim().length >= TOKEN_LENGTH;
|
|
82
|
+
}
|
|
83
|
+
/** Tokens accepted for authentication but never handed out. */
|
|
84
|
+
export function getAcceptedTokens() {
|
|
85
|
+
const dir = dataDir();
|
|
86
|
+
if (cachedAccepted?.dir === dir)
|
|
87
|
+
return cachedAccepted.tokens;
|
|
88
|
+
let tokens = [];
|
|
15
89
|
try {
|
|
16
|
-
if (existsSync(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
90
|
+
if (existsSync(acceptedFile(dir))) {
|
|
91
|
+
tokens = readFileSync(acceptedFile(dir), 'utf-8')
|
|
92
|
+
.split('\n')
|
|
93
|
+
.map(line => line.trim())
|
|
94
|
+
.filter(isWellFormedToken)
|
|
95
|
+
.slice(0, MAX_ACCEPTED_TOKENS);
|
|
22
96
|
}
|
|
23
97
|
}
|
|
24
98
|
catch {
|
|
25
|
-
|
|
99
|
+
tokens = [];
|
|
26
100
|
}
|
|
27
|
-
|
|
28
|
-
|
|
101
|
+
cachedAccepted = { dir, tokens };
|
|
102
|
+
return tokens;
|
|
103
|
+
}
|
|
104
|
+
function writeAcceptedTokens(tokens) {
|
|
105
|
+
const dir = dataDir();
|
|
106
|
+
const bounded = tokens.slice(0, MAX_ACCEPTED_TOKENS);
|
|
107
|
+
cachedAccepted = { dir, tokens: bounded };
|
|
29
108
|
try {
|
|
30
|
-
mkdirSync(
|
|
31
|
-
writeFileSync(
|
|
32
|
-
debug('auth', `Generated new auth token → ${TOKEN_FILE}`);
|
|
109
|
+
mkdirSync(dir, { recursive: true });
|
|
110
|
+
writeFileSync(acceptedFile(dir), bounded.map(t => t + '\n').join(''), { mode: 0o600 });
|
|
33
111
|
}
|
|
34
112
|
catch (err) {
|
|
35
|
-
debug('auth', `Failed to write token file: ${err}`);
|
|
113
|
+
debug('auth', `Failed to write accepted-token file: ${err}`);
|
|
36
114
|
}
|
|
37
|
-
|
|
38
|
-
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Adopt the pairing token of a daemon that is already serving this machine.
|
|
118
|
+
*
|
|
119
|
+
* A machine has ONE fleet of paired devices but can be served by either the
|
|
120
|
+
* Node CLI daemon or the macOS app's in-process Swift daemon, and the two keep
|
|
121
|
+
* their token in different files — the sandboxed app cannot read
|
|
122
|
+
* `~/.agentdeck/auth-token` and this process must not poke at the app's
|
|
123
|
+
* container. So the credential travels over the one channel both can always
|
|
124
|
+
* use: the incumbent's loopback `/health`, which carries `pairingToken` to
|
|
125
|
+
* same-machine callers only. Whoever starts second adopts what the incumbent
|
|
126
|
+
* is already serving, so a later handover in EITHER direction is
|
|
127
|
+
* credential-neutral and start order stops mattering.
|
|
128
|
+
*
|
|
129
|
+
* The token we were holding moves to the accepted list rather than being
|
|
130
|
+
* dropped: devices provisioned under it must keep working until they are
|
|
131
|
+
* re-armed.
|
|
132
|
+
*
|
|
133
|
+
* No new trust is granted — `probeDaemonHealth` dials 127.0.0.1, and
|
|
134
|
+
* same-machine peers are already fully trusted by this daemon's access model.
|
|
135
|
+
*
|
|
136
|
+
* Returns true when the served token actually changed.
|
|
137
|
+
*/
|
|
138
|
+
export function adoptPeerToken(peerToken) {
|
|
139
|
+
if (!isWellFormedToken(peerToken))
|
|
140
|
+
return false;
|
|
141
|
+
const adopted = peerToken.trim();
|
|
142
|
+
const current = getOrCreateToken();
|
|
143
|
+
if (adopted === current)
|
|
144
|
+
return false;
|
|
145
|
+
const accepted = [current, ...getAcceptedTokens().filter(t => t !== current && t !== adopted)];
|
|
146
|
+
writeAcceptedTokens(accepted);
|
|
147
|
+
const dir = dataDir();
|
|
148
|
+
try {
|
|
149
|
+
mkdirSync(dir, { recursive: true });
|
|
150
|
+
writeFileSync(tokenFile(dir), adopted + '\n', { mode: 0o600 });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
debug('auth', `Failed to persist adopted token: ${err}`);
|
|
154
|
+
}
|
|
155
|
+
cachedToken = { dir, token: adopted };
|
|
156
|
+
debug('auth', 'Adopted the incumbent daemon\'s pairing token; previous one still accepted');
|
|
157
|
+
return true;
|
|
39
158
|
}
|
|
40
159
|
/** Check if a connection originates from this machine (localhost or own LAN IPs). */
|
|
41
160
|
export function isLocalConnection(ip) {
|
|
@@ -62,24 +181,38 @@ export function isLocalConnection(ip) {
|
|
|
62
181
|
*/
|
|
63
182
|
export function rotateToken() {
|
|
64
183
|
const token = randomBytes(TOKEN_LENGTH / 2).toString('hex');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
184
|
+
const dir = dataDir();
|
|
185
|
+
mkdirSync(dir, { recursive: true });
|
|
186
|
+
writeFileSync(tokenFile(dir), token + '\n', { mode: 0o600 });
|
|
187
|
+
cachedToken = { dir, token };
|
|
188
|
+
// Retiring a leaked token has to retire the key ring with it, or the leaked
|
|
189
|
+
// one survives in the accepted list and rotation buys nothing.
|
|
190
|
+
writeAcceptedTokens([]);
|
|
191
|
+
debug('auth', `Rotated auth token → ${tokenFile(dir)}`);
|
|
69
192
|
return token;
|
|
70
193
|
}
|
|
71
|
-
|
|
72
|
-
export function validateToken(token) {
|
|
73
|
-
const stored = getOrCreateToken();
|
|
194
|
+
function matchesToken(candidate, stored) {
|
|
74
195
|
// Constant-time comparison to prevent timing attacks
|
|
75
|
-
if (
|
|
196
|
+
if (candidate.length !== stored.length)
|
|
76
197
|
return false;
|
|
77
198
|
let result = 0;
|
|
78
|
-
for (let i = 0; i <
|
|
79
|
-
result |=
|
|
199
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
200
|
+
result |= candidate.charCodeAt(i) ^ stored.charCodeAt(i);
|
|
80
201
|
}
|
|
81
202
|
return result === 0;
|
|
82
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Validate a token against the one we serve, then against the ones we still
|
|
206
|
+
* accept (a token superseded by `adoptPeerToken`). Every candidate is compared
|
|
207
|
+
* — no early exit — so the answer costs the same whichever key matched.
|
|
208
|
+
*/
|
|
209
|
+
export function validateToken(token) {
|
|
210
|
+
let ok = matchesToken(token, getOrCreateToken());
|
|
211
|
+
for (const accepted of getAcceptedTokens()) {
|
|
212
|
+
ok = matchesToken(token, accepted) || ok;
|
|
213
|
+
}
|
|
214
|
+
return ok;
|
|
215
|
+
}
|
|
83
216
|
/** Build a ws:// URL with auth token for QR code pairing. */
|
|
84
217
|
export function getWsUrl(port) {
|
|
85
218
|
const ip = getLanIp();
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACjD,MAAM,YAAY,GAAG,EAAE,CAAC,CAAC,0BAA0B;AACnD,+EAA+E;AAC/E,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,SAAS,OAAO;IACd,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,UAAU,CAAC;AACtD,CAAC;AACD,SAAS,SAAS,CAAC,GAAG,GAAG,OAAO,EAAE;IAChC,OAAO,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AACjC,CAAC;AACD;;;;;GAKG;AACH,SAAS,YAAY,CAAC,GAAG,GAAG,OAAO,EAAE;IACnC,OAAO,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;AAC1C,CAAC;AAED,+EAA+E;AAC/E,mEAAmE;AACnE,IAAI,WAAW,GAA0C,IAAI,CAAC;AAC9D,IAAI,cAAc,GAA6C,IAAI,CAAC;AAEpE,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,WAAW,EAAE,GAAG,KAAK,GAAG;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC;IAEvD,IAAI,KAAK,GAAG,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,8EAA8E;IAC9E,2EAA2E;IAC3E,2BAA2B;IAC3B,MAAM,SAAS,GAAG,KAAK,KAAK,IAAI,IAAI,GAAG,KAAK,UAAU;QACpD,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACtC,CAAC,CAAC,IAAI,CAAC;IACT,IAAI,SAAS;QAAE,KAAK,GAAG,SAAS,CAAC;IAEjC,MAAM,SAAS,GAAG,KAAK,KAAK,IAAI,CAAC;IACjC,IAAI,KAAK,KAAK,IAAI;QAAE,KAAK,GAAG,WAAW,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE1E,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACpC,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC7D,KAAK,CAAC,MAAM,EAAE,SAAS;gBACrB,CAAC,CAAC,8BAA8B,SAAS,CAAC,GAAG,CAAC,EAAE;gBAChD,CAAC,CAAC,wCAAwC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,MAAM,EAAE,+BAA+B,GAAG,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED,WAAW,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC7B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4DAA4D;AAC5D,SAAS,iBAAiB,CAAC,KAAc;IACvC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,YAAY,CAAC;AAC1E,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,iBAAiB;IAC/B,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,cAAc,EAAE,GAAG,KAAK,GAAG;QAAE,OAAO,cAAc,CAAC,MAAM,CAAC;IAC9D,IAAI,MAAM,GAAa,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAClC,MAAM,GAAG,YAAY,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;iBAC9C,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBACxB,MAAM,CAAC,iBAAiB,CAAC;iBACzB,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,EAAE,CAAC;IACd,CAAC;IACD,cAAc,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IACjC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,mBAAmB,CAAC,MAAgB;IAC3C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;IACrD,cAAc,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC1C,IAAI,CAAC;QACH,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,wCAAwC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,SAAkB;IAC/C,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;IACnC,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAEtC,MAAM,QAAQ,GAAG,CAAC,OAAO,EAAE,GAAG,iBAAiB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC;IAC/F,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAE9B,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,oCAAoC,GAAG,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,WAAW,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACtC,KAAK,CAAC,MAAM,EAAE,4EAA4E,CAAC,CAAC;IAC5F,OAAO,IAAI,CAAC;AACd,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,IAAI,EAAE,KAAK,WAAW,IAAI,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,kBAAkB;QAAE,OAAO,IAAI,CAAC;IAEjF,+DAA+D;IAC/D,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE,IAAI,UAAU,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE;gBAAE,OAAO,IAAI,CAAC;QACpE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,WAAW,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,WAAW,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC7B,4EAA4E;IAC5E,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACxB,KAAK,CAAC,MAAM,EAAE,wBAAwB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACxD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB,EAAE,MAAc;IACrD,qDAAqD;IACrD,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACjD,KAAK,MAAM,QAAQ,IAAI,iBAAiB,EAAE,EAAE,CAAC;QAC3C,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC;IACtB,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,OAAO,QAAQ,EAAE,IAAI,IAAI,UAAU,KAAK,EAAE,CAAC;AAC7C,CAAC"}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAyP1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AA+BD,gFAAgF;AAChF,QAAA,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,GAAG,WAAW,GAAG,UAAU,EAAE,MAAM,CAI5E,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAsBjE;AAKD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAyBV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAIR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,OAAO,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAER;AAID,QAAA,MAAM,OAAO,SAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAyP1D,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AA+BD,gFAAgF;AAChF,QAAA,MAAM,cAAc,EAAE,MAAM,CAAC,aAAa,GAAG,WAAW,GAAG,UAAU,EAAE,MAAM,CAI5E,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAsBjE;AAKD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,EAAE,CAyBV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAIR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,OAAO,cAAc,EACtC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,OAAO,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAER;AAID,QAAA,MAAM,OAAO,SAAgB,CAAC;AAs2E9B,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -590,6 +590,7 @@ daemon
|
|
|
590
590
|
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
591
591
|
.action(async (opts) => {
|
|
592
592
|
const { findExistingDaemon, probeDaemonHealth, readDaemonInfo, removeDaemonInfo, removeDaemonSession, requestDaemonStandDown, requestDaemonShutdown, waitForDaemonExit, waitForPortBindable } = await import('./session-registry.js');
|
|
593
|
+
const { adoptPeerToken } = await import('./auth.js');
|
|
593
594
|
// Reverse two-tier upgrade path: the macOS app may already own the canonical
|
|
594
595
|
// port with its in-process Swift daemon (Tier 1 — limited: no ADB devices,
|
|
595
596
|
// no subscription usage, …). A plain "already running → exit" would strand
|
|
@@ -601,6 +602,14 @@ daemon
|
|
|
601
602
|
const targetPort = opts.port ? parseInt(String(opts.port), 10) : BRIDGE_WS_PORT;
|
|
602
603
|
const incumbent = await probeDaemonHealth(targetPort);
|
|
603
604
|
if (incumbent?.mode === 'daemon') {
|
|
605
|
+
// Take over the fleet's credential along with the port. The app's daemon
|
|
606
|
+
// keeps its token inside its sandbox container, unreadable from here, so
|
|
607
|
+
// this loopback answer is the only place we can learn what every paired
|
|
608
|
+
// board is currently holding. Adopting it BEFORE the stand-down means the
|
|
609
|
+
// handover costs no device its pairing.
|
|
610
|
+
if (adoptPeerToken(incumbent.pairingToken)) {
|
|
611
|
+
log(`Adopted the incumbent daemon's pairing token so paired devices survive the handover.`);
|
|
612
|
+
}
|
|
604
613
|
if (incumbent.isSwift) {
|
|
605
614
|
log(`AgentDeck app's in-process daemon holds port ${targetPort} — requesting stand-down to take over with the full CLI feature set…`);
|
|
606
615
|
// Prefer /stand-down (clean demote: the app stays running as a client).
|
|
@@ -1349,6 +1358,96 @@ program
|
|
|
1349
1358
|
// qrcode not available
|
|
1350
1359
|
}
|
|
1351
1360
|
});
|
|
1361
|
+
program
|
|
1362
|
+
.command('pair')
|
|
1363
|
+
.description('Pair a device with a one-time code (no camera or USB needed)')
|
|
1364
|
+
.option('-p, --port <port>', 'Daemon port (auto-detected)')
|
|
1365
|
+
.option('-t, --ttl <seconds>', 'How long the window stays open')
|
|
1366
|
+
.option('-n, --devices <count>', 'How many devices this window may pair', '1')
|
|
1367
|
+
.option('--adopt <ip...>', 'Re-arm ESP32 board(s) at these IPs over WiFi — no cable. Read the address off the daemon\'s "Rejected <ip> (esp32)" line')
|
|
1368
|
+
.action(async (opts) => {
|
|
1369
|
+
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
1370
|
+
const { formatPairingCode, getLanIp, PAIRING_WINDOW_MS, PAIRING_MAX_FAILED_ATTEMPTS } = await import('@agentdeck/shared');
|
|
1371
|
+
const info = readDaemonInfo();
|
|
1372
|
+
const port = opts.port != null
|
|
1373
|
+
? parseInt(opts.port, 10)
|
|
1374
|
+
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
1375
|
+
const ttlMs = opts.ttl != null ? Math.round(parseFloat(opts.ttl) * 1000) : PAIRING_WINDOW_MS;
|
|
1376
|
+
const redemptions = parseInt(opts.devices, 10) || 1;
|
|
1377
|
+
const adoptEsp32Ips = Array.isArray(opts.adopt)
|
|
1378
|
+
? opts.adopt
|
|
1379
|
+
: (typeof opts.adopt === 'string' ? [opts.adopt] : []);
|
|
1380
|
+
const daemon = `http://127.0.0.1:${port}`;
|
|
1381
|
+
let opened;
|
|
1382
|
+
try {
|
|
1383
|
+
const res = await fetch(`${daemon}/pair/open`, {
|
|
1384
|
+
method: 'POST',
|
|
1385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1386
|
+
body: JSON.stringify({ ttlMs, redemptions, adoptEsp32Ips }),
|
|
1387
|
+
signal: AbortSignal.timeout(3000),
|
|
1388
|
+
});
|
|
1389
|
+
if (!res.ok)
|
|
1390
|
+
throw new Error(`daemon answered ${res.status}`);
|
|
1391
|
+
opened = await res.json();
|
|
1392
|
+
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
console.error(`Could not reach the daemon on port ${port}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1395
|
+
console.error('Start it with "agentdeck daemon start", then try again.');
|
|
1396
|
+
process.exitCode = 1;
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const lanIp = getLanIp();
|
|
1400
|
+
log('');
|
|
1401
|
+
log(` Pairing code: ${formatPairingCode(opened.code)}`);
|
|
1402
|
+
log('');
|
|
1403
|
+
log(` On the device: Settings → Connection → "Pair with code", enter it there.`);
|
|
1404
|
+
log(` The device finds this Mac itself (${lanIp}:${port}) — no cable, no QR scan.`);
|
|
1405
|
+
log('');
|
|
1406
|
+
log(` Valid for ${Math.round((opened.expiresAt - Date.now()) / 1000)}s, `
|
|
1407
|
+
+ `${opened.redemptions} device(s), ${PAIRING_MAX_FAILED_ATTEMPTS} wrong tries.`);
|
|
1408
|
+
if (opened.adoptEsp32Ips?.length) {
|
|
1409
|
+
log('');
|
|
1410
|
+
log(` Also re-arming ESP32 board(s) at ${opened.adoptEsp32Ips.join(', ')} —`);
|
|
1411
|
+
log(' they need no code; the credential is pushed on their next connect.');
|
|
1412
|
+
}
|
|
1413
|
+
log('');
|
|
1414
|
+
// Watch the window until it closes, so the operator sees who paired — and
|
|
1415
|
+
// sees a wrong code as it happens, which is the point of a short window
|
|
1416
|
+
// somebody is standing in front of.
|
|
1417
|
+
const seenRedemptions = new Set();
|
|
1418
|
+
let seenFailures = 0;
|
|
1419
|
+
for (;;) {
|
|
1420
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
1421
|
+
let status;
|
|
1422
|
+
try {
|
|
1423
|
+
const res = await fetch(`${daemon}/pair/status`, { signal: AbortSignal.timeout(2000) });
|
|
1424
|
+
status = await res.json();
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
console.error('Lost contact with the daemon — the pairing window is no longer being watched.');
|
|
1428
|
+
process.exitCode = 1;
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
for (const r of status.redemptions) {
|
|
1432
|
+
const key = `${r.at}:${r.ip}`;
|
|
1433
|
+
if (seenRedemptions.has(key))
|
|
1434
|
+
continue;
|
|
1435
|
+
seenRedemptions.add(key);
|
|
1436
|
+
log(` ✓ Paired ${r.name} (${r.kind}) at ${r.ip}`);
|
|
1437
|
+
}
|
|
1438
|
+
for (const f of status.failures.slice(seenFailures)) {
|
|
1439
|
+
log(` ✗ Wrong code from ${f.ip} — ${status.attemptsRemaining} attempt(s) left`);
|
|
1440
|
+
}
|
|
1441
|
+
seenFailures = status.failures.length;
|
|
1442
|
+
if (!status.open) {
|
|
1443
|
+
log('');
|
|
1444
|
+
log(seenRedemptions.size > 0
|
|
1445
|
+
? `Pairing window closed. ${seenRedemptions.size} device(s) paired.`
|
|
1446
|
+
: 'Pairing window closed with nothing paired. Run "agentdeck pair" again to retry.');
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1352
1451
|
program
|
|
1353
1452
|
.command('token')
|
|
1354
1453
|
.description('Show or rotate the pairing token (~/.agentdeck/auth-token)')
|