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/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: "codex-chatgpt",
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: "codex-chatgpt",
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
- { installDirectory },
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: { ...defaultServices, ...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,