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 +24 -24
- package/dist/cli.js +7 -7
- package/dist/src.js +0 -1
- package/dist/src2.js +34 -177
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
121
|
+
If `npx codex-relay qr` cannot find a server, start one first:
|
|
122
122
|
|
|
123
123
|
```sh
|
|
124
|
-
npx codex-relay
|
|
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
|
|
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
|
|
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
|
|
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
|
|
130
|
-
console.error("Or run it in the background with: npx codex-relay
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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)
|
|
645
|
-
if (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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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())
|
|
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:
|
|
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(
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?,
|
|
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
|
-
|
|
2735
|
-
|
|
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(
|
|
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
|
-
|
|
2792
|
-
|
|
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
|
|
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}`;
|