remote-codex 0.11.4 → 0.11.6

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.
@@ -6,7 +6,7 @@ import { ZodError } from "zod";
6
6
  import websocket from "@fastify/websocket";
7
7
  import Fastify from "fastify";
8
8
  import fs2 from "fs";
9
- import fsp2 from "fs/promises";
9
+ import fsp from "fs/promises";
10
10
  import path2 from "path";
11
11
  import { randomUUID } from "crypto";
12
12
  import { z } from "zod";
@@ -61,43 +61,49 @@ var RelayRequestBroker = class {
61
61
  // src/relay-store.ts
62
62
  import crypto from "crypto";
63
63
  import fs from "fs";
64
- import fsp from "fs/promises";
65
64
  import path from "path";
65
+ import Database from "better-sqlite3";
66
66
  var SESSION_TTL_MS = 1e3 * 60 * 60 * 24 * 14;
67
67
  var RelayStore = class _RelayStore {
68
- constructor(filePath, sessionSecret, registrationEnabled) {
69
- this.filePath = filePath;
68
+ constructor(databasePath, sessionSecret, registrationEnabled, legacyJsonPath) {
69
+ this.databasePath = databasePath;
70
70
  this.sessionSecret = sessionSecret;
71
- this.data = this.readData(registrationEnabled);
72
- }
73
- filePath;
71
+ fs.mkdirSync(path.dirname(this.databasePath), { recursive: true });
72
+ this.sqlite = new Database(this.databasePath);
73
+ this.sqlite.pragma("journal_mode = WAL");
74
+ this.sqlite.pragma("foreign_keys = ON");
75
+ this.migrate();
76
+ this.importLegacyJson(legacyJsonPath);
77
+ this.ensureRegistrationSetting(registrationEnabled);
78
+ }
79
+ databasePath;
74
80
  sessionSecret;
75
- data;
81
+ sqlite;
76
82
  static fromDataDir(dataDir, sessionSecret, registrationEnabled) {
83
+ const resolvedDataDir = path.resolve(dataDir);
77
84
  return new _RelayStore(
78
- path.join(path.resolve(dataDir), "relay-store.json"),
85
+ path.join(resolvedDataDir, "relay-store.sqlite"),
79
86
  sessionSecret,
80
- registrationEnabled
87
+ registrationEnabled,
88
+ path.join(resolvedDataDir, "relay-store.json")
81
89
  );
82
90
  }
83
91
  seedAdmin(input) {
84
- const username = normalizeUsername(input.username);
85
- const existing = this.data.users.find((user2) => user2.role === "admin");
92
+ const existing = this.getUsers().find((user2) => user2.role === "admin");
86
93
  if (existing) {
87
94
  return this.publicUser(existing);
88
95
  }
89
96
  const user = this.createStoredUser({
90
- email: input.email ?? `${username}@relay.local`,
91
- username,
97
+ email: input.email ?? `${normalizeUsername(input.username)}@relay.local`,
98
+ username: input.username,
92
99
  password: input.password,
93
100
  role: "admin"
94
101
  });
95
- this.data.users.push(user);
96
- void this.persist();
102
+ this.insertUser(user);
97
103
  return this.publicUser(user);
98
104
  }
99
105
  register(input) {
100
- if (!this.data.registrationEnabled) {
106
+ if (!this.registrationEnabled()) {
101
107
  throw new RelayStoreError(403, "forbidden", "Registration is currently disabled.");
102
108
  }
103
109
  const user = this.createStoredUser({
@@ -106,15 +112,12 @@ var RelayStore = class _RelayStore {
106
112
  password: input.password,
107
113
  role: "user"
108
114
  });
109
- this.data.users.push(user);
110
- void this.persist();
115
+ this.insertUser(user);
111
116
  return this.createLoginResult(user);
112
117
  }
113
118
  login(input) {
114
119
  const normalizedIdentifier = input.identifier.trim().toLowerCase();
115
- const user = this.data.users.find(
116
- (entry) => entry.email.toLowerCase() === normalizedIdentifier || entry.username.toLowerCase() === normalizedIdentifier
117
- );
120
+ const user = this.getUserByIdentifier(normalizedIdentifier);
118
121
  if (!user || !user.enabled) {
119
122
  throw new RelayStoreError(401, "unauthorized", "Invalid username or password.");
120
123
  }
@@ -131,14 +134,14 @@ var RelayStore = class _RelayStore {
131
134
  if (!payload) {
132
135
  return this.emptySession();
133
136
  }
134
- const user = this.data.users.find((entry) => entry.id === payload.userId);
137
+ const user = this.getUser(payload.userId);
135
138
  if (!user || !user.enabled) {
136
139
  return this.emptySession();
137
140
  }
138
141
  return {
139
142
  authenticated: true,
140
143
  user: this.publicUser(user),
141
- registrationEnabled: this.data.registrationEnabled
144
+ registrationEnabled: this.registrationEnabled()
142
145
  };
143
146
  }
144
147
  createDevice(ownerUserId, input) {
@@ -152,50 +155,51 @@ var RelayStore = class _RelayStore {
152
155
  tokenPreview: previewToken(token),
153
156
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
154
157
  };
155
- this.data.devices.push(device);
156
- void this.persist();
158
+ this.insertDevice(device);
157
159
  return {
158
160
  device: this.publicDevice(device, null),
159
161
  token
160
162
  };
161
163
  }
162
164
  deleteDevice(userId, deviceId) {
163
- const index = this.data.devices.findIndex(
164
- (device) => device.id === deviceId && device.ownerUserId === userId
165
- );
166
- if (index < 0) {
165
+ const result = this.sqlite.prepare("DELETE FROM relay_devices WHERE id = ? AND owner_user_id = ?").run(deviceId, userId);
166
+ if (result.changes < 1) {
167
167
  throw new RelayStoreError(404, "not_found", "Device was not found.");
168
168
  }
169
- this.data.devices.splice(index, 1);
170
- this.data.shares = this.data.shares.filter((share) => share.deviceId !== deviceId);
171
- void this.persist();
172
169
  }
173
170
  verifyDeviceToken(token) {
174
171
  if (!token) {
175
172
  return null;
176
173
  }
177
174
  const tokenHash = sha256(token);
178
- return this.data.devices.find((device) => device.tokenHash === tokenHash) ?? null;
175
+ return this.rowToDevice(
176
+ this.sqlite.prepare("SELECT * FROM relay_devices WHERE token_hash = ?").get(tokenHash)
177
+ );
179
178
  }
180
179
  createShare(ownerUserId, input) {
181
180
  const owner = this.requireUser(ownerUserId);
182
- const device = this.data.devices.find(
183
- (entry) => entry.id === input.deviceId && entry.ownerUserId === ownerUserId
184
- );
185
- if (!device) {
181
+ const device = this.getDevice(input.deviceId);
182
+ if (!device || device.ownerUserId !== ownerUserId) {
186
183
  throw new RelayStoreError(404, "not_found", "Device was not found.");
187
184
  }
188
- const target = this.data.users.find(
189
- (entry) => entry.username.toLowerCase() === input.targetUsername.trim().toLowerCase()
190
- );
185
+ const target = this.getUserByUsername(input.targetUsername);
191
186
  if (!target || !target.enabled) {
192
187
  throw new RelayStoreError(404, "not_found", "Target user was not found.");
193
188
  }
194
189
  if (target.id === ownerUserId) {
195
190
  throw new RelayStoreError(400, "bad_request", "You cannot share a session with yourself.");
196
191
  }
197
- const existing = this.data.shares.find(
198
- (share2) => share2.ownerUserId === ownerUserId && share2.targetUserId === target.id && share2.deviceId === input.deviceId && share2.threadId === input.threadId && !share2.revokedAt
192
+ const existing = this.rowToShare(
193
+ this.sqlite.prepare(
194
+ `
195
+ SELECT * FROM relay_shares
196
+ WHERE owner_user_id = ?
197
+ AND target_user_id = ?
198
+ AND device_id = ?
199
+ AND thread_id = ?
200
+ AND revoked_at IS NULL
201
+ `
202
+ ).get(ownerUserId, target.id, input.deviceId, input.threadId)
199
203
  );
200
204
  if (existing) {
201
205
  return existing;
@@ -213,60 +217,63 @@ var RelayStore = class _RelayStore {
213
217
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
214
218
  revokedAt: null
215
219
  };
216
- this.data.shares.push(share);
217
- void this.persist();
220
+ this.insertShare(share);
218
221
  return share;
219
222
  }
220
223
  revokeShare(userId, shareId) {
221
- const share = this.data.shares.find(
222
- (entry) => entry.id === shareId && entry.ownerUserId === userId
224
+ const share = this.rowToShare(
225
+ this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ?").get(shareId, userId)
223
226
  );
224
227
  if (!share) {
225
228
  throw new RelayStoreError(404, "not_found", "Share was not found.");
226
229
  }
227
- share.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
228
- void this.persist();
229
- return share;
230
+ const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
231
+ this.sqlite.prepare("UPDATE relay_shares SET revoked_at = ? WHERE id = ?").run(revokedAt, shareId);
232
+ return { ...share, revokedAt };
230
233
  }
231
234
  canAccessDevice(userId, deviceId, threadId) {
232
- const owned = this.data.devices.some(
233
- (device) => device.id === deviceId && device.ownerUserId === userId
234
- );
235
+ const owned = this.sqlite.prepare("SELECT 1 FROM relay_devices WHERE id = ? AND owner_user_id = ?").get(deviceId, userId);
235
236
  if (owned) {
236
237
  return true;
237
238
  }
238
239
  if (!threadId) {
239
240
  return false;
240
241
  }
241
- return this.data.shares.some(
242
- (share) => share.targetUserId === userId && share.deviceId === deviceId && !share.revokedAt && share.threadId === threadId
242
+ return Boolean(
243
+ this.sqlite.prepare(
244
+ `
245
+ SELECT 1 FROM relay_shares
246
+ WHERE target_user_id = ?
247
+ AND device_id = ?
248
+ AND thread_id = ?
249
+ AND revoked_at IS NULL
250
+ `
251
+ ).get(userId, deviceId, threadId)
243
252
  );
244
253
  }
245
254
  portalSummary(userId, connectedDevices) {
246
255
  const user = this.requireUser(userId);
256
+ const devices = this.getDevicesByOwner(userId);
257
+ const sharedWithMe = this.getSharesByTarget(userId);
258
+ const sharedByMe = this.getSharesByOwner(userId);
247
259
  return {
248
260
  user: this.publicUser(user),
249
- devices: this.data.devices.filter((device) => device.ownerUserId === userId).map((device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)),
250
- sharedWithMe: this.data.shares.filter(
251
- (share) => share.targetUserId === userId && !share.revokedAt
252
- ).map((share) => this.publicShare(share)),
253
- sharedByMe: this.data.shares.filter(
254
- (share) => share.ownerUserId === userId && !share.revokedAt
255
- ).map((share) => this.publicShare(share))
261
+ devices: devices.map((device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)),
262
+ sharedWithMe: sharedWithMe.map((share) => this.publicShare(share)),
263
+ sharedByMe: sharedByMe.map((share) => this.publicShare(share))
256
264
  };
257
265
  }
258
266
  adminSummary(connectedDevices) {
259
267
  return {
260
- users: this.data.users.map((user) => this.publicUser(user)),
261
- devices: this.data.devices.map(
268
+ users: this.getUsers().map((user) => this.publicUser(user)),
269
+ devices: this.getDevices().map(
262
270
  (device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)
263
271
  ),
264
- registrationEnabled: this.data.registrationEnabled
272
+ registrationEnabled: this.registrationEnabled()
265
273
  };
266
274
  }
267
275
  setRegistrationEnabled(enabled) {
268
- this.data.registrationEnabled = enabled;
269
- void this.persist();
276
+ this.setSetting("registrationEnabled", enabled ? "true" : "false");
270
277
  return enabled;
271
278
  }
272
279
  setUserEnabled(userId, enabled) {
@@ -274,15 +281,14 @@ var RelayStore = class _RelayStore {
274
281
  if (user.role === "admin" && !enabled) {
275
282
  throw new RelayStoreError(400, "bad_request", "The admin user cannot be disabled.");
276
283
  }
277
- user.enabled = enabled;
278
- void this.persist();
279
- return this.publicUser(user);
284
+ this.sqlite.prepare("UPDATE relay_users SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, userId);
285
+ return this.publicUser({ ...user, enabled });
280
286
  }
281
287
  emptySession() {
282
288
  return {
283
289
  authenticated: false,
284
290
  user: null,
285
- registrationEnabled: this.data.registrationEnabled
291
+ registrationEnabled: this.registrationEnabled()
286
292
  };
287
293
  }
288
294
  publicDevice(device, status) {
@@ -298,9 +304,9 @@ var RelayStore = class _RelayStore {
298
304
  };
299
305
  }
300
306
  publicShare(share) {
301
- const owner = this.data.users.find((user) => user.id === share.ownerUserId);
302
- const target = this.data.users.find((user) => user.id === share.targetUserId);
303
- const device = this.data.devices.find((entry) => entry.id === share.deviceId);
307
+ const owner = this.getUser(share.ownerUserId);
308
+ const target = this.getUser(share.targetUserId);
309
+ const device = this.getDevice(share.deviceId);
304
310
  return {
305
311
  ...share,
306
312
  ownerUsername: share.ownerUsername ?? owner?.username ?? "unknown",
@@ -308,6 +314,100 @@ var RelayStore = class _RelayStore {
308
314
  deviceName: share.deviceName ?? device?.name ?? "Remote Codex device"
309
315
  };
310
316
  }
317
+ migrate() {
318
+ this.sqlite.exec(`
319
+ CREATE TABLE IF NOT EXISTS relay_settings (
320
+ key TEXT PRIMARY KEY,
321
+ value TEXT NOT NULL
322
+ );
323
+
324
+ CREATE TABLE IF NOT EXISTS relay_users (
325
+ id TEXT PRIMARY KEY,
326
+ email TEXT NOT NULL UNIQUE,
327
+ username TEXT NOT NULL UNIQUE,
328
+ role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
329
+ enabled INTEGER NOT NULL DEFAULT 1,
330
+ created_at TEXT NOT NULL,
331
+ password_salt TEXT NOT NULL,
332
+ password_hash TEXT NOT NULL
333
+ );
334
+
335
+ CREATE TABLE IF NOT EXISTS relay_devices (
336
+ id TEXT PRIMARY KEY,
337
+ owner_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
338
+ name TEXT NOT NULL,
339
+ token_hash TEXT NOT NULL UNIQUE,
340
+ token_preview TEXT NOT NULL,
341
+ created_at TEXT NOT NULL
342
+ );
343
+
344
+ CREATE INDEX IF NOT EXISTS relay_devices_owner_idx ON relay_devices(owner_user_id);
345
+
346
+ CREATE TABLE IF NOT EXISTS relay_shares (
347
+ id TEXT PRIMARY KEY,
348
+ owner_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
349
+ owner_username TEXT,
350
+ target_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
351
+ target_username TEXT,
352
+ device_id TEXT NOT NULL REFERENCES relay_devices(id) ON DELETE CASCADE,
353
+ device_name TEXT,
354
+ thread_id TEXT NOT NULL,
355
+ label TEXT,
356
+ created_at TEXT NOT NULL,
357
+ revoked_at TEXT
358
+ );
359
+
360
+ CREATE INDEX IF NOT EXISTS relay_shares_owner_idx ON relay_shares(owner_user_id);
361
+ CREATE INDEX IF NOT EXISTS relay_shares_target_idx ON relay_shares(target_user_id);
362
+ CREATE INDEX IF NOT EXISTS relay_shares_device_thread_idx ON relay_shares(device_id, thread_id);
363
+ `);
364
+ }
365
+ importLegacyJson(legacyJsonPath) {
366
+ if (!legacyJsonPath || !fs.existsSync(legacyJsonPath)) {
367
+ return;
368
+ }
369
+ const existingUsers = this.sqlite.prepare("SELECT COUNT(*) AS count FROM relay_users").get();
370
+ const imported = this.getSetting("legacyJsonImported");
371
+ if (existingUsers.count > 0 || imported === legacyJsonPath) {
372
+ return;
373
+ }
374
+ const parsed = JSON.parse(fs.readFileSync(legacyJsonPath, "utf8"));
375
+ const data = {
376
+ registrationEnabled: typeof parsed.registrationEnabled === "boolean" ? parsed.registrationEnabled : true,
377
+ users: Array.isArray(parsed.users) ? parsed.users : [],
378
+ devices: Array.isArray(parsed.devices) ? parsed.devices : [],
379
+ shares: Array.isArray(parsed.shares) ? parsed.shares : []
380
+ };
381
+ const importData = this.sqlite.transaction(() => {
382
+ this.setSetting("registrationEnabled", data.registrationEnabled ? "true" : "false");
383
+ for (const user of data.users) {
384
+ this.insertUser(user);
385
+ }
386
+ for (const device of data.devices) {
387
+ this.insertDevice(device);
388
+ }
389
+ for (const share of data.shares) {
390
+ this.insertShare(share);
391
+ }
392
+ this.setSetting("legacyJsonImported", legacyJsonPath);
393
+ });
394
+ importData();
395
+ }
396
+ ensureRegistrationSetting(registrationEnabled) {
397
+ if (this.getSetting("registrationEnabled") === null) {
398
+ this.setSetting("registrationEnabled", registrationEnabled ? "true" : "false");
399
+ }
400
+ }
401
+ registrationEnabled() {
402
+ return this.getSetting("registrationEnabled") !== "false";
403
+ }
404
+ getSetting(key) {
405
+ const row = this.sqlite.prepare("SELECT value FROM relay_settings WHERE key = ?").get(key);
406
+ return row?.value ?? null;
407
+ }
408
+ setSetting(key, value) {
409
+ this.sqlite.prepare("INSERT INTO relay_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
410
+ }
311
411
  createStoredUser(input) {
312
412
  const email = input.email.trim().toLowerCase();
313
413
  const username = normalizeUsername(input.username);
@@ -320,9 +420,7 @@ var RelayStore = class _RelayStore {
320
420
  if (input.password.length < 8) {
321
421
  throw new RelayStoreError(400, "bad_request", "Password must be at least 8 characters.");
322
422
  }
323
- if (this.data.users.some(
324
- (user) => user.email.toLowerCase() === email || user.username.toLowerCase() === username
325
- )) {
423
+ if (this.getUserByIdentifier(email) || this.getUserByUsername(username)) {
326
424
  throw new RelayStoreError(409, "conflict", "A user with that email or username already exists.");
327
425
  }
328
426
  const passwordSalt = crypto.randomBytes(16).toString("base64url");
@@ -344,7 +442,7 @@ var RelayStore = class _RelayStore {
344
442
  session: {
345
443
  authenticated: true,
346
444
  user: this.publicUser(user),
347
- registrationEnabled: this.data.registrationEnabled
445
+ registrationEnabled: this.registrationEnabled()
348
446
  }
349
447
  };
350
448
  }
@@ -378,7 +476,7 @@ var RelayStore = class _RelayStore {
378
476
  }
379
477
  }
380
478
  requireUser(userId) {
381
- const user = this.data.users.find((entry) => entry.id === userId);
479
+ const user = this.getUser(userId);
382
480
  if (!user) {
383
481
  throw new RelayStoreError(404, "not_found", "User was not found.");
384
482
  }
@@ -394,28 +492,132 @@ var RelayStore = class _RelayStore {
394
492
  createdAt: user.createdAt
395
493
  };
396
494
  }
397
- readData(registrationEnabled) {
398
- try {
399
- const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
400
- return {
401
- registrationEnabled: typeof parsed.registrationEnabled === "boolean" ? parsed.registrationEnabled : registrationEnabled,
402
- users: Array.isArray(parsed.users) ? parsed.users : [],
403
- devices: Array.isArray(parsed.devices) ? parsed.devices : [],
404
- shares: Array.isArray(parsed.shares) ? parsed.shares : []
405
- };
406
- } catch {
407
- return {
408
- registrationEnabled,
409
- users: [],
410
- devices: [],
411
- shares: []
412
- };
413
- }
495
+ insertUser(user) {
496
+ this.sqlite.prepare(
497
+ `
498
+ INSERT INTO relay_users (
499
+ id, email, username, role, enabled, created_at, password_salt, password_hash
500
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
501
+ `
502
+ ).run(
503
+ user.id,
504
+ user.email,
505
+ user.username,
506
+ user.role,
507
+ user.enabled ? 1 : 0,
508
+ user.createdAt,
509
+ user.passwordSalt,
510
+ user.passwordHash
511
+ );
512
+ }
513
+ insertDevice(device) {
514
+ this.sqlite.prepare(
515
+ `
516
+ INSERT INTO relay_devices (
517
+ id, owner_user_id, name, token_hash, token_preview, created_at
518
+ ) VALUES (?, ?, ?, ?, ?, ?)
519
+ `
520
+ ).run(
521
+ device.id,
522
+ device.ownerUserId,
523
+ device.name,
524
+ device.tokenHash,
525
+ device.tokenPreview,
526
+ device.createdAt
527
+ );
528
+ }
529
+ insertShare(share) {
530
+ this.sqlite.prepare(
531
+ `
532
+ INSERT INTO relay_shares (
533
+ id, owner_user_id, owner_username, target_user_id, target_username,
534
+ device_id, device_name, thread_id, label, created_at, revoked_at
535
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
536
+ `
537
+ ).run(
538
+ share.id,
539
+ share.ownerUserId,
540
+ share.ownerUsername,
541
+ share.targetUserId,
542
+ share.targetUsername,
543
+ share.deviceId,
544
+ share.deviceName,
545
+ share.threadId,
546
+ share.label,
547
+ share.createdAt,
548
+ share.revokedAt
549
+ );
550
+ }
551
+ getUser(id) {
552
+ return this.rowToUser(this.sqlite.prepare("SELECT * FROM relay_users WHERE id = ?").get(id));
553
+ }
554
+ getUserByIdentifier(identifier) {
555
+ return this.rowToUser(
556
+ this.sqlite.prepare("SELECT * FROM relay_users WHERE email = ? OR username = ?").get(identifier.toLowerCase(), identifier.toLowerCase())
557
+ );
558
+ }
559
+ getUserByUsername(username) {
560
+ return this.rowToUser(
561
+ this.sqlite.prepare("SELECT * FROM relay_users WHERE username = ?").get(normalizeUsername(username))
562
+ );
563
+ }
564
+ getUsers() {
565
+ return this.sqlite.prepare("SELECT * FROM relay_users ORDER BY created_at ASC").all().map((row) => this.rowToUser(row)).filter((user) => Boolean(user));
566
+ }
567
+ getDevice(id) {
568
+ return this.rowToDevice(this.sqlite.prepare("SELECT * FROM relay_devices WHERE id = ?").get(id));
569
+ }
570
+ getDevices() {
571
+ return this.sqlite.prepare("SELECT * FROM relay_devices ORDER BY created_at ASC").all().map((row) => this.rowToDevice(row)).filter((device) => Boolean(device));
572
+ }
573
+ getDevicesByOwner(ownerUserId) {
574
+ return this.sqlite.prepare("SELECT * FROM relay_devices WHERE owner_user_id = ? ORDER BY created_at ASC").all(ownerUserId).map((row) => this.rowToDevice(row)).filter((device) => Boolean(device));
575
+ }
576
+ getSharesByOwner(ownerUserId) {
577
+ return this.sqlite.prepare("SELECT * FROM relay_shares WHERE owner_user_id = ? AND revoked_at IS NULL ORDER BY created_at ASC").all(ownerUserId).map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
578
+ }
579
+ getSharesByTarget(targetUserId) {
580
+ return this.sqlite.prepare("SELECT * FROM relay_shares WHERE target_user_id = ? AND revoked_at IS NULL ORDER BY created_at ASC").all(targetUserId).map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
581
+ }
582
+ rowToUser(row) {
583
+ if (!row) return null;
584
+ return {
585
+ id: row.id,
586
+ email: row.email,
587
+ username: row.username,
588
+ role: row.role,
589
+ enabled: Boolean(row.enabled),
590
+ createdAt: row.created_at,
591
+ passwordSalt: row.password_salt,
592
+ passwordHash: row.password_hash
593
+ };
414
594
  }
415
- async persist() {
416
- await fsp.mkdir(path.dirname(this.filePath), { recursive: true });
417
- await fsp.writeFile(this.filePath, `${JSON.stringify(this.data, null, 2)}
418
- `, "utf8");
595
+ rowToDevice(row) {
596
+ if (!row) return null;
597
+ return {
598
+ id: row.id,
599
+ ownerUserId: row.owner_user_id,
600
+ name: row.name,
601
+ tokenHash: row.token_hash,
602
+ tokenPreview: row.token_preview,
603
+ createdAt: row.created_at
604
+ };
605
+ }
606
+ rowToShare(row) {
607
+ if (!row) return null;
608
+ return {
609
+ id: row.id,
610
+ ownerUserId: row.owner_user_id,
611
+ ownerUsername: row.owner_username ?? "unknown",
612
+ targetUserId: row.target_user_id,
613
+ targetUsername: row.target_username ?? "unknown",
614
+ deviceId: row.device_id,
615
+ deviceName: row.device_name ?? "Remote Codex device",
616
+ threadId: row.thread_id,
617
+ label: row.label,
618
+ createdAt: row.created_at,
619
+ revokedAt: row.revoked_at
620
+ };
419
621
  }
420
622
  };
421
623
  var RelayStoreError = class extends Error {
@@ -607,6 +809,22 @@ function buildRelayServer(config2) {
607
809
  targetPath
608
810
  });
609
811
  });
812
+ app2.get("/relay/devices/:deviceId/healthz", async (request, reply) => {
813
+ const user = requireRelayUser(request, reply, store);
814
+ if (!user) {
815
+ return;
816
+ }
817
+ const { deviceId } = z.object({ deviceId: z.string().uuid() }).parse(request.params);
818
+ await forwardRelayHttp({
819
+ request,
820
+ reply,
821
+ state,
822
+ store,
823
+ user,
824
+ deviceId,
825
+ targetPath: "/healthz"
826
+ });
827
+ });
610
828
  app2.all("/relay/api/*", async (request, reply) => {
611
829
  const user = requireRelayUser(request, reply, store);
612
830
  if (!user) {
@@ -919,12 +1137,12 @@ function registerRelayWebApp(app2, distDirInput) {
919
1137
  return;
920
1138
  }
921
1139
  if (assetPath === indexFile) {
922
- const html = await fsp2.readFile(indexFile, "utf8");
1140
+ const html = await fsp.readFile(indexFile, "utf8");
923
1141
  const payload = injectRelayBootstrap(html);
924
1142
  reply.header("cache-control", "no-cache").header("content-length", Buffer.byteLength(payload)).type("text/html; charset=utf-8");
925
1143
  return reply.send(payload);
926
1144
  }
927
- const stat = await fsp2.stat(assetPath);
1145
+ const stat = await fsp.stat(assetPath);
928
1146
  reply.header("cache-control", "public, max-age=31536000, immutable").header("content-length", stat.size).type(mimeTypes.get(path2.extname(assetPath).toLowerCase()) ?? "application/octet-stream");
929
1147
  return reply.send(fs2.createReadStream(assetPath));
930
1148
  });
@@ -1136,7 +1354,7 @@ function escapeRegExp(value) {
1136
1354
  }
1137
1355
  async function safeStat(filePath) {
1138
1356
  try {
1139
- return await fsp2.stat(filePath);
1357
+ return await fsp.stat(filePath);
1140
1358
  } catch {
1141
1359
  return null;
1142
1360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",