remote-codex 0.11.23 → 0.11.24

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.
@@ -203,7 +203,7 @@ var RelayStore = class _RelayStore {
203
203
  ).get(ownerUserId, target.id, input.deviceId, input.threadId)
204
204
  );
205
205
  if (existing) {
206
- return this.updateShare(existing.id, {
206
+ return this.updateShareRecord(existing.id, {
207
207
  label: input.label?.trim() || null,
208
208
  workspaceId: input.workspaceId?.trim() || null,
209
209
  threadAccess: normalizeThreadAccess(input.threadAccess),
@@ -226,11 +226,31 @@ var RelayStore = class _RelayStore {
226
226
  workspaceAccess: normalizeWorkspaceAccess(input.workspaceAccess),
227
227
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
228
228
  revokedAt: null,
229
- expiresAt: normalizeExpiresAt(input.expiresAt)
229
+ expiresAt: normalizeExpiresAt(input.expiresAt),
230
+ lastAccessedAt: null,
231
+ lastAccessedByUsername: null,
232
+ accessEvents: []
230
233
  };
231
234
  this.insertShare(share);
232
235
  return share;
233
236
  }
237
+ updateShare(userId, shareId, input) {
238
+ const share = this.rowToShare(
239
+ this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ? AND revoked_at IS NULL").get(shareId, userId)
240
+ );
241
+ if (!share) {
242
+ throw new RelayStoreError(404, "not_found", "Share was not found.");
243
+ }
244
+ return this.publicShare(
245
+ this.updateShareRecord(shareId, {
246
+ label: input.label !== void 0 ? input.label?.trim() || null : share.label,
247
+ workspaceId: input.workspaceId !== void 0 ? input.workspaceId?.trim() || null : share.workspaceId,
248
+ threadAccess: input.threadAccess !== void 0 ? normalizeThreadAccess(input.threadAccess) : share.threadAccess,
249
+ workspaceAccess: input.workspaceAccess !== void 0 ? normalizeWorkspaceAccess(input.workspaceAccess) : share.workspaceAccess,
250
+ expiresAt: input.expiresAt !== void 0 ? normalizeExpiresAt(input.expiresAt) : share.expiresAt
251
+ })
252
+ );
253
+ }
234
254
  revokeShare(userId, shareId) {
235
255
  const share = this.rowToShare(
236
256
  this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ?").get(shareId, userId)
@@ -404,15 +424,33 @@ var RelayStore = class _RelayStore {
404
424
  createdAt: device.createdAt
405
425
  };
406
426
  }
427
+ recordShareAccess(share, user) {
428
+ if (share.revokedAt || share.expiresAt && share.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
429
+ return;
430
+ }
431
+ const accessedAt = (/* @__PURE__ */ new Date()).toISOString();
432
+ this.sqlite.prepare(
433
+ `
434
+ INSERT INTO relay_share_access_events (
435
+ id, share_id, user_id, username, accessed_at
436
+ ) VALUES (?, ?, ?, ?, ?)
437
+ `
438
+ ).run(crypto.randomUUID(), share.id, user.id, user.username, accessedAt);
439
+ }
407
440
  publicShare(share) {
408
441
  const owner = this.getUser(share.ownerUserId);
409
442
  const target = this.getUser(share.targetUserId);
410
443
  const device = this.getDevice(share.deviceId);
444
+ const accessEvents = this.getShareAccessEvents(share.id);
445
+ const lastAccess = accessEvents[0] ?? null;
411
446
  return {
412
447
  ...share,
413
448
  ownerUsername: share.ownerUsername ?? owner?.username ?? "unknown",
414
449
  targetUsername: share.targetUsername ?? target?.username ?? "unknown",
415
- deviceName: share.deviceName ?? device?.name ?? "Remote Codex device"
450
+ deviceName: share.deviceName ?? device?.name ?? "Remote Codex device",
451
+ lastAccessedAt: lastAccess?.accessedAt ?? null,
452
+ lastAccessedByUsername: lastAccess?.username ?? null,
453
+ accessEvents
416
454
  };
417
455
  }
418
456
  migrate() {
@@ -466,6 +504,16 @@ var RelayStore = class _RelayStore {
466
504
  CREATE INDEX IF NOT EXISTS relay_shares_owner_idx ON relay_shares(owner_user_id);
467
505
  CREATE INDEX IF NOT EXISTS relay_shares_target_idx ON relay_shares(target_user_id);
468
506
  CREATE INDEX IF NOT EXISTS relay_shares_device_thread_idx ON relay_shares(device_id, thread_id);
507
+
508
+ CREATE TABLE IF NOT EXISTS relay_share_access_events (
509
+ id TEXT PRIMARY KEY,
510
+ share_id TEXT NOT NULL REFERENCES relay_shares(id) ON DELETE CASCADE,
511
+ user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
512
+ username TEXT NOT NULL,
513
+ accessed_at TEXT NOT NULL
514
+ );
515
+
516
+ CREATE INDEX IF NOT EXISTS relay_share_access_events_share_idx ON relay_share_access_events(share_id, accessed_at DESC);
469
517
  `);
470
518
  this.ensureColumn("relay_devices", "token", "TEXT");
471
519
  this.ensureColumn("relay_shares", "workspace_id", "TEXT");
@@ -672,7 +720,23 @@ var RelayStore = class _RelayStore {
672
720
  share.expiresAt
673
721
  );
674
722
  }
675
- updateShare(shareId, input) {
723
+ getShareAccessEvents(shareId) {
724
+ return this.sqlite.prepare(
725
+ `
726
+ SELECT * FROM relay_share_access_events
727
+ WHERE share_id = ?
728
+ ORDER BY accessed_at DESC
729
+ LIMIT 8
730
+ `
731
+ ).all(shareId).map((row) => ({
732
+ id: row.id,
733
+ shareId: row.share_id,
734
+ userId: row.user_id,
735
+ username: row.username,
736
+ accessedAt: row.accessed_at
737
+ }));
738
+ }
739
+ updateShareRecord(shareId, input) {
676
740
  this.sqlite.prepare(
677
741
  `
678
742
  UPDATE relay_shares
@@ -768,7 +832,10 @@ var RelayStore = class _RelayStore {
768
832
  workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
769
833
  createdAt: row.created_at,
770
834
  revokedAt: row.revoked_at,
771
- expiresAt: row.expires_at ?? null
835
+ expiresAt: row.expires_at ?? null,
836
+ lastAccessedAt: null,
837
+ lastAccessedByUsername: null,
838
+ accessEvents: []
772
839
  };
773
840
  }
774
841
  };
@@ -854,6 +921,13 @@ var createShareSchema = z.object({
854
921
  message: "targetIdentifier is required.",
855
922
  path: ["targetIdentifier"]
856
923
  });
924
+ var updateShareSchema = z.object({
925
+ workspaceId: z.string().uuid().nullable().optional(),
926
+ label: z.string().trim().min(1).max(160).nullable().optional(),
927
+ threadAccess: threadAccessSchema.optional(),
928
+ workspaceAccess: workspaceAccessSchema.optional(),
929
+ expiresAt: z.string().datetime().nullable().optional()
930
+ });
857
931
  var setEnabledSchema = z.object({
858
932
  enabled: z.boolean()
859
933
  });
@@ -888,6 +962,58 @@ var WEBVIEW_CORS_ALLOW_METHODS = [
888
962
  "DELETE",
889
963
  "OPTIONS"
890
964
  ].join(", ");
965
+ var RELAY_REQUEST_HEADER_BLOCKLIST = /* @__PURE__ */ new Set([
966
+ "authorization",
967
+ "connection",
968
+ "content-length",
969
+ "cookie",
970
+ "expect",
971
+ "forwarded",
972
+ "host",
973
+ "keep-alive",
974
+ "origin",
975
+ "proxy-authenticate",
976
+ "proxy-authorization",
977
+ "proxy-connection",
978
+ "referer",
979
+ "referrer",
980
+ "set-cookie",
981
+ "te",
982
+ "trailer",
983
+ "transfer-encoding",
984
+ "upgrade",
985
+ "via",
986
+ "x-client-ip",
987
+ "x-forwarded-for",
988
+ "x-forwarded-host",
989
+ "x-forwarded-port",
990
+ "x-forwarded-proto",
991
+ "x-forwarded-protocol",
992
+ "x-forwarded-scheme",
993
+ "x-real-ip",
994
+ "x-remote-codex-relay-forwarded"
995
+ ]);
996
+ var RELAY_RESPONSE_HEADER_BLOCKLIST = /* @__PURE__ */ new Set([
997
+ "access-control-allow-credentials",
998
+ "access-control-allow-headers",
999
+ "access-control-allow-methods",
1000
+ "access-control-allow-origin",
1001
+ "access-control-expose-headers",
1002
+ "access-control-max-age",
1003
+ "access-control-request-headers",
1004
+ "access-control-request-method",
1005
+ "connection",
1006
+ "content-length",
1007
+ "keep-alive",
1008
+ "location",
1009
+ "proxy-authenticate",
1010
+ "refresh",
1011
+ "set-cookie",
1012
+ "te",
1013
+ "trailer",
1014
+ "transfer-encoding",
1015
+ "upgrade"
1016
+ ]);
891
1017
  function buildRelayServer(config2, options = {}) {
892
1018
  const app2 = Fastify({ logger: false });
893
1019
  app2.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
@@ -1038,6 +1164,15 @@ function buildRelayServer(config2, options = {}) {
1038
1164
  expiresAt: body.expiresAt ?? null
1039
1165
  });
1040
1166
  });
1167
+ app2.patch("/relay/shares/:shareId", async (request, reply) => {
1168
+ const user = requireRelayUser(request, reply, store);
1169
+ if (!user) {
1170
+ return;
1171
+ }
1172
+ const { shareId } = z.object({ shareId: z.string().uuid() }).parse(request.params);
1173
+ const body = updateShareSchema.parse(request.body ?? {});
1174
+ return store.updateShare(user.id, shareId, body);
1175
+ });
1041
1176
  app2.delete("/relay/shares/:shareId", async (request, reply) => {
1042
1177
  const user = requireRelayUser(request, reply, store);
1043
1178
  if (!user) {
@@ -1231,6 +1366,9 @@ function buildRelayServer(config2, options = {}) {
1231
1366
  socket.close(1013, "No supervisor is connected for this device.");
1232
1367
  return;
1233
1368
  }
1369
+ if (access.kind === "shared") {
1370
+ store.recordShareAccess(access.share, session.user);
1371
+ }
1234
1372
  connectRelayWebsocket(supervisor, socket, threadId, access);
1235
1373
  }
1236
1374
  });
@@ -1261,6 +1399,9 @@ function buildRelayServer(config2, options = {}) {
1261
1399
  socket.close(1008, "Device access is not allowed.");
1262
1400
  return;
1263
1401
  }
1402
+ if (access.kind === "shared") {
1403
+ store.recordShareAccess(access.share, session.user);
1404
+ }
1264
1405
  connectRelayWebsocket(supervisor, socket, threadId, access);
1265
1406
  }
1266
1407
  });
