@btatum5/codex-bridge 0.1.0 → 1.3.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/README.md +473 -23
- package/bin/codex-bridge.js +136 -100
- package/bin/phodex.js +8 -0
- package/bin/remodex.js +8 -0
- package/package.json +38 -24
- package/src/bridge.js +622 -0
- package/src/codex-desktop-refresher.js +776 -0
- package/src/codex-transport.js +238 -0
- package/src/daemon-state.js +170 -0
- package/src/desktop-handler.js +407 -0
- package/src/git-handler.js +1267 -0
- package/src/index.js +35 -0
- package/src/macos-launch-agent.js +457 -0
- package/src/notifications-handler.js +95 -0
- package/src/push-notification-completion-dedupe.js +147 -0
- package/src/push-notification-service-client.js +151 -0
- package/src/push-notification-tracker.js +688 -0
- package/src/qr.js +19 -0
- package/src/rollout-live-mirror.js +730 -0
- package/src/rollout-watch.js +853 -0
- package/src/scripts/codex-handoff.applescript +100 -0
- package/src/scripts/codex-refresh.applescript +51 -0
- package/src/secure-device-state.js +430 -0
- package/src/secure-transport.js +738 -0
- package/src/session-state.js +62 -0
- package/src/thread-context-handler.js +80 -0
- package/src/workspace-handler.js +464 -0
- package/server.mjs +0 -290
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
-- FILE: codex-handoff.applescript
|
|
2
|
+
-- Purpose: Performs an explicit Codex.app relaunch before opening the requested thread.
|
|
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 appName to my resolveAppName(bundleId, appPath)
|
|
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 id bundleId to activate
|
|
18
|
+
end try
|
|
19
|
+
|
|
20
|
+
delay 0.1
|
|
21
|
+
|
|
22
|
+
try
|
|
23
|
+
tell application id bundleId to quit
|
|
24
|
+
end try
|
|
25
|
+
|
|
26
|
+
my confirmQuitPrompt(appName)
|
|
27
|
+
my waitForAppExit(appName, 40)
|
|
28
|
+
my openCodex(bundleId, appPath, "")
|
|
29
|
+
delay 1.2
|
|
30
|
+
|
|
31
|
+
if targetUrl is not "" then
|
|
32
|
+
my openCodex(bundleId, appPath, targetUrl)
|
|
33
|
+
end if
|
|
34
|
+
|
|
35
|
+
delay 0.2
|
|
36
|
+
try
|
|
37
|
+
tell application id bundleId to activate
|
|
38
|
+
end try
|
|
39
|
+
end run
|
|
40
|
+
|
|
41
|
+
on resolveAppName(bundleId, appPath)
|
|
42
|
+
try
|
|
43
|
+
tell application id bundleId to return name
|
|
44
|
+
on error
|
|
45
|
+
return do shell script "basename " & quoted form of appPath & " .app"
|
|
46
|
+
end try
|
|
47
|
+
end resolveAppName
|
|
48
|
+
|
|
49
|
+
on confirmQuitPrompt(appName)
|
|
50
|
+
repeat 20 times
|
|
51
|
+
try
|
|
52
|
+
tell application "System Events"
|
|
53
|
+
if exists process appName then
|
|
54
|
+
tell process appName
|
|
55
|
+
repeat with candidateWindow in windows
|
|
56
|
+
if exists button "Quit" of candidateWindow then
|
|
57
|
+
click button "Quit" of candidateWindow
|
|
58
|
+
return
|
|
59
|
+
end if
|
|
60
|
+
end repeat
|
|
61
|
+
end tell
|
|
62
|
+
end if
|
|
63
|
+
end tell
|
|
64
|
+
end try
|
|
65
|
+
|
|
66
|
+
delay 0.15
|
|
67
|
+
end repeat
|
|
68
|
+
end confirmQuitPrompt
|
|
69
|
+
|
|
70
|
+
on waitForAppExit(appName, maxAttempts)
|
|
71
|
+
repeat maxAttempts times
|
|
72
|
+
try
|
|
73
|
+
tell application "System Events"
|
|
74
|
+
if not (exists process appName) then
|
|
75
|
+
return
|
|
76
|
+
end if
|
|
77
|
+
end tell
|
|
78
|
+
on error
|
|
79
|
+
return
|
|
80
|
+
end try
|
|
81
|
+
|
|
82
|
+
delay 0.15
|
|
83
|
+
end repeat
|
|
84
|
+
end waitForAppExit
|
|
85
|
+
|
|
86
|
+
on openCodex(bundleId, appPath, targetUrl)
|
|
87
|
+
try
|
|
88
|
+
if targetUrl is not "" then
|
|
89
|
+
do shell script "open -b " & quoted form of bundleId & " " & quoted form of targetUrl
|
|
90
|
+
else
|
|
91
|
+
do shell script "open -b " & quoted form of bundleId
|
|
92
|
+
end if
|
|
93
|
+
on error
|
|
94
|
+
if targetUrl is not "" then
|
|
95
|
+
do shell script "open -a " & quoted form of appPath & " " & quoted form of targetUrl
|
|
96
|
+
else
|
|
97
|
+
do shell script "open -a " & quoted form of appPath
|
|
98
|
+
end if
|
|
99
|
+
end try
|
|
100
|
+
end openCodex
|
|
@@ -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,430 @@
|
|
|
1
|
+
// FILE: secure-device-state.js
|
|
2
|
+
// Purpose: Persists canonical bridge identity and trusted-phone state for local QR pairing.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: loadOrCreateBridgeDeviceState, resetBridgeDeviceState, rememberTrustedPhone, getTrustedPhonePublicKey, resolveBridgeRelaySession
|
|
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 DEFAULT_STORE_DIR = path.join(os.homedir(), ".codex", "bridge");
|
|
14
|
+
const LEGACY_STORE_DIR = path.join(os.homedir(), ".remodex");
|
|
15
|
+
const DEFAULT_STORE_FILE = path.join(DEFAULT_STORE_DIR, "device-state.json");
|
|
16
|
+
const KEYCHAIN_SERVICE = "com.codex.bridge.device-state";
|
|
17
|
+
const LEGACY_KEYCHAIN_SERVICE = "com.remodex.bridge.device-state";
|
|
18
|
+
const KEYCHAIN_ACCOUNT = "default";
|
|
19
|
+
let hasLoggedKeychainMismatch = false;
|
|
20
|
+
|
|
21
|
+
// Loads the canonical bridge state or bootstraps a fresh one when no trusted state exists yet.
|
|
22
|
+
function loadOrCreateBridgeDeviceState() {
|
|
23
|
+
const fileRecord = readCanonicalFileStateRecord();
|
|
24
|
+
const keychainRecord = readKeychainStateRecord();
|
|
25
|
+
|
|
26
|
+
if (fileRecord.state) {
|
|
27
|
+
reconcileLegacyKeychainMirror(fileRecord.state, keychainRecord);
|
|
28
|
+
return fileRecord.state;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (fileRecord.error) {
|
|
32
|
+
if (keychainRecord.state) {
|
|
33
|
+
warnOnce(
|
|
34
|
+
"[codex-bridge] Recovering the canonical device-state.json from the legacy Keychain pairing mirror."
|
|
35
|
+
);
|
|
36
|
+
writeBridgeDeviceState(keychainRecord.state);
|
|
37
|
+
return keychainRecord.state;
|
|
38
|
+
}
|
|
39
|
+
throw corruptedStateError("device-state.json", fileRecord.error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (keychainRecord.error) {
|
|
43
|
+
throw corruptedStateError("legacy Keychain bridge state", keychainRecord.error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (keychainRecord.state) {
|
|
47
|
+
writeBridgeDeviceState(keychainRecord.state);
|
|
48
|
+
return keychainRecord.state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const nextState = createBridgeDeviceState();
|
|
52
|
+
writeBridgeDeviceState(nextState);
|
|
53
|
+
return nextState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Removes the saved bridge identity/trust state so the next `codex-bridge up` requires a fresh QR pairing.
|
|
57
|
+
function resetBridgeDeviceState() {
|
|
58
|
+
const removedCanonicalFile = deleteCanonicalFileState();
|
|
59
|
+
const removedKeychainMirror = deleteKeychainStateString();
|
|
60
|
+
return {
|
|
61
|
+
hadState: removedCanonicalFile || removedKeychainMirror,
|
|
62
|
+
removedCanonicalFile,
|
|
63
|
+
removedKeychainMirror,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Generates a fresh relay session for every bridge launch so QR pairing stays explicit per-run.
|
|
68
|
+
function resolveBridgeRelaySession(state, { persist = true } = {}) {
|
|
69
|
+
return {
|
|
70
|
+
deviceState: state,
|
|
71
|
+
isPersistent: false,
|
|
72
|
+
sessionId: randomUUID(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Persists the trusted iPhone identity so reconnects can be authenticated during the current pairing flow.
|
|
77
|
+
function rememberTrustedPhone(state, phoneDeviceId, phoneIdentityPublicKey, { persist = true } = {}) {
|
|
78
|
+
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
79
|
+
const normalizedPublicKey = normalizeNonEmptyString(phoneIdentityPublicKey);
|
|
80
|
+
if (!normalizedDeviceId || !normalizedPublicKey) {
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Codex supports one trusted iPhone per Mac, so a new trust record replaces old ones.
|
|
85
|
+
const nextState = normalizeBridgeDeviceState({
|
|
86
|
+
...state,
|
|
87
|
+
trustedPhones: {
|
|
88
|
+
[normalizedDeviceId]: normalizedPublicKey,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (persist) {
|
|
92
|
+
writeBridgeDeviceState(nextState);
|
|
93
|
+
}
|
|
94
|
+
return nextState;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getTrustedPhonePublicKey(state, phoneDeviceId) {
|
|
98
|
+
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
99
|
+
if (!normalizedDeviceId) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return state.trustedPhones?.[normalizedDeviceId] || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasTrustedPhones(state) {
|
|
106
|
+
return Object.keys(state?.trustedPhones || {}).length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createBridgeDeviceState() {
|
|
110
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
111
|
+
const privateJwk = privateKey.export({ format: "jwk" });
|
|
112
|
+
const publicJwk = publicKey.export({ format: "jwk" });
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
version: 1,
|
|
116
|
+
macDeviceId: randomUUID(),
|
|
117
|
+
macIdentityPublicKey: base64UrlToBase64(publicJwk.x),
|
|
118
|
+
macIdentityPrivateKey: base64UrlToBase64(privateJwk.d),
|
|
119
|
+
trustedPhones: {},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Reads the canonical file-backed state and distinguishes "missing" from "corrupted".
|
|
124
|
+
function readCanonicalFileStateRecord() {
|
|
125
|
+
const storeFile = resolveStoreFile();
|
|
126
|
+
if (!fs.existsSync(storeFile)) {
|
|
127
|
+
return { state: null, error: null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
return {
|
|
132
|
+
state: normalizeBridgeDeviceState(JSON.parse(fs.readFileSync(storeFile, "utf8"))),
|
|
133
|
+
error: null,
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return { state: null, error };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Reads the legacy Keychain mirror so old installs can be migrated into the canonical file.
|
|
141
|
+
function readKeychainStateRecord() {
|
|
142
|
+
const rawState = readKeychainStateString();
|
|
143
|
+
if (!rawState) {
|
|
144
|
+
return { state: null, error: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
return {
|
|
149
|
+
state: normalizeBridgeDeviceState(JSON.parse(rawState)),
|
|
150
|
+
error: null,
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return { state: null, error };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function writeBridgeDeviceState(state) {
|
|
158
|
+
const serialized = JSON.stringify(state, null, 2);
|
|
159
|
+
writeCanonicalFileStateString(serialized);
|
|
160
|
+
writeKeychainStateString(serialized);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Keeps the canonical file updated even when the optional Keychain mirror is unavailable.
|
|
164
|
+
function writeCanonicalFileStateString(serialized) {
|
|
165
|
+
const storeDir = resolveStoreDir();
|
|
166
|
+
const storeFile = resolveStoreFile();
|
|
167
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(storeFile, serialized, { mode: 0o600 });
|
|
169
|
+
try {
|
|
170
|
+
fs.chmodSync(storeFile, 0o600);
|
|
171
|
+
} catch {
|
|
172
|
+
// Best-effort only on filesystems that support POSIX modes.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveStoreDir() {
|
|
177
|
+
return firstNonEmptyString([
|
|
178
|
+
process.env.CODEX_BRIDGE_DEVICE_STATE_DIR,
|
|
179
|
+
process.env.REMODEX_DEVICE_STATE_DIR,
|
|
180
|
+
]) || resolveLegacyAwareStoreDir();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveStoreFile() {
|
|
184
|
+
return firstNonEmptyString([
|
|
185
|
+
process.env.CODEX_BRIDGE_DEVICE_STATE_FILE,
|
|
186
|
+
process.env.REMODEX_DEVICE_STATE_FILE,
|
|
187
|
+
])
|
|
188
|
+
|| path.join(resolveStoreDir(), "device-state.json");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveKeychainMirrorFile() {
|
|
192
|
+
return firstNonEmptyString([
|
|
193
|
+
process.env.CODEX_BRIDGE_DEVICE_STATE_KEYCHAIN_MOCK_FILE,
|
|
194
|
+
process.env.REMODEX_DEVICE_STATE_KEYCHAIN_MOCK_FILE,
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readKeychainStateString() {
|
|
199
|
+
const keychainMirrorFile = resolveKeychainMirrorFile();
|
|
200
|
+
if (keychainMirrorFile) {
|
|
201
|
+
try {
|
|
202
|
+
return fs.readFileSync(keychainMirrorFile, "utf8");
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (process.platform !== "darwin") {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const serviceName of [KEYCHAIN_SERVICE, LEGACY_KEYCHAIN_SERVICE]) {
|
|
213
|
+
try {
|
|
214
|
+
return execFileSync(
|
|
215
|
+
"security",
|
|
216
|
+
[
|
|
217
|
+
"find-generic-password",
|
|
218
|
+
"-s",
|
|
219
|
+
serviceName,
|
|
220
|
+
"-a",
|
|
221
|
+
KEYCHAIN_ACCOUNT,
|
|
222
|
+
"-w",
|
|
223
|
+
],
|
|
224
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
225
|
+
).trim();
|
|
226
|
+
} catch {
|
|
227
|
+
// Try the next compatible service name.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeKeychainStateString(value) {
|
|
235
|
+
const keychainMirrorFile = resolveKeychainMirrorFile();
|
|
236
|
+
if (keychainMirrorFile) {
|
|
237
|
+
try {
|
|
238
|
+
fs.mkdirSync(path.dirname(keychainMirrorFile), { recursive: true });
|
|
239
|
+
fs.writeFileSync(keychainMirrorFile, value, { mode: 0o600 });
|
|
240
|
+
return true;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (process.platform !== "darwin") {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
execFileSync(
|
|
252
|
+
"security",
|
|
253
|
+
[
|
|
254
|
+
"add-generic-password",
|
|
255
|
+
"-U",
|
|
256
|
+
"-s",
|
|
257
|
+
KEYCHAIN_SERVICE,
|
|
258
|
+
"-a",
|
|
259
|
+
KEYCHAIN_ACCOUNT,
|
|
260
|
+
"-w",
|
|
261
|
+
value,
|
|
262
|
+
],
|
|
263
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
264
|
+
);
|
|
265
|
+
return true;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function deleteKeychainStateString() {
|
|
272
|
+
const keychainMirrorFile = resolveKeychainMirrorFile();
|
|
273
|
+
if (keychainMirrorFile) {
|
|
274
|
+
const existed = fs.existsSync(keychainMirrorFile);
|
|
275
|
+
try {
|
|
276
|
+
fs.rmSync(keychainMirrorFile, { force: true });
|
|
277
|
+
return existed;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (process.platform !== "darwin") {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let removed = false;
|
|
288
|
+
for (const serviceName of [KEYCHAIN_SERVICE, LEGACY_KEYCHAIN_SERVICE]) {
|
|
289
|
+
try {
|
|
290
|
+
execFileSync(
|
|
291
|
+
"security",
|
|
292
|
+
[
|
|
293
|
+
"delete-generic-password",
|
|
294
|
+
"-s",
|
|
295
|
+
serviceName,
|
|
296
|
+
"-a",
|
|
297
|
+
KEYCHAIN_ACCOUNT,
|
|
298
|
+
],
|
|
299
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
300
|
+
);
|
|
301
|
+
removed = true;
|
|
302
|
+
} catch {
|
|
303
|
+
// Keep removing compatible service names.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return removed;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function deleteCanonicalFileState() {
|
|
311
|
+
const storeFile = resolveStoreFile();
|
|
312
|
+
const existed = fs.existsSync(storeFile);
|
|
313
|
+
try {
|
|
314
|
+
fs.rmSync(storeFile, { force: true });
|
|
315
|
+
return existed;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Prefers the canonical file, but repairs or warns about stale legacy Keychain mirrors.
|
|
322
|
+
function reconcileLegacyKeychainMirror(canonicalState, keychainRecord) {
|
|
323
|
+
if (keychainRecord.error) {
|
|
324
|
+
warnOnce("[codex-bridge] Ignoring unreadable legacy Keychain pairing mirror; using canonical device-state.json.");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!keychainRecord.state) {
|
|
329
|
+
writeKeychainStateString(JSON.stringify(canonicalState, null, 2));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (bridgeStatesEqual(canonicalState, keychainRecord.state)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
warnOnce("[codex-bridge] Canonical bridge pairing state differs from the legacy Keychain mirror; using device-state.json.");
|
|
338
|
+
writeKeychainStateString(JSON.stringify(canonicalState, null, 2));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeBridgeDeviceState(rawState) {
|
|
342
|
+
const macDeviceId = normalizeNonEmptyString(rawState?.macDeviceId);
|
|
343
|
+
const macIdentityPublicKey = normalizeNonEmptyString(rawState?.macIdentityPublicKey);
|
|
344
|
+
const macIdentityPrivateKey = normalizeNonEmptyString(rawState?.macIdentityPrivateKey);
|
|
345
|
+
|
|
346
|
+
if (!macDeviceId || !macIdentityPublicKey || !macIdentityPrivateKey) {
|
|
347
|
+
throw new Error("Bridge device state is incomplete");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const trustedPhones = {};
|
|
351
|
+
if (rawState?.trustedPhones && typeof rawState.trustedPhones === "object") {
|
|
352
|
+
for (const [deviceId, publicKey] of Object.entries(rawState.trustedPhones)) {
|
|
353
|
+
const normalizedDeviceId = normalizeNonEmptyString(deviceId);
|
|
354
|
+
const normalizedPublicKey = normalizeNonEmptyString(publicKey);
|
|
355
|
+
if (!normalizedDeviceId || !normalizedPublicKey) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
trustedPhones[normalizedDeviceId] = normalizedPublicKey;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
version: 1,
|
|
364
|
+
macDeviceId,
|
|
365
|
+
macIdentityPublicKey,
|
|
366
|
+
macIdentityPrivateKey,
|
|
367
|
+
trustedPhones,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function bridgeStatesEqual(left, right) {
|
|
372
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizeNonEmptyString(value) {
|
|
376
|
+
if (typeof value !== "string") {
|
|
377
|
+
return "";
|
|
378
|
+
}
|
|
379
|
+
return value.trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function firstNonEmptyString(values) {
|
|
383
|
+
for (const value of values) {
|
|
384
|
+
const normalized = normalizeNonEmptyString(value);
|
|
385
|
+
if (normalized) {
|
|
386
|
+
return normalized;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return "";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function resolveLegacyAwareStoreDir() {
|
|
393
|
+
return fs.existsSync(LEGACY_STORE_DIR)
|
|
394
|
+
? LEGACY_STORE_DIR
|
|
395
|
+
: DEFAULT_STORE_DIR;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function corruptedStateError(source, error) {
|
|
399
|
+
const detail = normalizeNonEmptyString(error?.message);
|
|
400
|
+
return new Error(
|
|
401
|
+
`The saved Codex pairing state in ${source} is unreadable. `
|
|
402
|
+
+ "Run `codex-bridge reset-pairing` to start fresh."
|
|
403
|
+
+ (detail ? ` (${detail})` : "")
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function warnOnce(message) {
|
|
408
|
+
if (hasLoggedKeychainMismatch) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
hasLoggedKeychainMismatch = true;
|
|
412
|
+
console.warn(message);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function base64UrlToBase64(value) {
|
|
416
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
417
|
+
return "";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const padded = `${value}${"=".repeat((4 - (value.length % 4 || 4)) % 4)}`;
|
|
421
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = {
|
|
425
|
+
getTrustedPhonePublicKey,
|
|
426
|
+
loadOrCreateBridgeDeviceState,
|
|
427
|
+
rememberTrustedPhone,
|
|
428
|
+
resetBridgeDeviceState,
|
|
429
|
+
resolveBridgeRelaySession,
|
|
430
|
+
};
|