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/CHANGELOG.md +50 -0
- package/README.md +42 -414
- package/docs/architecture.md +21 -9
- package/docs/faq.md +45 -0
- package/docs/getting-started.md +38 -0
- package/docs/local-endpoints-spec.md +128 -13
- package/docs/local-endpoints.md +129 -26
- package/docs/reference.md +77 -0
- package/docs/security.md +74 -15
- package/docs/troubleshooting.md +33 -0
- package/docs/vps-and-n8n.md +30 -0
- package/package.json +1 -1
- package/src/domain/local-endpoints.js +183 -13
- package/src/gateway/codex-chat.js +754 -0
- package/src/services/codex-login.js +11 -3
- package/src/services/local-chat-test.js +361 -0
- package/src/services/local-installer.js +94 -14
- package/src/ui/local.css +115 -1
- package/src/ui/local.html +119 -8
- package/src/ui/local.js +362 -38
- package/src/web/server.js +134 -4
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
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
element("
|
|
146
|
-
|
|
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
|
-
:
|
|
149
|
-
|
|
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
|
-
:
|
|
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
|
|
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 =
|
|
196
|
+
element("review-protocol").textContent = openAiApi
|
|
166
197
|
? "OpenAI-compatible HTTP /v1"
|
|
167
|
-
:
|
|
168
|
-
|
|
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 =
|
|
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
|
-
:
|
|
176
|
-
|
|
177
|
-
|
|
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 (
|
|
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
|
|
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 = !
|
|
203
|
-
element("codex-install-warning").hidden =
|
|
204
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
229
|
-
element("codex-login").hidden =
|
|
230
|
-
element("
|
|
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
|
-
:
|
|
233
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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 =
|
|
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.";
|