relmio 0.5.0 → 0.7.0

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/src/ui/local.js CHANGED
@@ -10,6 +10,14 @@ const state = {
10
10
  planId: null,
11
11
  plan: null,
12
12
  installedTarget: null,
13
+ chatTester: {
14
+ conversationId: null,
15
+ encryptedCredential: null,
16
+ endpointBaseUrl: null,
17
+ expiresAt: null,
18
+ generation: 0,
19
+ keyId: null,
20
+ },
13
21
  };
14
22
 
15
23
  const messageBox = element("global-message");
@@ -31,11 +39,16 @@ function setMessage(text) {
31
39
 
32
40
  function clearError() {
33
41
  errorText.textContent = "";
42
+ element("local-image-build-troubleshooting").hidden = true;
34
43
  errorBox.hidden = true;
35
44
  }
36
45
 
37
46
  function showError(error) {
38
- errorText.textContent = error?.message ?? "Something went wrong.";
47
+ const localImageBuildFailed = error?.message === "Local image build failed.";
48
+ errorText.textContent = localImageBuildFailed
49
+ ? "Relmio could not build the local image. Check that Docker is running, has enough disk space, and can pull its base image."
50
+ : error?.message ?? "Something went wrong.";
51
+ element("local-image-build-troubleshooting").hidden = !localImageBuildFailed;
39
52
  errorBox.hidden = false;
40
53
  errorBox.focus();
41
54
  }
@@ -129,6 +142,14 @@ function readAllowedOrigins() {
129
142
  .filter((value) => value !== "");
130
143
  }
131
144
 
145
+ function isCodexChat(target) {
146
+ return target === "codex-chat";
147
+ }
148
+
149
+ function isOpenAiApi(target) {
150
+ return target === "openai-api";
151
+ }
152
+
132
153
  function invalidatePlan() {
133
154
  state.planId = null;
134
155
  state.plan = null;
@@ -140,15 +161,24 @@ function renderTarget() {
140
161
  state.target = selectedTarget();
141
162
  invalidatePlan();
142
163
 
143
- const isOpenAiApi = state.target === "openai-api";
144
- element("local-port").value = isOpenAiApi ? "12435" : "14500";
145
- element("origins-field").hidden = !isOpenAiApi;
146
- element("target-guidance-title").textContent = isOpenAiApi
164
+ const openAiApi = isOpenAiApi(state.target);
165
+ const codexChat = isCodexChat(state.target);
166
+ element("local-port").value = openAiApi
167
+ ? "12435"
168
+ : codexChat
169
+ ? "14501"
170
+ : "14500";
171
+ element("origins-field").hidden = !openAiApi;
172
+ element("target-guidance-title").textContent = openAiApi
147
173
  ? "Uses an OpenAI Platform API key"
148
- : "Uses the official Codex ChatGPT sign-in";
149
- element("target-guidance-detail").textContent = isOpenAiApi
174
+ : codexChat
175
+ ? "Uses official Codex ChatGPT sign-in through the experimental Relmio-specific Chat Adapter"
176
+ : "Uses the official Codex ChatGPT sign-in";
177
+ element("target-guidance-detail").textContent = openAiApi
150
178
  ? "Your Platform key is seeded over stdin into a private Docker volume and is never returned to the browser. A separate local client credential is generated for your apps."
151
- : "This runs Codex App Server as its own experimental protocol. It does not translate the ChatGPT session into a generic OpenAI /v1 API and it does not accept direct browser connections.";
179
+ : codexChat
180
+ ? "This exposes Relmio-specific HTTP POST /chat for a trusted local backend or development server. It is not OpenAI /v1, has no CORS, and browser bundles must never connect directly."
181
+ : "This runs Codex App Server as its own experimental protocol. It does not translate the ChatGPT session into a generic OpenAI /v1 API and it does not accept direct browser connections.";
152
182
  }
153
183
 
154
184
  function appendPolicyNotice(container, heading, detail) {
@@ -160,33 +190,44 @@ function appendPolicyNotice(container, heading, detail) {
160
190
  }
161
191
 
162
192
  function renderPlan(plan) {
163
- const isOpenAiApi = plan.target === "openai-api";
193
+ const openAiApi = isOpenAiApi(plan.target);
194
+ const codexChat = isCodexChat(plan.target);
164
195
  element("review-endpoint").textContent = plan.endpoint;
165
- element("review-protocol").textContent = isOpenAiApi
196
+ element("review-protocol").textContent = openAiApi
166
197
  ? "OpenAI-compatible HTTP /v1"
167
- : "Codex App Server JSON-RPC over WebSocket";
168
- element("review-auth").textContent = isOpenAiApi
198
+ : codexChat
199
+ ? "Relmio Codex Chat HTTP: POST /chat"
200
+ : "Codex App Server JSON-RPC over WebSocket";
201
+ element("review-auth").textContent = openAiApi
169
202
  ? "OpenAI Platform API key"
170
203
  : "ChatGPT sign-in through official Codex";
171
- element("review-browser").textContent = isOpenAiApi
204
+ element("review-browser").textContent = openAiApi
172
205
  ? plan.allowedOrigins.length > 0
173
206
  ? `Only ${plan.allowedOrigins.length} exact allowed origin(s)`
174
207
  : "Native clients only until exact origins are added"
175
- : "No — trusted native local clients only";
176
- element("review-origins-row").hidden = !isOpenAiApi;
177
- element("review-origins").textContent = isOpenAiApi
208
+ : codexChat
209
+ ? "No trusted local backends and development servers only"
210
+ : "No trusted native local clients only";
211
+ element("review-origins-row").hidden = !openAiApi;
212
+ element("review-origins").textContent = openAiApi
178
213
  ? plan.allowedOrigins.length > 0
179
214
  ? plan.allowedOrigins.join(", ")
180
215
  : "None"
181
216
  : "";
182
217
  element("review-path").textContent = plan.managedPath;
183
218
 
184
- if (isOpenAiApi) {
219
+ if (openAiApi) {
185
220
  appendPolicyNotice(
186
221
  element("review-policy"),
187
222
  "OpenAI Platform terms and billing apply",
188
223
  "This option sends requests to the OpenAI API with your developer Platform key. ChatGPT subscriptions and open-source program benefits do not turn a ChatGPT credential into an API key.",
189
224
  );
225
+ } else if (codexChat) {
226
+ appendPolicyNotice(
227
+ element("review-policy"),
228
+ "Experimental Codex Chat Adapter — trusted local backends or development servers only",
229
+ "This option runs a small authenticated Relmio HTTP adapter on loopback. It accepts only POST /chat from a trusted local backend or development server. It has no CORS and is not OpenAI /v1. ChatGPT credentials stay in the isolated Codex Docker volume and never become Platform API keys.",
230
+ );
190
231
  } else {
191
232
  appendPolicyNotice(
192
233
  element("review-policy"),
@@ -197,18 +238,31 @@ function renderPlan(plan) {
197
238
  }
198
239
 
199
240
  function prepareInstallPanel() {
200
- const isOpenAiApi = state.plan.target === "openai-api";
241
+ const openAiApi = isOpenAiApi(state.plan.target);
242
+ const codexChat = isCodexChat(state.plan.target);
201
243
  const apiKey = element("platform-api-key");
202
- element("api-key-field").hidden = !isOpenAiApi;
203
- element("codex-install-warning").hidden = isOpenAiApi;
204
- apiKey.required = isOpenAiApi;
244
+ element("api-key-field").hidden = !openAiApi;
245
+ element("codex-install-warning").hidden = openAiApi;
246
+ element("codex-install-warning-title").textContent = codexChat
247
+ ? "Credential for trusted local backends or development servers only"
248
+ : "High-trust capability";
249
+ element("codex-install-warning-detail").textContent = codexChat
250
+ ? "The generated bearer authorizes chat turns through your signed-in Codex container. Keep it only in a trusted local backend or development server; never put it in browser code."
251
+ : "Anyone holding the generated client credential can control Codex inside its isolated container, act through your signed-in ChatGPT session, and may be able to recover that container's ChatGPT session credential. Treat this capability like your ChatGPT password and give it only to a trusted native local app.";
252
+ apiKey.required = openAiApi;
205
253
  apiKey.value = "";
206
- element("install-intro").textContent = isOpenAiApi
254
+ element("install-intro").textContent = openAiApi
207
255
  ? "Enter the OpenAI Platform API key this endpoint will use upstream. It is sent only to this local Relmio process."
208
- : "Relmio will install the official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready.";
256
+ : codexChat
257
+ ? "Relmio will install the experimental Codex Chat Adapter and official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready."
258
+ : "Relmio will install the official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready.";
209
259
  setButtonLabel(
210
260
  element("install-button"),
211
- isOpenAiApi ? "Install OpenAI API endpoint" : "Install Codex App Server",
261
+ openAiApi
262
+ ? "Install OpenAI API endpoint"
263
+ : codexChat
264
+ ? "Install Codex Chat Adapter"
265
+ : "Install Codex App Server",
212
266
  );
213
267
  }
214
268
 
@@ -216,30 +270,159 @@ function renderInstallResult(result) {
216
270
  if (
217
271
  typeof result.endpoint !== "string" ||
218
272
  typeof result.clientCredential !== "string" ||
219
- !["openai-api", "codex-chatgpt"].includes(result.target)
273
+ !["openai-api", "codex-chatgpt", "codex-chat"].includes(result.target)
220
274
  ) {
221
275
  throw new Error("The local installer returned an unexpected response.");
222
276
  }
223
277
 
224
- const isOpenAiApi = result.target === "openai-api";
278
+ const openAiApi = isOpenAiApi(result.target);
279
+ const codexChat = isCodexChat(result.target);
225
280
  state.installedTarget = result.target;
226
281
  element("result-endpoint").textContent = result.endpoint;
227
282
  element("result-credential").textContent = result.clientCredential;
228
- element("codex-production-warning").hidden = isOpenAiApi;
229
- element("codex-login").hidden = isOpenAiApi;
230
- element("done-title").textContent = isOpenAiApi
283
+ element("codex-production-warning").hidden = openAiApi;
284
+ element("codex-login").hidden = openAiApi;
285
+ element("chat-tester").hidden = !codexChat;
286
+ if (codexChat && !state.chatTester.keyId) {
287
+ element("chat-tester-endpoint").value = result.endpoint;
288
+ }
289
+ element("codex-production-warning-title").textContent = codexChat
290
+ ? "Experimental Chat Adapter — trusted local backends or development servers only"
291
+ : "Experimental WebSocket transport";
292
+ element("codex-production-warning-detail").textContent = codexChat
293
+ ? "The adapter is for trusted local backends or development servers only. It has a Relmio-specific POST /chat contract, no CORS, and is not OpenAI /v1."
294
+ : "Codex App Server WebSocket is experimental and unsupported for production workloads.";
295
+ element("done-title").textContent = openAiApi
231
296
  ? "OpenAI API endpoint is ready"
232
- : "Codex App Server is installed";
233
- element("done-detail").textContent = isOpenAiApi
297
+ : codexChat
298
+ ? "Codex Chat Adapter is installed"
299
+ : "Codex App Server is installed";
300
+ element("done-detail").textContent = openAiApi
234
301
  ? "Copy the endpoint and generated bearer credential into your local app."
235
- : "Copy the endpoint and capability, then sign the isolated Codex container in to ChatGPT.";
302
+ : codexChat
303
+ ? "Copy the endpoint and generated bearer credential into your trusted local backend or development server, then sign the isolated Codex container in to ChatGPT."
304
+ : "Copy the endpoint and capability, then sign the isolated Codex container in to ChatGPT.";
236
305
  appendPolicyNotice(
237
306
  element("client-warning"),
238
- isOpenAiApi ? "For local OpenAI-compatible clients" : "For trusted native Codex clients only",
239
- isOpenAiApi
307
+ openAiApi
308
+ ? "For local OpenAI-compatible clients"
309
+ : codexChat
310
+ ? "For trusted local backends or development servers only"
311
+ : "For trusted native Codex clients only",
312
+ openAiApi
240
313
  ? "Use the generated client credential as the bearer API key. Your upstream Platform key remains private in the managed Docker volume."
241
- : "This capability is not an OpenAI API key. Treat it like your ChatGPT password: the client is trusted to control the isolated container and may recover its ChatGPT session credential. It must speak official Codex App Server JSON-RPC over WebSocket.",
314
+ : codexChat
315
+ ? "Use this one-time credential only as a Bearer token for the Relmio-specific POST /chat endpoint. It is not an OpenAI API key, has no browser CORS support, and must remain in a trusted local backend or development server."
316
+ : "This capability is not an OpenAI API key. Treat it like your ChatGPT password: the client is trusted to control the isolated container and may recover its ChatGPT session credential. It must speak official Codex App Server JSON-RPC over WebSocket.",
317
+ );
318
+ }
319
+
320
+ function setChatTesterStatus(text) {
321
+ element("chat-tester-status").textContent = text;
322
+ }
323
+
324
+ function clearChatTesterError() {
325
+ element("chat-tester-error").textContent = "";
326
+ element("chat-tester-error").hidden = true;
327
+ }
328
+
329
+ function showChatTesterError(error) {
330
+ element("chat-tester-error").textContent =
331
+ error?.message ?? "The local adapter test could not be completed.";
332
+ element("chat-tester-error").hidden = false;
333
+ }
334
+
335
+ function appendChatTesterTurn(kind, text) {
336
+ const item = document.createElement("li");
337
+ const heading = document.createElement("strong");
338
+ const content = document.createElement("p");
339
+ heading.textContent = kind === "user" ? "You" : "Local adapter";
340
+ content.textContent = text;
341
+ item.className = `chat-tester-turn chat-tester-turn-${kind}`;
342
+ item.append(heading, content);
343
+ element("chat-tester-transcript").append(item);
344
+ }
345
+
346
+ function clearChatTesterState() {
347
+ state.chatTester.conversationId = null;
348
+ state.chatTester.encryptedCredential = null;
349
+ state.chatTester.endpointBaseUrl = null;
350
+ state.chatTester.expiresAt = null;
351
+ state.chatTester.keyId = null;
352
+ state.chatTester.generation += 1;
353
+ element("chat-tester-credential").value = "";
354
+ element("chat-tester-endpoint").value = "";
355
+ element("chat-tester-input").value = "";
356
+ element("chat-tester-secure-form").hidden = false;
357
+ element("chat-tester-message-form").hidden = true;
358
+ element("chat-tester-transcript").replaceChildren();
359
+ clearChatTesterError();
360
+ }
361
+
362
+ function assertChatTesterKey(result) {
363
+ if (
364
+ !result ||
365
+ typeof result !== "object" ||
366
+ typeof result.keyId !== "string" ||
367
+ typeof result.expiresAt !== "string" ||
368
+ result.algorithm !== "RSA-OAEP-256" ||
369
+ !result.publicKeyJwk ||
370
+ result.publicKeyJwk.kty !== "RSA" ||
371
+ typeof result.publicKeyJwk.n !== "string" ||
372
+ typeof result.publicKeyJwk.e !== "string"
373
+ ) {
374
+ throw new Error("The local tester returned an unexpected encryption key.");
375
+ }
376
+ return result;
377
+ }
378
+
379
+ async function encryptChatTesterCredential(publicKeyJwk, clientCredential) {
380
+ if (!window.crypto?.subtle) {
381
+ throw new Error("This browser cannot create the required local test encryption key.");
382
+ }
383
+ const publicKey = await window.crypto.subtle.importKey(
384
+ "jwk",
385
+ publicKeyJwk,
386
+ { name: "RSA-OAEP", hash: "SHA-256" },
387
+ false,
388
+ ["encrypt"],
389
+ );
390
+ const encrypted = await window.crypto.subtle.encrypt(
391
+ { name: "RSA-OAEP" },
392
+ publicKey,
393
+ new TextEncoder().encode(clientCredential),
242
394
  );
395
+ const bytes = new Uint8Array(encrypted);
396
+ let binary = "";
397
+ for (const byte of bytes) {
398
+ binary += String.fromCodePoint(byte);
399
+ }
400
+ return window.btoa(binary);
401
+ }
402
+
403
+ async function forgetChatTester({ announce = true } = {}) {
404
+ const keyId = state.chatTester.keyId;
405
+ clearChatTesterState();
406
+ if (!keyId) {
407
+ if (announce) {
408
+ setChatTesterStatus("The tester is cleared. Secure a credential to begin again.");
409
+ }
410
+ return;
411
+ }
412
+ try {
413
+ await api("/api/local/chat-test/reset", {
414
+ method: "POST",
415
+ body: { keyId },
416
+ });
417
+ if (announce) {
418
+ setChatTesterStatus("The tester key and transcript were forgotten.");
419
+ }
420
+ } catch (error) {
421
+ if (announce) {
422
+ showChatTesterError(error);
423
+ setChatTesterStatus("The browser tester was cleared. The local key will expire shortly.");
424
+ }
425
+ }
243
426
  }
244
427
 
245
428
  function validateVerificationUrl(value) {
@@ -432,7 +615,9 @@ element("install-button").addEventListener("click", async (event) => {
432
615
  setMessage(
433
616
  result.target === "openai-api"
434
617
  ? "Local OpenAI API endpoint verified. Copy its one-time client credential now."
435
- : "Codex App Server verified. Copy its one-time capability and complete ChatGPT sign-in.",
618
+ : result.target === "codex-chat"
619
+ ? "Codex Chat Adapter for trusted local backends or development servers verified. Copy its one-time bearer and complete ChatGPT sign-in."
620
+ : "Codex App Server verified. Copy its one-time capability and complete ChatGPT sign-in.",
436
621
  );
437
622
  } catch (error) {
438
623
  invalidatePlan();
@@ -459,6 +644,9 @@ element("rotate-credential-button").addEventListener("click", async (event) => {
459
644
  "Generating a replacement credential before activating it…",
460
645
  );
461
646
  try {
647
+ if (isCodexChat(state.installedTarget)) {
648
+ await forgetChatTester({ announce: false });
649
+ }
462
650
  const staged = await api("/api/local/client-credential/rotate", {
463
651
  method: "POST",
464
652
  body: { target: state.installedTarget },
@@ -484,6 +672,140 @@ element("rotate-credential-button").addEventListener("click", async (event) => {
484
672
  }
485
673
  });
486
674
 
675
+ element("chat-tester-secure-form").addEventListener("submit", async (event) => {
676
+ event.preventDefault();
677
+ const form = event.currentTarget;
678
+ const button = element("chat-tester-secure");
679
+ const resetButton = element("chat-tester-reset");
680
+ const endpointInput = element("chat-tester-endpoint");
681
+ const clientCredentialInput = element("chat-tester-credential");
682
+ if (!form.reportValidity()) {
683
+ return;
684
+ }
685
+
686
+ clearChatTesterError();
687
+ let clientCredential = clientCredentialInput.value;
688
+ clientCredentialInput.value = "";
689
+ let issuedKey;
690
+ resetButton.disabled = true;
691
+ setBusy(button, true, "Securing credential…");
692
+ setChatTesterStatus("Creating a short-lived local encryption key…");
693
+ try {
694
+ issuedKey = assertChatTesterKey(
695
+ await api("/api/local/chat-test/key", { method: "POST", body: {} }),
696
+ );
697
+ const encryptedCredential = await encryptChatTesterCredential(
698
+ issuedKey.publicKeyJwk,
699
+ clientCredential,
700
+ );
701
+ clientCredential = undefined;
702
+ state.chatTester.conversationId = null;
703
+ state.chatTester.encryptedCredential = encryptedCredential;
704
+ state.chatTester.endpointBaseUrl = endpointInput.value;
705
+ state.chatTester.expiresAt = issuedKey.expiresAt;
706
+ state.chatTester.keyId = issuedKey.keyId;
707
+ element("chat-tester-secure-form").hidden = true;
708
+ element("chat-tester-message-form").hidden = false;
709
+ setChatTesterStatus("Temporary test session secured. Send a message before the key expires.");
710
+ element("chat-tester-input").focus();
711
+ } catch (error) {
712
+ clientCredential = undefined;
713
+ if (issuedKey?.keyId) {
714
+ try {
715
+ await api("/api/local/chat-test/reset", {
716
+ method: "POST",
717
+ body: { keyId: issuedKey.keyId },
718
+ });
719
+ } catch {
720
+ // The only remaining server material is an expiring private key.
721
+ }
722
+ }
723
+ clearChatTesterState();
724
+ showChatTesterError(error);
725
+ setChatTesterStatus("The credential was cleared. Secure it again to retry.");
726
+ } finally {
727
+ clientCredential = undefined;
728
+ clientCredentialInput.value = "";
729
+ resetButton.disabled = false;
730
+ setBusy(button, false);
731
+ }
732
+ });
733
+
734
+ element("chat-tester-message-form").addEventListener("submit", async (event) => {
735
+ event.preventDefault();
736
+ const form = event.currentTarget;
737
+ const button = element("chat-tester-send");
738
+ const resetButton = element("chat-tester-reset");
739
+ const input = element("chat-tester-input");
740
+ if (!form.reportValidity()) {
741
+ return;
742
+ }
743
+ if (
744
+ !state.chatTester.keyId ||
745
+ !state.chatTester.encryptedCredential ||
746
+ !state.chatTester.endpointBaseUrl
747
+ ) {
748
+ showChatTesterError(
749
+ new Error("This test credential has expired or was forgotten. Secure it again."),
750
+ );
751
+ return;
752
+ }
753
+
754
+ clearChatTesterError();
755
+ const text = input.value;
756
+ const generation = state.chatTester.generation;
757
+ input.value = "";
758
+ appendChatTesterTurn("user", text);
759
+ resetButton.disabled = true;
760
+ setBusy(button, true, "Waiting for adapter…");
761
+ setChatTesterStatus("The local wizard is testing the adapter without exposing its credential to the page.");
762
+ try {
763
+ const result = await api("/api/local/chat-test/message", {
764
+ method: "POST",
765
+ body: {
766
+ endpointBaseUrl: state.chatTester.endpointBaseUrl,
767
+ keyId: state.chatTester.keyId,
768
+ encryptedCredential: state.chatTester.encryptedCredential,
769
+ input: text,
770
+ ...(state.chatTester.conversationId
771
+ ? { conversationId: state.chatTester.conversationId }
772
+ : {}),
773
+ },
774
+ });
775
+ if (
776
+ typeof result.conversationId !== "string" ||
777
+ typeof result.output !== "string"
778
+ ) {
779
+ throw new Error("The local adapter returned an unexpected response.");
780
+ }
781
+ if (state.chatTester.generation !== generation) {
782
+ return;
783
+ }
784
+ state.chatTester.conversationId = result.conversationId;
785
+ appendChatTesterTurn("assistant", result.output);
786
+ setChatTesterStatus("Response received. Continue this conversation or forget the tester.");
787
+ } catch (error) {
788
+ if (state.chatTester.generation === generation) {
789
+ showChatTesterError(error);
790
+ setChatTesterStatus("No adapter response was added. You can retry or forget this tester.");
791
+ }
792
+ } finally {
793
+ resetButton.disabled = false;
794
+ setBusy(button, false);
795
+ }
796
+ });
797
+
798
+ element("chat-tester-reset").addEventListener("click", async (event) => {
799
+ const button = event.currentTarget;
800
+ clearChatTesterError();
801
+ setBusy(button, true, "Forgetting tester…");
802
+ try {
803
+ await forgetChatTester();
804
+ } finally {
805
+ setBusy(button, false);
806
+ }
807
+ });
808
+
487
809
  element("codex-login-button").addEventListener("click", async (event) => {
488
810
  const button = event.currentTarget;
489
811
  const resultBox = element("device-code-result");
@@ -494,7 +816,7 @@ element("codex-login-button").addEventListener("click", async (event) => {
494
816
  try {
495
817
  const result = await api("/api/local/codex/login", {
496
818
  method: "POST",
497
- body: {},
819
+ body: { target: state.installedTarget },
498
820
  });
499
821
  const verificationUrl = validateVerificationUrl(result.verificationUrl);
500
822
  const userCode = validateDeviceCode(result.userCode);
@@ -504,7 +826,9 @@ element("codex-login-button").addEventListener("click", async (event) => {
504
826
  resultBox.hidden = false;
505
827
  setMessage("Open the official OpenAI page and enter the displayed device code.");
506
828
  await waitForCodexLogin();
507
- status.textContent = "ChatGPT sign-in completed. Codex is ready for your trusted native client.";
829
+ status.textContent = isCodexChat(state.installedTarget)
830
+ ? "ChatGPT sign-in completed. Codex Chat Adapter is ready for your trusted local backend or development server."
831
+ : "ChatGPT sign-in completed. Codex is ready for your trusted native client.";
508
832
  setMessage("Codex ChatGPT sign-in completed successfully.");
509
833
  } catch (error) {
510
834
  status.textContent = "ChatGPT sign-in did not complete.";