@4xeoz/re-entry 0.2.6 → 0.2.8
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Re-entry Local Connector
|
|
2
2
|
|
|
3
|
-
Install it once on the Mac where Codex should open; after one
|
|
3
|
+
Install it once on the Mac where Codex should open; after one dashboard pairing code, a macOS
|
|
4
4
|
LaunchAgent keeps the outbound Connector running at login.
|
|
5
5
|
|
|
6
6
|
> Current boundary: this is a verified macOS Connector preview with a publish-ready npm package.
|
|
@@ -67,11 +67,11 @@ npx --yes --package=@4xeoz/re-entry re-entry install \
|
|
|
67
67
|
```text
|
|
68
68
|
choose Desktop, the current folder, or another Codex workspace
|
|
69
69
|
-> check Node + find Codex + validate the selected directory
|
|
70
|
-
->
|
|
71
|
-
-> show the verification URL and wait for the user to press Enter
|
|
70
|
+
-> show the Re-entry account URL and wait for the user to press Enter
|
|
72
71
|
-> open Re-entry in the default browser
|
|
73
|
-
-> sign in or create
|
|
74
|
-
->
|
|
72
|
+
-> sign in or create a user account
|
|
73
|
+
-> click Pair this Mac in the dashboard
|
|
74
|
+
-> enter the short pairing code in the CLI
|
|
75
75
|
-> save the device credential with mode 0600
|
|
76
76
|
-> install and start a per-user macOS LaunchAgent
|
|
77
77
|
```
|
|
@@ -89,8 +89,10 @@ re-entry --help
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
The status view checks the local authorization, background job, Receiver reachability, Node, and
|
|
92
|
-
Codex.
|
|
93
|
-
|
|
92
|
+
Codex. If the Receiver rejects a previously saved device credential, the Connector pauses instead
|
|
93
|
+
of retrying forever, and `status`/`listen` tell you to run `re-entry connect` again. `listen` watches
|
|
94
|
+
the already-running background Connector and displays new activity until you press Ctrl+C; it does
|
|
95
|
+
not start a competing second poller. Useful development commands are:
|
|
94
96
|
|
|
95
97
|
```sh
|
|
96
98
|
re-entry doctor --codex-cd /absolute/path/to/project
|
|
@@ -131,8 +133,8 @@ Copy this prompt into a coding-agent task:
|
|
|
131
133
|
```text
|
|
132
134
|
Install the Re-entry Local Connector on this Mac. First read the package README. Verify Node.js 24
|
|
133
135
|
or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
|
|
134
|
-
project directory. Let the human
|
|
135
|
-
|
|
136
|
+
project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
|
|
137
|
+
enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
|
|
136
138
|
into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
|
|
137
139
|
results without claiming Browser/WebMCP or production deployment.
|
|
138
140
|
```
|
|
@@ -140,7 +142,7 @@ results without claiming Browser/WebMCP or production deployment.
|
|
|
140
142
|
## Package map
|
|
141
143
|
|
|
142
144
|
- `src/main.mjs` — `re-entry` CLI and long-running poll loop.
|
|
143
|
-
- `src/pairing-client.mjs` — account-
|
|
145
|
+
- `src/pairing-client.mjs` — account pairing-code redemption and legacy pairing compatibility.
|
|
144
146
|
- `src/credentials.mjs` — atomic local credential storage.
|
|
145
147
|
- `src/macos-service.mjs` — per-user LaunchAgent install, stop, uninstall, and status.
|
|
146
148
|
- `src/workspace-picker.mjs` — interactive Codex workspace selection.
|
|
@@ -249,9 +251,10 @@ npm start -- \
|
|
|
249
251
|
--codex-cd "$HOME/Code/my-host-project"
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
On the first run, the Connector checks the Mac,
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
On the first run, the Connector checks the Mac, opens the Re-entry account page, waits for you to
|
|
255
|
+
create or sign in to an account and click Pair this Mac, then asks for the
|
|
256
|
+
dashboard code and stores the credential. On later runs it reuses the saved credential and skips
|
|
257
|
+
pairing:
|
|
255
258
|
|
|
256
259
|
```sh
|
|
257
260
|
npm start -- --codex-cd "$HOME/Code/my-host-project"
|
|
@@ -269,13 +272,13 @@ events instead so another process can consume it:
|
|
|
269
272
|
npm start -- --json --codex-cd "$HOME/Code/my-host-project"
|
|
270
273
|
```
|
|
271
274
|
|
|
272
|
-
##
|
|
275
|
+
## Compatibility: Host-code pairing (legacy)
|
|
273
276
|
|
|
274
277
|
Pairing connects one local Connector to one already-authenticated Host user. It is separate from
|
|
275
278
|
the Host organization's API key and does not give the Connector permission to create Grants or
|
|
276
279
|
send Host events.
|
|
277
280
|
|
|
278
|
-
The flow is:
|
|
281
|
+
The older compatibility flow is:
|
|
279
282
|
|
|
280
283
|
```text
|
|
281
284
|
Host backend -> Receiver: start pairing for its Host-user reference
|
|
@@ -288,7 +291,8 @@ Receiver -> Connector: one Connector credential
|
|
|
288
291
|
Connector -> local disk: save the credential with mode 0600
|
|
289
292
|
```
|
|
290
293
|
|
|
291
|
-
The
|
|
294
|
+
The current preview does not use this Host-issued code path. Use `re-entry install` above, where
|
|
295
|
+
the authenticated Re-entry dashboard creates the code.
|
|
292
296
|
|
|
293
297
|
```sh
|
|
294
298
|
curl -s -X POST http://127.0.0.1:43218/v0.1/pairing-sessions \
|
|
@@ -42,7 +42,7 @@ export function createCloudReceiverHttpHandler(options) {
|
|
|
42
42
|
requireReceiver(options.receiver);
|
|
43
43
|
|
|
44
44
|
return function cloudReceiverHttpHandler(request, response) {
|
|
45
|
-
handleRequest(options.receiver, request, response).catch((error) => {
|
|
45
|
+
return handleRequest(options.receiver, request, response).catch((error) => {
|
|
46
46
|
if (response.destroyed) return;
|
|
47
47
|
if (response.headersSent) {
|
|
48
48
|
response.destroy();
|
package/package.json
CHANGED
package/src/credentials.mjs
CHANGED
|
@@ -49,6 +49,48 @@ export class LocalConnectorCredentialStore {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function reauthorizationMarkerPath(filename) {
|
|
53
|
+
if (typeof filename !== "string" || filename.length === 0) {
|
|
54
|
+
throw new TypeError("Credential filename is required");
|
|
55
|
+
}
|
|
56
|
+
return `${filename}.reauthorization-required.json`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function markConnectorReauthorizationRequired(filename, value = {}) {
|
|
60
|
+
const marker = reauthorizationMarkerPath(filename);
|
|
61
|
+
await mkdir(dirname(marker), { recursive: true, mode: 0o700 });
|
|
62
|
+
await writeFile(marker, `${JSON.stringify({
|
|
63
|
+
receiver_origin: value.receiver_origin ?? null,
|
|
64
|
+
reason: "connector_identity_invalid",
|
|
65
|
+
observed_at: new Date().toISOString(),
|
|
66
|
+
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
67
|
+
await chmod(marker, 0o600);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function hasConnectorReauthorizationRequired(filename) {
|
|
71
|
+
try {
|
|
72
|
+
await readFile(reauthorizationMarkerPath(filename), "utf8");
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code === "ENOENT") return false;
|
|
76
|
+
throw credentialFailure(
|
|
77
|
+
"connector_status_unreadable",
|
|
78
|
+
"Connector authorization status could not be read",
|
|
79
|
+
error,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function clearConnectorReauthorizationRequired(filename) {
|
|
85
|
+
await unlink(reauthorizationMarkerPath(filename)).catch((error) => {
|
|
86
|
+
if (error?.code !== "ENOENT") throw credentialFailure(
|
|
87
|
+
"connector_status_unwritable",
|
|
88
|
+
"Connector authorization status could not be cleared",
|
|
89
|
+
error,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
52
94
|
function normalizeCredentials(value) {
|
|
53
95
|
requireExactRecord(value, CREDENTIAL_FIELDS, CREDENTIAL_FIELDS, "Connector credentials");
|
|
54
96
|
if (value.version !== 1) throw credentialFailure("connector_credentials_invalid", "Connector credential version is unsupported");
|
package/src/macos-service.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export async function inspectMacConnectorService(options = {}) {
|
|
|
91
91
|
return Object.freeze({
|
|
92
92
|
supported: true,
|
|
93
93
|
installed: true,
|
|
94
|
-
running: result.code === 0,
|
|
94
|
+
running: result.code === 0 && isLoadedAndRunning(result.stdout),
|
|
95
95
|
plistPath,
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -127,6 +127,7 @@ export async function uninstallMacConnectorService(options = {}) {
|
|
|
127
127
|
const paths = [...new Set([
|
|
128
128
|
service.plistPath,
|
|
129
129
|
credentialFile,
|
|
130
|
+
`${credentialFile}.reauthorization-required.json`,
|
|
130
131
|
join(stateDirectory, "connector.log"),
|
|
131
132
|
join(stateDirectory, "connector-error.log"),
|
|
132
133
|
])];
|
|
@@ -192,6 +193,7 @@ ${argumentsXml}
|
|
|
192
193
|
function runLaunchctl(argumentsList) {
|
|
193
194
|
return new Promise((resolve) => {
|
|
194
195
|
const child = spawn("launchctl", argumentsList, { stdio: ["ignore", "pipe", "pipe"] });
|
|
196
|
+
let stdout = "";
|
|
195
197
|
let stderr = "";
|
|
196
198
|
let settled = false;
|
|
197
199
|
const finish = (result) => {
|
|
@@ -199,15 +201,29 @@ function runLaunchctl(argumentsList) {
|
|
|
199
201
|
settled = true;
|
|
200
202
|
resolve(result);
|
|
201
203
|
};
|
|
204
|
+
child.stdout.setEncoding("utf8");
|
|
205
|
+
child.stdout.on("data", (chunk) => {
|
|
206
|
+
if (stdout.length < 16_384) stdout += chunk;
|
|
207
|
+
});
|
|
202
208
|
child.stderr.setEncoding("utf8");
|
|
203
209
|
child.stderr.on("data", (chunk) => {
|
|
204
210
|
if (stderr.length < 4_096) stderr += chunk;
|
|
205
211
|
});
|
|
206
|
-
child.once("error", (error) => finish({ code: -1, stderr: error.message }));
|
|
207
|
-
child.once("close", (code) => finish({ code: code ?? -1, stderr }));
|
|
212
|
+
child.once("error", (error) => finish({ code: -1, stdout, stderr: error.message }));
|
|
213
|
+
child.once("close", (code) => finish({ code: code ?? -1, stdout, stderr }));
|
|
208
214
|
});
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
function isLoadedAndRunning(stdout) {
|
|
218
|
+
// Test doubles historically return only { code: 0 }; preserve that contract while
|
|
219
|
+
// treating launchctl's explicit not-running state as stopped in production.
|
|
220
|
+
if (stdout === undefined) return true;
|
|
221
|
+
const state = stdout.match(/^[\t ]*state\s*=\s*([^\r\n]+)$/m)?.[1]?.trim();
|
|
222
|
+
if (state) return state === "running";
|
|
223
|
+
const activeCount = stdout.match(/^[\t ]*active count\s*=\s*(\d+)$/m)?.[1];
|
|
224
|
+
return activeCount !== undefined && Number(activeCount) > 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
function requireServiceConfiguration(options, operation) {
|
|
212
228
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
213
229
|
throw serviceFailure("connector_service_input_invalid", `Service ${operation} options are invalid`);
|
package/src/main.mjs
CHANGED
|
@@ -18,9 +18,13 @@ import {
|
|
|
18
18
|
verifyCodexExecutable,
|
|
19
19
|
} from "./codex-discovery.mjs";
|
|
20
20
|
import { LocalConnector } from "./local-connector.mjs";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
import {
|
|
22
|
+
LocalConnectorCredentialStore,
|
|
23
|
+
clearConnectorReauthorizationRequired,
|
|
24
|
+
hasConnectorReauthorizationRequired,
|
|
25
|
+
markConnectorReauthorizationRequired,
|
|
26
|
+
} from "./credentials.mjs";
|
|
27
|
+
import { LocalConnectorPairingClient, openBrowser } from "./pairing-client.mjs";
|
|
24
28
|
import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
|
|
25
29
|
import {
|
|
26
30
|
inspectMacConnectorService,
|
|
@@ -130,7 +134,7 @@ Commands:
|
|
|
130
134
|
test Start one fresh local Codex session with the supplied prompt
|
|
131
135
|
stop Stop the background Connector without removing its credential
|
|
132
136
|
uninstall Stop the Connector and remove its local service data
|
|
133
|
-
connect
|
|
137
|
+
connect Open Re-entry, redeem a dashboard pairing code, and connect this Mac
|
|
134
138
|
start Poll for approved work until stopped
|
|
135
139
|
doctor Check Node.js, Codex, and the optional project directory
|
|
136
140
|
claim-once Check once for approved work
|
|
@@ -267,14 +271,15 @@ async function pair(flags, ui, options = {}) {
|
|
|
267
271
|
|
|
268
272
|
async function connect(flags, ui, options = {}) {
|
|
269
273
|
if (!options.quietHeader && ui.interactive) {
|
|
270
|
-
ui.begin("Connect this Mac", "
|
|
274
|
+
ui.begin("Connect this Mac", "Create a Re-entry account, pair this Mac, then leave it running");
|
|
271
275
|
}
|
|
272
276
|
const receiver = flags.receiver ?? DEFAULT_RECEIVER_ORIGIN;
|
|
273
277
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
274
278
|
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
275
279
|
const current = await store.load();
|
|
280
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
|
|
276
281
|
const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
|
|
277
|
-
if (currentIsValid && current.receiver_origin === receiver) {
|
|
282
|
+
if (currentIsValid && current.receiver_origin === receiver && !reauthorizationRequired) {
|
|
278
283
|
if (ui.interactive) {
|
|
279
284
|
ui.success("Account", "already connected");
|
|
280
285
|
if (!options.guidedInstall) {
|
|
@@ -302,28 +307,28 @@ async function connect(flags, ui, options = {}) {
|
|
|
302
307
|
baseUrl: receiver,
|
|
303
308
|
requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
|
|
304
309
|
});
|
|
305
|
-
|
|
306
|
-
const credentials = await client.connect(
|
|
307
|
-
{ deviceName: flags["device-name"] ?? defaultDeviceName() },
|
|
308
|
-
async ({ verificationUri }) => {
|
|
309
|
-
if (ui.interactive) {
|
|
310
|
-
ui.stopWait("Secure link", "ready");
|
|
311
|
-
ui.info("Browser link", verificationUri);
|
|
312
|
-
await waitForEnterToOpenBrowser();
|
|
313
|
-
ui.wait("Waiting for approval in your browser…");
|
|
314
|
-
} else {
|
|
315
|
-
process.stdout.write(`${JSON.stringify({
|
|
316
|
-
event: "connector_authorization_waiting",
|
|
317
|
-
verification_uri: verificationUri,
|
|
318
|
-
})}\n`);
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
);
|
|
310
|
+
const portalUrl = `${receiver}/register?flow=pair&next=${encodeURIComponent("/dashboard")}`;
|
|
322
311
|
if (ui.interactive) {
|
|
323
|
-
ui.stopWait("
|
|
324
|
-
|
|
325
|
-
|
|
312
|
+
ui.stopWait("Secure link", "ready");
|
|
313
|
+
ui.info("Re-entry", portalUrl);
|
|
314
|
+
ui.info("Next", "Create or sign in, click Pair this Mac, then return here with the code.");
|
|
315
|
+
if (!(await openBrowser(portalUrl))) {
|
|
316
|
+
ui.warning("Browser", "could not open automatically; use the URL above");
|
|
326
317
|
}
|
|
318
|
+
} else {
|
|
319
|
+
process.stdout.write(`${JSON.stringify({ event: "connector_account_pairing", portal_url: portalUrl })}\n`);
|
|
320
|
+
}
|
|
321
|
+
const pairingCode = flags["pairing-code"] ?? await askForPairingCode(
|
|
322
|
+
ui,
|
|
323
|
+
" Enter the pairing code shown in your Re-entry dashboard (XXXX-XXXX): ",
|
|
324
|
+
);
|
|
325
|
+
if (ui.interactive) ui.step("Pairing code", "received");
|
|
326
|
+
const credentials = await client.connectWithPairingCode({
|
|
327
|
+
pairingCode,
|
|
328
|
+
deviceName: flags["device-name"] ?? defaultDeviceName(),
|
|
329
|
+
});
|
|
330
|
+
if (ui.interactive) {
|
|
331
|
+
ui.success("Account", "connected");
|
|
327
332
|
}
|
|
328
333
|
const saved = {
|
|
329
334
|
version: 1,
|
|
@@ -333,6 +338,7 @@ async function connect(flags, ui, options = {}) {
|
|
|
333
338
|
connector_expires_at: credentials.connector_expires_at,
|
|
334
339
|
};
|
|
335
340
|
await store.save(saved);
|
|
341
|
+
await clearConnectorReauthorizationRequired(credentialFile);
|
|
336
342
|
if (ui.interactive) {
|
|
337
343
|
if (!options.guidedInstall) {
|
|
338
344
|
ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
|
|
@@ -351,23 +357,32 @@ async function status(flags, ui) {
|
|
|
351
357
|
if (ui.interactive) ui.begin("Status", "A quick check of this Mac and Re-entry.");
|
|
352
358
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
353
359
|
const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
|
|
360
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
|
|
354
361
|
const readiness = inspectReadiness(flags);
|
|
355
362
|
const service = await inspectMacConnectorService();
|
|
356
363
|
const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
|
|
357
|
-
const connected = Boolean(
|
|
364
|
+
const connected = Boolean(
|
|
365
|
+
credentials &&
|
|
366
|
+
Date.parse(credentials.connector_expires_at) > Date.now() &&
|
|
367
|
+
!reauthorizationRequired,
|
|
368
|
+
);
|
|
358
369
|
if (ui.interactive) {
|
|
359
370
|
ui.section("SYSTEM", "This Mac");
|
|
360
371
|
showReadiness(readiness, ui);
|
|
361
372
|
ui.section("CONNECTION", "Re-entry");
|
|
362
|
-
if (
|
|
373
|
+
if (reauthorizationRequired) ui.warning("Account", "reconnect required");
|
|
374
|
+
else if (connected) ui.success("Account", "connected");
|
|
363
375
|
else ui.warning("Account", "not connected");
|
|
364
|
-
if (service.running) ui.
|
|
376
|
+
if (reauthorizationRequired && service.running) ui.warning("Background", "paused until this Mac is reconnected");
|
|
377
|
+
else if (service.running) ui.success("Background", "running");
|
|
365
378
|
else if (service.installed) ui.warning("Background", "stopped");
|
|
366
379
|
else ui.warning("Background", "not installed");
|
|
367
|
-
if (
|
|
368
|
-
else if (
|
|
380
|
+
if (receiverReady) ui.success("Cloud", "online");
|
|
381
|
+
else if (credentials) ui.warning("Cloud", "unavailable");
|
|
369
382
|
|
|
370
|
-
if (
|
|
383
|
+
if (reauthorizationRequired) {
|
|
384
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser; the old credential was rejected.");
|
|
385
|
+
} else if (!connected || !service.running) {
|
|
371
386
|
ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
|
|
372
387
|
} else if (!receiverReady) {
|
|
373
388
|
ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
|
|
@@ -379,6 +394,7 @@ async function status(flags, ui) {
|
|
|
379
394
|
process.stdout.write(`${JSON.stringify({
|
|
380
395
|
event: "connector_status",
|
|
381
396
|
connected,
|
|
397
|
+
reauthorization_required: reauthorizationRequired,
|
|
382
398
|
connector_id: credentials?.connector_id ?? null,
|
|
383
399
|
receiver_origin: credentials?.receiver_origin ?? null,
|
|
384
400
|
receiver_ready: receiverReady,
|
|
@@ -399,8 +415,13 @@ async function listen(flags, ui) {
|
|
|
399
415
|
const credentials = await new LocalConnectorCredentialStore({
|
|
400
416
|
filename: defaultCredentialFile(),
|
|
401
417
|
}).load();
|
|
402
|
-
|
|
418
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(defaultCredentialFile());
|
|
419
|
+
if (reauthorizationRequired || !service.running || !credentials) {
|
|
403
420
|
if (ui.interactive) {
|
|
421
|
+
if (reauthorizationRequired) {
|
|
422
|
+
ui.warning("Account", "reconnect required; the Cloud Receiver rejected this Mac");
|
|
423
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
424
|
+
}
|
|
404
425
|
if (!credentials) ui.warning("Account", "not connected");
|
|
405
426
|
if (!service.running) ui.warning("Background", "not running");
|
|
406
427
|
ui.next("re-entry install", "Finish setup and start the background Connector.");
|
|
@@ -408,6 +429,7 @@ async function listen(flags, ui) {
|
|
|
408
429
|
process.stdout.write(`${JSON.stringify({
|
|
409
430
|
event: "connector_listener_unavailable",
|
|
410
431
|
connected: Boolean(credentials),
|
|
432
|
+
reauthorization_required: reauthorizationRequired,
|
|
411
433
|
service_running: service.running,
|
|
412
434
|
})}\n`);
|
|
413
435
|
}
|
|
@@ -482,6 +504,10 @@ function reportLiveActivity(event, ui) {
|
|
|
482
504
|
accepted ? "a fresh Codex session was started" : String(event.outcome ?? "unknown"),
|
|
483
505
|
accepted ? "success" : "warning",
|
|
484
506
|
);
|
|
507
|
+
} else if (event.event === "connector_reauthorization_required" || event.code === "connector_identity_invalid") {
|
|
508
|
+
ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
|
|
509
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
510
|
+
return;
|
|
485
511
|
} else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
|
|
486
512
|
ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
|
|
487
513
|
} else if (event.event === "connector_ready") {
|
|
@@ -545,7 +571,7 @@ async function install(flags, ui) {
|
|
|
545
571
|
ui.success("Selected", readiness.workingDirectory);
|
|
546
572
|
ui.section("2 OF 3", "System check");
|
|
547
573
|
showReadiness(readiness, ui, { includeWorkspace: false });
|
|
548
|
-
ui.section("3 OF 3", "Connect Re-entry", "
|
|
574
|
+
ui.section("3 OF 3", "Connect Re-entry", "Create or sign in to your account, then pair this Mac from the dashboard.");
|
|
549
575
|
}
|
|
550
576
|
const credentials = await connect(runtimeFlags, ui, { quietHeader: true, guidedInstall: true });
|
|
551
577
|
if (ui.interactive) ui.wait("Starting Re-entry in the background…");
|
|
@@ -592,8 +618,16 @@ async function start(flags, ui) {
|
|
|
592
618
|
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
593
619
|
let credentials = await store.load();
|
|
594
620
|
if (!credentials) {
|
|
595
|
-
if (ui.interactive) ui.info("Re-entry", "first run —
|
|
621
|
+
if (ui.interactive) ui.info("Re-entry", "first run — create an account and enter a dashboard pairing code");
|
|
596
622
|
credentials = await connect(runtimeFlags, ui, { quietHeader: true });
|
|
623
|
+
} else if (await hasConnectorReauthorizationRequired(credentialFile)) {
|
|
624
|
+
if (ui.interactive) {
|
|
625
|
+
ui.warning("Account", "this Mac needs to be connected again");
|
|
626
|
+
ui.next("re-entry connect", "Create a new dashboard pairing code and connect this Mac again.");
|
|
627
|
+
} else {
|
|
628
|
+
process.stdout.write('{"event":"connector_reauthorization_required"}\n');
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
597
631
|
} else if (ui.interactive) {
|
|
598
632
|
ui.success("Re-entry", "existing account connection loaded");
|
|
599
633
|
}
|
|
@@ -629,6 +663,18 @@ async function start(flags, ui) {
|
|
|
629
663
|
if (ui.interactive) ui.wait("Connected. Waiting for approved work…");
|
|
630
664
|
}
|
|
631
665
|
} catch (error) {
|
|
666
|
+
if (error?.code === "connector_identity_invalid") {
|
|
667
|
+
await markConnectorReauthorizationRequired(credentialFile, {
|
|
668
|
+
receiver_origin: credentials.receiver_origin,
|
|
669
|
+
});
|
|
670
|
+
if (ui.interactive) {
|
|
671
|
+
ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
|
|
672
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
673
|
+
} else {
|
|
674
|
+
process.stdout.write('{"event":"connector_reauthorization_required"}\n');
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
632
678
|
consecutiveErrors += 1;
|
|
633
679
|
if (ui.interactive) {
|
|
634
680
|
ui.stopWait("Receiver unavailable", `${safeErrorMessage(error)} · ${consecutiveErrors}/${maximumErrors}`, "warning");
|
|
@@ -810,7 +856,7 @@ function validateCommandFlags(command, flags, positionals) {
|
|
|
810
856
|
const allowedByCommand = {
|
|
811
857
|
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
812
858
|
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
813
|
-
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
859
|
+
connect: new Set(["receiver", "device-name", "credential-file", "pairing-code", "json"]),
|
|
814
860
|
status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
|
|
815
861
|
listen: new Set(["json"]),
|
|
816
862
|
test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
|
|
@@ -822,6 +868,7 @@ function validateCommandFlags(command, flags, positionals) {
|
|
|
822
868
|
"credential-file",
|
|
823
869
|
"codex-cd",
|
|
824
870
|
"codex-binary",
|
|
871
|
+
"pairing-code",
|
|
825
872
|
"json",
|
|
826
873
|
]),
|
|
827
874
|
"claim-once": new Set([
|
|
@@ -957,13 +1004,13 @@ function safeErrorMessage(error) {
|
|
|
957
1004
|
: error.message;
|
|
958
1005
|
}
|
|
959
1006
|
|
|
960
|
-
async function askForPairingCode(ui) {
|
|
1007
|
+
async function askForPairingCode(ui, prompt = " Enter the pairing code: ") {
|
|
961
1008
|
if (!ui.interactive || process.stdin.isTTY !== true) {
|
|
962
1009
|
throw cliFailure("connector_pairing_code_missing");
|
|
963
1010
|
}
|
|
964
1011
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
965
1012
|
try {
|
|
966
|
-
const answer = await readline.question(
|
|
1013
|
+
const answer = await readline.question(prompt);
|
|
967
1014
|
return answer.trim();
|
|
968
1015
|
} finally {
|
|
969
1016
|
readline.close();
|
|
@@ -990,6 +1037,8 @@ function errorHint(error) {
|
|
|
990
1037
|
connector_node_unsupported: "use Node.js 24 or newer, then run the command again",
|
|
991
1038
|
connector_credentials_missing: "connect this Mac once with `re-entry connect`",
|
|
992
1039
|
connector_credentials_expired: "run `re-entry connect` to authorize this Mac again",
|
|
1040
|
+
connector_identity_invalid: "run `re-entry connect` and approve this Mac again",
|
|
1041
|
+
connector_reauthorization_required: "run `re-entry connect` and approve this Mac again",
|
|
993
1042
|
connector_pairing_code_missing: "ask the Host backend for a new pairing code, then run pair in a terminal",
|
|
994
1043
|
pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
|
|
995
1044
|
host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
|
package/src/pairing-client.mjs
CHANGED
|
@@ -2,15 +2,21 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { TextDecoder } from "node:util";
|
|
3
3
|
|
|
4
4
|
export const PAIRING_CLIENT_ROUTES = Object.freeze({
|
|
5
|
+
accountPairingClaim: "/v0.1/account/pairing-sessions/claim",
|
|
5
6
|
claim: "/v0.1/pairing-sessions/claim",
|
|
6
7
|
poll: "/v0.1/pairing-sessions/poll",
|
|
7
8
|
deviceStart: "/v0.1/device-authorizations",
|
|
8
9
|
devicePoll: "/v0.1/device-authorizations/poll",
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
export async function openBrowser(url) {
|
|
13
|
+
return defaultOpenBrowser(url);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const OPTION_FIELDS = Object.freeze(["baseUrl", "requestTimeoutMs", "openBrowser", "sleep"]);
|
|
12
17
|
const PAIR_INPUT_FIELDS = Object.freeze(["userCode"]);
|
|
13
18
|
const CONNECT_INPUT_FIELDS = Object.freeze(["deviceName"]);
|
|
19
|
+
const ACCOUNT_PAIRING_INPUT_FIELDS = Object.freeze(["pairingCode", "deviceName"]);
|
|
14
20
|
const DEVICE_AUTHORIZATION_FIELDS = Object.freeze([
|
|
15
21
|
"type",
|
|
16
22
|
"protocol_version",
|
|
@@ -68,6 +74,7 @@ const ERROR_BODY_FIELDS = Object.freeze(["code"]);
|
|
|
68
74
|
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
|
|
69
75
|
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
70
76
|
const USER_CODE_PATTERN = /^[A-F0-9]{16}$/;
|
|
77
|
+
const ACCOUNT_PAIRING_CODE_PATTERN = /^[A-F0-9]{8}$/;
|
|
71
78
|
const CONTENT_TYPE_PATTERN = /^application\/json(?:\s*;\s*charset=utf-8)?$/i;
|
|
72
79
|
const MIN_REQUEST_TIMEOUT_MS = 100;
|
|
73
80
|
const MAX_REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -197,6 +204,26 @@ export class LocalConnectorPairingClient {
|
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
async connectWithPairingCode(input) {
|
|
208
|
+
requireExactRecord(
|
|
209
|
+
input,
|
|
210
|
+
ACCOUNT_PAIRING_INPUT_FIELDS,
|
|
211
|
+
ACCOUNT_PAIRING_INPUT_FIELDS,
|
|
212
|
+
"Account pairing input",
|
|
213
|
+
);
|
|
214
|
+
const pairingCode = normalizeAccountPairingCode(input.pairingCode);
|
|
215
|
+
const deviceName = requireDeviceName(input.deviceName);
|
|
216
|
+
const response = await this.#post(PAIRING_CLIENT_ROUTES.accountPairingClaim, {
|
|
217
|
+
pairing_code: pairingCode,
|
|
218
|
+
device_name: deviceName,
|
|
219
|
+
});
|
|
220
|
+
if (response.status !== 200) throw await parseHttpFailure(response);
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
...normalizeCredentials(await parseJsonResponse(response)),
|
|
223
|
+
browserOpened: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
200
227
|
async #post(path, body) {
|
|
201
228
|
let response;
|
|
202
229
|
try {
|
|
@@ -331,6 +358,17 @@ function normalizeCredentials(value) {
|
|
|
331
358
|
};
|
|
332
359
|
}
|
|
333
360
|
|
|
361
|
+
function normalizeAccountPairingCode(value) {
|
|
362
|
+
if (typeof value !== "string") {
|
|
363
|
+
throw pairingFailure("account_pairing_code_invalid", "Pairing code is invalid");
|
|
364
|
+
}
|
|
365
|
+
const normalized = value.replaceAll("-", "").trim().toUpperCase();
|
|
366
|
+
if (!ACCOUNT_PAIRING_CODE_PATTERN.test(normalized)) {
|
|
367
|
+
throw pairingFailure("account_pairing_code_invalid", "Pairing code is invalid");
|
|
368
|
+
}
|
|
369
|
+
return normalized;
|
|
370
|
+
}
|
|
371
|
+
|
|
334
372
|
async function parseJsonResponse(response) {
|
|
335
373
|
const declared = response.headers.get("content-length");
|
|
336
374
|
if (declared !== null && (!/^(?:0|[1-9][0-9]*)$/.test(declared) || Number(declared) > MAX_RESPONSE_BYTES)) {
|