remote-codex 0.11.24 → 0.11.26

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.
@@ -18,16 +18,17 @@ var RelayRequestBroker = class {
18
18
  }
19
19
  timeoutMs;
20
20
  pendingRequests = /* @__PURE__ */ new Map();
21
- forward(socket, message) {
21
+ forward(socket, message, options) {
22
22
  return new Promise((resolve, reject) => {
23
23
  if (message.type !== "relay.request") {
24
24
  reject(new Error("Only relay.request messages can be forwarded."));
25
25
  return;
26
26
  }
27
+ const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
27
28
  const timeout = setTimeout(() => {
28
29
  this.pendingRequests.delete(message.requestId);
29
30
  reject(new Error("Supervisor relay request timed out."));
30
- }, this.timeoutMs);
31
+ }, timeoutMs);
31
32
  this.pendingRequests.set(message.requestId, {
32
33
  resolve,
33
34
  reject,
@@ -115,6 +116,53 @@ var RelayStore = class _RelayStore {
115
116
  this.insertUser(user);
116
117
  return this.createLoginResult(user);
117
118
  }
119
+ requestRegistrationApproval(input) {
120
+ if (!this.registrationEnabled()) {
121
+ throw new RelayStoreError(403, "forbidden", "Registration is currently disabled.");
122
+ }
123
+ const email = input.email.trim().toLowerCase();
124
+ const username = normalizeUsername(input.username);
125
+ if (!email.includes("@")) {
126
+ throw new RelayStoreError(400, "bad_request", "A valid email address is required.");
127
+ }
128
+ if (username.length < 3) {
129
+ throw new RelayStoreError(400, "bad_request", "Username must be at least 3 characters.");
130
+ }
131
+ if (input.password.length < 8) {
132
+ throw new RelayStoreError(400, "bad_request", "Password must be at least 8 characters.");
133
+ }
134
+ if (this.getUserByIdentifier(email) || this.getUserByUsername(username)) {
135
+ throw new RelayStoreError(409, "conflict", "A user with that email or username already exists.");
136
+ }
137
+ const existing = this.rowToPendingRegistration(
138
+ this.sqlite.prepare(
139
+ `
140
+ SELECT * FROM relay_pending_registrations
141
+ WHERE status = 'pending'
142
+ AND (email = ? OR username = ?)
143
+ ORDER BY created_at DESC
144
+ LIMIT 1
145
+ `
146
+ ).get(email, username)
147
+ );
148
+ if (existing) {
149
+ return this.publicPendingRegistration(existing);
150
+ }
151
+ const passwordSalt = crypto.randomBytes(16).toString("base64url");
152
+ const record = {
153
+ id: crypto.randomUUID(),
154
+ email,
155
+ username,
156
+ passwordSalt,
157
+ passwordHash: hashSecret(input.password, passwordSalt),
158
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
159
+ status: "pending",
160
+ reviewedAt: null,
161
+ reviewedByUserId: null
162
+ };
163
+ this.insertPendingRegistration(record);
164
+ return this.publicPendingRegistration(record);
165
+ }
118
166
  login(input) {
119
167
  const normalizedIdentifier = input.identifier.trim().toLowerCase();
120
168
  const user = this.getUserByIdentifier(normalizedIdentifier);
@@ -220,7 +268,9 @@ var RelayStore = class _RelayStore {
220
268
  deviceId: input.deviceId,
221
269
  deviceName: device.name,
222
270
  threadId: input.threadId,
271
+ threadTitle: null,
223
272
  workspaceId: input.workspaceId?.trim() || null,
273
+ workspaceLabel: null,
224
274
  label: input.label?.trim() || null,
225
275
  threadAccess: normalizeThreadAccess(input.threadAccess),
226
276
  workspaceAccess: normalizeWorkspaceAccess(input.workspaceAccess),
@@ -353,12 +403,35 @@ var RelayStore = class _RelayStore {
353
403
  sharedByMe: sharedByMe.map((share) => this.publicShare(share))
354
404
  };
355
405
  }
356
- adminSummary(connectedDevices) {
406
+ sharedThreadsForDevice(userId, deviceId) {
407
+ return this.getSharesByTarget(userId).filter((share) => share.deviceId === deviceId).map((share) => this.publicShare(share));
408
+ }
409
+ adminSummary(connectedDevices, options = {}) {
410
+ const conversationWindowDays = normalizeConversationWindowDays(options.conversationWindowDays);
411
+ const users = this.getUsers();
412
+ const devices = this.getDevices();
413
+ const deviceCounts = /* @__PURE__ */ new Map();
414
+ for (const device of devices) {
415
+ deviceCounts.set(device.ownerUserId, (deviceCounts.get(device.ownerUserId) ?? 0) + 1);
416
+ }
417
+ const conversationCounts = this.conversationCountsByUser(conversationWindowDays);
357
418
  return {
358
- users: this.getUsers().map((user) => this.publicUser(user)),
359
- devices: this.getDevices().map(
360
- (device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)
419
+ users: users.map(
420
+ (user) => this.publicAdminUser(user, deviceCounts.get(user.id) ?? 0, conversationCounts.get(user.id) ?? 0)
361
421
  ),
422
+ devices: devices.map((device) => {
423
+ const owner = users.find((user) => user.id === device.ownerUserId);
424
+ return this.publicAdminDevice(
425
+ device,
426
+ connectedDevices.get(device.id) ?? null,
427
+ owner,
428
+ options.metadata
429
+ );
430
+ }),
431
+ shares: this.getShares({ includeRevoked: true }).map((share) => this.publicShare(share)),
432
+ pendingRegistrations: this.pendingRegistrations(),
433
+ settings: this.registrationSettings(),
434
+ conversationWindowDays,
362
435
  registrationEnabled: this.registrationEnabled()
363
436
  };
364
437
  }
@@ -366,6 +439,95 @@ var RelayStore = class _RelayStore {
366
439
  this.setSetting("registrationEnabled", enabled ? "true" : "false");
367
440
  return enabled;
368
441
  }
442
+ registrationSettings() {
443
+ return {
444
+ enabled: this.registrationEnabled(),
445
+ registrationPassword: this.getSetting("registrationPassword"),
446
+ approvalRequired: this.getSetting("registrationApprovalRequired") === "true"
447
+ };
448
+ }
449
+ updateRegistrationSettings(input) {
450
+ if (input.enabled !== void 0) {
451
+ this.setRegistrationEnabled(input.enabled);
452
+ }
453
+ if (input.registrationPassword !== void 0) {
454
+ const password = input.registrationPassword?.trim() || null;
455
+ if (password !== null && password.length < 8) {
456
+ throw new RelayStoreError(400, "bad_request", "Registration password must be at least 8 characters.");
457
+ }
458
+ if (password === null) {
459
+ this.deleteSetting("registrationPassword");
460
+ } else {
461
+ this.setSetting("registrationPassword", password);
462
+ }
463
+ }
464
+ if (input.approvalRequired !== void 0) {
465
+ this.setSetting("registrationApprovalRequired", input.approvalRequired ? "true" : "false");
466
+ }
467
+ return this.registrationSettings();
468
+ }
469
+ ensureRegistrationPassword(password) {
470
+ if (!password || this.getSetting("registrationPassword") !== null) {
471
+ return;
472
+ }
473
+ this.setSetting("registrationPassword", password);
474
+ }
475
+ approvePendingRegistration(adminUserId, requestId) {
476
+ const record = this.requirePendingRegistration(requestId);
477
+ const user = {
478
+ id: crypto.randomUUID(),
479
+ email: record.email,
480
+ username: record.username,
481
+ role: "user",
482
+ enabled: true,
483
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
484
+ lastSeenAt: null,
485
+ passwordSalt: record.passwordSalt,
486
+ passwordHash: record.passwordHash
487
+ };
488
+ const approve = this.sqlite.transaction(() => {
489
+ this.insertUser(user);
490
+ this.sqlite.prepare(
491
+ `
492
+ UPDATE relay_pending_registrations
493
+ SET status = 'approved', reviewed_at = ?, reviewed_by_user_id = ?
494
+ WHERE id = ?
495
+ `
496
+ ).run((/* @__PURE__ */ new Date()).toISOString(), adminUserId, requestId);
497
+ });
498
+ approve();
499
+ return this.publicUser(user);
500
+ }
501
+ rejectPendingRegistration(adminUserId, requestId) {
502
+ this.requirePendingRegistration(requestId);
503
+ this.sqlite.prepare(
504
+ `
505
+ UPDATE relay_pending_registrations
506
+ SET status = 'rejected', reviewed_at = ?, reviewed_by_user_id = ?
507
+ WHERE id = ?
508
+ `
509
+ ).run((/* @__PURE__ */ new Date()).toISOString(), adminUserId, requestId);
510
+ return { id: requestId };
511
+ }
512
+ recordUserSeen(userId, at = (/* @__PURE__ */ new Date()).toISOString()) {
513
+ this.sqlite.prepare("UPDATE relay_users SET last_seen_at = ? WHERE id = ?").run(at, userId);
514
+ }
515
+ recordConversationEvent(input) {
516
+ this.sqlite.prepare(
517
+ `
518
+ INSERT INTO relay_conversation_events (
519
+ id, user_id, device_id, thread_id, workspace_id, occurred_at
520
+ ) VALUES (?, ?, ?, ?, ?, ?)
521
+ `
522
+ ).run(
523
+ crypto.randomUUID(),
524
+ input.userId,
525
+ input.deviceId,
526
+ input.threadId,
527
+ input.workspaceId,
528
+ (/* @__PURE__ */ new Date()).toISOString()
529
+ );
530
+ }
369
531
  setUserEnabled(userId, enabled) {
370
532
  const user = this.requireUser(userId);
371
533
  if (user.role === "admin" && !enabled) {
@@ -374,6 +536,30 @@ var RelayStore = class _RelayStore {
374
536
  this.sqlite.prepare("UPDATE relay_users SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, userId);
375
537
  return this.publicUser({ ...user, enabled });
376
538
  }
539
+ deleteUser(userId) {
540
+ const user = this.requireUser(userId);
541
+ if (user.role === "admin") {
542
+ throw new RelayStoreError(400, "bad_request", "The admin user cannot be deleted.");
543
+ }
544
+ this.sqlite.prepare("DELETE FROM relay_users WHERE id = ?").run(userId);
545
+ }
546
+ adminResetUserPassword(userId, password) {
547
+ const user = this.requireUser(userId);
548
+ if (user.role === "admin") {
549
+ throw new RelayStoreError(400, "bad_request", "The admin user password cannot be reset here.");
550
+ }
551
+ if (password.length < 8) {
552
+ throw new RelayStoreError(400, "bad_request", "Password must be at least 8 characters.");
553
+ }
554
+ const passwordSalt = crypto.randomBytes(16).toString("base64url");
555
+ const passwordHash = hashSecret(password, passwordSalt);
556
+ this.sqlite.prepare("UPDATE relay_users SET password_salt = ?, password_hash = ? WHERE id = ?").run(passwordSalt, passwordHash, user.id);
557
+ return this.publicUser({
558
+ ...user,
559
+ passwordSalt,
560
+ passwordHash
561
+ });
562
+ }
377
563
  updateAccount(userId, input) {
378
564
  const user = this.requireUser(userId);
379
565
  const username = input.username !== void 0 ? normalizeUsername(input.username) : user.username;
@@ -424,6 +610,24 @@ var RelayStore = class _RelayStore {
424
610
  createdAt: device.createdAt
425
611
  };
426
612
  }
613
+ publicAdminUser(user, deviceCount, conversationCount) {
614
+ return {
615
+ ...this.publicUser(user),
616
+ lastSeenAt: user.lastSeenAt,
617
+ deviceCount,
618
+ conversationCount
619
+ };
620
+ }
621
+ publicAdminDevice(device, status, owner, metadata) {
622
+ return {
623
+ ...this.publicDevice(device, status),
624
+ ownerUsername: owner?.username ?? "unknown",
625
+ ownerEmail: owner?.email ?? "unknown",
626
+ ipAddress: status?.ipAddress ?? null,
627
+ workspaces: metadata?.workspacesByDeviceId?.get(device.id) ?? [],
628
+ threads: metadata?.threadsByDeviceId?.get(device.id) ?? []
629
+ };
630
+ }
427
631
  recordShareAccess(share, user) {
428
632
  if (share.revokedAt || share.expiresAt && share.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
429
633
  return;
@@ -466,6 +670,7 @@ var RelayStore = class _RelayStore {
466
670
  username TEXT NOT NULL UNIQUE,
467
671
  role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
468
672
  enabled INTEGER NOT NULL DEFAULT 1,
673
+ last_seen_at TEXT,
469
674
  created_at TEXT NOT NULL,
470
675
  password_salt TEXT NOT NULL,
471
676
  password_hash TEXT NOT NULL
@@ -514,7 +719,33 @@ var RelayStore = class _RelayStore {
514
719
  );
515
720
 
516
721
  CREATE INDEX IF NOT EXISTS relay_share_access_events_share_idx ON relay_share_access_events(share_id, accessed_at DESC);
722
+
723
+ CREATE TABLE IF NOT EXISTS relay_conversation_events (
724
+ id TEXT PRIMARY KEY,
725
+ user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
726
+ device_id TEXT NOT NULL REFERENCES relay_devices(id) ON DELETE CASCADE,
727
+ thread_id TEXT,
728
+ workspace_id TEXT,
729
+ occurred_at TEXT NOT NULL
730
+ );
731
+
732
+ CREATE INDEX IF NOT EXISTS relay_conversation_events_user_time_idx ON relay_conversation_events(user_id, occurred_at DESC);
733
+
734
+ CREATE TABLE IF NOT EXISTS relay_pending_registrations (
735
+ id TEXT PRIMARY KEY,
736
+ email TEXT NOT NULL,
737
+ username TEXT NOT NULL,
738
+ password_salt TEXT NOT NULL,
739
+ password_hash TEXT NOT NULL,
740
+ created_at TEXT NOT NULL,
741
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
742
+ reviewed_at TEXT,
743
+ reviewed_by_user_id TEXT
744
+ );
745
+
746
+ CREATE INDEX IF NOT EXISTS relay_pending_registrations_status_idx ON relay_pending_registrations(status, created_at DESC);
517
747
  `);
748
+ this.ensureColumn("relay_users", "last_seen_at", "TEXT");
518
749
  this.ensureColumn("relay_devices", "token", "TEXT");
519
750
  this.ensureColumn("relay_shares", "workspace_id", "TEXT");
520
751
  this.ensureColumn("relay_shares", "thread_access", "TEXT NOT NULL DEFAULT 'control'");
@@ -574,6 +805,9 @@ var RelayStore = class _RelayStore {
574
805
  setSetting(key, value) {
575
806
  this.sqlite.prepare("INSERT INTO relay_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
576
807
  }
808
+ deleteSetting(key) {
809
+ this.sqlite.prepare("DELETE FROM relay_settings WHERE key = ?").run(key);
810
+ }
577
811
  createStoredUser(input) {
578
812
  const email = input.email.trim().toLowerCase();
579
813
  const username = normalizeUsername(input.username);
@@ -597,6 +831,7 @@ var RelayStore = class _RelayStore {
597
831
  role: input.role,
598
832
  enabled: true,
599
833
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
834
+ lastSeenAt: null,
600
835
  passwordSalt,
601
836
  passwordHash: hashSecret(input.password, passwordSalt)
602
837
  };
@@ -662,8 +897,8 @@ var RelayStore = class _RelayStore {
662
897
  this.sqlite.prepare(
663
898
  `
664
899
  INSERT INTO relay_users (
665
- id, email, username, role, enabled, created_at, password_salt, password_hash
666
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
900
+ id, email, username, role, enabled, last_seen_at, created_at, password_salt, password_hash
901
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
667
902
  `
668
903
  ).run(
669
904
  user.id,
@@ -671,6 +906,7 @@ var RelayStore = class _RelayStore {
671
906
  user.username,
672
907
  user.role,
673
908
  user.enabled ? 1 : 0,
909
+ user.lastSeenAt,
674
910
  user.createdAt,
675
911
  user.passwordSalt,
676
912
  user.passwordHash
@@ -790,6 +1026,72 @@ var RelayStore = class _RelayStore {
790
1026
  getSharesByTarget(targetUserId) {
791
1027
  return this.sqlite.prepare("SELECT * FROM relay_shares WHERE target_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC").all(targetUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
792
1028
  }
1029
+ getShares(options = {}) {
1030
+ const sql = options.includeRevoked ? "SELECT * FROM relay_shares ORDER BY created_at DESC" : "SELECT * FROM relay_shares WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at DESC";
1031
+ const rows = options.includeRevoked ? this.sqlite.prepare(sql).all() : this.sqlite.prepare(sql).all((/* @__PURE__ */ new Date()).toISOString());
1032
+ return rows.map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
1033
+ }
1034
+ conversationCountsByUser(days) {
1035
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
1036
+ const rows = this.sqlite.prepare(
1037
+ `
1038
+ SELECT user_id, COUNT(*) AS count
1039
+ FROM relay_conversation_events
1040
+ WHERE occurred_at >= ?
1041
+ GROUP BY user_id
1042
+ `
1043
+ ).all(since);
1044
+ return new Map(rows.map((row) => [row.user_id, row.count]));
1045
+ }
1046
+ pendingRegistrations() {
1047
+ return this.sqlite.prepare(
1048
+ `
1049
+ SELECT * FROM relay_pending_registrations
1050
+ WHERE status = 'pending'
1051
+ ORDER BY created_at ASC
1052
+ `
1053
+ ).all().map((row) => this.rowToPendingRegistration(row)).filter((record) => Boolean(record)).map((record) => this.publicPendingRegistration(record));
1054
+ }
1055
+ requirePendingRegistration(id) {
1056
+ const record = this.rowToPendingRegistration(
1057
+ this.sqlite.prepare("SELECT * FROM relay_pending_registrations WHERE id = ? AND status = 'pending'").get(id)
1058
+ );
1059
+ if (!record) {
1060
+ throw new RelayStoreError(404, "not_found", "Pending registration was not found.");
1061
+ }
1062
+ if (this.getUserByIdentifier(record.email) || this.getUserByUsername(record.username)) {
1063
+ throw new RelayStoreError(409, "conflict", "A user with that email or username already exists.");
1064
+ }
1065
+ return record;
1066
+ }
1067
+ publicPendingRegistration(record) {
1068
+ return {
1069
+ id: record.id,
1070
+ email: record.email,
1071
+ username: record.username,
1072
+ createdAt: record.createdAt
1073
+ };
1074
+ }
1075
+ insertPendingRegistration(record) {
1076
+ this.sqlite.prepare(
1077
+ `
1078
+ INSERT INTO relay_pending_registrations (
1079
+ id, email, username, password_salt, password_hash,
1080
+ created_at, status, reviewed_at, reviewed_by_user_id
1081
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1082
+ `
1083
+ ).run(
1084
+ record.id,
1085
+ record.email,
1086
+ record.username,
1087
+ record.passwordSalt,
1088
+ record.passwordHash,
1089
+ record.createdAt,
1090
+ record.status,
1091
+ record.reviewedAt,
1092
+ record.reviewedByUserId
1093
+ );
1094
+ }
793
1095
  rowToUser(row) {
794
1096
  if (!row) return null;
795
1097
  return {
@@ -799,6 +1101,7 @@ var RelayStore = class _RelayStore {
799
1101
  role: row.role,
800
1102
  enabled: Boolean(row.enabled),
801
1103
  createdAt: row.created_at,
1104
+ lastSeenAt: row.last_seen_at ?? null,
802
1105
  passwordSalt: row.password_salt,
803
1106
  passwordHash: row.password_hash
804
1107
  };
@@ -826,7 +1129,9 @@ var RelayStore = class _RelayStore {
826
1129
  deviceId: row.device_id,
827
1130
  deviceName: row.device_name ?? "Remote Codex device",
828
1131
  threadId: row.thread_id,
1132
+ threadTitle: null,
829
1133
  workspaceId: row.workspace_id ?? null,
1134
+ workspaceLabel: null,
830
1135
  label: row.label,
831
1136
  threadAccess: normalizeThreadAccess(row.thread_access),
832
1137
  workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
@@ -838,6 +1143,20 @@ var RelayStore = class _RelayStore {
838
1143
  accessEvents: []
839
1144
  };
840
1145
  }
1146
+ rowToPendingRegistration(row) {
1147
+ if (!row) return null;
1148
+ return {
1149
+ id: row.id,
1150
+ email: row.email,
1151
+ username: row.username,
1152
+ passwordSalt: row.password_salt,
1153
+ passwordHash: row.password_hash,
1154
+ createdAt: row.created_at,
1155
+ status: row.status,
1156
+ reviewedAt: row.reviewed_at ?? null,
1157
+ reviewedByUserId: row.reviewed_by_user_id ?? null
1158
+ };
1159
+ }
841
1160
  };
842
1161
  var RelayStoreError = class extends Error {
843
1162
  constructor(statusCode, code, message) {
@@ -867,6 +1186,12 @@ function normalizeExpiresAt(value) {
867
1186
  const timestamp = Date.parse(value);
868
1187
  return Number.isNaN(timestamp) ? null : new Date(timestamp).toISOString();
869
1188
  }
1189
+ function normalizeConversationWindowDays(value) {
1190
+ if (!Number.isFinite(value ?? NaN)) {
1191
+ return 7;
1192
+ }
1193
+ return Math.min(365, Math.max(1, Math.floor(value)));
1194
+ }
870
1195
  function hashSecret(secret, salt) {
871
1196
  return crypto.scryptSync(secret, salt, 32).toString("base64url");
872
1197
  }
@@ -890,6 +1215,7 @@ function safeEqual(left, right) {
890
1215
 
891
1216
  // src/app.ts
892
1217
  var RELAY_REQUEST_TIMEOUT_MS = 3e4;
1218
+ var RELAY_PORTAL_METADATA_TIMEOUT_MS = 900;
893
1219
  var WEBSOCKET_OPEN = 1;
894
1220
  var RELAY_COOKIE_NAME = "remote_codex_relay_session";
895
1221
  var threadAccessSchema = z.enum(["read", "control"]);
@@ -931,6 +1257,17 @@ var updateShareSchema = z.object({
931
1257
  var setEnabledSchema = z.object({
932
1258
  enabled: z.boolean()
933
1259
  });
1260
+ var adminResetPasswordSchema = z.object({
1261
+ password: z.string().min(8)
1262
+ });
1263
+ var adminQuerySchema = z.object({
1264
+ days: z.coerce.number().int().positive().max(365).optional()
1265
+ });
1266
+ var updateRegistrationSettingsSchema = z.object({
1267
+ enabled: z.boolean().optional(),
1268
+ registrationPassword: z.string().nullable().optional(),
1269
+ approvalRequired: z.boolean().optional()
1270
+ });
934
1271
  var updateAccountSchema = z.object({
935
1272
  username: z.string().trim().min(3).optional()
936
1273
  });
@@ -1027,6 +1364,7 @@ function buildRelayServer(config2, options = {}) {
1027
1364
  if (config2.registrationEnabledConfigured) {
1028
1365
  store.setRegistrationEnabled(config2.registrationEnabled);
1029
1366
  }
1367
+ store.ensureRegistrationPassword(config2.registrationPassword);
1030
1368
  store.seedAdmin({
1031
1369
  username: config2.adminUsername,
1032
1370
  email: config2.adminEmail,
@@ -1064,21 +1402,35 @@ function buildRelayServer(config2, options = {}) {
1064
1402
  });
1065
1403
  app2.post("/relay/auth/register", async (request, reply) => {
1066
1404
  const body = registerSchema.parse(request.body ?? {});
1067
- if (config2.registrationPassword && body.registrationPassword !== config2.registrationPassword) {
1405
+ const settings = store.registrationSettings();
1406
+ if (settings.registrationPassword && body.registrationPassword !== settings.registrationPassword) {
1068
1407
  reply.status(403).send({
1069
1408
  code: "forbidden",
1070
1409
  message: "Invalid registration password."
1071
1410
  });
1072
1411
  return;
1073
1412
  }
1074
- const { registrationPassword: _registrationPassword, ...registerInput } = body;
1413
+ const registerInput = {
1414
+ email: body.email,
1415
+ username: body.username,
1416
+ password: body.password
1417
+ };
1418
+ if (settings.approvalRequired) {
1419
+ reply.status(202);
1420
+ return {
1421
+ pendingApproval: true,
1422
+ request: store.requestRegistrationApproval(registerInput)
1423
+ };
1424
+ }
1075
1425
  const result = store.register(registerInput);
1426
+ store.recordUserSeen(result.session.user.id);
1076
1427
  attachRelayCookie(reply, result.token);
1077
1428
  return result;
1078
1429
  });
1079
1430
  app2.post("/relay/auth/login", async (request, reply) => {
1080
1431
  const body = loginSchema.parse(request.body ?? {});
1081
1432
  const result = store.login(body);
1433
+ store.recordUserSeen(result.session.user.id);
1082
1434
  attachRelayCookie(reply, result.token);
1083
1435
  return result;
1084
1436
  });
@@ -1109,7 +1461,7 @@ function buildRelayServer(config2, options = {}) {
1109
1461
  if (!user) {
1110
1462
  return;
1111
1463
  }
1112
- return store.portalSummary(user.id, connectionStatus(state));
1464
+ return enrichPortalSummary(store.portalSummary(user.id, connectionStatus(state)), state);
1113
1465
  });
1114
1466
  app2.get("/relay/access", async (request, reply) => {
1115
1467
  const user = requireRelayUser(request, reply, store);
@@ -1186,15 +1538,27 @@ function buildRelayServer(config2, options = {}) {
1186
1538
  if (!user) {
1187
1539
  return;
1188
1540
  }
1189
- return store.adminSummary(connectionStatus(state));
1541
+ const query = adminQuerySchema.parse(request.query ?? {});
1542
+ const baseSummary = store.adminSummary(connectionStatus(state), {
1543
+ ...query.days !== void 0 ? { conversationWindowDays: query.days } : {}
1544
+ });
1545
+ return enrichAdminSummary(baseSummary, state, store, query.days);
1190
1546
  });
1191
1547
  app2.patch("/relay/admin/settings/registration", async (request, reply) => {
1192
1548
  const user = requireRelayUser(request, reply, store, { admin: true });
1193
1549
  if (!user) {
1194
1550
  return;
1195
1551
  }
1196
- const body = setEnabledSchema.parse(request.body ?? {});
1197
- return { registrationEnabled: store.setRegistrationEnabled(body.enabled) };
1552
+ const body = updateRegistrationSettingsSchema.parse(request.body ?? {});
1553
+ const settings = store.updateRegistrationSettings({
1554
+ ...body.enabled !== void 0 ? { enabled: body.enabled } : {},
1555
+ ...body.registrationPassword !== void 0 ? { registrationPassword: body.registrationPassword } : {},
1556
+ ...body.approvalRequired !== void 0 ? { approvalRequired: body.approvalRequired } : {}
1557
+ });
1558
+ return {
1559
+ registrationEnabled: settings.enabled,
1560
+ settings
1561
+ };
1198
1562
  });
1199
1563
  app2.patch("/relay/admin/users/:userId", async (request, reply) => {
1200
1564
  const user = requireRelayUser(request, reply, store, { admin: true });
@@ -1205,6 +1569,40 @@ function buildRelayServer(config2, options = {}) {
1205
1569
  const body = setEnabledSchema.parse(request.body ?? {});
1206
1570
  return store.setUserEnabled(userId, body.enabled);
1207
1571
  });
1572
+ app2.delete("/relay/admin/users/:userId", async (request, reply) => {
1573
+ const user = requireRelayUser(request, reply, store, { admin: true });
1574
+ if (!user) {
1575
+ return;
1576
+ }
1577
+ const { userId } = z.object({ userId: z.string().uuid() }).parse(request.params);
1578
+ store.deleteUser(userId);
1579
+ return { id: userId };
1580
+ });
1581
+ app2.post("/relay/admin/users/:userId/reset-password", async (request, reply) => {
1582
+ const user = requireRelayUser(request, reply, store, { admin: true });
1583
+ if (!user) {
1584
+ return;
1585
+ }
1586
+ const { userId } = z.object({ userId: z.string().uuid() }).parse(request.params);
1587
+ const body = adminResetPasswordSchema.parse(request.body ?? {});
1588
+ return store.adminResetUserPassword(userId, body.password);
1589
+ });
1590
+ app2.post("/relay/admin/registrations/:requestId/approve", async (request, reply) => {
1591
+ const user = requireRelayUser(request, reply, store, { admin: true });
1592
+ if (!user) {
1593
+ return;
1594
+ }
1595
+ const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
1596
+ return store.approvePendingRegistration(user.id, requestId);
1597
+ });
1598
+ app2.post("/relay/admin/registrations/:requestId/reject", async (request, reply) => {
1599
+ const user = requireRelayUser(request, reply, store, { admin: true });
1600
+ if (!user) {
1601
+ return;
1602
+ }
1603
+ const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
1604
+ return store.rejectPendingRegistration(user.id, requestId);
1605
+ });
1208
1606
  app2.all("/relay/devices/:deviceId/api/*", async (request, reply) => {
1209
1607
  const user = requireRelayUser(request, reply, store);
1210
1608
  if (!user) {
@@ -1297,7 +1695,8 @@ function buildRelayServer(config2, options = {}) {
1297
1695
  clientSockets: /* @__PURE__ */ new Map(),
1298
1696
  connected: true,
1299
1697
  connectedAt,
1300
- lastHeartbeatAt: connectedAt
1698
+ lastHeartbeatAt: connectedAt,
1699
+ ipAddress: relayClientIp(request)
1301
1700
  };
1302
1701
  state.supervisors.set(deviceId, connection);
1303
1702
  socket.send(
@@ -1451,16 +1850,40 @@ function applyWebViewCorsHeaders(reply, origin) {
1451
1850
  async function forwardRelayHttp(input) {
1452
1851
  const threadId = threadIdFromPath(input.targetPath);
1453
1852
  const workspaceId = workspaceIdFromPath(input.targetPath);
1853
+ const targetUrl = new URL(input.targetPath, "http://relay.local");
1854
+ const sharedThreadListRequest = input.request.method.toUpperCase() === "GET" && targetUrl.pathname === "/api/threads";
1855
+ const sharedRuntimeMetadataRequest = isAllowedSharedRuntimeMetadataRequest(
1856
+ input.request.method,
1857
+ targetUrl.pathname
1858
+ );
1454
1859
  const access = input.store.effectiveAccess(input.user.id, input.deviceId, {
1455
1860
  threadId,
1456
1861
  workspaceId
1457
1862
  });
1863
+ let allowSharedRuntimeMetadata = false;
1458
1864
  if (!access) {
1459
- input.reply.status(403).send({
1460
- code: "forbidden",
1461
- message: "Device access is not allowed."
1462
- });
1463
- return;
1865
+ const shares = sharedThreadListRequest || sharedRuntimeMetadataRequest ? input.store.sharedThreadsForDevice(input.user.id, input.deviceId) : [];
1866
+ if (sharedThreadListRequest) {
1867
+ if (shares.length > 0) {
1868
+ await forwardSharedThreadList({
1869
+ reply: input.reply,
1870
+ state: input.state,
1871
+ store: input.store,
1872
+ user: input.user,
1873
+ deviceId: input.deviceId,
1874
+ shares
1875
+ });
1876
+ return;
1877
+ }
1878
+ }
1879
+ allowSharedRuntimeMetadata = sharedRuntimeMetadataRequest && shares.length > 0;
1880
+ if (!allowSharedRuntimeMetadata) {
1881
+ input.reply.status(403).send({
1882
+ code: "forbidden",
1883
+ message: "Device access is not allowed."
1884
+ });
1885
+ return;
1886
+ }
1464
1887
  }
1465
1888
  if (!isAllowedRelayTarget(input.targetPath)) {
1466
1889
  input.reply.status(403).send({
@@ -1469,16 +1892,29 @@ async function forwardRelayHttp(input) {
1469
1892
  });
1470
1893
  return;
1471
1894
  }
1472
- if (!isAllowedForRelayAccess(access, input.request.method, input.targetPath)) {
1895
+ if (access && !isAllowedForRelayAccess(access, input.request.method, input.targetPath)) {
1473
1896
  input.reply.status(403).send({
1474
1897
  code: "forbidden",
1475
1898
  message: "This shared session does not allow that operation."
1476
1899
  });
1477
1900
  return;
1478
1901
  }
1479
- if (access.kind === "shared") {
1902
+ if (access?.kind === "shared") {
1480
1903
  input.store.recordShareAccess(access.share, input.user);
1481
1904
  }
1905
+ const conversationEvent = conversationEventFromRequest(
1906
+ input.request.method,
1907
+ input.targetPath,
1908
+ input.request.body
1909
+ );
1910
+ if (conversationEvent) {
1911
+ input.store.recordConversationEvent({
1912
+ userId: input.user.id,
1913
+ deviceId: input.deviceId,
1914
+ threadId: conversationEvent.threadId,
1915
+ workspaceId: conversationEvent.workspaceId
1916
+ });
1917
+ }
1482
1918
  const supervisor = input.state.supervisors.get(input.deviceId);
1483
1919
  if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
1484
1920
  input.reply.status(503).send({
@@ -1517,6 +1953,30 @@ async function forwardRelayHttp(input) {
1517
1953
  });
1518
1954
  }
1519
1955
  }
1956
+ async function forwardSharedThreadList(input) {
1957
+ const supervisor = input.state.supervisors.get(input.deviceId);
1958
+ if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
1959
+ input.reply.status(503).send({
1960
+ code: "service_unavailable",
1961
+ message: "No supervisor is connected for this device."
1962
+ });
1963
+ return;
1964
+ }
1965
+ const threads = await Promise.all(
1966
+ input.shares.map(async (share) => {
1967
+ input.store.recordShareAccess(share, input.user);
1968
+ const payload = await forwardSupervisorJson(
1969
+ supervisor,
1970
+ input.deviceId,
1971
+ `/api/threads/${encodeURIComponent(share.threadId)}?limit=1`,
1972
+ { timeoutMs: RELAY_REQUEST_TIMEOUT_MS }
1973
+ );
1974
+ const thread = isObject(payload) && isObject(payload.thread) ? payload.thread : payload;
1975
+ return isObject(thread) ? thread : null;
1976
+ })
1977
+ );
1978
+ input.reply.send(threads.filter((thread) => Boolean(thread)));
1979
+ }
1520
1980
  function relayResponseBody(response) {
1521
1981
  if (response.bodyEncoding === "base64") {
1522
1982
  return Buffer.from(response.body, "base64");
@@ -1653,6 +2113,14 @@ function requireRelayUser(request, reply, store, options = {}) {
1653
2113
  });
1654
2114
  return null;
1655
2115
  }
2116
+ if (!options.admin && session.user.role === "admin") {
2117
+ reply.status(403).send({
2118
+ code: "forbidden",
2119
+ message: "Use the relay admin panel for this account."
2120
+ });
2121
+ return null;
2122
+ }
2123
+ store.recordUserSeen(session.user.id);
1656
2124
  request.relayUser = session.user;
1657
2125
  return session.user;
1658
2126
  }
@@ -1677,11 +2145,199 @@ function connectionStatus(state) {
1677
2145
  statuses.set(deviceId, {
1678
2146
  connected: true,
1679
2147
  connectedAt: supervisor.connectedAt,
1680
- lastHeartbeatAt: supervisor.lastHeartbeatAt
2148
+ lastHeartbeatAt: supervisor.lastHeartbeatAt,
2149
+ ipAddress: supervisor.ipAddress ?? null
1681
2150
  });
1682
2151
  }
1683
2152
  return statuses;
1684
2153
  }
2154
+ async function enrichAdminSummary(summary, state, store, conversationWindowDays) {
2155
+ const workspacesByDeviceId = /* @__PURE__ */ new Map();
2156
+ const threadsByDeviceId = /* @__PURE__ */ new Map();
2157
+ await Promise.all(
2158
+ summary.devices.map(async (device) => {
2159
+ const supervisor = state.supervisors.get(device.id);
2160
+ if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
2161
+ return;
2162
+ }
2163
+ const [workspaces, threads] = await Promise.all([
2164
+ fetchRelayWorkspaces(supervisor, device.id),
2165
+ fetchRelayThreads(supervisor, device.id)
2166
+ ]);
2167
+ workspacesByDeviceId.set(device.id, workspaces);
2168
+ const workspaceLabelById = new Map(workspaces.map((workspace) => [workspace.id, workspace.label]));
2169
+ threadsByDeviceId.set(
2170
+ device.id,
2171
+ threads.map((thread) => ({
2172
+ ...thread,
2173
+ workspaceLabel: thread.workspaceId ? workspaceLabelById.get(thread.workspaceId) ?? thread.workspaceLabel : null
2174
+ }))
2175
+ );
2176
+ })
2177
+ );
2178
+ const enriched = store.adminSummary(connectionStatus(state), {
2179
+ metadata: {
2180
+ workspacesByDeviceId,
2181
+ threadsByDeviceId
2182
+ },
2183
+ ...conversationWindowDays !== void 0 ? { conversationWindowDays } : {}
2184
+ });
2185
+ return {
2186
+ ...enriched,
2187
+ shares: enriched.shares.map((share) => {
2188
+ const thread = threadsByDeviceId.get(share.deviceId)?.find((item) => item.id === share.threadId);
2189
+ const workspace = share.workspaceId ? workspacesByDeviceId.get(share.deviceId)?.find((item) => item.id === share.workspaceId) : null;
2190
+ return {
2191
+ ...share,
2192
+ threadTitle: thread?.title ?? share.threadTitle,
2193
+ workspaceLabel: workspace?.label ?? thread?.workspaceLabel ?? share.workspaceLabel
2194
+ };
2195
+ })
2196
+ };
2197
+ }
2198
+ async function fetchRelayWorkspaces(supervisor, deviceId) {
2199
+ const payload = await forwardSupervisorJson(supervisor, deviceId, "/api/workspaces");
2200
+ const rows = Array.isArray(payload) ? payload : [];
2201
+ const workspaces = [];
2202
+ for (const workspace of rows.filter(isObject)) {
2203
+ const id = stringField(workspace, "id");
2204
+ const label = stringField(workspace, "label");
2205
+ if (!id || !label) {
2206
+ continue;
2207
+ }
2208
+ workspaces.push({
2209
+ id,
2210
+ label,
2211
+ absPath: stringField(workspace, "absPath")
2212
+ });
2213
+ }
2214
+ return workspaces.slice(0, 50);
2215
+ }
2216
+ async function fetchRelayThreads(supervisor, deviceId) {
2217
+ const payload = await forwardSupervisorJson(supervisor, deviceId, "/api/threads");
2218
+ const rows = Array.isArray(payload) ? payload : [];
2219
+ const threads = [];
2220
+ for (const thread of rows.filter(isObject)) {
2221
+ const id = stringField(thread, "id");
2222
+ if (!id) {
2223
+ continue;
2224
+ }
2225
+ threads.push({
2226
+ id,
2227
+ title: stringField(thread, "title") ?? "Untitled thread",
2228
+ workspaceId: stringField(thread, "workspaceId"),
2229
+ workspaceLabel: null,
2230
+ status: stringField(thread, "status"),
2231
+ updatedAt: stringField(thread, "updatedAt") ?? stringField(thread, "createdAt")
2232
+ });
2233
+ }
2234
+ return threads.slice(0, 80);
2235
+ }
2236
+ async function enrichPortalSummary(portal, state) {
2237
+ const threadCache = /* @__PURE__ */ new Map();
2238
+ const workspaceCache = /* @__PURE__ */ new Map();
2239
+ const enrichShare = async (share) => {
2240
+ const supervisor = state.supervisors.get(share.deviceId);
2241
+ if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
2242
+ return share;
2243
+ }
2244
+ const threadCacheKey = `${share.deviceId}:${share.threadId}`;
2245
+ let threadTitlePromise = threadCache.get(threadCacheKey);
2246
+ if (!threadTitlePromise) {
2247
+ threadTitlePromise = fetchRelayThreadTitle(supervisor, share.deviceId, share.threadId);
2248
+ threadCache.set(threadCacheKey, threadTitlePromise);
2249
+ }
2250
+ let workspaceLabelPromise = Promise.resolve(null);
2251
+ if (share.workspaceId) {
2252
+ const workspaceCacheKey = `${share.deviceId}:${share.workspaceId}`;
2253
+ const cached = workspaceCache.get(workspaceCacheKey);
2254
+ if (cached) {
2255
+ workspaceLabelPromise = cached;
2256
+ } else {
2257
+ workspaceLabelPromise = fetchRelayWorkspaceLabel(
2258
+ supervisor,
2259
+ share.deviceId,
2260
+ share.workspaceId
2261
+ );
2262
+ workspaceCache.set(workspaceCacheKey, workspaceLabelPromise);
2263
+ }
2264
+ }
2265
+ const [threadTitle, workspaceLabel] = await Promise.all([
2266
+ threadTitlePromise,
2267
+ workspaceLabelPromise
2268
+ ]);
2269
+ return {
2270
+ ...share,
2271
+ threadTitle,
2272
+ workspaceLabel
2273
+ };
2274
+ };
2275
+ const [sharedWithMe, sharedByMe] = await Promise.all([
2276
+ Promise.all(portal.sharedWithMe.map(enrichShare)),
2277
+ Promise.all(portal.sharedByMe.map(enrichShare))
2278
+ ]);
2279
+ return {
2280
+ ...portal,
2281
+ sharedWithMe,
2282
+ sharedByMe
2283
+ };
2284
+ }
2285
+ async function fetchRelayThreadTitle(supervisor, deviceId, threadId) {
2286
+ const payload = await forwardSupervisorJson(
2287
+ supervisor,
2288
+ deviceId,
2289
+ `/api/threads/${encodeURIComponent(threadId)}?limit=1`
2290
+ );
2291
+ const thread = isObject(payload) && isObject(payload.thread) ? payload.thread : payload;
2292
+ return stringField(thread, "title");
2293
+ }
2294
+ async function fetchRelayWorkspaceLabel(supervisor, deviceId, workspaceId) {
2295
+ const payload = await forwardSupervisorJson(supervisor, deviceId, `/api/workspaces/${encodeURIComponent(workspaceId)}`);
2296
+ return stringField(payload, "label");
2297
+ }
2298
+ async function forwardSupervisorJson(supervisor, deviceId, targetPath, options = {}) {
2299
+ try {
2300
+ const response = await supervisor.requestBroker.forward(
2301
+ supervisor.socket,
2302
+ {
2303
+ type: "relay.request",
2304
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2305
+ requestId: randomUUID(),
2306
+ deviceId,
2307
+ payload: {
2308
+ method: "GET",
2309
+ path: targetPath,
2310
+ headers: {},
2311
+ body: null
2312
+ }
2313
+ },
2314
+ { timeoutMs: options.timeoutMs ?? RELAY_PORTAL_METADATA_TIMEOUT_MS }
2315
+ );
2316
+ if (response.statusCode < 200 || response.statusCode >= 300) {
2317
+ return null;
2318
+ }
2319
+ const body = relayJsonBody(response);
2320
+ return JSON.parse(body);
2321
+ } catch {
2322
+ return null;
2323
+ }
2324
+ }
2325
+ function relayJsonBody(response) {
2326
+ if (response.bodyEncoding === "base64") {
2327
+ return Buffer.from(response.body, "base64").toString("utf8");
2328
+ }
2329
+ return response.body;
2330
+ }
2331
+ function isObject(value) {
2332
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2333
+ }
2334
+ function stringField(value, field) {
2335
+ if (!isObject(value)) {
2336
+ return null;
2337
+ }
2338
+ const fieldValue = value[field];
2339
+ return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue : null;
2340
+ }
1685
2341
  function relayAccessDto(access) {
1686
2342
  return {
1687
2343
  kind: access.kind,
@@ -1703,6 +2359,15 @@ function isAllowedRelayTarget(pathValue) {
1703
2359
  const pathname = new URL(pathValue, "http://relay.local").pathname;
1704
2360
  return pathname === "/healthz" || pathname.startsWith("/api/");
1705
2361
  }
2362
+ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
2363
+ if (method.toUpperCase() !== "GET") {
2364
+ return false;
2365
+ }
2366
+ if (pathname === "/api/agent-runtimes") {
2367
+ return true;
2368
+ }
2369
+ return /^\/api\/agent-runtimes\/[^/]+\/(?:status|models)$/.test(pathname);
2370
+ }
1706
2371
  function threadIdFromPath(pathValue) {
1707
2372
  const pathname = new URL(pathValue, "http://relay.local").pathname;
1708
2373
  const match = /^\/api\/threads\/([^/?#]+)/.exec(pathname);
@@ -1713,6 +2378,39 @@ function workspaceIdFromPath(pathValue) {
1713
2378
  const match = /^\/api\/workspaces\/([^/?#]+)/.exec(pathname);
1714
2379
  return match ? decodeURIComponent(match[1]) : null;
1715
2380
  }
2381
+ function conversationEventFromRequest(method, pathValue, body) {
2382
+ if (method.toUpperCase() !== "POST") {
2383
+ return null;
2384
+ }
2385
+ const pathname = new URL(pathValue, "http://relay.local").pathname;
2386
+ if (pathname === "/api/threads/start") {
2387
+ return {
2388
+ threadId: null,
2389
+ workspaceId: isObject(body) && typeof body.workspaceId === "string" ? body.workspaceId : null
2390
+ };
2391
+ }
2392
+ const promptMatch = /^\/api\/threads\/([^/?#]+)\/prompt$/.exec(pathname);
2393
+ if (promptMatch) {
2394
+ return {
2395
+ threadId: decodeURIComponent(promptMatch[1]),
2396
+ workspaceId: null
2397
+ };
2398
+ }
2399
+ return null;
2400
+ }
2401
+ function relayClientIp(request) {
2402
+ const forwarded = firstHeaderValue(request.headers["cf-connecting-ip"]) ?? firstHeaderValue(request.headers["x-real-ip"]) ?? firstHeaderValue(request.headers["x-forwarded-for"]);
2403
+ if (forwarded) {
2404
+ return forwarded.split(",")[0]?.trim() || forwarded;
2405
+ }
2406
+ return request.ip || null;
2407
+ }
2408
+ function firstHeaderValue(value) {
2409
+ if (Array.isArray(value)) {
2410
+ return value[0];
2411
+ }
2412
+ return value;
2413
+ }
1716
2414
  function isAllowedForRelayAccess(access, method, pathValue) {
1717
2415
  if (access.kind === "owner") {
1718
2416
  return true;
@@ -1751,16 +2449,36 @@ function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
1751
2449
  if (access.threadAccess !== "control") {
1752
2450
  return false;
1753
2451
  }
2452
+ const controlReadPatterns = [
2453
+ new RegExp(`^/api/threads/${escapedThreadId}/fork-turns$`)
2454
+ ];
2455
+ if (methodName === "GET") {
2456
+ return controlReadPatterns.some((pattern) => pattern.test(pathname));
2457
+ }
1754
2458
  const controlPatterns = [
1755
2459
  new RegExp(`^/api/threads/${escapedThreadId}/goal$`),
1756
2460
  new RegExp(`^/api/threads/${escapedThreadId}/resume$`),
1757
2461
  new RegExp(`^/api/threads/${escapedThreadId}/prompt$`),
1758
2462
  new RegExp(`^/api/threads/${escapedThreadId}/interrupt$`),
2463
+ new RegExp(`^/api/threads/${escapedThreadId}/compact$`),
2464
+ new RegExp(`^/api/threads/${escapedThreadId}/fork$`),
2465
+ new RegExp(`^/api/threads/${escapedThreadId}/hooks$`),
2466
+ new RegExp(`^/api/threads/${escapedThreadId}/hooks/trust$`),
2467
+ new RegExp(`^/api/threads/${escapedThreadId}/hooks/untrust$`),
1759
2468
  new RegExp(`^/api/threads/${escapedThreadId}/requests/[^/]+/respond$`)
1760
2469
  ];
1761
2470
  if (methodName === "PATCH") {
2471
+ return [
2472
+ new RegExp(`^/api/threads/${escapedThreadId}/goal$`),
2473
+ new RegExp(`^/api/threads/${escapedThreadId}/settings$`)
2474
+ ].some((pattern) => pattern.test(pathname));
2475
+ }
2476
+ if (methodName === "DELETE") {
1762
2477
  return new RegExp(`^/api/threads/${escapedThreadId}/goal$`).test(pathname);
1763
2478
  }
2479
+ if (methodName === "PUT") {
2480
+ return new RegExp(`^/api/threads/${escapedThreadId}/hooks$`).test(pathname);
2481
+ }
1764
2482
  if (methodName === "POST") {
1765
2483
  return controlPatterns.some((pattern) => pattern.test(pathname));
1766
2484
  }