remote-codex 0.11.5 → 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.
- package/apps/relay-server/dist/index.js +305 -103
- package/package.json +1 -1
|
@@ -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
|
|
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(
|
|
69
|
-
this.
|
|
68
|
+
constructor(databasePath, sessionSecret, registrationEnabled, legacyJsonPath) {
|
|
69
|
+
this.databasePath = databasePath;
|
|
70
70
|
this.sessionSecret = sessionSecret;
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
+
sqlite;
|
|
76
82
|
static fromDataDir(dataDir, sessionSecret, registrationEnabled) {
|
|
83
|
+
const resolvedDataDir = path.resolve(dataDir);
|
|
77
84
|
return new _RelayStore(
|
|
78
|
-
path.join(
|
|
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
|
|
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.
|
|
96
|
-
void this.persist();
|
|
102
|
+
this.insertUser(user);
|
|
97
103
|
return this.publicUser(user);
|
|
98
104
|
}
|
|
99
105
|
register(input) {
|
|
100
|
-
if (!this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
164
|
-
|
|
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.
|
|
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.
|
|
183
|
-
|
|
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.
|
|
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.
|
|
198
|
-
|
|
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.
|
|
217
|
-
void this.persist();
|
|
220
|
+
this.insertShare(share);
|
|
218
221
|
return share;
|
|
219
222
|
}
|
|
220
223
|
revokeShare(userId, shareId) {
|
|
221
|
-
const share = this.
|
|
222
|
-
(
|
|
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
|
-
|
|
228
|
-
|
|
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.
|
|
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
|
|
242
|
-
|
|
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:
|
|
250
|
-
sharedWithMe: this.
|
|
251
|
-
|
|
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.
|
|
261
|
-
devices: this.
|
|
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.
|
|
272
|
+
registrationEnabled: this.registrationEnabled()
|
|
265
273
|
};
|
|
266
274
|
}
|
|
267
275
|
setRegistrationEnabled(enabled) {
|
|
268
|
-
this.
|
|
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
|
-
|
|
278
|
-
|
|
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.
|
|
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.
|
|
302
|
-
const target = this.
|
|
303
|
-
const device = this.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
);
|
|
414
512
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
};
|
|
594
|
+
}
|
|
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 {
|
|
@@ -935,12 +1137,12 @@ function registerRelayWebApp(app2, distDirInput) {
|
|
|
935
1137
|
return;
|
|
936
1138
|
}
|
|
937
1139
|
if (assetPath === indexFile) {
|
|
938
|
-
const html = await
|
|
1140
|
+
const html = await fsp.readFile(indexFile, "utf8");
|
|
939
1141
|
const payload = injectRelayBootstrap(html);
|
|
940
1142
|
reply.header("cache-control", "no-cache").header("content-length", Buffer.byteLength(payload)).type("text/html; charset=utf-8");
|
|
941
1143
|
return reply.send(payload);
|
|
942
1144
|
}
|
|
943
|
-
const stat = await
|
|
1145
|
+
const stat = await fsp.stat(assetPath);
|
|
944
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");
|
|
945
1147
|
return reply.send(fs2.createReadStream(assetPath));
|
|
946
1148
|
});
|
|
@@ -1152,7 +1354,7 @@ function escapeRegExp(value) {
|
|
|
1152
1354
|
}
|
|
1153
1355
|
async function safeStat(filePath) {
|
|
1154
1356
|
try {
|
|
1155
|
-
return await
|
|
1357
|
+
return await fsp.stat(filePath);
|
|
1156
1358
|
} catch {
|
|
1157
1359
|
return null;
|
|
1158
1360
|
}
|