codex-relay 1.0.1-beta.0 → 1.0.1

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 CHANGED
@@ -13,7 +13,7 @@ Codex Relay runs a local bridge server for the Codex Relay mobile app. Keep Code
13
13
  Run the server from the workspace you want Codex to use:
14
14
 
15
15
  ```sh
16
- npx codex-relay@latest
16
+ npx codex-relay
17
17
  ```
18
18
 
19
19
  The CLI prints a QR code, a mobile URL, and a `codex-relay://pair...` pairing payload. Scan the QR code from the mobile app. If scanning is not available, paste the full pairing payload into the app.
@@ -21,7 +21,7 @@ The CLI prints a QR code, a mobile URL, and a `codex-relay://pair...` pairing pa
21
21
  When the app shows an approval code, approve it on the computer:
22
22
 
23
23
  ```sh
24
- npx codex-relay@latest approve XXXX-XXXX
24
+ npx codex-relay approve XXXX-XXXX
25
25
  ```
26
26
 
27
27
  After approval, the phone can list Codex threads, start new work, stream messages, and handle approval prompts from the local Codex runtime.
@@ -31,7 +31,7 @@ After approval, the phone can list Codex threads, start new work, stream message
31
31
  To keep the relay running after the command returns:
32
32
 
33
33
  ```sh
34
- npx codex-relay@latest --bg
34
+ npx codex-relay --bg
35
35
  ```
36
36
 
37
37
  Background mode writes runtime files under `.codex-relay/` in the current directory:
@@ -44,7 +44,7 @@ Background mode writes runtime files under `.codex-relay/` in the current direct
44
44
  Print the current pairing QR again:
45
45
 
46
46
  ```sh
47
- npx codex-relay@latest qr
47
+ npx codex-relay qr
48
48
  ```
49
49
 
50
50
  Stop a background server with the printed process id:
@@ -56,25 +56,25 @@ kill -TERM <pid>
56
56
  ## Commands
57
57
 
58
58
  ```sh
59
- npx codex-relay@latest
59
+ npx codex-relay
60
60
  ```
61
61
 
62
62
  Start the relay in the foreground.
63
63
 
64
64
  ```sh
65
- npx codex-relay@latest --bg
65
+ npx codex-relay --bg
66
66
  ```
67
67
 
68
68
  Start the relay in the background.
69
69
 
70
70
  ```sh
71
- npx codex-relay@latest qr
71
+ npx codex-relay qr
72
72
  ```
73
73
 
74
74
  Print the latest pairing QR for an already running relay.
75
75
 
76
76
  ```sh
77
- npx codex-relay@latest approve XXXX-XXXX
77
+ npx codex-relay approve XXXX-XXXX
78
78
  ```
79
79
 
80
80
  Approve a pending mobile pairing request.
@@ -83,29 +83,29 @@ Approve a pending mobile pairing request.
83
83
 
84
84
  The relay listens on `0.0.0.0:8787` by default. Configure it with environment variables:
85
85
 
86
- | Variable | Purpose |
87
- | ----------------------------- | -------------------------------------------------------------------------------------------------- |
88
- | `PORT` | Server port. Defaults to `8787`. |
89
- | `HOST` | Listen host. Defaults to `0.0.0.0`. |
90
- | `CODEX_RELAY_PUBLIC_URL` | URL printed into the pairing QR, for example a Tailscale or tunnel URL. |
91
- | `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay@latest`. |
92
- | `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
93
- | `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
94
- | `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
95
- | `CODEX_BIN` | Codex CLI executable path. |
86
+ | Variable | Purpose |
87
+ | ----------------------------- | ------------------------------------------------------------------------------------------- |
88
+ | `PORT` | Server port. Defaults to `8787`. |
89
+ | `HOST` | Listen host. Defaults to `0.0.0.0`. |
90
+ | `CODEX_RELAY_PUBLIC_URL` | URL printed into the pairing QR, for example a Tailscale or tunnel URL. |
91
+ | `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay`. |
92
+ | `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
93
+ | `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
94
+ | `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
95
+ | `CODEX_BIN` | Codex CLI executable path. |
96
96
 
97
97
  Examples:
98
98
 
99
99
  ```sh
100
- PORT=8788 npx codex-relay@latest
100
+ PORT=8788 npx codex-relay
101
101
  ```
