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/web/server.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
prepareLocalClientCredentialRotation,
|
|
35
35
|
} from "../services/local-installer.js";
|
|
36
36
|
import { startCodexDeviceLogin } from "../services/codex-login.js";
|
|
37
|
+
import { createLocalChatTestService } from "../services/local-chat-test.js";
|
|
37
38
|
|
|
38
39
|
const MAX_BODY_BYTES = 32 * 1024;
|
|
39
40
|
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
|
@@ -87,6 +88,7 @@ const defaultServices = {
|
|
|
87
88
|
resolveLocalInstallRoot,
|
|
88
89
|
restartLocalCodex,
|
|
89
90
|
startCodexDeviceLogin,
|
|
91
|
+
createLocalChatTestService,
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
function setSecurityHeaders(response) {
|
|
@@ -373,6 +375,67 @@ function createSafeProjectMeta(result) {
|
|
|
373
375
|
};
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
function createSafeLocalChatTestKey(result) {
|
|
379
|
+
if (
|
|
380
|
+
!result ||
|
|
381
|
+
typeof result !== "object" ||
|
|
382
|
+
Array.isArray(result) ||
|
|
383
|
+
typeof result.keyId !== "string" ||
|
|
384
|
+
result.keyId.length === 0 ||
|
|
385
|
+
result.keyId.length > 128 ||
|
|
386
|
+
!result.publicKeyJwk ||
|
|
387
|
+
typeof result.publicKeyJwk !== "object" ||
|
|
388
|
+
Array.isArray(result.publicKeyJwk) ||
|
|
389
|
+
result.publicKeyJwk.kty !== "RSA" ||
|
|
390
|
+
typeof result.publicKeyJwk.n !== "string" ||
|
|
391
|
+
typeof result.publicKeyJwk.e !== "string" ||
|
|
392
|
+
result.publicKeyJwk.n.length === 0 ||
|
|
393
|
+
result.publicKeyJwk.n.length > 1_024 ||
|
|
394
|
+
result.publicKeyJwk.e.length === 0 ||
|
|
395
|
+
result.publicKeyJwk.e.length > 32 ||
|
|
396
|
+
result.algorithm !== "RSA-OAEP-256" ||
|
|
397
|
+
typeof result.expiresAt !== "string" ||
|
|
398
|
+
Number.isNaN(Date.parse(result.expiresAt))
|
|
399
|
+
) {
|
|
400
|
+
throw Object.assign(new Error("The local tester could not start safely."), {
|
|
401
|
+
statusCode: 502,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
keyId: result.keyId,
|
|
406
|
+
publicKeyJwk: {
|
|
407
|
+
kty: "RSA",
|
|
408
|
+
n: result.publicKeyJwk.n,
|
|
409
|
+
e: result.publicKeyJwk.e,
|
|
410
|
+
},
|
|
411
|
+
algorithm: result.algorithm,
|
|
412
|
+
expiresAt: result.expiresAt,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createSafeLocalChatTestResponse(result) {
|
|
417
|
+
if (
|
|
418
|
+
!result ||
|
|
419
|
+
typeof result !== "object" ||
|
|
420
|
+
Array.isArray(result) ||
|
|
421
|
+
typeof result.conversationId !== "string" ||
|
|
422
|
+
result.conversationId.length === 0 ||
|
|
423
|
+
result.conversationId.length > 160 ||
|
|
424
|
+
typeof result.output !== "string" ||
|
|
425
|
+
result.output.length === 0 ||
|
|
426
|
+
result.output.length > 12 * 1_024
|
|
427
|
+
) {
|
|
428
|
+
throw Object.assign(
|
|
429
|
+
new Error("The local adapter returned an unexpected response."),
|
|
430
|
+
{ statusCode: 502 },
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
conversationId: result.conversationId,
|
|
435
|
+
output: result.output,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
376
439
|
function getPendingLocalCredentialRotation(state) {
|
|
377
440
|
if (
|
|
378
441
|
state.localCredentialRotationPending &&
|
|
@@ -409,6 +472,15 @@ function requireLiveLocalAction(state, action) {
|
|
|
409
472
|
}
|
|
410
473
|
}
|
|
411
474
|
|
|
475
|
+
function requireReadyLocalChatTester(state) {
|
|
476
|
+
if (state.localInstalledTarget !== "codex-chat") {
|
|
477
|
+
throw Object.assign(
|
|
478
|
+
new Error("Install the Codex Chat Adapter before starting its local tester."),
|
|
479
|
+
{ statusCode: 409 },
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
412
484
|
async function handleApi(request, response, path, state) {
|
|
413
485
|
requireApiToken(request, state);
|
|
414
486
|
requireSameOrigin(request, state);
|
|
@@ -603,6 +675,46 @@ async function handleApi(request, response, path, state) {
|
|
|
603
675
|
|
|
604
676
|
const body = await readJsonBody(request);
|
|
605
677
|
|
|
678
|
+
if (path === "/api/local/chat-test/key") {
|
|
679
|
+
requireLiveLocalAction(state, "Local chat testing");
|
|
680
|
+
requireReadyLocalChatTester(state);
|
|
681
|
+
enforceRateLimit(state, path);
|
|
682
|
+
sendJson(
|
|
683
|
+
response,
|
|
684
|
+
200,
|
|
685
|
+
createSafeLocalChatTestKey(await state.localChatTest.issueKey()),
|
|
686
|
+
);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (path === "/api/local/chat-test/message") {
|
|
691
|
+
requireLiveLocalAction(state, "Local chat testing");
|
|
692
|
+
requireReadyLocalChatTester(state);
|
|
693
|
+
enforceRateLimit(state, path);
|
|
694
|
+
try {
|
|
695
|
+
sendJson(
|
|
696
|
+
response,
|
|
697
|
+
200,
|
|
698
|
+
createSafeLocalChatTestResponse(await state.localChatTest.message(body)),
|
|
699
|
+
);
|
|
700
|
+
} finally {
|
|
701
|
+
if (body && typeof body === "object") {
|
|
702
|
+
body.encryptedCredential = undefined;
|
|
703
|
+
body.input = undefined;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (path === "/api/local/chat-test/reset") {
|
|
710
|
+
requireLiveLocalAction(state, "Local chat testing");
|
|
711
|
+
requireReadyLocalChatTester(state);
|
|
712
|
+
enforceRateLimit(state, path);
|
|
713
|
+
await state.localChatTest.reset(body);
|
|
714
|
+
sendJson(response, 200, { forgotten: true });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
606
718
|
if (path === "/api/oauth/cancel") {
|
|
607
719
|
requireLiveLocalAction(state, "Live ChatGPT sign-in");
|
|
608
720
|
const login = state.oauthLogin;
|
|
@@ -670,12 +782,14 @@ async function handleApi(request, response, path, state) {
|
|
|
670
782
|
state.localInstallInFlight = true;
|
|
671
783
|
acquiredInstallLock = true;
|
|
672
784
|
state.localPlan = null;
|
|
785
|
+
state.localInstalledTarget = null;
|
|
673
786
|
const result = await state.services.installLocalEndpoint({
|
|
674
787
|
plan: pending.plan,
|
|
675
788
|
apiKey: body.apiKey,
|
|
676
789
|
confirmed: body.confirmed,
|
|
677
790
|
});
|
|
678
791
|
|
|
792
|
+
state.localInstalledTarget = pending.plan.target;
|
|
679
793
|
sendJson(response, 200, createSafeLocalInstallResult(result));
|
|
680
794
|
} finally {
|
|
681
795
|
if (acquiredInstallLock) {
|
|
@@ -707,6 +821,7 @@ async function handleApi(request, response, path, state) {
|
|
|
707
821
|
|
|
708
822
|
state.localCredentialRotationInFlight = true;
|
|
709
823
|
try {
|
|
824
|
+
state.localChatTest.resetAll?.();
|
|
710
825
|
const result = await state.services.prepareLocalClientCredentialRotation({
|
|
711
826
|
target: validateLocalTarget(body.target),
|
|
712
827
|
});
|
|
@@ -749,6 +864,7 @@ async function handleApi(request, response, path, state) {
|
|
|
749
864
|
state.localCredentialRotationPending = null;
|
|
750
865
|
state.localCredentialRotationInFlight = true;
|
|
751
866
|
try {
|
|
867
|
+
state.localChatTest.resetAll?.();
|
|
752
868
|
const result = await state.services.activateLocalClientCredentialRotation({
|
|
753
869
|
target: pending.target,
|
|
754
870
|
clientCredential: body.clientCredential,
|
|
@@ -776,6 +892,10 @@ async function handleApi(request, response, path, state) {
|
|
|
776
892
|
);
|
|
777
893
|
}
|
|
778
894
|
|
|
895
|
+
const target = validateLocalTarget(body?.target ?? "codex-chatgpt");
|
|
896
|
+
if (target !== "codex-chatgpt" && target !== "codex-chat") {
|
|
897
|
+
throw new Error("Choose a Codex local endpoint before signing in.");
|
|
898
|
+
}
|
|
779
899
|
state.codexLoginStartInFlight = true;
|
|
780
900
|
let finishStart;
|
|
781
901
|
const startPromise = new Promise((resolvePromise) => {
|
|
@@ -802,7 +922,7 @@ async function handleApi(request, response, path, state) {
|
|
|
802
922
|
}
|
|
803
923
|
|
|
804
924
|
releaseChangeLock = await state.services.acquireLocalEndpointChangeLock({
|
|
805
|
-
target
|
|
925
|
+
target,
|
|
806
926
|
});
|
|
807
927
|
if (state.closing) {
|
|
808
928
|
throw Object.assign(new Error("The local wizard is closing."), {
|
|
@@ -810,11 +930,12 @@ async function handleApi(request, response, path, state) {
|
|
|
810
930
|
});
|
|
811
931
|
}
|
|
812
932
|
const installDirectory = await state.services.resolveLocalInstallRoot({
|
|
813
|
-
target
|
|
933
|
+
target,
|
|
814
934
|
});
|
|
815
935
|
const { dockerHost, projectName } =
|
|
816
936
|
await state.services.attestLocalCodexInstallation({
|
|
817
937
|
installDirectory,
|
|
938
|
+
...(target === "codex-chat" ? { target } : {}),
|
|
818
939
|
});
|
|
819
940
|
|
|
820
941
|
const attempt = await state.services.startCodexDeviceLogin({
|
|
@@ -847,7 +968,10 @@ async function handleApi(request, response, path, state) {
|
|
|
847
968
|
return;
|
|
848
969
|
}
|
|
849
970
|
await state.services.restartLocalCodex(
|
|
850
|
-
{
|
|
971
|
+
{
|
|
972
|
+
installDirectory,
|
|
973
|
+
...(target === "codex-chat" ? { target } : {}),
|
|
974
|
+
},
|
|
851
975
|
{ changeLockHeld: true },
|
|
852
976
|
);
|
|
853
977
|
if (state.codexLogin === login && !state.closing) {
|
|
@@ -1071,9 +1195,13 @@ export async function startWizardServer({
|
|
|
1071
1195
|
throw new TypeError("A strong wizard session token is required.");
|
|
1072
1196
|
}
|
|
1073
1197
|
|
|
1198
|
+
const resolvedServices = { ...defaultServices, ...services };
|
|
1074
1199
|
const state = {
|
|
1075
1200
|
sessionToken,
|
|
1076
|
-
services:
|
|
1201
|
+
services: resolvedServices,
|
|
1202
|
+
localChatTest:
|
|
1203
|
+
resolvedServices.localChatTest ??
|
|
1204
|
+
resolvedServices.createLocalChatTestService(),
|
|
1077
1205
|
uiFiles: uiFiles ?? (await loadDefaultUiFiles()),
|
|
1078
1206
|
origin: "http://127.0.0.1",
|
|
1079
1207
|
connection: null,
|
|
@@ -1086,6 +1214,7 @@ export async function startWizardServer({
|
|
|
1086
1214
|
oauthLoginStartInFlight: false,
|
|
1087
1215
|
oauthLoginStartPromise: null,
|
|
1088
1216
|
localPlan: null,
|
|
1217
|
+
localInstalledTarget: null,
|
|
1089
1218
|
localInstallInFlight: false,
|
|
1090
1219
|
localCredentialRotationInFlight: false,
|
|
1091
1220
|
localCredentialRotationPending: null,
|
|
@@ -1114,6 +1243,7 @@ export async function startWizardServer({
|
|
|
1114
1243
|
origin: state.origin,
|
|
1115
1244
|
async close() {
|
|
1116
1245
|
state.closing = true;
|
|
1246
|
+
state.localChatTest.dispose?.();
|
|
1117
1247
|
await waitForBoundedResult(
|
|
1118
1248
|
state.oauthLoginStartPromise,
|
|
1119
1249
|
state.oauthShutdownWaitMs,
|