relmio 0.2.9 → 0.2.11

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/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ checks the registry separately after publication.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.2.11] - 2026-08-04
11
+
12
+ ### Fixed
13
+
14
+ - Open the private Windows wizard URL through the documented default-browser
15
+ association instead of asking Explorer to treat the URL as a folder, while
16
+ retaining the printed URL and Enter-to-retry fallback.
17
+ - Make the Command Prompt installer copy call the system Windows PowerShell
18
+ executable directly, avoiding ambiguous `powershell` command resolution.
19
+ - Parse the supported `openai-oauth@2.0.0` login line across Windows terminal
20
+ control sequences and chunk boundaries, and report a sanitized, actionable
21
+ callback-port conflict instead of a generic missing-link error.
22
+
23
+ ### Security
24
+
25
+ - Pin the hosted web tooling to `brace-expansion` 5.0.9, which includes the
26
+ upstream denial-of-service fix required by the release audit.
27
+
28
+ ## [0.2.10] - 2026-08-03
29
+
30
+ ### Fixed
31
+
32
+ - Probe installed Windows Node.js runtimes with the literal `node --version`
33
+ output instead of a `node -p` expression, avoiding the PowerShell `[eval]:1`
34
+ quoting failure while still reusing Node.js 22 or newer.
35
+ - Let interactive wizard terminals reopen the local browser page when the user
36
+ presses Enter, while retaining the printed private URL as the fallback for
37
+ noninteractive launches.
38
+ - Navigate the preopened local OAuth tab before severing its opener access,
39
+ show an immediate preparing state, and close an unnavigated waiting tab if
40
+ sign-in setup fails.
41
+
10
42
  ## [0.2.9] - 2026-08-02
11
43
 
12
44
  ### Fixed
package/README.md CHANGED
@@ -105,7 +105,7 @@ irm https://relmio.vercel.app/install.ps1 | iex
105
105
  ### Windows Command Prompt
106
106
 
107
107
  ```bat
108
- powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
108
+ "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
109
109
  ```
110
110
 
111
111
  No Node.js or Git Bash installation is required first. The
@@ -128,6 +128,11 @@ Keep the active setup terminal open while using the wizard. If the page says
128
128
  the wizard link is incomplete, close that tab and open the full `Local wizard:`
129
129
  URL printed by the current terminal instead of refreshing the stripped page.
130
130
 
131
+ If the browser does not open automatically, press Enter in an interactive
132
+ terminal to open it again. The printed `Local wizard:` URL remains the fallback
133
+ for a terminal that cannot accept input or when the launcher still cannot open
134
+ a browser.
135
+
131
136
  ### Requirements
132
137
 
133
138
  - On macOS/Linux/WSL/Git Bash: `curl`, `awk`, `tar`, and either `sha256sum` or
@@ -49,7 +49,7 @@ irm https://relmio.vercel.app/install.ps1 | iex
49
49
  Windows Command Prompt:
50
50
 
51
51
  ```bat
52
- powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
52
+ "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -Command "irm https://relmio.vercel.app/install.ps1 | iex"
53
53
  ```
54
54
 
55
55
  These commands do not require Node.js to be installed. The Windows options do
@@ -66,9 +66,15 @@ npm view relmio version
66
66
  npx --yes --ignore-scripts relmio@latest
67
67
  ```
68
68
 
69
- Keep the terminal open. If the browser does not open automatically, copy the
70
- newest printed `http://127.0.0.1:...` URL into the browser. That URL contains a
71
- temporary setup token: do not post it in an issue or screenshot.
69
+ Keep the terminal open. If the running wizard accepts terminal input, press
70
+ Enter to ask it to open the browser again. Otherwise, copy the newest printed
71
+ `http://127.0.0.1:...` URL into the browser. That URL contains a temporary setup
72
+ token: do not post it in an issue or screenshot.
73
+
74
+ The local wizard may be displayed in a VS Code embedded browser. Its validated
75
+ manual link, **Open fresh ChatGPT sign-in**, remains available if that embedded
76
+ browser blocks the popup or no new tab opens. Use that link only from the active
77
+ wizard attempt; it points to the fresh `auth.openai.com` authorization URL.
72
78
 