102
102
 
103
103
  ```sh
104
- CODEX_RELAY_PUBLIC_URL=http://100.64.0.10:8787 npx codex-relay@latest
104
+ CODEX_RELAY_PUBLIC_URL=http://100.64.0.10:8787 npx codex-relay
105
105
  ```
106
106
 
107
107
  ```sh
108
- CODEX_RELAY_WORKSPACE_PATH=/path/to/project npx codex-relay@latest
108
+ CODEX_RELAY_WORKSPACE_PATH=/path/to/project npx codex-relay
109
109
  ```
110
110
 
111
111
  ## Network Notes
@@ -118,16 +118,16 @@ The phone must be able to reach the URL printed as `Mobile:` by the relay.
118
118
 
119
119
  ## Troubleshooting
120
120
 
121
- If `npx codex-relay@latest qr` cannot find a server, start one first:
121
+ If `npx codex-relay qr` cannot find a server, start one first:
122
122
 
123
123
  ```sh
124
- npx codex-relay@latest
124
+ npx codex-relay
125
125
  ```
126
126
 
127
127
  If the relay says another process is using the local pairing database, use the existing server:
128
128
 
129
129
  ```sh
130
- npx codex-relay@latest qr
130
+ npx codex-relay qr
131
131
  ```
132
132
 
133
133
  Or stop the background process shown by the CLI:
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ async function startBackgroundServer() {
31
31
  if (existingPid) {
32
32
  console.log(`codex-relay is already running in the background (pid ${existingPid}).`);
33
33
  console.log(`Logs: ${logPath}`);
34
- console.log("Print the current pairing QR with: npx codex-relay@latest qr");
34
+ console.log("Print the current pairing QR with: npx codex-relay qr");
35
35
  return;
36
36
  }
37
37
  await unlink(pidPath).catch(() => void 0);
@@ -66,7 +66,7 @@ async function startBackgroundServer() {
66
66
  }
67
67
  console.log(`Started codex-relay in the background (pid ${startedPid}).`);
68
68
  console.log(`Logs: ${logPath}`);
69
- console.log("Print the pairing QR later with: npx codex-relay@latest qr");
69
+ console.log("Print the pairing QR later with: npx codex-relay qr");
70
70
  }
