androdex 1.1.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/LICENSE +15 -0
- package/README.md +84 -0
- package/bin/androdex.js +8 -0
- package/bin/cli.js +105 -0
- package/package.json +40 -0
- package/src/bridge.js +363 -0
- package/src/codex-desktop-launcher.js +93 -0
- package/src/codex-desktop-refresher.js +723 -0
- package/src/codex-transport.js +218 -0
- package/src/daemon-control.js +191 -0
- package/src/daemon-runtime.js +135 -0
- package/src/daemon-store.js +92 -0
- package/src/git-handler.js +672 -0
- package/src/host-runtime.js +554 -0
- package/src/index.js +38 -0
- package/src/qr.js +26 -0
- package/src/rollout-watch.js +308 -0
- package/src/scripts/codex-refresh.applescript +51 -0
- package/src/secure-device-state.js +257 -0
- package/src/secure-transport.js +737 -0
- package/src/session-state.js +60 -0
- package/src/workspace-handler.js +464 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// FILE: rollout-watch.js
|
|
2
|
+
// Purpose: Shared rollout-file lookup/watch helpers for CLI inspection and desktop refresh.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: watchThreadRollout, createThreadRolloutActivityWatcher
|
|
5
|
+
// Depends on: fs, os, path, ./session-state
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { readLastActiveThread } = require("./session-state");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_WATCH_INTERVAL_MS = 1_000;
|
|
13
|
+
const DEFAULT_LOOKUP_TIMEOUT_MS = 5_000;
|
|
14
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 10_000;
|
|
15
|
+
const DEFAULT_TRANSIENT_ERROR_RETRY_LIMIT = 2;
|
|
16
|
+
|
|
17
|
+
// Polls one rollout file until it materializes and then reports size growth.
|
|
18
|
+
function createThreadRolloutActivityWatcher({
|
|
19
|
+
threadId,
|
|
20
|
+
intervalMs = DEFAULT_WATCH_INTERVAL_MS,
|
|
21
|
+
lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS,
|
|
22
|
+
idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
|
|
23
|
+
now = () => Date.now(),
|
|
24
|
+
fsModule = fs,
|
|
25
|
+
transientErrorRetryLimit = DEFAULT_TRANSIENT_ERROR_RETRY_LIMIT,
|
|
26
|
+
onEvent = () => {},
|
|
27
|
+
onIdle = () => {},
|
|
28
|
+
onTimeout = () => {},
|
|
29
|
+
onError = () => {},
|
|
30
|
+
} = {}) {
|
|
31
|
+
const resolvedThreadId = resolveThreadId(threadId);
|
|
32
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
33
|
+
const startedAt = now();
|
|
34
|
+
|
|
35
|
+
let isStopped = false;
|
|
36
|
+
let rolloutPath = null;
|
|
37
|
+
let lastSize = null;
|
|
38
|
+
let lastGrowthAt = startedAt;
|
|
39
|
+
let transientErrorCount = 0;
|
|
40
|
+
|
|
41
|
+
const tick = () => {
|
|
42
|
+
if (isStopped) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const currentTime = now();
|
|
48
|
+
|
|
49
|
+
if (!rolloutPath) {
|
|
50
|
+
if (currentTime - startedAt >= lookupTimeoutMs) {
|
|
51
|
+
onTimeout({ threadId: resolvedThreadId });
|
|
52
|
+
stop();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
rolloutPath = findRolloutFileForThread(sessionsRoot, resolvedThreadId, { fsModule });
|
|
57
|
+
if (!rolloutPath) {
|
|
58
|
+
transientErrorCount = 0;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lastSize = readFileSize(rolloutPath, fsModule);
|
|
63
|
+
lastGrowthAt = currentTime;
|
|
64
|
+
transientErrorCount = 0;
|
|
65
|
+
onEvent({
|
|
66
|
+
reason: "materialized",
|
|
67
|
+
threadId: resolvedThreadId,
|
|
68
|
+
rolloutPath,
|
|
69
|
+
size: lastSize,
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextSize = readFileSize(rolloutPath, fsModule);
|
|
75
|
+
transientErrorCount = 0;
|
|
76
|
+
if (nextSize > lastSize) {
|
|
77
|
+
lastSize = nextSize;
|
|
78
|
+
lastGrowthAt = currentTime;
|
|
79
|
+
onEvent({
|
|
80
|
+
reason: "growth",
|
|
81
|
+
threadId: resolvedThreadId,
|
|
82
|
+
rolloutPath,
|
|
83
|
+
size: nextSize,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (currentTime - lastGrowthAt >= idleTimeoutMs) {
|
|
89
|
+
onIdle({
|
|
90
|
+
threadId: resolvedThreadId,
|
|
91
|
+
rolloutPath,
|
|
92
|
+
size: lastSize,
|
|
93
|
+
});
|
|
94
|
+
stop();
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isRetryableFilesystemError(error) && transientErrorCount < transientErrorRetryLimit) {
|
|
98
|
+
transientErrorCount += 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onError(error);
|
|
103
|
+
stop();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const intervalId = setInterval(tick, intervalMs);
|
|
108
|
+
tick();
|
|
109
|
+
|
|
110
|
+
function stop() {
|
|
111
|
+
if (isStopped) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
isStopped = true;
|
|
116
|
+
clearInterval(intervalId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
stop,
|
|
121
|
+
get threadId() {
|
|
122
|
+
return resolvedThreadId;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function watchThreadRollout(threadId = "") {
|
|
128
|
+
const resolvedThreadId = resolveThreadId(threadId);
|
|
129
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
130
|
+
const rolloutPath = findRolloutFileForThread(sessionsRoot, resolvedThreadId);
|
|
131
|
+
|
|
132
|
+
if (!rolloutPath) {
|
|
133
|
+
throw new Error(`No rollout file found for thread ${resolvedThreadId}.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let offset = fs.statSync(rolloutPath).size;
|
|
137
|
+
let partialLine = "";
|
|
138
|
+
|
|
139
|
+
console.log(`[androdex] Watching thread ${resolvedThreadId}`);
|
|
140
|
+
console.log(`[androdex] Rollout file: ${rolloutPath}`);
|
|
141
|
+
console.log("[androdex] Waiting for new persisted events... (Ctrl+C to stop)");
|
|
142
|
+
|
|
143
|
+
const onChange = (current, previous) => {
|
|
144
|
+
if (current.size <= previous.size) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stream = fs.createReadStream(rolloutPath, {
|
|
149
|
+
start: offset,
|
|
150
|
+
end: current.size - 1,
|
|
151
|
+
encoding: "utf8",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let chunkBuffer = "";
|
|
155
|
+
stream.on("data", (chunk) => {
|
|
156
|
+
chunkBuffer += chunk;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
stream.on("end", () => {
|
|
160
|
+
offset = current.size;
|
|
161
|
+
const combined = partialLine + chunkBuffer;
|
|
162
|
+
const lines = combined.split("\n");
|
|
163
|
+
partialLine = lines.pop() || "";
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const formatted = formatRolloutLine(line);
|
|
167
|
+
if (formatted) {
|
|
168
|
+
console.log(formatted);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
fs.watchFile(rolloutPath, { interval: 700 }, onChange);
|
|
175
|
+
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
fs.unwatchFile(rolloutPath, onChange);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
process.on("SIGINT", cleanup);
|
|
182
|
+
process.on("SIGTERM", cleanup);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveThreadId(threadId) {
|
|
186
|
+
if (threadId && typeof threadId === "string") {
|
|
187
|
+
return threadId;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const last = readLastActiveThread();
|
|
191
|
+
if (last?.threadId) {
|
|
192
|
+
return last.threadId;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw new Error("No thread id provided and no remembered Androdex thread found.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resolveSessionsRoot() {
|
|
199
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
200
|
+
return path.join(codexHome, "sessions");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function findRolloutFileForThread(root, threadId, { fsModule = fs } = {}) {
|
|
204
|
+
if (!fsModule.existsSync(root)) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const stack = [root];
|
|
209
|
+
|
|
210
|
+
while (stack.length > 0) {
|
|
211
|
+
const current = stack.pop();
|
|
212
|
+
const entries = fsModule.readdirSync(current, { withFileTypes: true });
|
|
213
|
+
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const fullPath = path.join(current, entry.name);
|
|
216
|
+
if (entry.isDirectory()) {
|
|
217
|
+
stack.push(fullPath);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!entry.isFile()) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (entry.name.includes(threadId) && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
226
|
+
return fullPath;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatRolloutLine(rawLine) {
|
|
235
|
+
const trimmed = rawLine.trim();
|
|
236
|
+
if (!trimmed) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let parsed = null;
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(trimmed);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const timestamp = formatTimestamp(parsed.timestamp);
|
|
248
|
+
const payload = parsed.payload || {};
|
|
249
|
+
|
|
250
|
+
if (parsed.type === "event_msg") {
|
|
251
|
+
const eventType = payload.type;
|
|
252
|
+
if (eventType === "user_message") {
|
|
253
|
+
return `${timestamp} Phone: ${previewText(payload.message)}`;
|
|
254
|
+
}
|
|
255
|
+
if (eventType === "agent_message") {
|
|
256
|
+
return `${timestamp} Codex: ${previewText(payload.message)}`;
|
|
257
|
+
}
|
|
258
|
+
if (eventType === "task_started") {
|
|
259
|
+
return `${timestamp} Task started`;
|
|
260
|
+
}
|
|
261
|
+
if (eventType === "task_complete") {
|
|
262
|
+
return `${timestamp} Task complete`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatTimestamp(value) {
|
|
270
|
+
if (!value || typeof value !== "string") {
|
|
271
|
+
return "[time?]";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const date = new Date(value);
|
|
275
|
+
if (Number.isNaN(date.getTime())) {
|
|
276
|
+
return "[time?]";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return `[${date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}]`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function previewText(value) {
|
|
283
|
+
if (typeof value !== "string") {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
288
|
+
if (normalized.length <= 120) {
|
|
289
|
+
return normalized;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return `${normalized.slice(0, 117)}...`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function readFileSize(filePath, fsModule = fs) {
|
|
296
|
+
return fsModule.statSync(filePath).size;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isRetryableFilesystemError(error) {
|
|
300
|
+
return ["ENOENT", "EACCES", "EPERM", "EBUSY"].includes(error?.code);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
watchThreadRollout,
|
|
305
|
+
createThreadRolloutActivityWatcher,
|
|
306
|
+
resolveSessionsRoot,
|
|
307
|
+
findRolloutFileForThread,
|
|
308
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- FILE: codex-refresh.applescript
|
|
2
|
+
-- Purpose: Forces a non-destructive route bounce inside Codex so the target thread remounts without killing runs.
|
|
3
|
+
-- Layer: UI automation helper
|
|
4
|
+
-- Args: bundle id, app path fallback, optional target deep link
|
|
5
|
+
|
|
6
|
+
on run argv
|
|
7
|
+
set bundleId to item 1 of argv
|
|
8
|
+
set appPath to item 2 of argv
|
|
9
|
+
set targetUrl to ""
|
|
10
|
+
set bounceUrl to "codex://settings"
|
|
11
|
+
|
|
12
|
+
if (count of argv) is greater than or equal to 3 then
|
|
13
|
+
set targetUrl to item 3 of argv
|
|
14
|
+
end if
|
|
15
|
+
|
|
16
|
+
try
|
|
17
|
+
tell application "Finder" to activate
|
|
18
|
+
end try
|
|
19
|
+
|
|
20
|
+
delay 0.12
|
|
21
|
+
|
|
22
|
+
my openCodexUrl(bundleId, appPath, bounceUrl)
|
|
23
|
+
delay 0.18
|
|
24
|
+
|
|
25
|
+
if targetUrl is not "" then
|
|
26
|
+
my openCodexUrl(bundleId, appPath, targetUrl)
|
|
27
|
+
else
|
|
28
|
+
my openCodexUrl(bundleId, appPath, "")
|
|
29
|
+
end if
|
|
30
|
+
|
|
31
|
+
delay 0.18
|
|
32
|
+
try
|
|
33
|
+
tell application id bundleId to activate
|
|
34
|
+
end try
|
|
35
|
+
end run
|
|
36
|
+
|
|
37
|
+
on openCodexUrl(bundleId, appPath, targetUrl)
|
|
38
|
+
try
|
|
39
|
+
if targetUrl is not "" then
|
|
40
|
+
do shell script "open -b " & quoted form of bundleId & " " & quoted form of targetUrl
|
|
41
|
+
else
|
|
42
|
+
do shell script "open -b " & quoted form of bundleId
|
|
43
|
+
end if
|
|
44
|
+
on error
|
|
45
|
+
if targetUrl is not "" then
|
|
46
|
+
do shell script "open -a " & quoted form of appPath & " " & quoted form of targetUrl
|
|
47
|
+
else
|
|
48
|
+
do shell script "open -a " & quoted form of appPath
|
|
49
|
+
end if
|
|
50
|
+
end try
|
|
51
|
+
end openCodexUrl
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// FILE: secure-device-state.js
|
|
2
|
+
// Purpose: Persists the bridge device identity and trusted phone registry for E2EE pairing.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: loadOrCreateBridgeDeviceState, resetBridgeDeviceState, rememberTrustedPhone, getTrustedPhonePublicKey
|
|
5
|
+
// Depends on: fs, os, path, crypto, child_process
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { randomUUID, generateKeyPairSync } = require("crypto");
|
|
11
|
+
const { execFileSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const STORE_DIR = path.join(os.homedir(), ".androdex");
|
|
14
|
+
const STORE_FILE = path.join(STORE_DIR, "device-state.json");
|
|
15
|
+
const KEYCHAIN_SERVICE = "io.androdex.bridge.device-state";
|
|
16
|
+
const KEYCHAIN_ACCOUNT = "default";
|
|
17
|
+
|
|
18
|
+
function loadOrCreateBridgeDeviceState() {
|
|
19
|
+
const existingState = readBridgeDeviceState();
|
|
20
|
+
if (existingState) {
|
|
21
|
+
if (!existingState.hostId || existingState.version < 2) {
|
|
22
|
+
writeBridgeDeviceState(existingState);
|
|
23
|
+
}
|
|
24
|
+
return existingState;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nextState = createBridgeDeviceState();
|
|
28
|
+
writeBridgeDeviceState(nextState);
|
|
29
|
+
return nextState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resetBridgeDeviceState() {
|
|
33
|
+
const removedFileState = deleteStoredDeviceStateString();
|
|
34
|
+
const removedKeychainState = deleteKeychainStateString();
|
|
35
|
+
return {
|
|
36
|
+
hadState: removedFileState || removedKeychainState,
|
|
37
|
+
removedFileState,
|
|
38
|
+
removedKeychainState,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rememberTrustedPhone(state, phoneDeviceId, phoneIdentityPublicKey) {
|
|
43
|
+
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
44
|
+
const normalizedPublicKey = normalizeNonEmptyString(phoneIdentityPublicKey);
|
|
45
|
+
if (!normalizedDeviceId || !normalizedPublicKey) {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Androdex supports one trusted mobile client per bridge state, so a new trust record replaces old ones.
|
|
50
|
+
const nextState = {
|
|
51
|
+
...state,
|
|
52
|
+
trustedPhones: {
|
|
53
|
+
[normalizedDeviceId]: normalizedPublicKey,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
writeBridgeDeviceState(nextState);
|
|
57
|
+
return nextState;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getTrustedPhonePublicKey(state, phoneDeviceId) {
|
|
61
|
+
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
62
|
+
if (!normalizedDeviceId) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return state.trustedPhones?.[normalizedDeviceId] || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createBridgeDeviceState() {
|
|
69
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
70
|
+
const privateJwk = privateKey.export({ format: "jwk" });
|
|
71
|
+
const publicJwk = publicKey.export({ format: "jwk" });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
version: 2,
|
|
75
|
+
hostId: randomUUID(),
|
|
76
|
+
macDeviceId: randomUUID(),
|
|
77
|
+
macIdentityPublicKey: base64UrlToBase64(publicJwk.x),
|
|
78
|
+
macIdentityPrivateKey: base64UrlToBase64(privateJwk.d),
|
|
79
|
+
trustedPhones: {},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readBridgeDeviceState() {
|
|
84
|
+
const rawState = readStoredDeviceStateString();
|
|
85
|
+
if (!rawState) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return normalizeBridgeDeviceState(JSON.parse(rawState));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeBridgeDeviceState(state) {
|
|
97
|
+
const serialized = JSON.stringify(state, null, 2);
|
|
98
|
+
if (process.platform === "darwin" && writeKeychainStateString(serialized)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
103
|
+
fs.writeFileSync(STORE_FILE, serialized, { mode: 0o600 });
|
|
104
|
+
try {
|
|
105
|
+
fs.chmodSync(STORE_FILE, 0o600);
|
|
106
|
+
} catch {
|
|
107
|
+
// Best-effort only on filesystems that support POSIX modes.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readStoredDeviceStateString() {
|
|
112
|
+
if (process.platform === "darwin") {
|
|
113
|
+
const keychainValue = readKeychainStateString();
|
|
114
|
+
if (keychainValue) {
|
|
115
|
+
return keychainValue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(STORE_FILE)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return fs.readFileSync(STORE_FILE, "utf8");
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readKeychainStateString() {
|
|
131
|
+
try {
|
|
132
|
+
return execFileSync(
|
|
133
|
+
"security",
|
|
134
|
+
[
|
|
135
|
+
"find-generic-password",
|
|
136
|
+
"-s",
|
|
137
|
+
KEYCHAIN_SERVICE,
|
|
138
|
+
"-a",
|
|
139
|
+
KEYCHAIN_ACCOUNT,
|
|
140
|
+
"-w",
|
|
141
|
+
],
|
|
142
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
143
|
+
).trim();
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeKeychainStateString(value) {
|
|
150
|
+
try {
|
|
151
|
+
execFileSync(
|
|
152
|
+
"security",
|
|
153
|
+
[
|
|
154
|
+
"add-generic-password",
|
|
155
|
+
"-U",
|
|
156
|
+
"-s",
|
|
157
|
+
KEYCHAIN_SERVICE,
|
|
158
|
+
"-a",
|
|
159
|
+
KEYCHAIN_ACCOUNT,
|
|
160
|
+
"-w",
|
|
161
|
+
value,
|
|
162
|
+
],
|
|
163
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
164
|
+
);
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function deleteStoredDeviceStateString() {
|
|
172
|
+
const existed = fs.existsSync(STORE_FILE);
|
|
173
|
+
try {
|
|
174
|
+
fs.rmSync(STORE_FILE, { force: true });
|
|
175
|
+
return existed;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function deleteKeychainStateString() {
|
|
182
|
+
if (process.platform !== "darwin") {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
execFileSync(
|
|
188
|
+
"security",
|
|
189
|
+
[
|
|
190
|
+
"delete-generic-password",
|
|
191
|
+
"-s",
|
|
192
|
+
KEYCHAIN_SERVICE,
|
|
193
|
+
"-a",
|
|
194
|
+
KEYCHAIN_ACCOUNT,
|
|
195
|
+
],
|
|
196
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
197
|
+
);
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeBridgeDeviceState(rawState) {
|
|
205
|
+
const hostId = normalizeNonEmptyString(rawState?.hostId) || randomUUID();
|
|
206
|
+
const macDeviceId = normalizeNonEmptyString(rawState?.macDeviceId);
|
|
207
|
+
const macIdentityPublicKey = normalizeNonEmptyString(rawState?.macIdentityPublicKey);
|
|
208
|
+
const macIdentityPrivateKey = normalizeNonEmptyString(rawState?.macIdentityPrivateKey);
|
|
209
|
+
|
|
210
|
+
if (!macDeviceId || !macIdentityPublicKey || !macIdentityPrivateKey) {
|
|
211
|
+
throw new Error("Bridge device state is incomplete");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const trustedPhones = {};
|
|
215
|
+
if (rawState?.trustedPhones && typeof rawState.trustedPhones === "object") {
|
|
216
|
+
for (const [deviceId, publicKey] of Object.entries(rawState.trustedPhones)) {
|
|
217
|
+
const normalizedDeviceId = normalizeNonEmptyString(deviceId);
|
|
218
|
+
const normalizedPublicKey = normalizeNonEmptyString(publicKey);
|
|
219
|
+
if (!normalizedDeviceId || !normalizedPublicKey) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
trustedPhones[normalizedDeviceId] = normalizedPublicKey;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
version: 2,
|
|
228
|
+
hostId,
|
|
229
|
+
macDeviceId,
|
|
230
|
+
macIdentityPublicKey,
|
|
231
|
+
macIdentityPrivateKey,
|
|
232
|
+
trustedPhones,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeNonEmptyString(value) {
|
|
237
|
+
if (typeof value !== "string") {
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
return value.trim();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function base64UrlToBase64(value) {
|
|
244
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const padded = `${value}${"=".repeat((4 - (value.length % 4 || 4)) % 4)}`;
|
|
249
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
getTrustedPhonePublicKey,
|
|
254
|
+
loadOrCreateBridgeDeviceState,
|
|
255
|
+
rememberTrustedPhone,
|
|
256
|
+
resetBridgeDeviceState,
|
|
257
|
+
};
|