@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 browser approval, a macOS
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
- -> request a device authorization from Re-entry
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 an account
74
- -> approve this Mac once
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. `listen` watches the already-running background Connector and displays new activity until
93
- you press Ctrl+C; it does not start a competing second poller. Useful development commands are:
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 complete the Re-entry
135
- browser approval; never copy browser cookies, organization keys, Connector tokens, or private keys
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-first browser device authorization.
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, asks for the one-time code from the Host
253
- backend, opens the approval page, waits for the user to approve it, stores the credential, and
254
- checks once for approved work. On later runs it reuses the saved credential and skips pairing:
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
- ## Pair this Mac with a Host user
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 Host backend starts pairing with its organization credential. In the current local preview:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
@@ -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");
@@ -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 { LocalConnectorCredentialStore } from "./credentials.mjs";
22
- import { LocalConnectorPairingClient } from "./pairing-client.mjs";
23
- import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
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 Authorize this Mac without installing the background service
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", "One browser approval, then Re-entry stays connected");
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
- if (ui.interactive) ui.wait("Contacting Re-entry…");
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("Account", "connected");
324
- if (!credentials.browserOpened) {
325
- ui.warning("Browser", "did not open automatically; use the URL shown above");
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(credentials && Date.parse(credentials.connector_expires_at) > Date.now());
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 (connected) ui.success("Account", "connected");
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.success("Background", "running");
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 (connected && receiverReady) ui.success("Cloud", "online");
368
- else if (connected) ui.warning("Cloud", "unavailable");
380
+ if (receiverReady) ui.success("Cloud", "online");
381
+ else if (credentials) ui.warning("Cloud", "unavailable");
369
382
 
370
- if (!connected || !service.running) {
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
- if (!service.running || !credentials) {
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", "Approve this Mac in your browser.");
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 — browser approval is required once");
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(" Enter the pairing code from your Host (XXXX-XXXX-XXXX-XXXX): ");
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",
@@ -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)) {