@4xeoz/re-entry 0.2.7 → 0.2.9

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,10 @@ 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
72
- -> open Re-entry in the default browser
73
- -> sign in or create an account
74
- -> approve this Mac once
70
+ -> open the dedicated Re-entry user account page in the default browser
71
+ -> sign in or create a user account
72
+ -> click Pair this Mac in the dashboard
73
+ -> enter the short pairing code in the CLI
75
74
  -> save the device credential with mode 0600
76
75
  -> install and start a per-user macOS LaunchAgent
77
76
  ```
@@ -133,8 +132,8 @@ Copy this prompt into a coding-agent task:
133
132
  ```text
134
133
  Install the Re-entry Local Connector on this Mac. First read the package README. Verify Node.js 24
135
134
  or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
136
- project directory. Let the human complete the Re-entry
137
- browser approval; never copy browser cookies, organization keys, Connector tokens, or private keys
135
+ project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
136
+ enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
138
137
  into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
139
138
  results without claiming Browser/WebMCP or production deployment.
140
139
  ```
@@ -142,7 +141,7 @@ results without claiming Browser/WebMCP or production deployment.
142
141
  ## Package map
143
142
 
144
143
  - `src/main.mjs` — `re-entry` CLI and long-running poll loop.
145
- - `src/pairing-client.mjs` — account-first browser device authorization.
144
+ - `src/pairing-client.mjs` — account pairing-code redemption and legacy pairing compatibility.
146
145
  - `src/credentials.mjs` — atomic local credential storage.
147
146
  - `src/macos-service.mjs` — per-user LaunchAgent install, stop, uninstall, and status.
148
147
  - `src/workspace-picker.mjs` — interactive Codex workspace selection.
@@ -251,9 +250,10 @@ npm start -- \
251
250
  --codex-cd "$HOME/Code/my-host-project"
252
251
  ```
253
252
 
254
- On the first run, the Connector checks the Mac, asks for the one-time code from the Host
255
- backend, opens the approval page, waits for the user to approve it, stores the credential, and
256
- checks once for approved work. On later runs it reuses the saved credential and skips pairing:
253
+ On the first run, the Connector checks the Mac, opens the dedicated Re-entry user account page,
254
+ waits for you to create or sign in to an account and click Pair this Mac, then asks for the
255
+ dashboard code and stores the credential. On later runs it reuses the saved credential and skips
256
+ pairing:
257
257
 
258
258
  ```sh
259
259
  npm start -- --codex-cd "$HOME/Code/my-host-project"
@@ -271,13 +271,13 @@ events instead so another process can consume it:
271
271
  npm start -- --json --codex-cd "$HOME/Code/my-host-project"
272
272
  ```
273
273
 
274
- ## Pair this Mac with a Host user
274
+ ## Compatibility: Host-code pairing (legacy)
275
275
 
276
276
  Pairing connects one local Connector to one already-authenticated Host user. It is separate from
277
277
  the Host organization's API key and does not give the Connector permission to create Grants or
278
278
  send Host events.
279
279
 
280
- The flow is:
280
+ The older compatibility flow is:
281
281
 
282
282
  ```text
283
283
  Host backend -> Receiver: start pairing for its Host-user reference
@@ -290,7 +290,8 @@ Receiver -> Connector: one Connector credential
290
290
  Connector -> local disk: save the credential with mode 0600
291
291
  ```
292
292
 
293
- The Host backend starts pairing with its organization credential. In the current local preview:
293
+ The current preview does not use this Host-issued code path. Use `re-entry install` above, where
294
+ the authenticated Re-entry dashboard creates the code.
294
295
 