73
79
  You do not need to sign in to npm, configure npm 2FA, or own this package to
74
80
  run any public command. npm authentication is required only for the
@@ -111,13 +117,15 @@ bypassed.
111
117
  | Symptom | Meaning | Fix |
112
118
  |---|---|---|
113
119
  | `node: command not found`, `node is not recognized`, or Node is older than 22 | The NPX fallback cannot use the local runtime. | Use the macOS/Linux curl command or the native Windows PowerShell/Command Prompt command above. Either can run with a verified temporary runtime. Do not install Node.js on the VPS for the wizard. |
114
- | `curl` or `sh` is not recognized on Windows | The macOS/Linux command was pasted into a native Windows terminal. | Use the PowerShell command in PowerShell, or the longer `powershell -NoProfile ...` command in Command Prompt. Git Bash is not required. |
120
+ | `curl` or `sh` is not recognized on Windows | The macOS/Linux command was pasted into a native Windows terminal. | Use the PowerShell command in PowerShell, or the `"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile ...` command in Command Prompt. Git Bash is not required. |
115
121
  | A bootstrap reports a checksum mismatch | The Node.js download did not match the official SHA-256 manifest, so it was not executed. | Retry on a trusted connection. Do not bypass the check. If it repeats, use an existing Node.js 22+ installation and report the sanitized error. |
122
+ | Windows PowerShell shows `[eval]:1` before the wizard starts | An older bootstrap passed a JavaScript expression through `node -p`; PowerShell native-argument handling can alter that expression. | Update to the latest `relmio@latest` and rerun the same PowerShell or Command Prompt command. The current bootstrap parses the literal `node --version` output and reuses Node.js 22 or newer. |
116
123
  | Windows reports `spawn EINVAL` when starting ChatGPT sign-in | An older wizard tried to execute `npx.cmd` directly; Windows requires the current Node runtime to launch npm's JavaScript CLI. | Update to the latest `relmio@latest` release and restart the setup command. The current wizard keeps the macOS/Linux/WSL/Git Bash `npx` path unchanged. |
117
- | The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
124
+ | The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and press Enter to ask an interactive wizard terminal to open it again. If the terminal is noninteractive or the launcher still fails, copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
118
125
  | The wizard says `This wizard link is incomplete` | The page was refreshed or opened without the private `?session=...` token; the token is removed from the address bar after startup. | Close the tab and open the complete `Local wizard:` URL printed by the active setup terminal. Do not reuse a URL from a closed terminal. |
119
126
  | An old wizard page reports an invalid or expired setup session | The local server was closed or a newer wizard run created a different one-time session token. | Close the old page and use only the URL printed by the currently running terminal. |
120
127
  | `npx` appears to run an older wizard | An old terminal or tab is still active, or the package was run without an explicit tag. | Close old runs, check `npm view relmio version`, then run `npx --yes --ignore-scripts relmio@latest`. |
128
+ | A white `about:blank` tab remains after selecting **Sign in with ChatGPT** | An older local wizard cleared the preopened tab's `opener` before navigating it to the validated sign-in URL. | Update to the latest `relmio@latest`, close the stranded tab, and start one fresh sign-in. The current wizard shows a preparing state, navigates the preopened tab, then clears its opener before the remote page loads. Use **Open fresh ChatGPT sign-in** only when the current wizard shows that validated manual link. |
121
129
  | `This sign-in request expired` | The OAuth tab is old or the five-minute callback window ended. | Close the old tab and select **Refresh ChatGPT sign-in** from the newest active wizard. |
