relmio 0.4.1 → 0.6.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 +51 -0
- package/README.md +40 -16
- package/docs/architecture.md +21 -9
- package/docs/local-endpoints-spec.md +101 -13
- package/docs/local-endpoints.md +101 -30
- package/docs/security.md +40 -15
- package/package.json +1 -1
- package/src/domain/local-endpoints.js +187 -14
- package/src/gateway/codex-chat.js +754 -0
- package/src/services/codex-login.js +11 -3
- package/src/services/local-installer.js +859 -20
- package/src/ui/local.css +140 -2
- package/src/ui/local.html +89 -9
- package/src/ui/local.js +157 -37
- package/src/web/server.js +247 -18
package/src/ui/local.css
CHANGED
|
@@ -18,6 +18,128 @@
|
|
|
18
18
|
grid-column: 2;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
.local-wizard .header-actions {
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
justify-content: flex-end;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.local-wizard .local-support-button,
|
|
27
|
+
.local-wizard .local-repository-button {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
border: 1px solid var(--border-strong);
|
|
32
|
+
border-radius: 999px;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
transition:
|
|
35
|
+
border-color 180ms var(--ease),
|
|
36
|
+
background 180ms var(--ease),
|
|
37
|
+
color 180ms var(--ease),
|
|
38
|
+
transform 180ms var(--ease);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.local-wizard .local-support-button {
|
|
42
|
+
width: 2.5rem;
|
|
43
|
+
min-width: 2.5rem;
|
|
44
|
+
min-height: 2.5rem;
|
|
45
|
+
background: var(--surface);
|
|
46
|
+
color: var(--text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.local-wizard .local-support-button svg {
|
|
50
|
+
width: 1.125rem;
|
|
51
|
+
height: 1.125rem;
|
|
52
|
+
fill: none;
|
|
53
|
+
stroke: currentColor;
|
|
54
|
+
stroke-linecap: round;
|
|
55
|
+
stroke-linejoin: round;
|
|
56
|
+
stroke-width: 1.8;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.local-wizard .local-repository-button {
|
|
60
|
+
min-height: 2.5rem;
|
|
61
|
+
gap: 0.5rem;
|
|
62
|
+
padding: 0.3125rem 0.375rem 0.3125rem 0.75rem;
|
|
63
|
+
border-color: var(--text);
|
|
64
|
+
background: var(--text);
|
|
65
|
+
color: var(--background);
|
|
66
|
+
font-size: 0.75rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.local-wizard .local-repository-button > svg {
|
|
70
|
+
width: 1.125rem;
|
|
71
|
+
height: 1.125rem;
|
|
72
|
+
flex: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.local-wizard .local-repository-action,
|
|
76
|
+
.local-wizard .package-version {
|
|
77
|
+
min-height: 1.75rem;
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
padding: 0 0.5625rem;
|
|
81
|
+
border-radius: 999px;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
font: 700 0.6875rem/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.local-wizard .local-repository-action {
|
|
87
|
+
background: var(--background);
|
|
88
|
+
color: var(--text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.local-wizard .package-version {
|
|
92
|
+
background: var(--background);
|
|
93
|
+
color: var(--text);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.local-wizard .local-support-button:hover,
|
|
97
|
+
.local-wizard .local-repository-button:hover {
|
|
98
|
+
border-color: var(--accent);
|
|
99
|
+
color: var(--accent-deep);
|
|
100
|
+
transform: translateY(-1px);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.local-wizard .local-repository-button:hover {
|
|
104
|
+
background: var(--accent);
|
|
105
|
+
color: #fff;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.local-wizard .local-support-button:focus-visible,
|
|
109
|
+
.local-wizard .local-repository-button:focus-visible {
|
|
110
|
+
outline: 3px solid #f2a94a;
|
|
111
|
+
outline-offset: 2px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.local-wizard .toast-stack {
|
|
115
|
+
display: grid;
|
|
116
|
+
min-width: 0;
|
|
117
|
+
overflow: visible;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.local-wizard .toast-stack .toast {
|
|
121
|
+
width: 100%;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
flex: 0 1 auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.local-wizard .safety-note > span {
|
|
127
|
+
display: block;
|
|
128
|
+
min-width: 0;
|
|
129
|
+
overflow-wrap: anywhere;
|
|
130
|
+
white-space: normal;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.local-wizard #global-message-text,
|
|
134
|
+
.local-wizard .error > span {
|
|
135
|
+
min-width: 0;
|
|
136
|
+
overflow: visible;
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
text-overflow: clip;
|
|
139
|
+
white-space: normal;
|
|
140
|
+
word-break: break-word;
|
|
141
|
+
}
|
|
142
|
+
|
|
21
143
|
.target-picker {
|
|
22
144
|
display: grid;
|
|
23
145
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -137,7 +259,8 @@
|
|
|
137
259
|
.policy-note,
|
|
138
260
|
.high-trust-warning,
|
|
139
261
|
.one-time-note,
|
|
140
|
-
.codex-login
|
|
262
|
+
.codex-login,
|
|
263
|
+
.credential-rotation-note {
|
|
141
264
|
margin-top: 1rem;
|
|
142
265
|
padding: 0.875rem 1rem;
|
|
143
266
|
border: 1px solid var(--accent-border);
|
|
@@ -148,7 +271,8 @@
|
|
|
148
271
|
.policy-note p,
|
|
149
272
|
.high-trust-warning p,
|
|
150
273
|
.one-time-note p,
|
|
151
|
-
.codex-login p
|
|
274
|
+
.codex-login p,
|
|
275
|
+
.credential-rotation-note p {
|
|
152
276
|
margin: 0.35rem 0 0;
|
|
153
277
|
color: var(--text-muted);
|
|
154
278
|
font-size: 0.875rem;
|
|
@@ -175,6 +299,20 @@
|
|
|
175
299
|
color: var(--warning);
|
|
176
300
|
}
|
|
177
301
|
|
|
302
|
+
.credential-rotation-note {
|
|
303
|
+
border-color: var(--warning-border);
|
|
304
|
+
background: var(--warning-soft);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.credential-rotation-note strong,
|
|
308
|
+
.credential-rotation-note p {
|
|
309
|
+
color: var(--warning);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.credential-rotation-note .button {
|
|
313
|
+
margin-top: 0.875rem;
|
|
314
|
+
}
|
|
315
|
+
|
|
178
316
|
.local-review-grid {
|
|
179
317
|
margin-top: 1rem;
|
|
180
318
|
}
|
package/src/ui/local.html
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
/>
|
|
15
15
|
<link rel="stylesheet" href="/styles.css" />
|
|
16
16
|
<link rel="stylesheet" href="/local.css" />
|
|
17
|
+
<script src="/theme.js" type="module"></script>
|
|
17
18
|
<script src="/local.js" type="module"></script>
|
|
18
19
|
</head>
|
|
19
20
|
<body class="local-wizard" data-current-step="1">
|
|
@@ -32,7 +33,58 @@
|
|
|
32
33
|
</svg>
|
|
33
34
|
<span>Relmio</span>
|
|
34
35
|
</div>
|
|
35
|
-
<
|
|
36
|
+
<div class="header-actions">
|
|
37
|
+
<fieldset class="theme-picker">
|
|
38
|
+
<legend class="visually-hidden">Color theme</legend>
|
|
39
|
+
<label title="Use system appearance">
|
|
40
|
+
<input type="radio" name="color-theme" value="system" checked />
|
|
41
|
+
<span class="theme-icon theme-icon-system" aria-hidden="true"></span>
|
|
42
|
+
<span class="visually-hidden">System</span>
|
|
43
|
+
</label>
|
|
44
|
+
<label title="Use light appearance">
|
|
45
|
+
<input type="radio" name="color-theme" value="light" />
|
|
46
|
+
<span class="theme-icon theme-icon-light" aria-hidden="true"></span>
|
|
47
|
+
<span class="visually-hidden">Light</span>
|
|
48
|
+
</label>
|
|
49
|
+
<label title="Use dark appearance">
|
|
50
|
+
<input type="radio" name="color-theme" value="dark" />
|
|
51
|
+
<span class="theme-icon theme-icon-dark" aria-hidden="true"></span>
|
|
52
|
+
<span class="visually-hidden">Dark</span>
|
|
53
|
+
</label>
|
|
54
|
+
</fieldset>
|
|
55
|
+
<a
|
|
56
|
+
class="local-support-button"
|
|
57
|
+
href="https://ko-fi.com/paldogies"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
aria-label="Support Relmio on Ko-fi (opens in a new tab)"
|
|
61
|
+
title="Support Relmio on Ko-fi"
|
|
62
|
+
>
|
|
63
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
64
|
+
<path d="M5 8h12v5a6 6 0 0 1-12 0V8Z" />
|
|
65
|
+
<path d="M17 9h1.5a2.5 2.5 0 0 1 0 5H17M8 3v2m4-2v2m4-2v2" />
|
|
66
|
+
</svg>
|
|
67
|
+
</a>
|
|
68
|
+
<a
|
|
69
|
+
id="local-repository-button"
|
|
70
|
+
class="local-repository-button"
|
|
71
|
+
href="https://github.com/Demonbane18/relmio"
|
|
72
|
+
target="_blank"
|
|
73
|
+
rel="noopener noreferrer"
|
|
74
|
+
aria-label="Open Relmio version __RELMIO_PACKAGE_VERSION__ on GitHub to leave a star (opens in a new tab)"
|
|
75
|
+
>
|
|
76
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
77
|
+
<path fill="currentColor" d="M12 .7a11.5 11.5 0 0 0-3.64 22.41c.58.11.79-.25.79-.56v-2.23c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.19 1.77 1.19 1.04 1.77 2.72 1.26 3.38.96.1-.75.4-1.26.73-1.55-2.57-.29-5.28-1.29-5.28-5.69 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.16 1.18a10.9 10.9 0 0 1 5.76 0c2.19-1.49 3.16-1.18 3.16-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.42-2.71 5.4-5.29 5.69.42.36.79 1.06.79 2.14v3.17c0 .31.21.68.8.56A11.5 11.5 0 0 0 12 .7Z" />
|
|
78
|
+
</svg>
|
|
79
|
+
<strong>GitHub</strong>
|
|
80
|
+
<span class="local-repository-action">
|
|
81
|
+
<span aria-hidden="true">★</span>
|
|
82
|
+
<span id="local-repository-stars">?</span>
|
|
83
|
+
</span>
|
|
84
|
+
<span class="package-version">v__RELMIO_PACKAGE_VERSION__</span>
|
|
85
|
+
</a>
|
|
86
|
+
<span class="local-badge">Bound to this computer only</span>
|
|
87
|
+
</div>
|
|
36
88
|
</header>
|
|
37
89
|
|
|
38
90
|
<main id="main-content" class="shell" tabindex="-1">
|
|
@@ -40,8 +92,8 @@
|
|
|
40
92
|
<section class="intro" aria-labelledby="page-title">
|
|
41
93
|
<h1 id="page-title">Run Relmio on localhost</h1>
|
|
42
94
|
<p class="lede">
|
|
43
|
-
Choose the official credential path that matches your client.
|
|
44
|
-
options run in
|
|
95
|
+
Choose the official credential path that matches your client. All
|
|
96
|
+
three options run in hardened Docker containers and bind only to
|
|
45
97
|
<code>127.0.0.1</code>.
|
|
46
98
|
</p>
|
|
47
99
|
</section>
|
|
@@ -148,6 +200,20 @@
|
|
|
148
200
|
</span>
|
|
149
201
|
</span>
|
|
150
202
|
</label>
|
|
203
|
+
|
|
204
|
+
<label class="target-card warning-card">
|
|
205
|
+
<input type="radio" name="target" value="codex-chat" />
|
|
206
|
+
<span class="target-card-copy">
|
|
207
|
+
<strong>Codex Chat Adapter</strong>
|
|
208
|
+
<span class="target-meta">Experimental · trusted local backends or development servers only</span>
|
|
209
|
+
<span>
|
|
210
|
+
Relmio-specific authenticated <code>POST /chat</code>, backed by
|
|
211
|
+
official Codex App Server and ChatGPT sign-in. This is not an
|
|
212
|
+
OpenAI-compatible <code>/v1</code> endpoint and browser bundles
|
|
213
|
+
must never connect to it.
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
</label>
|
|
151
217
|
</fieldset>
|
|
152
218
|
|
|
153
219
|
<div class="field-grid local-fields">
|
|
@@ -306,8 +372,8 @@
|
|
|
306
372
|
</div>
|
|
307
373
|
|
|
308
374
|
<aside id="codex-install-warning" class="high-trust-warning" hidden>
|
|
309
|
-
<strong>High-trust capability</strong>
|
|
310
|
-
<p>
|
|
375
|
+
<strong id="codex-install-warning-title">High-trust capability</strong>
|
|
376
|
+
<p id="codex-install-warning-detail">
|
|
311
377
|
Anyone holding the generated client credential can control Codex
|
|
312
378
|
inside its isolated container, act through your signed-in ChatGPT
|
|
313
379
|
session, and may be able to recover that container's ChatGPT session
|
|
@@ -343,6 +409,22 @@
|
|
|
343
409
|
</p>
|
|
344
410
|
</aside>
|
|
345
411
|
|
|
412
|
+
<aside class="credential-rotation-note">
|
|
413
|
+
<strong>Rotate client credential</strong>
|
|
414
|
+
<p>
|
|
415
|
+
Relmio shows the replacement first, then activates and verifies it.
|
|
416
|
+
After successful activation, the previous credential no longer works.
|
|
417
|
+
Copy the replacement before leaving this page.
|
|
418
|
+
</p>
|
|
419
|
+
<button
|
|
420
|
+
id="rotate-credential-button"
|
|
421
|
+
class="button secondary"
|
|
422
|
+
type="button"
|
|
423
|
+
>
|
|
424
|
+
Rotate client credential
|
|
425
|
+
</button>
|
|
426
|
+
</aside>
|
|
427
|
+
|
|
346
428
|
<dl class="result-list">
|
|
347
429
|
<div>
|
|
348
430
|
<dt>Endpoint</dt>
|
|
@@ -377,10 +459,8 @@
|
|
|
377
459
|
</dl>
|
|
378
460
|
|
|
379
461
|
<aside id="codex-production-warning" class="high-trust-warning" hidden>
|
|
380
|
-
<strong>Experimental
|
|
381
|
-
<p>
|
|
382
|
-
Codex App Server WebSocket is experimental and unsupported for production workloads.
|
|
383
|
-
</p>
|
|
462
|
+
<strong id="codex-production-warning-title">Experimental Codex integration</strong>
|
|
463
|
+
<p id="codex-production-warning-detail"></p>
|
|
384
464
|
</aside>
|
|
385
465
|
|
|
386
466
|
<section id="codex-login" class="codex-login" aria-labelledby="codex-login-title" hidden>
|
package/src/ui/local.js
CHANGED
|
@@ -9,6 +9,7 @@ const state = {
|
|
|
9
9
|
dockerAvailable: false,
|
|
10
10
|
planId: null,
|
|
11
11
|
plan: null,
|
|
12
|
+
installedTarget: null,
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const messageBox = element("global-message");
|
|
@@ -128,6 +129,14 @@ function readAllowedOrigins() {
|
|
|
128
129
|
.filter((value) => value !== "");
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function isCodexChat(target) {
|
|
133
|
+
return target === "codex-chat";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isOpenAiApi(target) {
|
|
137
|
+
return target === "openai-api";
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
function invalidatePlan() {
|
|
132
141
|
state.planId = null;
|
|
133
142
|
state.plan = null;
|
|
@@ -139,15 +148,24 @@ function renderTarget() {
|
|
|
139
148
|
state.target = selectedTarget();
|
|
140
149
|
invalidatePlan();
|
|
141
150
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
element("
|
|
145
|
-
|
|
151
|
+
const openAiApi = isOpenAiApi(state.target);
|
|
152
|
+
const codexChat = isCodexChat(state.target);
|
|
153
|
+
element("local-port").value = openAiApi
|
|
154
|
+
? "12435"
|
|
155
|
+
: codexChat
|
|
156
|
+
? "14501"
|
|
157
|
+
: "14500";
|
|
158
|
+
element("origins-field").hidden = !openAiApi;
|
|
159
|
+
element("target-guidance-title").textContent = openAiApi
|
|
146
160
|
? "Uses an OpenAI Platform API key"
|
|
147
|
-
:
|
|
148
|
-
|
|
161
|
+
: codexChat
|
|
162
|
+
? "Uses official Codex ChatGPT sign-in through the experimental Relmio-specific Chat Adapter"
|
|
163
|
+
: "Uses the official Codex ChatGPT sign-in";
|
|
164
|
+
element("target-guidance-detail").textContent = openAiApi
|
|
149
165
|
? "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."
|
|
150
|
-
:
|
|
166
|
+
: codexChat
|
|
167
|
+
? "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."
|
|
168
|
+
: "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.";
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
function appendPolicyNotice(container, heading, detail) {
|
|
@@ -159,33 +177,44 @@ function appendPolicyNotice(container, heading, detail) {
|
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
function renderPlan(plan) {
|
|
162
|
-
const
|
|
180
|
+
const openAiApi = isOpenAiApi(plan.target);
|
|
181
|
+
const codexChat = isCodexChat(plan.target);
|
|
163
182
|
element("review-endpoint").textContent = plan.endpoint;
|
|
164
|
-
element("review-protocol").textContent =
|
|
183
|
+
element("review-protocol").textContent = openAiApi
|
|
165
184
|
? "OpenAI-compatible HTTP /v1"
|
|
166
|
-
:
|
|
167
|
-
|
|
185
|
+
: codexChat
|
|
186
|
+
? "Relmio Codex Chat HTTP: POST /chat"
|
|
187
|
+
: "Codex App Server JSON-RPC over WebSocket";
|
|
188
|
+
element("review-auth").textContent = openAiApi
|
|
168
189
|
? "OpenAI Platform API key"
|
|
169
190
|
: "ChatGPT sign-in through official Codex";
|
|
170
|
-
element("review-browser").textContent =
|
|
191
|
+
element("review-browser").textContent = openAiApi
|
|
171
192
|
? plan.allowedOrigins.length > 0
|
|
172
193
|
? `Only ${plan.allowedOrigins.length} exact allowed origin(s)`
|
|
173
194
|
: "Native clients only until exact origins are added"
|
|
174
|
-
:
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
: codexChat
|
|
196
|
+
? "No — trusted local backends and development servers only"
|
|
197
|
+
: "No — trusted native local clients only";
|
|
198
|
+
element("review-origins-row").hidden = !openAiApi;
|
|
199
|
+
element("review-origins").textContent = openAiApi
|
|
177
200
|
? plan.allowedOrigins.length > 0
|
|
178
201
|
? plan.allowedOrigins.join(", ")
|
|
179
202
|
: "None"
|
|
180
203
|
: "";
|
|
181
204
|
element("review-path").textContent = plan.managedPath;
|
|
182
205
|
|
|
183
|
-
if (
|
|
206
|
+
if (openAiApi) {
|
|
184
207
|
appendPolicyNotice(
|
|
185
208
|
element("review-policy"),
|
|
186
209
|
"OpenAI Platform terms and billing apply",
|
|
187
210
|
"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.",
|
|
188
211
|
);
|
|
212
|
+
} else if (codexChat) {
|
|
213
|
+
appendPolicyNotice(
|
|
214
|
+
element("review-policy"),
|
|
215
|
+
"Experimental Codex Chat Adapter — trusted local backends or development servers only",
|
|
216
|
+
"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.",
|
|
217
|
+
);
|
|
189
218
|
} else {
|
|
190
219
|
appendPolicyNotice(
|
|
191
220
|
element("review-policy"),
|
|
@@ -196,18 +225,31 @@ function renderPlan(plan) {
|
|
|
196
225
|
}
|
|
197
226
|
|
|
198
227
|
function prepareInstallPanel() {
|
|
199
|
-
const
|
|
228
|
+
const openAiApi = isOpenAiApi(state.plan.target);
|
|
229
|
+
const codexChat = isCodexChat(state.plan.target);
|
|
200
230
|
const apiKey = element("platform-api-key");
|
|
201
|
-
element("api-key-field").hidden = !
|
|
202
|
-
element("codex-install-warning").hidden =
|
|
203
|
-
|
|
231
|
+
element("api-key-field").hidden = !openAiApi;
|
|
232
|
+
element("codex-install-warning").hidden = openAiApi;
|
|
233
|
+
element("codex-install-warning-title").textContent = codexChat
|
|
234
|
+
? "Credential for trusted local backends or development servers only"
|
|
235
|
+
: "High-trust capability";
|
|
236
|
+
element("codex-install-warning-detail").textContent = codexChat
|
|
237
|
+
? "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."
|
|
238
|
+
: "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.";
|
|
239
|
+
apiKey.required = openAiApi;
|
|
204
240
|
apiKey.value = "";
|
|
205
|
-
element("install-intro").textContent =
|
|
241
|
+
element("install-intro").textContent = openAiApi
|
|
206
242
|
? "Enter the OpenAI Platform API key this endpoint will use upstream. It is sent only to this local Relmio process."
|
|
207
|
-
:
|
|
243
|
+
: codexChat
|
|
244
|
+
? "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."
|
|
245
|
+
: "Relmio will install the official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready.";
|
|
208
246
|
setButtonLabel(
|
|
209
247
|
element("install-button"),
|
|
210
|
-
|
|
248
|
+
openAiApi
|
|
249
|
+
? "Install OpenAI API endpoint"
|
|
250
|
+
: codexChat
|
|
251
|
+
? "Install Codex Chat Adapter"
|
|
252
|
+
: "Install Codex App Server",
|
|
211
253
|
);
|
|
212
254
|
}
|
|
213
255
|
|
|
@@ -215,28 +257,46 @@ function renderInstallResult(result) {
|
|
|
215
257
|
if (
|
|
216
258
|
typeof result.endpoint !== "string" ||
|
|
217
259
|
typeof result.clientCredential !== "string" ||
|
|
218
|
-
!["openai-api", "codex-chatgpt"].includes(result.target)
|
|
260
|
+
!["openai-api", "codex-chatgpt", "codex-chat"].includes(result.target)
|
|
219
261
|
) {
|
|
220
262
|
throw new Error("The local installer returned an unexpected response.");
|
|
221
263
|
}
|
|
222
264
|
|
|
223
|
-
const
|
|
265
|
+
const openAiApi = isOpenAiApi(result.target);
|
|
266
|
+
const codexChat = isCodexChat(result.target);
|
|
267
|
+
state.installedTarget = result.target;
|
|
224
268
|
element("result-endpoint").textContent = result.endpoint;
|
|
225
269
|
element("result-credential").textContent = result.clientCredential;
|
|
226
|
-
element("codex-production-warning").hidden =
|
|
227
|
-
element("codex-login").hidden =
|
|
228
|
-
element("
|
|
270
|
+
element("codex-production-warning").hidden = openAiApi;
|
|
271
|
+
element("codex-login").hidden = openAiApi;
|
|
272
|
+
element("codex-production-warning-title").textContent = codexChat
|
|
273
|
+
? "Experimental Chat Adapter — trusted local backends or development servers only"
|
|
274
|
+
: "Experimental WebSocket transport";
|
|
275
|
+
element("codex-production-warning-detail").textContent = codexChat
|
|
276
|
+
? "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."
|
|
277
|
+
: "Codex App Server WebSocket is experimental and unsupported for production workloads.";
|
|
278
|
+
element("done-title").textContent = openAiApi
|
|
229
279
|
? "OpenAI API endpoint is ready"
|
|
230
|
-
:
|
|
231
|
-
|
|
280
|
+
: codexChat
|
|
281
|
+
? "Codex Chat Adapter is installed"
|
|
282
|
+
: "Codex App Server is installed";
|
|
283
|
+
element("done-detail").textContent = openAiApi
|
|
232
284
|
? "Copy the endpoint and generated bearer credential into your local app."
|
|
233
|
-
:
|
|
285
|
+
: codexChat
|
|
286
|
+
? "Copy the endpoint and generated bearer credential into your trusted local backend or development server, then sign the isolated Codex container in to ChatGPT."
|
|
287
|
+
: "Copy the endpoint and capability, then sign the isolated Codex container in to ChatGPT.";
|
|
234
288
|
appendPolicyNotice(
|
|
235
289
|
element("client-warning"),
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
openAiApi
|
|
291
|
+
? "For local OpenAI-compatible clients"
|
|
292
|
+
: codexChat
|
|
293
|
+
? "For trusted local backends or development servers only"
|
|
294
|
+
: "For trusted native Codex clients only",
|
|
295
|
+
openAiApi
|
|
238
296
|
? "Use the generated client credential as the bearer API key. Your upstream Platform key remains private in the managed Docker volume."
|
|
239
|
-
:
|
|
297
|
+
: codexChat
|
|
298
|
+
? "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."
|
|
299
|
+
: "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.",
|
|
240
300
|
);
|
|
241
301
|
}
|
|
242
302
|
|
|
@@ -430,7 +490,9 @@ element("install-button").addEventListener("click", async (event) => {
|
|
|
430
490
|
setMessage(
|
|
431
491
|
result.target === "openai-api"
|
|
432
492
|
? "Local OpenAI API endpoint verified. Copy its one-time client credential now."
|
|
433
|
-
:
|
|
493
|
+
: result.target === "codex-chat"
|
|
494
|
+
? "Codex Chat Adapter for trusted local backends or development servers verified. Copy its one-time bearer and complete ChatGPT sign-in."
|
|
495
|
+
: "Codex App Server verified. Copy its one-time capability and complete ChatGPT sign-in.",
|
|
434
496
|
);
|
|
435
497
|
} catch (error) {
|
|
436
498
|
invalidatePlan();
|
|
@@ -444,6 +506,44 @@ element("install-button").addEventListener("click", async (event) => {
|
|
|
444
506
|
}
|
|
445
507
|
});
|
|
446
508
|
|
|
509
|
+
element("rotate-credential-button").addEventListener("click", async (event) => {
|
|
510
|
+
const button = event.currentTarget;
|
|
511
|
+
clearError();
|
|
512
|
+
if (!state.installedTarget) {
|
|
513
|
+
showError(new Error("Install a local endpoint before rotating its credential."));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
setBusy(button, true, "Rotating credential…");
|
|
518
|
+
setMessage(
|
|
519
|
+
"Generating a replacement credential before activating it…",
|
|
520
|
+
);
|
|
521
|
+
try {
|
|
522
|
+
const staged = await api("/api/local/client-credential/rotate", {
|
|
523
|
+
method: "POST",
|
|
524
|
+
body: { target: state.installedTarget },
|
|
525
|
+
});
|
|
526
|
+
renderInstallResult(staged);
|
|
527
|
+
setMessage("Replacement credential received. Activating and verifying it now…");
|
|
528
|
+
await new Promise((resolvePromise) => window.requestAnimationFrame(resolvePromise));
|
|
529
|
+
await new Promise((resolvePromise) => window.requestAnimationFrame(resolvePromise));
|
|
530
|
+
const activated = await api("/api/local/client-credential/activate", {
|
|
531
|
+
method: "POST",
|
|
532
|
+
body: {
|
|
533
|
+
rotationId: staged.rotationId,
|
|
534
|
+
clientCredential: staged.clientCredential,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
renderInstallResult({ ...staged, ...activated });
|
|
538
|
+
setMessage("Client credential rotated. Copy the new one now; the previous one no longer works.");
|
|
539
|
+
} catch (error) {
|
|
540
|
+
setMessage("The replacement credential was not confirmed active. Follow the error guidance before retrying.");
|
|
541
|
+
showError(error);
|
|
542
|
+
} finally {
|
|
543
|
+
setBusy(button, false);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
447
547
|
element("codex-login-button").addEventListener("click", async (event) => {
|
|
448
548
|
const button = event.currentTarget;
|
|
449
549
|
const resultBox = element("device-code-result");
|
|
@@ -454,7 +554,7 @@ element("codex-login-button").addEventListener("click", async (event) => {
|
|
|
454
554
|
try {
|
|
455
555
|
const result = await api("/api/local/codex/login", {
|
|
456
556
|
method: "POST",
|
|
457
|
-
body: {},
|
|
557
|
+
body: { target: state.installedTarget },
|
|
458
558
|
});
|
|
459
559
|
const verificationUrl = validateVerificationUrl(result.verificationUrl);
|
|
460
560
|
const userCode = validateDeviceCode(result.userCode);
|
|
@@ -464,7 +564,9 @@ element("codex-login-button").addEventListener("click", async (event) => {
|
|
|
464
564
|
resultBox.hidden = false;
|
|
465
565
|
setMessage("Open the official OpenAI page and enter the displayed device code.");
|
|
466
566
|
await waitForCodexLogin();
|
|
467
|
-
status.textContent =
|
|
567
|
+
status.textContent = isCodexChat(state.installedTarget)
|
|
568
|
+
? "ChatGPT sign-in completed. Codex Chat Adapter is ready for your trusted local backend or development server."
|
|
569
|
+
: "ChatGPT sign-in completed. Codex is ready for your trusted native client.";
|
|
468
570
|
setMessage("Codex ChatGPT sign-in completed successfully.");
|
|
469
571
|
} catch (error) {
|
|
470
572
|
status.textContent = "ChatGPT sign-in did not complete.";
|
|
@@ -546,5 +648,23 @@ async function refreshDockerStatus() {
|
|
|
546
648
|
}
|
|
547
649
|
}
|
|
548
650
|
|
|
651
|
+
async function refreshProjectMeta() {
|
|
652
|
+
const result = await api("/api/local/project-meta");
|
|
653
|
+
const stars = Number.isSafeInteger(result.stars) && result.stars >= 0
|
|
654
|
+
? new Intl.NumberFormat("en", {
|
|
655
|
+
notation: result.stars >= 1_000 ? "compact" : "standard",
|
|
656
|
+
maximumFractionDigits: 1,
|
|
657
|
+
}).format(result.stars)
|
|
658
|
+
: "?";
|
|
659
|
+
element("local-repository-stars").textContent = stars;
|
|
660
|
+
element("local-repository-button").setAttribute(
|
|
661
|
+
"aria-label",
|
|
662
|
+
`Open Relmio version ${result.version} on GitHub. ${
|
|
663
|
+
stars === "?" ? "GitHub star count is unavailable." : `${result.stars} GitHub stars.`
|
|
664
|
+
} Opens in a new tab.`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
549
668
|
renderTarget();
|
|
550
669
|
refreshDockerStatus().catch(showError);
|
|
670
|
+
refreshProjectMeta().catch(() => {});
|