295
296
  ```sh
296
297
  curl -s -X POST http://127.0.0.1:43218/v0.1/pairing-sessions \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
package/src/main.mjs CHANGED
@@ -24,8 +24,7 @@ import {
24
24
  hasConnectorReauthorizationRequired,
25
25
  markConnectorReauthorizationRequired,
26
26
  } from "./credentials.mjs";
27
- import { LocalConnectorPairingClient } from "./pairing-client.mjs";
28
- import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
27
+ import { LocalConnectorPairingClient, openBrowser } from "./pairing-client.mjs";
29
28
  import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
30
29
  import {
31
30
  inspectMacConnectorService,
@@ -135,7 +134,7 @@ Commands:
135
134
  test Start one fresh local Codex session with the supplied prompt
136
135
  stop Stop the background Connector without removing its credential
137
136
  uninstall Stop the Connector and remove its local service data
138
- connect Authorize this Mac without installing the background service
137
+ connect Open Re-entry, redeem a dashboard pairing code, and connect this Mac
139
138
  start Poll for approved work until stopped
140
139
  doctor Check Node.js, Codex, and the optional project directory
141
140
  claim-once Check once for approved work
@@ -272,7 +271,7 @@ async function pair(flags, ui, options = {}) {
272
271
 
273
272
  async function connect(flags, ui, options = {}) {
274
273
  if (!options.quietHeader && ui.interactive) {
275
- 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");
276
275
  }
277
276
  const receiver = flags.receiver ?? DEFAULT_RECEIVER_ORIGIN;
278
277
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
@@ -308,28 +307,28 @@ async function connect(flags, ui, options = {}) {
308
307
  baseUrl: receiver,
309
308
  requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
310
309
  });
311
- if (ui.interactive) ui.wait("Contacting Re-entry…");
312
- const credentials = await client.connect(
313
- { deviceName: flags["device-name"] ?? defaultDeviceName() },
314
- async ({ verificationUri }) => {
315
- if (ui.interactive) {
316
- ui.stopWait("Secure link", "ready");
317
- ui.info("Browser link", verificationUri);
318
- await waitForEnterToOpenBrowser();
319
- ui.wait("Waiting for approval in your browser…");
320
- } else {
321
- process.stdout.write(`${JSON.stringify({
322
- event: "connector_authorization_waiting",
323
- verification_uri: verificationUri,
324
- })}\n`);
325
- }
326
- },
327
- );
310
+ const portalUrl = `${receiver}/user-register?next=${encodeURIComponent("/dashboard")}`;
328
311
  if (ui.interactive) {
329
- ui.stopWait("Account", "connected");
330
- if (!credentials.browserOpened) {
331
- 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");
332
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");
333
332
  }
334
333
  const saved = {
335
334
  version: 1,
@@ -572,7 +571,7 @@ async function install(flags, ui) {
572
571
  ui.success("Selected", readiness.workingDirectory);
573
572
  ui.section("2 OF 3", "System check");
574
573
  showReadiness(readiness, ui, { includeWorkspace: false });
575
- 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.");
576
575
  }
577
576
  const credentials = await connect(runtimeFlags, ui, { quietHeader: true, guidedInstall: true });
578
577
  if (ui.interactive) ui.wait("Starting Re-entry in the background…");
@@ -619,12 +618,12 @@ async function start(flags, ui) {
619
618
  const store = new LocalConnectorCredentialStore({ filename: credentialFile });
620
619
  let credentials = await store.load();
621
620
  if (!credentials) {
622
- 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");
623
622
  credentials = await connect(runtimeFlags, ui, { quietHeader: true });
624
623
  } else if (await hasConnectorReauthorizationRequired(credentialFile)) {
625
624
  if (ui.interactive) {
626
625
  ui.warning("Account", "this Mac needs to be connected again");
627
- ui.next("re-entry connect", "Approve this Mac in your browser, then start the Connector again.");
626
+ ui.next("re-entry connect", "Create a new dashboard pairing code and connect this Mac again.");
628
627
  } else {
629
628
  process.stdout.write('{"event":"connector_reauthorization_required"}\n');
630
629
  }
@@ -857,7 +856,7 @@ function validateCommandFlags(command, flags, positionals) {
857
856
  const allowedByCommand = {
858
857
  doctor: new Set(["codex-binary", "codex-cd", "json"]),
859
858
  pair: new Set(["receiver", "code", "credential-file", "json"]),
860
- connect: new Set(["receiver", "device-name", "credential-file", "json"]),
859
+ connect: new Set(["receiver", "device-name", "credential-file", "pairing-code", "json"]),
861
860
  status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
862
861
  listen: new Set(["json"]),
863
862
  test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
@@ -869,6 +868,7 @@ function validateCommandFlags(command, flags, positionals) {
869
868
  "credential-file",
870
869
  "codex-cd",
871
870
  "codex-binary",
871
+ "pairing-code",
872
872
  "json",
873
873
  ]),
874
874
  "claim-once": new Set([
@@ -1004,13 +1004,13 @@ function safeErrorMessage(error) {
1004
1004
  : error.message;
1005
1005
  }
1006
1006
 
1007
- async function askForPairingCode(ui) {
1007
+ async function askForPairingCode(ui, prompt = " Enter the pairing code: ") {
1008
1008
  if (!ui.interactive || process.stdin.isTTY !== true) {
1009
1009
  throw cliFailure("connector_pairing_code_missing");
1010
1010
  }
1011
1011
  const readline = createInterface({ input: process.stdin, output: process.stdout });
1012
1012
  try {
1013
- const answer = await readline.question(" Enter the pairing code from your Host (XXXX-XXXX-XXXX-XXXX): ");
1013
+ const answer = await readline.question(prompt);
1014
1014
  return answer.trim();
1015
1015
  } finally {
1016
1016
  readline.close();
@@ -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)) {