@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.
|
|
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
|
|
@@ -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,7 +18,12 @@ import {
|
|
|
18
18
|
verifyCodexExecutable,
|
|
19
19
|
} from "./codex-discovery.mjs";
|
|
20
20
|
import { LocalConnector } from "./local-connector.mjs";
|
|
21
|
-
import {
|
|
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(
|
|
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 (
|
|
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.
|
|
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 (
|
|
368
|
-
else if (
|
|
381
|
+
if (receiverReady) ui.success("Cloud", "online");
|
|
382
|
+
else if (credentials) ui.warning("Cloud", "unavailable");
|
|
369
383
|
|
|
370
|
-
if (
|
|
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
|
-
|
|
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",
|