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/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: { ...defaultServices, ...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,