71
71
  async function readRunningPid(pidPath) {
72
72
  const value = await readFile(pidPath, "utf8").catch(() => void 0);
@@ -126,8 +126,8 @@ async function printPairingQr() {
126
126
  const state = storedState?.pairingPayload ? storedState : await readServerLogState();
127
127
  if (!state?.pairingPayload) {
128
128
  console.error("No running Codex Relay server state was found.");
129
- console.error("Start the server first with: npx codex-relay@latest");
130
- console.error("Or run it in the background with: npx codex-relay@latest --bg");
129
+ console.error("Start the server first with: npx codex-relay");
130
+ console.error("Or run it in the background with: npx codex-relay --bg");
131
131
  process.exitCode = 1;
132
132
  return;
133
133
  }
@@ -152,7 +152,7 @@ async function handleServerStartError(error) {
152
152
  if (existingPid) {
153
153
  console.error(`A background server appears to be running (pid ${existingPid}).`);
154
154
  console.error("Use the existing server instead of starting a second one:");
155
- console.error(" npx codex-relay@latest qr");
155
+ console.error(" npx codex-relay qr");
156
156
  console.error("");
157
157
  console.error("To stop the background server:");
158
158
  console.error(` kill -TERM ${existingPid}`);
@@ -162,7 +162,7 @@ async function handleServerStartError(error) {
162
162
  console.error("Another Codex Relay process is already running or exited without cleanup.");
163
163
  if (state?.pairingPayload) {
164
164
  console.error("Use the existing server instead of starting a second one:");
165
- console.error(" npx codex-relay@latest qr");
165
+ console.error(" npx codex-relay qr");
166
166
  console.error("");
167
167
  }
168
168
  console.error("Find it with:");
@@ -174,7 +174,7 @@ async function handleServerStartError(error) {
174
174
  }
175
175
  console.error("");
176
176
  console.error("If you wanted a persistent server, start it once with:");
177
- console.error(" npx codex-relay@latest --bg");
177
+ console.error(" npx codex-relay --bg");
178
178
  process.exitCode = 1;
179
179
  }
180
180
  async function readApprovalSecret() {
package/dist/src.js CHANGED
@@ -207,7 +207,6 @@ const WebPreviewTargetSchema = z.object({
207
207
  detectedAt: IsoDateTimeSchema
208
208
  });
209
209
  const PairRequestSchema = z.object({
210
- clientSessionId: z.string().trim().min(1).max(120).optional(),
211
210
  clientName: z.string().trim().min(1).max(80).optional(),
212
211
  secure: z.object({
213
212
  clientEphemeralPublicKey: z.string().min(1),
package/dist/src2.js CHANGED
@@ -540,8 +540,7 @@ function createApp(options = {}) {
540
540
  const tokenHash = token ? options.pairing.hashClientToken(token) : void 0;
541
541
  const validSession = tokenHash ? await options.pairing.sessions.getValidSession(tokenHash, Date.now()) : void 0;
542
542
  if (!tokenHash || !validSession) return c.json(apiError("unauthorized", "Pair this device with the Codex Relay server."), 401);
543
- if (options.pairing.serverIdentity && !secureSessionsByTokenHash.has(tokenHash)) if (validSession.secureSession) secureSessionsByTokenHash.set(tokenHash, validSession.secureSession);
544
- else return c.json(apiError("secure_session_required", "Secure session expired. Pair this device again."), 401);
543
+ if (options.pairing.serverIdentity && !secureSessionsByTokenHash.has(tokenHash)) return c.json(apiError("secure_session_required", "Secure session expired. Pair this device again."), 401);
545
544
  await next();
546
545
  });
547
546
  app.post(apiPaths.pair, async (c) => {
@@ -557,7 +556,6 @@ function createApp(options = {}) {
557
556
  approved: false,
558
557
  approvalCode,
559
558
  clientEphemeralPublicKey: parsed.data.secure.clientEphemeralPublicKey,
560
- clientSessionId: parsed.data.clientSessionId,
561
559
  clientName: parsed.data.clientName,
562
560
  clientNonce: parsed.data.secure.clientNonce,
563
561
  expiresAt,
@@ -586,6 +584,15 @@ function createApp(options = {}) {
586
584
  const clientToken = options.pairing.createClientToken();
587
585
  const expiresAt = Date.now() + options.pairing.tokenTtlMs;
588
586
  const tokenHash = options.pairing.hashClientToken(clientToken);
587
+ const tokenCount = await options.pairing.sessions.createSession(tokenHash, {
588
+ clientName: pending.clientName,
589
+ expiresAt
590
+ });
591
+ await options.pairing.sessions.deletePendingPairing(approvalCode);
592
+ options.pairing.onPaired?.({
593
+ clientName: pending.clientName,
594
+ tokenCount
595
+ });
589
596
  const clientTokenExpiresAt = new Date(expiresAt).toISOString();
590
597
  const pairing = createSecurePairing({
591
598
  approvalCode,
@@ -597,17 +604,6 @@ function createApp(options = {}) {
597
604
  serverIdentity: options.pairing.serverIdentity,
598
605
  serverUrl: pending.serverUrl
599
606
  });
600
- const tokenCount = await options.pairing.sessions.createSession(tokenHash, {
601
- clientSessionId: pending.clientSessionId,
602
- clientName: pending.clientName,
603
- expiresAt,
604
- secureSession: pairing.session
605
- });
606
- await options.pairing.sessions.deletePendingPairing(approvalCode);
607
- options.pairing.onPaired?.({
608
- clientName: pending.clientName,
609
- tokenCount
610
- });
611
607
  secureSessionsByTokenHash.set(tokenHash, pairing.session);
612
608
  return c.json(PairResponseSchema.parse({ secure: pairing.response }), 201);
613
609
  });
@@ -634,15 +630,15 @@ function createApp(options = {}) {
634
630
  const expiresAt = Date.now() + options.pairing.tokenTtlMs;
635
631
  const oldTokenHash = options.pairing.hashClientToken(oldToken);
636
632
  const newTokenHash = options.pairing.hashClientToken(clientToken);
637
- const clientSessionId = normalizeClientSessionId(c.req.header("x-codex-relay-client-session-id")) ?? oldSession.clientSessionId;
638
633
  const tokenCount = await options.pairing.sessions.rotateSession(oldTokenHash, newTokenHash, {
639
- clientSessionId,
640
634
  clientName: oldSession.clientName,
641
- expiresAt,
642
- secureSession: secureSessionsByTokenHash.get(oldTokenHash) ?? oldSession.secureSession
635
+ expiresAt
643
636
  });
644
- const secureSession = secureSessionsByTokenHash.get(oldTokenHash) ?? oldSession.secureSession;
645
- if (secureSession) secureSessionsByTokenHash.set(oldTokenHash, secureSession);
637
+ const secureSession = secureSessionsByTokenHash.get(oldTokenHash);
638
+ if (secureSession) {
639
+ secureSessionsByTokenHash.delete(oldTokenHash);
640
+ secureSessionsByTokenHash.set(newTokenHash, secureSession);
641
+ }
646
642
  options.pairing.onTokenRefreshed?.({
647
643
  clientName: oldSession.clientName,
648
644
  tokenCount
@@ -651,13 +647,7 @@ function createApp(options = {}) {
651
647
  clientToken,
652
648
  clientTokenExpiresAt: new Date(expiresAt).toISOString()
653
649
  });
654
- const jsonResponse = await secureJson(c, options.pairing, secureSessionsByTokenHash, response, 201);
655
- if (secureSession) {
656
- secureSessionsByTokenHash.delete(oldTokenHash);
657
- secureSessionsByTokenHash.set(newTokenHash, secureSession);
658
- await options.pairing.sessions.updateSecureSession(newTokenHash, secureSession);
659
- }
660
- return jsonResponse;
650
+ return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 201);
661
651
  });
662
652
  app.get(apiPaths.status, (c) => {
663
653
  const response = StatusResponseSchema.parse({
@@ -997,10 +987,6 @@ function normalizeApprovalCode(value) {
997
987
  const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
998
988
  return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
999
989
  }
1000
- function normalizeClientSessionId(value) {
1001
- const trimmed = value?.trim();
1002
- return trimmed ? trimmed.slice(0, 120) : void 0;
1003
- }
1004
990
  async function validateThreadWorkspacePath(rootPath, requestedPath) {
1005
991
  const resolved = resolve(requestedPath ?? rootPath);
1006
992
  try {
@@ -1533,8 +1519,7 @@ async function parseRequestJson(c, pairing, secureSessionsByTokenHash, schema) {
1533
1519
  const envelope = EncryptedPayloadSchema.safeParse(payload);
1534
1520
  if (!envelope.success) return schema.safeParse({ __invalidEncryptedPayload: true });
1535
1521
  try {
1536
- payload = JSON.parse(decryptFromMobile(secureSession.session, envelope.data));
1537
- await secureSession.persist();
1522
+ payload = JSON.parse(decryptFromMobile(secureSession, envelope.data));
1538
1523
  } catch {
1539
1524
  payload = { __invalidEncryptedPayload: true };
1540
1525
  }
@@ -1550,30 +1535,15 @@ async function parsePlainJson(request, schema) {
1550
1535
  }
1551
1536
  return schema.safeParse(payload);
1552
1537
  }
1553
- async function secureJson(c, pairing, secureSessionsByTokenHash, payload, status) {
1538
+ function secureJson(c, pairing, secureSessionsByTokenHash, payload, status) {
1554
1539
  const secureSession = getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash);
1555
1540
  if (!secureSession) return c.json(payload, status);
1556
- const encrypted = EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(payload)));
1557
- await secureSession.persist();
1541
+ const encrypted = EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(payload)));
1558
1542
  return c.json(encrypted, status);
1559
1543
  }
1560
1544
  function getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash) {
1561
1545
  const token = parseBearerToken(c.req.header("authorization"));
1562
- if (!token || !pairing) return;
1563
- const tokenHash = pairing.hashClientToken(token);
1564
- const session = secureSessionsByTokenHash.get(tokenHash);
1565
- return session ? createSecureSessionHandle(pairing, tokenHash, session) : void 0;
1566
- }
1567
- function createSecureSessionHandle(pairing, tokenHash, session) {
1568
- let pendingPersist = Promise.resolve();
1569
- return {
1570
- persist: () => {
1571
- pendingPersist = pendingPersist.catch(() => void 0).then(() => pairing.sessions.updateSecureSession(tokenHash, session));
1572
- return pendingPersist;
1573
- },
1574
- session,
1575
- tokenHash
1576
- };
1546
+ return token && pairing ? secureSessionsByTokenHash.get(pairing.hashClientToken(token)) : void 0;
1577
1547
  }
1578
1548
  function sortedThreads(threads) {
1579
1549
  return [...threads.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -1700,8 +1670,7 @@ function updateThread(threads, messagesByThreadId, threadId, update) {
1700
1670
  }
1701
1671
  function sendSse(controller, encoder, secureSession, event) {
1702
1672
  const parsed = StreamThreadRunEventSchema.parse(event);
1703
- const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
1704
- if (secureSession) secureSession.persist().catch(() => void 0);
1673
+ const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(parsed))) : parsed;
1705
1674
  controller.enqueue(encoder.encode(`event: ${parsed.type}\n`));
1706
1675
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
1707
1676
  }
@@ -2454,7 +2423,7 @@ function normalizeFileChanges(value) {
2454
2423
  async function readWorkspaceChanges(workspacePath) {
2455
2424
  const repo = await openRepository(workspacePath);
2456
2425
  const [currentBranch, branches] = await Promise.all([currentGitBranch(workspacePath), listGitBranches(workspacePath)]);
2457
- const statusEntries = collectIterator(repo.statuses().iter()).filter((entry) => !entry.status().ignored);
2426
+ const statusEntries = collectIterator(repo.statuses().iter());
2458
2427
  const statusByPath = new Map(statusEntries.map((entry) => [entry.path(), entry]));
2459
2428
  const status = statusEntries.map((entry) => formatStatusLine(entry.path(), entry.status())).join("\n");
2460
2429
  const structuredDiff = createWorkspaceDiff(repo);
@@ -2502,11 +2471,6 @@ async function readWorkspaceChanges(workspacePath) {
2502
2471
  });
2503
2472
  }
2504
2473
  const files = [...filesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
2505
- const fileStats = {
2506
- additions: files.reduce((total, file) => total + file.additions, 0),
2507
- deletions: files.reduce((total, file) => total + file.deletions, 0),
2508
- filesChanged: files.length
2509
- };
2510
2474
  return {
2511
2475
  branches,
2512
2476
  currentBranch,
@@ -2514,10 +2478,10 @@ async function readWorkspaceChanges(workspacePath) {
2514
2478
  files,
2515
2479
  hasChanges: files.length > 0 || Boolean(status.trim() || diff.trim()),
2516
2480
  status,
2517
- stats: files.length > 0 ? fileStats : {
2481
+ stats: {
2518
2482
  additions: Number(stats.insertions),
2519
2483
  deletions: Number(stats.deletions),
2520
- filesChanged: Number(stats.filesChanged)
2484
+ filesChanged: files.length || Number(stats.filesChanged)
2521
2485
  }
2522
2486
  };
2523
2487
  }
@@ -2638,21 +2602,14 @@ async function createTursoPairingSessionStore(path) {
2638
2602
  await db.exec(`
2639
2603
  CREATE TABLE IF NOT EXISTS pairing_sessions (
2640
2604
  token_hash TEXT PRIMARY KEY,
2641
- client_session_id TEXT,
2642
2605
  client_name TEXT,
2643
2606
  expires_at INTEGER NOT NULL,
2644
- key_epoch INTEGER,
2645
- mobile_to_server_key TEXT,
2646
- server_to_mobile_key TEXT,
2647
- last_mobile_counter INTEGER,
2648
- next_server_counter INTEGER,
2649
2607
  created_at INTEGER NOT NULL,
2650
2608
  updated_at INTEGER NOT NULL
2651
2609
  );
2652
2610
 
2653
2611
  CREATE TABLE IF NOT EXISTS pending_pairings (
2654
2612
  approval_code TEXT PRIMARY KEY,
2655
- client_session_id TEXT,
2656
2613
  client_name TEXT,
2657
2614
  client_ephemeral_public_key TEXT NOT NULL,
2658
2615
  client_nonce TEXT NOT NULL,
@@ -2663,9 +2620,8 @@ async function createTursoPairingSessionStore(path) {
2663
2620
  updated_at INTEGER NOT NULL
2664
2621
  );
2665
2622
  `);
2666
- await ensurePairingSessionColumns();
2667
2623
  async function countActive(now) {
2668
- const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
2624
+ const row = await db.prepare("SELECT COUNT(*) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
2669
2625
  return Number(row?.count ?? 0);
2670
2626
  }
2671
2627
  async function deleteSession(tokenHash) {
@@ -2676,7 +2632,6 @@ async function createTursoPairingSessionStore(path) {
2676
2632
  }
2677
2633
  async function getPendingPairing(approvalCode, now) {
2678
2634
  const row = await db.prepare(`SELECT approval_code AS approvalCode,
2679
- client_session_id AS clientSessionId,
2680
2635
  client_name AS clientName,
2681
2636
  client_ephemeral_public_key AS clientEphemeralPublicKey,
2682
2637
  client_nonce AS clientNonce,
@@ -2695,7 +2650,6 @@ async function createTursoPairingSessionStore(path) {
2695
2650
  approvalCode: String(row.approvalCode),
2696
2651
  approved: Number(row.approved) === 1,
2697
2652
  clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
2698
- clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
2699
2653
  clientName: typeof row.clientName === "string" ? row.clientName : void 0,
2700
2654
  clientNonce: String(row.clientNonce),
2701
2655
  expiresAt,
@@ -2717,7 +2671,6 @@ async function createTursoPairingSessionStore(path) {
2717
2671
  const now = Date.now();
2718
2672
  await db.prepare(`INSERT INTO pending_pairings (
2719
2673
  approval_code,
2720
- client_session_id,
2721
2674
  client_name,
2722
2675
  client_ephemeral_public_key,
2723
2676
  client_nonce,
@@ -2727,45 +2680,19 @@ async function createTursoPairingSessionStore(path) {
2727
2680
  created_at,
2728
2681
  updated_at
2729
2682
  )
2730
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
2683
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
2731
2684
  },
2732
2685
  async createSession(tokenHash, session) {
2733
2686
  const now = Date.now();
2734
- const secure = encodeSecureSession(session.secureSession);
2735
- if (session.clientSessionId) {
2736
- await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
2737
- if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
2738
- }
2739
- await db.prepare(`INSERT INTO pairing_sessions (
2740
- token_hash,
2741
- client_session_id,
2742
- client_name,
2743
- expires_at,
2744
- key_epoch,
2745
- mobile_to_server_key,
2746
- server_to_mobile_key,
2747
- last_mobile_counter,
2748
- next_server_counter,
2749
- created_at,
2750
- updated_at
2751
- )
2752
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(tokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
2687
+ await db.prepare(`INSERT INTO pairing_sessions (token_hash, client_name, expires_at, created_at, updated_at)
2688
+ VALUES (?, ?, ?, ?, ?)`).run(tokenHash, session.clientName ?? null, session.expiresAt, now, now);
2753
2689
  return countActive(now);
2754
2690
  },
2755
2691
  deleteSession,
2756
2692
  deletePendingPairing,
2757
2693
  getPendingPairing,
2758
2694
  async getValidSession(tokenHash, now) {
2759
- const row = await db.prepare(`SELECT client_name AS clientName,
2760
- client_session_id AS clientSessionId,
2761
- expires_at AS expiresAt,
2762
- key_epoch AS keyEpoch,
2763
- mobile_to_server_key AS mobileToServerKey,
2764
- server_to_mobile_key AS serverToMobileKey,
2765
- last_mobile_counter AS lastMobileCounter,
2766
- next_server_counter AS nextServerCounter
2767
- FROM pairing_sessions
2768
- WHERE token_hash = ?`).get(tokenHash);
2695
+ const row = await db.prepare("SELECT client_name AS clientName, expires_at AS expiresAt FROM pairing_sessions WHERE token_hash = ?").get(tokenHash);
2769
2696
  if (!row) return;
2770
2697
  const expiresAt = Number(row.expiresAt);
2771
2698
  if (now > expiresAt) {
@@ -2773,10 +2700,8 @@ async function createTursoPairingSessionStore(path) {
2773
2700
  return;
2774
2701
  }
2775
2702
  return {
2776
- clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
2777
2703
  clientName: typeof row.clientName === "string" ? row.clientName : void 0,
2778
- expiresAt,
2779
- secureSession: decodeSecureSession(row)
2704
+ expiresAt
2780
2705
  };
2781
2706
  },
2782
2707
  async pruneExpired(now) {
@@ -2785,82 +2710,14 @@ async function createTursoPairingSessionStore(path) {
2785
2710
  },
2786
2711
  async rotateSession(oldTokenHash, newTokenHash, session) {
2787
2712
  const now = Date.now();
2788
- const secure = encodeSecureSession(session.secureSession);
2789
2713
  await db.transaction(async () => {
2790
2714
  await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
2791
- if (session.clientSessionId) {
2792
- await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
2793
- if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
2794
- }
2795
- await db.prepare(`INSERT INTO pairing_sessions (
2796
- token_hash,
2797
- client_session_id,
2798
- client_name,
2799
- expires_at,
2800
- key_epoch,
2801
- mobile_to_server_key,
2802
- server_to_mobile_key,
2803
- last_mobile_counter,
2804
- next_server_counter,
2805
- created_at,
2806
- updated_at
2807
- )
2808
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newTokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
2715
+ await db.prepare(`INSERT INTO pairing_sessions (token_hash, client_name, expires_at, created_at, updated_at)
2716
+ VALUES (?, ?, ?, ?, ?)`).run(newTokenHash, session.clientName ?? null, session.expiresAt, now, now);
2809
2717
  })();
2810
2718
  return countActive(now);
2811
- },
2812
- async updateSecureSession(tokenHash, secureSession) {
2813
- const secure = encodeSecureSession(secureSession);
2814
- const now = Date.now();
2815
- await db.prepare(`UPDATE pairing_sessions
2816
- SET key_epoch = ?,
2817
- mobile_to_server_key = ?,
2818
- server_to_mobile_key = ?,
2819
- last_mobile_counter = ?,
2820
- next_server_counter = ?,
2821
- updated_at = ?
2822
- WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
2823
2719
  }
2824
2720
  };
2825
- async function ensurePairingSessionColumns() {
2826
- const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
2827
- const columns = new Set(resultRows(rows).map((row) => String(row.name)));
2828
- for (const [column, sql] of [
2829
- ["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
2830
- ["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
2831
- ["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
2832
- ["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
2833
- ["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
2834
- ["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
2835
- ]) if (!columns.has(column)) await db.exec(sql);
2836
- const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
2837
- if (!new Set(resultRows(pendingRows).map((row) => String(row.name))).has("client_session_id")) await db.exec("ALTER TABLE pending_pairings ADD COLUMN client_session_id TEXT");
2838
- }
2839
- }
2840
- function encodeSecureSession(session) {
2841
- if (!session) return;
2842
- return {
2843
- keyEpoch: session.keyEpoch,
2844
- lastMobileCounter: session.lastMobileCounter,
2845
- mobileToServerKey: fromByteArray(session.mobileToServerKey),
2846
- nextServerCounter: session.nextServerCounter,
2847
- serverToMobileKey: fromByteArray(session.serverToMobileKey)
2848
- };
2849
- }
2850
- function decodeSecureSession(row) {
2851
- if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
2852
- return {
2853
- keyEpoch: Number(row.keyEpoch),
2854
- lastMobileCounter: Number(row.lastMobileCounter),
2855
- mobileToServerKey: toByteArray(row.mobileToServerKey),
2856
- nextServerCounter: Number(row.nextServerCounter),
2857
- serverToMobileKey: toByteArray(row.serverToMobileKey)
2858
- };
2859
- }
2860
- function resultRows(result) {
2861
- if (Array.isArray(result)) return result;
2862
- if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
2863
- return [];
2864
2721
  }
2865
2722
  //#endregion
2866
2723
  //#region src/index.ts
@@ -2995,7 +2852,7 @@ async function writeBackgroundPid() {
2995
2852
  await writeFile(path, `${process.pid}\n`, { mode: 384 });
2996
2853
  }
2997
2854
  function formatApprovalCommand(approvalCode, activePort) {
2998
- return activePort === 8787 ? `npx codex-relay@latest approve ${approvalCode}` : `PORT=${activePort} npx codex-relay@latest approve ${approvalCode}`;
2855
+ return activePort === 8787 ? `npx codex-relay approve ${approvalCode}` : `PORT=${activePort} npx codex-relay approve ${approvalCode}`;
2999
2856
  }
3000
2857
  function getLocalNetworkConnectUrl(port) {
3001
2858
  for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.0.1-beta.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "repository": {
6
6
  "type": "git",