@4xeoz/re-entry 0.2.6 → 0.2.7

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
@@ -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
@@ -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.7",
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,7 +18,12 @@ import {
18
18
  verifyCodexExecutable,
19
19
  } from "./codex-discovery.mjs";
20
20
  import { LocalConnector } from "./local-connector.mjs";
21
- import { LocalConnectorCredentialStore } from "./credentials.mjs";
21
+ import {
22
+ LocalConnectorCredentialStore,
23
+ clearConnectorReauthorizationRequired,
24
+ hasConnectorReauthorizationRequired,
25
+ markConnectorReauthorizationRequired,
26
+ } from "./credentials.mjs";
22
27
  import { LocalConnectorPairingClient } from "./pairing-client.mjs";
23
28
  import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
24
29
  import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
@@ -273,8 +278,9 @@ async function connect(flags, ui, options = {}) {
273
278
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
274
279
  const store = new LocalConnectorCredentialStore({ filename: credentialFile });
275
280
  const current = await store.load();
281
+ const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
276
282
  const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
277
- if (currentIsValid && current.receiver_origin === receiver) {
283
+ if (currentIsValid && current.receiver_origin === receiver && !reauthorizationRequired) {
278
284
  if (ui.interactive) {
279
285
  ui.success("Account", "already connected");
280
286
  if (!options.guidedInstall) {
@@ -333,6 +339,7 @@ async function connect(flags, ui, options = {}) {
333
339
  connector_expires_at: credentials.connector_expires_at,
334
340
  };
335
341
  await store.save(saved);
342
+ await clearConnectorReauthorizationRequired(credentialFile);
336
343
  if (ui.interactive) {
337
344
  if (!options.guidedInstall) {
338
345
  ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
@@ -351,23 +358,32 @@ async function status(flags, ui) {
351
358
  if (ui.interactive) ui.begin("Status", "A quick check of this Mac and Re-entry.");
352
359
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
353
360
  const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
361
+ const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
354
362
  const readiness = inspectReadiness(flags);
355
363
  const service = await inspectMacConnectorService();
356
364
  const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
357
- const connected = Boolean(credentials && Date.parse(credentials.connector_expires_at) > Date.now());
365
+ const connected = Boolean(
366
+ credentials &&
367
+ Date.parse(credentials.connector_expires_at) > Date.now() &&
368
+ !reauthorizationRequired,
369
+ );
358
370
  if (ui.interactive) {
359
371
  ui.section("SYSTEM", "This Mac");
360
372
  showReadiness(readiness, ui);
361
373
  ui.section("CONNECTION", "Re-entry");
362
- if (connected) ui.success("Account", "connected");
374
+ if (reauthorizationRequired) ui.warning("Account", "reconnect required");
375
+ else if (connected) ui.success("Account", "connected");
363
376
  else ui.warning("Account", "not connected");
364
- if (service.running) ui.success("Background", "running");
377
+ if (reauthorizationRequired && service.running) ui.warning("Background", "paused until this Mac is reconnected");
378
+ else if (service.running) ui.success("Background", "running");
365
379
  else if (service.installed) ui.warning("Background", "stopped");
366
380
  else ui.warning("Background", "not installed");
367
- if (connected && receiverReady) ui.success("Cloud", "online");
368
- else if (connected) ui.warning("Cloud", "unavailable");
381
+ if (receiverReady) ui.success("Cloud", "online");
382
+ else if (credentials) ui.warning("Cloud", "unavailable");
369
383
 
370
- if (!connected || !service.running) {
384
+ if (reauthorizationRequired) {
385
+ ui.next("re-entry connect", "Approve this Mac again in your browser; the old credential was rejected.");
386
+ } else if (!connected || !service.running) {
371
387
  ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
372
388
  } else if (!receiverReady) {
373
389
  ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
@@ -379,6 +395,7 @@ async function status(flags, ui) {
379
395
  process.stdout.write(`${JSON.stringify({
380
396
  event: "connector_status",
381
397
  connected,
398
+ reauthorization_required: reauthorizationRequired,
382
399
  connector_id: credentials?.connector_id ?? null,
383
400
  receiver_origin: credentials?.receiver_origin ?? null,
384
401
  receiver_ready: receiverReady,
@@ -399,8 +416,13 @@ async function listen(flags, ui) {
399
416
  const credentials = await new LocalConnectorCredentialStore({
400
417
  filename: defaultCredentialFile(),
401
418
  }).load();
402
- if (!service.running || !credentials) {
419
+ const reauthorizationRequired = await hasConnectorReauthorizationRequired(defaultCredentialFile());
420
+ if (reauthorizationRequired || !service.running || !credentials) {
403
421
  if (ui.interactive) {
422
+ if (reauthorizationRequired) {
423
+ ui.warning("Account", "reconnect required; the Cloud Receiver rejected this Mac");
424
+ ui.next("re-entry connect", "Approve this Mac again in your browser.");
425
+ }
404
426
  if (!credentials) ui.warning("Account", "not connected");
405
427
  if (!service.running) ui.warning("Background", "not running");
406
428
  ui.next("re-entry install", "Finish setup and start the background Connector.");
@@ -408,6 +430,7 @@ async function listen(flags, ui) {
408
430
  process.stdout.write(`${JSON.stringify({
409
431
  event: "connector_listener_unavailable",
410
432
  connected: Boolean(credentials),
433
+ reauthorization_required: reauthorizationRequired,
411
434
  service_running: service.running,
412
435
  })}\n`);
413
436
  }
@@ -482,6 +505,10 @@ function reportLiveActivity(event, ui) {
482
505
  accepted ? "a fresh Codex session was started" : String(event.outcome ?? "unknown"),
483
506
  accepted ? "success" : "warning",
484
507
  );
508
+ } else if (event.event === "connector_reauthorization_required" || event.code === "connector_identity_invalid") {
509
+ ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
510
+ ui.next("re-entry connect", "Approve this Mac again in your browser.");
511
+ return;
485
512
  } else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
486
513
  ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
487
514
  } else if (event.event === "connector_ready") {
@@ -594,6 +621,14 @@ async function start(flags, ui) {
594
621
  if (!credentials) {
595
622
  if (ui.interactive) ui.info("Re-entry", "first run — browser approval is required once");
596
623
  credentials = await connect(runtimeFlags, ui, { quietHeader: true });
624
+ } else if (await hasConnectorReauthorizationRequired(credentialFile)) {
625
+ if (ui.interactive) {
626
+ 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.");
628
+ } else {
629
+ process.stdout.write('{"event":"connector_reauthorization_required"}\n');
630
+ }
631
+ return;
597
632
  } else if (ui.interactive) {
598
633
  ui.success("Re-entry", "existing account connection loaded");
599
634
  }
@@ -629,6 +664,18 @@ async function start(flags, ui) {
629
664
  if (ui.interactive) ui.wait("Connected. Waiting for approved work…");
630
665
  }
631
666
  } catch (error) {
667
+ if (error?.code === "connector_identity_invalid") {
668
+ await markConnectorReauthorizationRequired(credentialFile, {
669
+ receiver_origin: credentials.receiver_origin,
670
+ });
671
+ if (ui.interactive) {
672
+ ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
673
+ ui.next("re-entry connect", "Approve this Mac again in your browser.");
674
+ } else {
675
+ process.stdout.write('{"event":"connector_reauthorization_required"}\n');
676
+ }
677
+ return;
678
+ }
632
679
  consecutiveErrors += 1;
633
680
  if (ui.interactive) {
634
681
  ui.stopWait("Receiver unavailable", `${safeErrorMessage(error)} · ${consecutiveErrors}/${maximumErrors}`, "warning");
@@ -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",