@btatum5/codex-bridge 0.1.0 → 1.3.2

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.
@@ -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
+ };