@@ -1335,6 +1476,9 @@ async function forwardRelayHttp(input) {
1335
1476
  });
1336
1477
  return;
1337
1478
  }
1479
+ if (access.kind === "shared") {
1480
+ input.store.recordShareAccess(access.share, input.user);
1481
+ }
1338
1482
  const supervisor = input.state.supervisors.get(input.deviceId);
1339
1483
  if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
1340
1484
  input.reply.status(503).send({
@@ -1678,7 +1822,7 @@ function relayRequestHeaders(headers) {
1678
1822
  const output = {};
1679
1823
  for (const [name, value] of Object.entries(headers)) {
1680
1824
  const lower = name.toLowerCase();
1681
- if (lower === "authorization" || lower === "content-length" || lower === "transfer-encoding") {
1825
+ if (RELAY_REQUEST_HEADER_BLOCKLIST.has(lower) || lower.startsWith("x-forwarded-")) {
1682
1826
  continue;
1683
1827
  }
1684
1828
  if (Array.isArray(value)) {
@@ -1691,7 +1835,7 @@ function relayRequestHeaders(headers) {
1691
1835
  }
1692
1836
  function canForwardResponseHeader(name) {
1693
1837
  const lower = name.toLowerCase();
1694
- return lower !== "content-length" && lower !== "transfer-encoding";
1838
+ return !RELAY_RESPONSE_HEADER_BLOCKLIST.has(lower);
1695
1839
  }
1696
1840
  function bearerToken(value) {
1697
1841
  const match = /^Bearer\s+(.+)$/i.exec(value ?? "");
@@ -27428,6 +27428,7 @@ function constantTimeEqual(left, right) {
27428
27428
 
27429
27429
  // src/relay-tunnel-client.ts
27430
27430
  var RELAY_HEARTBEAT_INTERVAL_MS = 3e4;
27431
+ var RELAY_CONNECT_TIMEOUT_MS = 15e3;
27431
27432
  var RELAY_RECONNECT_INITIAL_DELAY_MS = 1e3;
27432
27433
  var RELAY_RECONNECT_MAX_DELAY_MS = 3e4;
27433
27434
  var RelayTunnelClient = class {
@@ -27443,6 +27444,7 @@ var RelayTunnelClient = class {
27443
27444
  handleClientMessage;
27444
27445
  socket = null;
27445
27446
  heartbeatHandle = null;
27447
+ connectTimeoutHandle = null;
27446
27448
  reconnectHandle = null;
27447
27449
  reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27448
27450
  stopped = false;
@@ -27464,42 +27466,50 @@ var RelayTunnelClient = class {
27464
27466
  const url = new URL("/supervisor/tunnel", this.config.serverUrl ?? void 0);
27465
27467
  url.searchParams.set("token", this.config.agentToken ?? "");
27466
27468
  url.searchParams.set("deviceToken", this.config.agentToken ?? "");
27467
- this.socket = new WebSocket(url);
27468
- this.socket.addEventListener("open", () => {
27469
+ const socket = new WebSocket(url);
27470
+ this.socket = socket;
27471
+ this.connectTimeoutHandle = setTimeout(() => {
27472
+ if (this.socket === socket && socket.readyState !== WebSocket.OPEN) {
27473
+ this.closeAndReconnect(socket);
27474
+ }
27475
+ }, RELAY_CONNECT_TIMEOUT_MS);
27476
+ socket.addEventListener("open", () => {
27477
+ this.clearConnectTimeout();
27469
27478
  this.reconnectDelayMs = RELAY_RECONNECT_INITIAL_DELAY_MS;
27470
27479
  this.sendHeartbeat();
27480
+ this.clearHeartbeat();
27471
27481
  this.heartbeatHandle = setInterval(() => {
27472
27482
  this.sendHeartbeat();
27473
27483
  }, RELAY_HEARTBEAT_INTERVAL_MS);
27474
27484
  });
27475
- this.socket.addEventListener("close", () => {
27476
- this.clearHeartbeat();
27477
- this.cleanupRelayClients();
27478
- this.socket = null;
27479
- this.scheduleReconnect();
27485
+ socket.addEventListener("close", () => {
27486
+ this.closeAndReconnect(socket);
27487
+ });
27488
+ socket.addEventListener("error", () => {
27489
+ this.closeAndReconnect(socket);
27480
27490
  });
27481
- this.socket.addEventListener("message", (event) => {
27491
+ socket.addEventListener("message", (event) => {
27482
27492
  void this.handleMessage(String(event.data));
27483
27493
  });
27484
27494
  }
27485
27495
  stop() {
27486
27496
  this.stopped = true;
27487
27497
  this.clearHeartbeat();
27498
+ this.clearConnectTimeout();
27488
27499
  this.clearReconnect();
27489
27500
  this.cleanupRelayClients();
27490
27501
  this.socket?.close();
27491
27502
  this.socket = null;
27492
27503
  }
27493
27504
  sendHeartbeat() {
27494
- if (this.socket?.readyState !== WebSocket.OPEN) {
27505
+ const socket = this.socket;
27506
+ if (socket?.readyState !== WebSocket.OPEN) {
27495
27507
  return;
27496
27508
  }
27497
- this.socket.send(
27498
- JSON.stringify({
27499
- type: "relay.heartbeat",
27500
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
27501
- })
27502
- );
27509
+ this.sendEnvelope(socket, {
27510
+ type: "relay.heartbeat",
27511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
27512
+ });
27503
27513
  }
27504
27514
  async handleMessage(rawMessage) {
27505
27515
  let parsed;
@@ -27530,27 +27540,52 @@ var RelayTunnelClient = class {
27530
27540
  return;
27531
27541
  }
27532
27542
  const response = await this.handleRequest(parsed.payload);
27533
- this.socket?.send(
27534
- JSON.stringify({
27543
+ const socket = this.socket;
27544
+ if (socket?.readyState !== WebSocket.OPEN) {
27545
+ return;
27546
+ }
27547
+ this.sendEnvelope(
27548
+ socket,
27549
+ {
27535
27550
  type: "relay.response",
27536
27551
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27537
27552
  requestId: parsed.requestId,
27538
27553
  payload: response
27539
- })
27554
+ }
27540
27555
  );
27541
27556
  }
27542
27557
  sendClientMessage(clientId, message) {
27543
- if (this.socket?.readyState !== WebSocket.OPEN) {
27558
+ const socket = this.socket;
27559
+ if (socket?.readyState !== WebSocket.OPEN) {
27544
27560
  return;
27545
27561
  }
27546
- this.socket.send(
27547
- JSON.stringify({
27548
- type: "relay.server.message",
27549
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27550
- clientId,
27551
- payload: message
27552
- })
27553
- );
27562
+ this.sendEnvelope(socket, {
27563
+ type: "relay.server.message",
27564
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
27565
+ clientId,
27566
+ payload: message
27567
+ });
27568
+ }
27569
+ sendEnvelope(socket, message) {
27570
+ try {
27571
+ socket.send(JSON.stringify(message));
27572
+ } catch {
27573
+ this.closeAndReconnect(socket);
27574
+ }
27575
+ }
27576
+ closeAndReconnect(socket) {
27577
+ if (this.socket !== socket) {
27578
+ return;
27579
+ }
27580
+ this.clearHeartbeat();
27581
+ this.clearConnectTimeout();
27582
+ this.cleanupRelayClients();
27583
+ this.socket = null;
27584
+ try {
27585
+ socket.close();
27586
+ } catch {
27587
+ }
27588
+ this.scheduleReconnect();
27554
27589
  }
27555
27590
  clearHeartbeat() {
27556
27591
  if (this.heartbeatHandle) {
@@ -27558,6 +27593,12 @@ var RelayTunnelClient = class {
27558
27593
  this.heartbeatHandle = null;
27559
27594
  }
27560
27595
  }
27596
+ clearConnectTimeout() {
27597
+ if (this.connectTimeoutHandle) {
27598
+ clearTimeout(this.connectTimeoutHandle);
27599
+ this.connectTimeoutHandle = null;
27600
+ }
27601
+ }
27561
27602
  scheduleReconnect() {
27562
27603
  if (this.stopped || this.reconnectHandle) {
27563
27604
  return;