relmio 0.6.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 +26 -0
- package/README.md +42 -429
- package/docs/faq.md +45 -0
- package/docs/getting-started.md +38 -0
- package/docs/local-endpoints-spec.md +27 -0
- package/docs/local-endpoints.md +52 -11
- package/docs/reference.md +77 -0
- package/docs/security.md +34 -0
- package/docs/troubleshooting.md +33 -0
- package/docs/vps-and-n8n.md +30 -0
- package/package.json +1 -1
- package/src/services/local-chat-test.js +361 -0
- package/src/ui/local.css +115 -1
- package/src/ui/local.html +99 -0
- package/src/ui/local.js +263 -1
- package/src/web/server.js +123 -1
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,
|
|
@@ -1079,9 +1195,13 @@ export async function startWizardServer({
|
|
|
1079
1195
|
throw new TypeError("A strong wizard session token is required.");
|
|
1080
1196
|
}
|
|
1081
1197
|
|
|
1198
|
+
const resolvedServices = { ...defaultServices, ...services };
|
|
1082
1199
|
const state = {
|
|
1083
1200
|
sessionToken,
|
|
1084
|
-
services:
|
|
1201
|
+
services: resolvedServices,
|
|
1202
|
+
localChatTest:
|
|
1203
|
+
resolvedServices.localChatTest ??
|
|
1204
|
+
resolvedServices.createLocalChatTestService(),
|
|
1085
1205
|
uiFiles: uiFiles ?? (await loadDefaultUiFiles()),
|
|
1086
1206
|
origin: "http://127.0.0.1",
|
|
1087
1207
|
connection: null,
|
|
@@ -1094,6 +1214,7 @@ export async function startWizardServer({
|
|
|
1094
1214
|
oauthLoginStartInFlight: false,
|
|
1095
1215
|
oauthLoginStartPromise: null,
|
|
1096
1216
|
localPlan: null,
|
|
1217
|
+
localInstalledTarget: null,
|
|
1097
1218
|
localInstallInFlight: false,
|
|
1098
1219
|
localCredentialRotationInFlight: false,
|
|
1099
1220
|
localCredentialRotationPending: null,
|
|
@@ -1122,6 +1243,7 @@ export async function startWizardServer({
|
|
|
1122
1243
|
origin: state.origin,
|
|
1123
1244
|
async close() {
|
|
1124
1245
|
state.closing = true;
|
|
1246
|
+
state.localChatTest.dispose?.();
|
|
1125
1247
|
await waitForBoundedResult(
|
|
1126
1248
|
state.oauthLoginStartPromise,
|
|
1127
1249
|
state.oauthShutdownWaitMs,
|