122
130
  | An **OpenAI OAuth** extension page says the sign-in request expired | A browser extension intercepted the `localhost:1455` callback that belongs to the wizard's fresh login. | Temporarily disable the **Sign in with ChatGPT** or **OpenAI OAuth** extension, then select **Refresh ChatGPT sign-in** in the wizard. Re-enable the extension afterward if you still use it elsewhere. |
123
131
  | `ChatGPT sign-in did not finish` appears immediately when refreshing an existing credential | Wizard versions through `0.1.3` attempted to reuse `~/.codex/auth.json`, but the bridge CLI requires an interactive terminal before replacing that file. | Update to `0.1.4` or newer. The wizard signs in through its own new credential file and leaves the Codex app credential untouched. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relmio",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Turn a supported ChatGPT/Codex OAuth sign-in into a private OpenAI-compatible endpoint, starting with self-hosted n8n.",
5
5
  "keywords": [
6
6
  "relmio",
package/src/browser.js ADDED
@@ -0,0 +1,92 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function browserCommand(url, platform = process.platform) {
4
+ if (platform === "darwin") {
5
+ return { file: "open", args: [url] };
6
+ }
7
+ if (platform === "win32") {
8
+ return {
9
+ file: "cmd.exe",
10
+ args: ["/d", "/c", "start", "Relmio local wizard", url],
11
+ };
12
+ }
13
+ return { file: "xdg-open", args: [url] };
14
+ }
15
+
16
+ function isPrivateWizardUrl(value) {
17
+ if (typeof value !== "string" || /[\u0000-\u001F\u007F]/u.test(value)) {
18
+ return false;
19
+ }
20
+
21
+ try {
22
+ const url = new URL(value);
23
+ const session = url.searchParams.get("session");
24
+ return (
25
+ url.protocol === "http:" &&
26
+ url.hostname === "127.0.0.1" &&
27
+ url.port !== "" &&
28
+ Number(url.port) > 0 &&
29
+ url.pathname === "/" &&
30
+ url.username === "" &&
31
+ url.password === "" &&
32
+ url.hash === "" &&
33
+ typeof session === "string" &&
34
+ /^[A-Za-z0-9_-]{43}$/u.test(session) &&
35
+ url.search === `?session=${session}`
36
+ );
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export function openBrowser(
43
+ url,
44
+ { platform = process.platform, spawnProcess = spawn } = {},
45
+ ) {
46
+ if (!isPrivateWizardUrl(url)) {
47
+ return false;
48
+ }
49
+ const command = browserCommand(url, platform);
50
+ let child;
51
+ try {
52
+ child = spawnProcess(command.file, command.args, {
53
+ detached: true,
54
+ stdio: "ignore",
55
+ shell: false,
56
+ });
57
+ } catch {
58
+ return false;
59
+ }
60
+ child.once("error", () => {
61
+ // The URL is also printed, so users can open it manually.
62
+ });
63
+ child.unref();
64
+ return true;
65
+ }
66
+
67
+ export function attachBrowserReopenOnEnter({
68
+ input = process.stdin,
69
+ url,
70
+ open = openBrowser,
71
+ write = console.log,
72
+ }) {
73
+ if (!input.isTTY) {
74
+ return () => {};
75
+ }
76
+
77
+ const onData = (data) => {
78
+ if (/\r|\n/u.test(String(data))) {
79
+ open(url);
80
+ }
81
+ };
82
+
83
+ input.setEncoding?.("utf8");
84
+ input.resume?.();
85
+ input.on("data", onData);
86
+ write("If the wizard did not open automatically, press Enter to open it again.");
87
+
88
+ return () => {
89
+ input.off("data", onData);
90
+ input.pause?.();
91
+ };
92
+ }
package/src/cli.js CHANGED
@@ -1,29 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { randomBytes } from "node:crypto";
4
- import { spawn } from "node:child_process";
5
4
 
5
+ import { attachBrowserReopenOnEnter, openBrowser } from "./browser.js";
6
6
  import { startWizardServer } from "./web/server.js";
7
7
 
8
- function openBrowser(url) {
9
- const command =
10
- process.platform === "darwin"
11
- ? { file: "open", args: [url] }
12
- : process.platform === "win32"
13
- ? { file: "explorer.exe", args: [url] }
14
- : { file: "xdg-open", args: [url] };
15
-
16
- const child = spawn(command.file, command.args, {
17
- detached: true,
18
- stdio: "ignore",
19
- shell: false,
20
- });
21
- child.once("error", () => {
22
- // The URL is also printed, so users can open it manually.
23
- });
24
- child.unref();
25
- }
26
-
27
8
  const sessionToken = randomBytes(32).toString("base64url");
28
9
  const wizard = await startWizardServer({ sessionToken });
29
10
  const url = `${wizard.origin}/?session=${sessionToken}`;
@@ -39,6 +20,7 @@ console.log("Press Control+C to stop.");
39
20
  console.log("");
40
21
 
41
22
  openBrowser(url);
23
+ const detachBrowserReopen = attachBrowserReopenOnEnter({ url });
42
24
 
43
25
  let closing = false;
44
26
  async function close() {
@@ -46,6 +28,7 @@ async function close() {
46
28
  return;
47
29
  }
48
30
  closing = true;
31
+ detachBrowserReopen();
49
32
  await wizard.close();
50
33
  }
51
34
 
@@ -12,6 +12,11 @@ const PROCESS_TIMEOUT_MS = LOGIN_TIMEOUT_MS + 15_000;
12
12
  const CREDENTIAL_POLL_INTERVAL_MS = 100;
13
13
  const LOGIN_URL_PREFIX = "OpenAI OAuth login URL: ";
14
14
  const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
15
+ const SUPPORTED_LOOPBACK_REDIRECT_HOSTNAMES = new Set([
16
+ "localhost",
17
+ "127.0.0.1",
18
+ "[::1]",
19
+ ]);
15
20
 
16
21
  const wait = (milliseconds) =>
17
22
  new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
@@ -124,6 +129,10 @@ function validateAuthorizationUrl(value) {
124
129
 
125
130
  if (
126
131
  url.origin !== OPENAI_AUTH_ORIGIN ||
132
+ url.username !== "" ||
133
+ url.password !== "" ||
134
+ url.hash !== "" ||
135
+ url.href.includes("#") ||
127
136
  url.pathname !== "/oauth/authorize" ||
128
137
  url.searchParams.get("response_type") !== "code" ||
129
138
  !url.searchParams.get("state") ||
@@ -139,7 +148,14 @@ function validateAuthorizationUrl(value) {
139
148
  throw new Error("The sign-in command returned an invalid callback.");
140
149
  }
141
150
  if (
142
- !["localhost", "127.0.0.1", "::1"].includes(redirect.hostname) ||
151
+ redirect.protocol !== "http:" ||
152
+ redirect.username !== "" ||
153
+ redirect.password !== "" ||
154
+ redirect.search !== "" ||
155
+ redirect.hash !== "" ||
156
+ redirect.href.includes("?") ||
157
+ redirect.href.includes("#") ||
158
+ !SUPPORTED_LOOPBACK_REDIRECT_HOSTNAMES.has(redirect.hostname) ||
143
159
  redirect.port !== "1455" ||
144
160
  redirect.pathname !== "/auth/callback"
145
161
  ) {
@@ -149,15 +165,38 @@ function validateAuthorizationUrl(value) {
149
165
  return url.toString();
150
166
  }
151
167
 
152
- function extractAuthorizationUrl(output) {
153
- for (const line of output.split(/\r?\n/u)) {
154
- if (line.startsWith(LOGIN_URL_PREFIX)) {
155
- return validateAuthorizationUrl(line.slice(LOGIN_URL_PREFIX.length));
168
+ function stripTerminalControlSequences(value) {
169
+ return value
170
+ .replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/gu, "")
171
+ .replace(/\u001B\[[0-?]*[ -/]*[@-~]/gu, "")
172
+ .replace(/[\u0000-\u0009\u000B-\u001F\u007F-\u009F]/gu, "");
173
+ }
174
+
175
+ function extractAuthorizationUrl(output, { includeFinalLine = false } = {}) {
176
+ const lines = stripTerminalControlSequences(output).split("\n");
177
+ const lineCount = includeFinalLine ? lines.length : lines.length - 1;
178
+ for (const line of lines.slice(0, lineCount)) {
179
+ const markerIndex = line.indexOf(LOGIN_URL_PREFIX);
180
+ if (markerIndex >= 0) {
181
+ return validateAuthorizationUrl(
182
+ line.slice(markerIndex + LOGIN_URL_PREFIX.length).trim(),
183
+ );
156
184
  }
157
185
  }
158
186
  return null;
159
187
  }
160
188
 
189
+ function loginStartupError(stderr) {
190
+ const callbackPortConflict =
191
+ "OpenAI OAuth login needs http://localhost:1455/auth/callback, but port 1455 is already in use.";
192
+ if (stripTerminalControlSequences(stderr).includes(callbackPortConflict)) {
193
+ return new Error(
194
+ `${callbackPortConflict} Stop the process using that port and try again.`,
195
+ );
196
+ }
197
+ return new Error("The sign-in command did not return an authorization URL.");
198
+ }
199
+
161
200
  export async function startOAuthLogin({
162
201
  fileSystem = defaultFileSystem,
163
202
  env = process.env,
@@ -205,9 +244,8 @@ export async function startOAuthLogin({
205
244
  { cause: error },
206
245
  );
207
246
  }
208
- child.stderr?.resume();
209
-
210
- let loginOutput = "";
247
+ const loginOutput = { stdout: "", stderr: "" };
248
+ let loginOutputBytes = 0;
211
249
  let resolveAuthorizationUrl;
212
250
  let rejectAuthorizationUrl;
213
251
  let authorizationUrlSettled = false;
@@ -227,20 +265,25 @@ export async function startOAuthLogin({
227
265
  }
228
266
  };
229
267
 
230
- child.stdout?.on("data", (chunk) => {
268
+ const captureLoginOutput = (stream, chunk) => {
231
269
  if (authorizationUrlSettled) {
232
270
  return;
233
271
  }
234
- loginOutput += Buffer.from(chunk).toString("utf8");
235
- if (Buffer.byteLength(loginOutput) > MAX_LOGIN_OUTPUT_BYTES) {
272
+ const output = Buffer.from(chunk).toString("utf8");
273
+ loginOutputBytes += Buffer.byteLength(output);
274
+ if (loginOutputBytes > MAX_LOGIN_OUTPUT_BYTES) {
236
275
  settleAuthorizationUrl(
237
276
  new Error("The sign-in command returned too much output."),
238
277
  );
239
278
  child.kill?.("SIGTERM");
240
279
  return;
241
280
  }
281
+ loginOutput[stream] += output;
282
+ if (stream !== "stdout") {
283
+ return;
284
+ }
242
285
  try {
243
- const authorizationUrl = extractAuthorizationUrl(loginOutput);
286
+ const authorizationUrl = extractAuthorizationUrl(loginOutput.stdout);
244
287
  if (authorizationUrl) {
245
288
  settleAuthorizationUrl(null, authorizationUrl);
246
289
  }
@@ -248,24 +291,27 @@ export async function startOAuthLogin({
248
291
  settleAuthorizationUrl(error);
249
292
  child.kill?.("SIGTERM");
250
293
  }
251
- });
294
+ };
252
295
 
253
- let resolveExit;
254
- let rejectExit;
255
- let exitSettled = false;
256
- const exitPromise = new Promise((resolvePromise, rejectPromise) => {
257
- resolveExit = resolvePromise;
258
- rejectExit = rejectPromise;
296
+ child.stdout?.on?.("data", (chunk) => captureLoginOutput("stdout", chunk));
297
+ child.stderr?.on?.("data", (chunk) => captureLoginOutput("stderr", chunk));
298
+
299
+ let resolveProcessClose;
300
+ let rejectProcessClose;
301
+ let processCloseSettled = false;
302
+ const processClosePromise = new Promise((resolvePromise, rejectPromise) => {
303
+ resolveProcessClose = resolvePromise;
304
+ rejectProcessClose = rejectPromise;
259
305
  });
260
- const settleExit = (error, code) => {
261
- if (exitSettled) {
306
+ const settleProcessClose = (error, code) => {
307
+ if (processCloseSettled) {
262
308
  return;
263
309
  }
264
- exitSettled = true;
310
+ processCloseSettled = true;
265
311
  if (error) {
266
- rejectExit(error);
312
+ rejectProcessClose(error);
267
313
  } else {
268
- resolveExit(code);
314
+ resolveProcessClose(code);
269
315
  }
270
316
  };
271
317
 
@@ -274,15 +320,24 @@ export async function startOAuthLogin({
274
320
  "The local sign-in command could not start. Install Node.js 22 and try again.",
275
321
  );
276
322
  settleAuthorizationUrl(error);
277
- settleExit(error);
323
+ settleProcessClose(error);
278
324
  });
279
- child.once("exit", (code) => {
325
+ child.once("close", (code) => {
280
326
  if (!authorizationUrlSettled) {
281
- settleAuthorizationUrl(
282
- new Error("The sign-in command did not return an authorization URL."),
283
- );
327
+ try {
328
+ const authorizationUrl = extractAuthorizationUrl(loginOutput.stdout, {
329
+ includeFinalLine: true,
330
+ });
331
+ if (authorizationUrl) {
332
+ settleAuthorizationUrl(null, authorizationUrl);
333
+ } else {
334
+ settleAuthorizationUrl(loginStartupError(loginOutput.stderr));
335
+ }
336
+ } catch (error) {
337
+ settleAuthorizationUrl(error);
338
+ }
284
339
  }
285
- settleExit(null, code);
340
+ settleProcessClose(null, code);
286
341
  });
287
342
 
288
343
  const loginUrlTimeout = setTimeout(() => {
@@ -326,7 +381,7 @@ export async function startOAuthLogin({
326
381
  try {
327
382
  return await Promise.race([
328
383
  pendingCredentialPromise,
329
- exitPromise.then(async (code) => {
384
+ processClosePromise.then(async (code) => {
330
385
  if (code !== 0) {
331
386
  throw new Error(
332
387
  "ChatGPT sign-in did not finish. Start a fresh login.",
@@ -348,7 +403,7 @@ export async function startOAuthLogin({
348
403
  keepPollingForCredential = false;
349
404
  clearTimeout(processTimeout);
350
405
  clearTimeout(loginUrlTimeout);
351
- if (!exitSettled) {
406
+ if (!processCloseSettled) {
352
407
  child.kill?.("SIGTERM");
353
408
  }
354
409
  try {
package/src/ui/app.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { formatAuthUpdatedAt } from "./time.js";
2
+ import { prepareOAuthPopup } from "./oauth-popup.js";
2
3
 
3
4
  const token = new URLSearchParams(window.location.search).get("session");
4
5
  window.history.replaceState(null, "", window.location.pathname);
@@ -309,15 +310,14 @@ element("login-button").addEventListener("click", async (event) => {
309
310
  const button = event.currentTarget;
310
311
  const loginLink = element("login-link");
311
312
  const loginWindow = window.open("about:blank", "_blank");
312
- if (loginWindow) {
313
- loginWindow.opener = null;
314
- }
313
+ let loginWindowNavigated = false;
314
+ prepareOAuthPopup(loginWindow);
315
315
  clearError();
316
316
  loginLink.hidden = true;
317
317
  loginLink.removeAttribute("href");
318
318
  setBusy(button, true, "Waiting for browser sign-in…");
319
319
  setMessage(
320
- "Creating one fresh OpenAI sign-in link. The existing local credential will be replaced only after sign-in succeeds.",
320
+ "Preparing a fresh ChatGPT sign-in. The existing local credential will be replaced only after sign-in succeeds.",
321
321
  );
322
322
  try {
323
323
  const result = await api("/api/oauth/login", {
@@ -329,6 +329,8 @@ element("login-button").addEventListener("click", async (event) => {
329
329
  loginLink.hidden = false;
330
330
  if (loginWindow) {
331
331
  loginWindow.location.replace(authorizationUrl);
332
+ loginWindowNavigated = true;
333
+ loginWindow.opener = null;
332
334
  }
333
335
  setMessage(
334
336
  "Complete the newly opened sign-in within five minutes. If no tab opened, use “Open fresh ChatGPT sign-in” below. If an OpenAI OAuth browser extension intercepts the callback, disable it temporarily and start again.",
@@ -338,7 +340,7 @@ element("login-button").addEventListener("click", async (event) => {
338
340
  loginLink.removeAttribute("href");
339
341
  await refreshAuthStatus({ fresh: true });
340
342
  } catch (error) {
341
- if (loginWindow && loginWindow.location.href === "about:blank") {
343
+ if (loginWindow && !loginWindowNavigated) {
342
344
  loginWindow.close();
343
345
  }
344
346
  showError(error);
@@ -0,0 +1,18 @@
1
+ export function prepareOAuthPopup(loginWindow) {
2
+ if (!loginWindow) {
3
+ return false;
4
+ }
5
+
6
+ try {
7
+ const popupDocument = loginWindow.document;
8
+ const heading = popupDocument.createElement("h1");
9
+ const detail = popupDocument.createElement("p");
10
+ heading.textContent = "Preparing ChatGPT sign-in…";
11
+ detail.textContent = "Relmio is creating a fresh sign-in link.";
12
+ popupDocument.title = "Preparing ChatGPT sign-in";
13
+ popupDocument.body.replaceChildren(heading, detail);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
package/src/web/server.js CHANGED
@@ -179,6 +179,7 @@ async function loadDefaultUiFiles() {
179
179
  const files = await Promise.all([
180
180
  readFile(new URL("../ui/index.html", import.meta.url), "utf8"),
181
181
  readFile(new URL("../ui/app.js", import.meta.url), "utf8"),
182
+ readFile(new URL("../ui/oauth-popup.js", import.meta.url), "utf8"),
182
183
  readFile(new URL("../ui/theme.js", import.meta.url), "utf8"),
183
184
  readFile(new URL("../ui/time.js", import.meta.url), "utf8"),
184
185
  readFile(new URL("../ui/styles.css", import.meta.url), "utf8"),
@@ -190,12 +191,13 @@ async function loadDefaultUiFiles() {
190
191
  return {
191
192
  "/": files[0],
192
193
  "/app.js": files[1],
193
- "/theme.js": files[2],
194
- "/time.js": files[3],
195
- "/styles.css": files[4],
196
- "/icons/monitor.svg": files[5],
197
- "/icons/sun.svg": files[6],
198
- "/icons/moon.svg": files[7],
194
+ "/oauth-popup.js": files[2],
195
+ "/theme.js": files[3],
196
+ "/time.js": files[4],
197
+ "/styles.css": files[5],
198
+ "/icons/monitor.svg": files[6],
199
+ "/icons/sun.svg": files[7],
200
+ "/icons/moon.svg": files[8],
199
201
  };
200
202
  }
201
203