remote-codex 0.11.24 → 0.11.25
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 +674 -16
- package/apps/supervisor-api/dist/index.js +0 -4
- package/apps/supervisor-web/dist/assets/index-Ch_S2n2I.css +1 -0
- package/apps/supervisor-web/dist/assets/index-JrzRGVx3.js +5 -0
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/shared/src/index.ts +54 -4
- package/apps/supervisor-web/dist/assets/index-DeQ67jTv.js +0 -5
- package/apps/supervisor-web/dist/assets/index-DgSdRu7a.css +0 -1
|
@@ -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
|
-
},
|
|
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,32 @@ var RelayStore = class _RelayStore {
|
|
|
353
403
|
sharedByMe: sharedByMe.map((share) => this.publicShare(share))
|
|
354
404
|
};
|
|
355
405
|
}
|
|
356
|
-
adminSummary(connectedDevices) {
|
|
406
|
+
adminSummary(connectedDevices, options = {}) {
|
|
407
|
+
const conversationWindowDays = normalizeConversationWindowDays(options.conversationWindowDays);
|
|
408
|
+
const users = this.getUsers();
|
|
409
|
+
const devices = this.getDevices();
|
|
410
|
+
const deviceCounts = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const device of devices) {
|
|
412
|
+
deviceCounts.set(device.ownerUserId, (deviceCounts.get(device.ownerUserId) ?? 0) + 1);
|
|
413
|
+
}
|
|
414
|
+
const conversationCounts = this.conversationCountsByUser(conversationWindowDays);
|
|
357
415
|
return {
|
|
358
|
-
users:
|
|
359
|
-
|
|
360
|
-
(device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)
|
|
416
|
+
users: users.map(
|
|
417
|
+
(user) => this.publicAdminUser(user, deviceCounts.get(user.id) ?? 0, conversationCounts.get(user.id) ?? 0)
|
|
361
418
|
),
|
|
419
|
+
devices: devices.map((device) => {
|
|
420
|
+
const owner = users.find((user) => user.id === device.ownerUserId);
|
|
421
|
+
return this.publicAdminDevice(
|
|
422
|
+
device,
|
|
423
|
+
connectedDevices.get(device.id) ?? null,
|
|
424
|
+
owner,
|
|
425
|
+
options.metadata
|
|
426
|
+
);
|
|
427
|
+
}),
|
|
428
|
+
shares: this.getShares({ includeRevoked: true }).map((share) => this.publicShare(share)),
|
|
429
|
+
pendingRegistrations: this.pendingRegistrations(),
|
|
430
|
+
settings: this.registrationSettings(),
|
|
431
|
+
conversationWindowDays,
|
|
362
432
|
registrationEnabled: this.registrationEnabled()
|
|
363
433
|
};
|
|
364
434
|
}
|
|
@@ -366,6 +436,95 @@ var RelayStore = class _RelayStore {
|
|
|
366
436
|
this.setSetting("registrationEnabled", enabled ? "true" : "false");
|
|
367
437
|
return enabled;
|
|
368
438
|
}
|
|
439
|
+
registrationSettings() {
|
|
440
|
+
return {
|
|
441
|
+
enabled: this.registrationEnabled(),
|
|
442
|
+
registrationPassword: this.getSetting("registrationPassword"),
|
|
443
|
+
approvalRequired: this.getSetting("registrationApprovalRequired") === "true"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
updateRegistrationSettings(input) {
|
|
447
|
+
if (input.enabled !== void 0) {
|
|
448
|
+
this.setRegistrationEnabled(input.enabled);
|
|
449
|
+
}
|
|
450
|
+
if (input.registrationPassword !== void 0) {
|
|
451
|
+
const password = input.registrationPassword?.trim() || null;
|
|
452
|
+
if (password !== null && password.length < 8) {
|
|
453
|
+
throw new RelayStoreError(400, "bad_request", "Registration password must be at least 8 characters.");
|
|
454
|
+
}
|
|
455
|
+
if (password === null) {
|
|
456
|
+
this.deleteSetting("registrationPassword");
|
|
457
|
+
} else {
|
|
458
|
+
this.setSetting("registrationPassword", password);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (input.approvalRequired !== void 0) {
|
|
462
|
+
this.setSetting("registrationApprovalRequired", input.approvalRequired ? "true" : "false");
|
|
463
|
+
}
|
|
464
|
+
return this.registrationSettings();
|
|
465
|
+
}
|
|
466
|
+
ensureRegistrationPassword(password) {
|
|
467
|
+
if (!password || this.getSetting("registrationPassword") !== null) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.setSetting("registrationPassword", password);
|
|
471
|
+
}
|
|
472
|
+
approvePendingRegistration(adminUserId, requestId) {
|
|
473
|
+
const record = this.requirePendingRegistration(requestId);
|
|
474
|
+
const user = {
|
|
475
|
+
id: crypto.randomUUID(),
|
|
476
|
+
email: record.email,
|
|
477
|
+
username: record.username,
|
|
478
|
+
role: "user",
|
|
479
|
+
enabled: true,
|
|
480
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
481
|
+
lastSeenAt: null,
|
|
482
|
+
passwordSalt: record.passwordSalt,
|
|
483
|
+
passwordHash: record.passwordHash
|
|
484
|
+
};
|
|
485
|
+
const approve = this.sqlite.transaction(() => {
|
|
486
|
+
this.insertUser(user);
|
|
487
|
+
this.sqlite.prepare(
|
|
488
|
+
`
|
|
489
|
+
UPDATE relay_pending_registrations
|
|
490
|
+
SET status = 'approved', reviewed_at = ?, reviewed_by_user_id = ?
|
|
491
|
+
WHERE id = ?
|
|
492
|
+
`
|
|
493
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), adminUserId, requestId);
|
|
494
|
+
});
|
|
495
|
+
approve();
|
|
496
|
+
return this.publicUser(user);
|
|
497
|
+
}
|
|
498
|
+
rejectPendingRegistration(adminUserId, requestId) {
|
|
499
|
+
this.requirePendingRegistration(requestId);
|
|
500
|
+
this.sqlite.prepare(
|
|
501
|
+
`
|
|
502
|
+
UPDATE relay_pending_registrations
|
|
503
|
+
SET status = 'rejected', reviewed_at = ?, reviewed_by_user_id = ?
|
|
504
|
+
WHERE id = ?
|
|
505
|
+
`
|
|
506
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), adminUserId, requestId);
|
|
507
|
+
return { id: requestId };
|
|
508
|
+
}
|
|
509
|
+
recordUserSeen(userId, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
510
|
+
this.sqlite.prepare("UPDATE relay_users SET last_seen_at = ? WHERE id = ?").run(at, userId);
|
|
511
|
+
}
|
|
512
|
+
recordConversationEvent(input) {
|
|
513
|
+
this.sqlite.prepare(
|
|
514
|
+
`
|
|
515
|
+
INSERT INTO relay_conversation_events (
|
|
516
|
+
id, user_id, device_id, thread_id, workspace_id, occurred_at
|
|
517
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
518
|
+
`
|
|
519
|
+
).run(
|
|
520
|
+
crypto.randomUUID(),
|
|
521
|
+
input.userId,
|
|
522
|
+
input.deviceId,
|
|
523
|
+
input.threadId,
|
|
524
|
+
input.workspaceId,
|
|
525
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
526
|
+
);
|
|
527
|
+
}
|
|
369
528
|
setUserEnabled(userId, enabled) {
|
|
370
529
|
const user = this.requireUser(userId);
|
|
371
530
|
if (user.role === "admin" && !enabled) {
|
|
@@ -374,6 +533,30 @@ var RelayStore = class _RelayStore {
|
|
|
374
533
|
this.sqlite.prepare("UPDATE relay_users SET enabled = ? WHERE id = ?").run(enabled ? 1 : 0, userId);
|
|
375
534
|
return this.publicUser({ ...user, enabled });
|
|
376
535
|
}
|
|
536
|
+
deleteUser(userId) {
|
|
537
|
+
const user = this.requireUser(userId);
|
|
538
|
+
if (user.role === "admin") {
|
|
539
|
+
throw new RelayStoreError(400, "bad_request", "The admin user cannot be deleted.");
|
|
540
|
+
}
|
|
541
|
+
this.sqlite.prepare("DELETE FROM relay_users WHERE id = ?").run(userId);
|
|
542
|
+
}
|
|
543
|
+
adminResetUserPassword(userId, password) {
|
|
544
|
+
const user = this.requireUser(userId);
|
|
545
|
+
if (user.role === "admin") {
|
|
546
|
+
throw new RelayStoreError(400, "bad_request", "The admin user password cannot be reset here.");
|
|
547
|
+
}
|
|
548
|
+
if (password.length < 8) {
|
|
549
|
+
throw new RelayStoreError(400, "bad_request", "Password must be at least 8 characters.");
|
|
550
|
+
}
|
|
551
|
+
const passwordSalt = crypto.randomBytes(16).toString("base64url");
|
|
552
|
+
const passwordHash = hashSecret(password, passwordSalt);
|
|
553
|
+
this.sqlite.prepare("UPDATE relay_users SET password_salt = ?, password_hash = ? WHERE id = ?").run(passwordSalt, passwordHash, user.id);
|
|
554
|
+
return this.publicUser({
|
|
555
|
+
...user,
|
|
556
|
+
passwordSalt,
|
|
557
|
+
passwordHash
|
|
558
|
+
});
|
|
559
|
+
}
|
|
377
560
|
updateAccount(userId, input) {
|
|
378
561
|
const user = this.requireUser(userId);
|
|
379
562
|
const username = input.username !== void 0 ? normalizeUsername(input.username) : user.username;
|
|
@@ -424,6 +607,24 @@ var RelayStore = class _RelayStore {
|
|
|
424
607
|
createdAt: device.createdAt
|
|
425
608
|
};
|
|
426
609
|
}
|
|
610
|
+
publicAdminUser(user, deviceCount, conversationCount) {
|
|
611
|
+
return {
|
|
612
|
+
...this.publicUser(user),
|
|
613
|
+
lastSeenAt: user.lastSeenAt,
|
|
614
|
+
deviceCount,
|
|
615
|
+
conversationCount
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
publicAdminDevice(device, status, owner, metadata) {
|
|
619
|
+
return {
|
|
620
|
+
...this.publicDevice(device, status),
|
|
621
|
+
ownerUsername: owner?.username ?? "unknown",
|
|
622
|
+
ownerEmail: owner?.email ?? "unknown",
|
|
623
|
+
ipAddress: status?.ipAddress ?? null,
|
|
624
|
+
workspaces: metadata?.workspacesByDeviceId?.get(device.id) ?? [],
|
|
625
|
+
threads: metadata?.threadsByDeviceId?.get(device.id) ?? []
|
|
626
|
+
};
|
|
627
|
+
}
|
|
427
628
|
recordShareAccess(share, user) {
|
|
428
629
|
if (share.revokedAt || share.expiresAt && share.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
|
|
429
630
|
return;
|
|
@@ -466,6 +667,7 @@ var RelayStore = class _RelayStore {
|
|
|
466
667
|
username TEXT NOT NULL UNIQUE,
|
|
467
668
|
role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
|
|
468
669
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
670
|
+
last_seen_at TEXT,
|
|
469
671
|
created_at TEXT NOT NULL,
|
|
470
672
|
password_salt TEXT NOT NULL,
|
|
471
673
|
password_hash TEXT NOT NULL
|
|
@@ -514,7 +716,33 @@ var RelayStore = class _RelayStore {
|
|
|
514
716
|
);
|
|
515
717
|
|
|
516
718
|
CREATE INDEX IF NOT EXISTS relay_share_access_events_share_idx ON relay_share_access_events(share_id, accessed_at DESC);
|
|
719
|
+
|
|
720
|
+
CREATE TABLE IF NOT EXISTS relay_conversation_events (
|
|
721
|
+
id TEXT PRIMARY KEY,
|
|
722
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
723
|
+
device_id TEXT NOT NULL REFERENCES relay_devices(id) ON DELETE CASCADE,
|
|
724
|
+
thread_id TEXT,
|
|
725
|
+
workspace_id TEXT,
|
|
726
|
+
occurred_at TEXT NOT NULL
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
CREATE INDEX IF NOT EXISTS relay_conversation_events_user_time_idx ON relay_conversation_events(user_id, occurred_at DESC);
|
|
730
|
+
|
|
731
|
+
CREATE TABLE IF NOT EXISTS relay_pending_registrations (
|
|
732
|
+
id TEXT PRIMARY KEY,
|
|
733
|
+
email TEXT NOT NULL,
|
|
734
|
+
username TEXT NOT NULL,
|
|
735
|
+
password_salt TEXT NOT NULL,
|
|
736
|
+
password_hash TEXT NOT NULL,
|
|
737
|
+
created_at TEXT NOT NULL,
|
|
738
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
|
|
739
|
+
reviewed_at TEXT,
|
|
740
|
+
reviewed_by_user_id TEXT
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
CREATE INDEX IF NOT EXISTS relay_pending_registrations_status_idx ON relay_pending_registrations(status, created_at DESC);
|
|
517
744
|
`);
|
|
745
|
+
this.ensureColumn("relay_users", "last_seen_at", "TEXT");
|
|
518
746
|
this.ensureColumn("relay_devices", "token", "TEXT");
|
|
519
747
|
this.ensureColumn("relay_shares", "workspace_id", "TEXT");
|
|
520
748
|
this.ensureColumn("relay_shares", "thread_access", "TEXT NOT NULL DEFAULT 'control'");
|
|
@@ -574,6 +802,9 @@ var RelayStore = class _RelayStore {
|
|
|
574
802
|
setSetting(key, value) {
|
|
575
803
|
this.sqlite.prepare("INSERT INTO relay_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
576
804
|
}
|
|
805
|
+
deleteSetting(key) {
|
|
806
|
+
this.sqlite.prepare("DELETE FROM relay_settings WHERE key = ?").run(key);
|
|
807
|
+
}
|
|
577
808
|
createStoredUser(input) {
|
|
578
809
|
const email = input.email.trim().toLowerCase();
|
|
579
810
|
const username = normalizeUsername(input.username);
|
|
@@ -597,6 +828,7 @@ var RelayStore = class _RelayStore {
|
|
|
597
828
|
role: input.role,
|
|
598
829
|
enabled: true,
|
|
599
830
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
831
|
+
lastSeenAt: null,
|
|
600
832
|
passwordSalt,
|
|
601
833
|
passwordHash: hashSecret(input.password, passwordSalt)
|
|
602
834
|
};
|
|
@@ -662,8 +894,8 @@ var RelayStore = class _RelayStore {
|
|
|
662
894
|
this.sqlite.prepare(
|
|
663
895
|
`
|
|
664
896
|
INSERT INTO relay_users (
|
|
665
|
-
id, email, username, role, enabled, created_at, password_salt, password_hash
|
|
666
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
897
|
+
id, email, username, role, enabled, last_seen_at, created_at, password_salt, password_hash
|
|
898
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
667
899
|
`
|
|
668
900
|
).run(
|
|
669
901
|
user.id,
|
|
@@ -671,6 +903,7 @@ var RelayStore = class _RelayStore {
|
|
|
671
903
|
user.username,
|
|
672
904
|
user.role,
|
|
673
905
|
user.enabled ? 1 : 0,
|
|
906
|
+
user.lastSeenAt,
|
|
674
907
|
user.createdAt,
|
|
675
908
|
user.passwordSalt,
|
|
676
909
|
user.passwordHash
|
|
@@ -790,6 +1023,72 @@ var RelayStore = class _RelayStore {
|
|
|
790
1023
|
getSharesByTarget(targetUserId) {
|
|
791
1024
|
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
1025
|
}
|
|
1026
|
+
getShares(options = {}) {
|
|
1027
|
+
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";
|
|
1028
|
+
const rows = options.includeRevoked ? this.sqlite.prepare(sql).all() : this.sqlite.prepare(sql).all((/* @__PURE__ */ new Date()).toISOString());
|
|
1029
|
+
return rows.map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1030
|
+
}
|
|
1031
|
+
conversationCountsByUser(days) {
|
|
1032
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1033
|
+
const rows = this.sqlite.prepare(
|
|
1034
|
+
`
|
|
1035
|
+
SELECT user_id, COUNT(*) AS count
|
|
1036
|
+
FROM relay_conversation_events
|
|
1037
|
+
WHERE occurred_at >= ?
|
|
1038
|
+
GROUP BY user_id
|
|
1039
|
+
`
|
|
1040
|
+
).all(since);
|
|
1041
|
+
return new Map(rows.map((row) => [row.user_id, row.count]));
|
|
1042
|
+
}
|
|
1043
|
+
pendingRegistrations() {
|
|
1044
|
+
return this.sqlite.prepare(
|
|
1045
|
+
`
|
|
1046
|
+
SELECT * FROM relay_pending_registrations
|
|
1047
|
+
WHERE status = 'pending'
|
|
1048
|
+
ORDER BY created_at ASC
|
|
1049
|
+
`
|
|
1050
|
+
).all().map((row) => this.rowToPendingRegistration(row)).filter((record) => Boolean(record)).map((record) => this.publicPendingRegistration(record));
|
|
1051
|
+
}
|
|
1052
|
+
requirePendingRegistration(id) {
|
|
1053
|
+
const record = this.rowToPendingRegistration(
|
|
1054
|
+
this.sqlite.prepare("SELECT * FROM relay_pending_registrations WHERE id = ? AND status = 'pending'").get(id)
|
|
1055
|
+
);
|
|
1056
|
+
if (!record) {
|
|
1057
|
+
throw new RelayStoreError(404, "not_found", "Pending registration was not found.");
|
|
1058
|
+
}
|
|
1059
|
+
if (this.getUserByIdentifier(record.email) || this.getUserByUsername(record.username)) {
|
|
1060
|
+
throw new RelayStoreError(409, "conflict", "A user with that email or username already exists.");
|
|
1061
|
+
}
|
|
1062
|
+
return record;
|
|
1063
|
+
}
|
|
1064
|
+
publicPendingRegistration(record) {
|
|
1065
|
+
return {
|
|
1066
|
+
id: record.id,
|
|
1067
|
+
email: record.email,
|
|
1068
|
+
username: record.username,
|
|
1069
|
+
createdAt: record.createdAt
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
insertPendingRegistration(record) {
|
|
1073
|
+
this.sqlite.prepare(
|
|
1074
|
+
`
|
|
1075
|
+
INSERT INTO relay_pending_registrations (
|
|
1076
|
+
id, email, username, password_salt, password_hash,
|
|
1077
|
+
created_at, status, reviewed_at, reviewed_by_user_id
|
|
1078
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1079
|
+
`
|
|
1080
|
+
).run(
|
|
1081
|
+
record.id,
|
|
1082
|
+
record.email,
|
|
1083
|
+
record.username,
|
|
1084
|
+
record.passwordSalt,
|
|
1085
|
+
record.passwordHash,
|
|
1086
|
+
record.createdAt,
|
|
1087
|
+
record.status,
|
|
1088
|
+
record.reviewedAt,
|
|
1089
|
+
record.reviewedByUserId
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
793
1092
|
rowToUser(row) {
|
|
794
1093
|
if (!row) return null;
|
|
795
1094
|
return {
|
|
@@ -799,6 +1098,7 @@ var RelayStore = class _RelayStore {
|
|
|
799
1098
|
role: row.role,
|
|
800
1099
|
enabled: Boolean(row.enabled),
|
|
801
1100
|
createdAt: row.created_at,
|
|
1101
|
+
lastSeenAt: row.last_seen_at ?? null,
|
|
802
1102
|
passwordSalt: row.password_salt,
|
|
803
1103
|
passwordHash: row.password_hash
|
|
804
1104
|
};
|
|
@@ -826,7 +1126,9 @@ var RelayStore = class _RelayStore {
|
|
|
826
1126
|
deviceId: row.device_id,
|
|
827
1127
|
deviceName: row.device_name ?? "Remote Codex device",
|
|
828
1128
|
threadId: row.thread_id,
|
|
1129
|
+
threadTitle: null,
|
|
829
1130
|
workspaceId: row.workspace_id ?? null,
|
|
1131
|
+
workspaceLabel: null,
|
|
830
1132
|
label: row.label,
|
|
831
1133
|
threadAccess: normalizeThreadAccess(row.thread_access),
|
|
832
1134
|
workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
|
|
@@ -838,6 +1140,20 @@ var RelayStore = class _RelayStore {
|
|
|
838
1140
|
accessEvents: []
|
|
839
1141
|
};
|
|
840
1142
|
}
|
|
1143
|
+
rowToPendingRegistration(row) {
|
|
1144
|
+
if (!row) return null;
|
|
1145
|
+
return {
|
|
1146
|
+
id: row.id,
|
|
1147
|
+
email: row.email,
|
|
1148
|
+
username: row.username,
|
|
1149
|
+
passwordSalt: row.password_salt,
|
|
1150
|
+
passwordHash: row.password_hash,
|
|
1151
|
+
createdAt: row.created_at,
|
|
1152
|
+
status: row.status,
|
|
1153
|
+
reviewedAt: row.reviewed_at ?? null,
|
|
1154
|
+
reviewedByUserId: row.reviewed_by_user_id ?? null
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
841
1157
|
};
|
|
842
1158
|
var RelayStoreError = class extends Error {
|
|
843
1159
|
constructor(statusCode, code, message) {
|
|
@@ -867,6 +1183,12 @@ function normalizeExpiresAt(value) {
|
|
|
867
1183
|
const timestamp = Date.parse(value);
|
|
868
1184
|
return Number.isNaN(timestamp) ? null : new Date(timestamp).toISOString();
|
|
869
1185
|
}
|
|
1186
|
+
function normalizeConversationWindowDays(value) {
|
|
1187
|
+
if (!Number.isFinite(value ?? NaN)) {
|
|
1188
|
+
return 7;
|
|
1189
|
+
}
|
|
1190
|
+
return Math.min(365, Math.max(1, Math.floor(value)));
|
|
1191
|
+
}
|
|
870
1192
|
function hashSecret(secret, salt) {
|
|
871
1193
|
return crypto.scryptSync(secret, salt, 32).toString("base64url");
|
|
872
1194
|
}
|
|
@@ -890,6 +1212,7 @@ function safeEqual(left, right) {
|
|
|
890
1212
|
|
|
891
1213
|
// src/app.ts
|
|
892
1214
|
var RELAY_REQUEST_TIMEOUT_MS = 3e4;
|
|
1215
|
+
var RELAY_PORTAL_METADATA_TIMEOUT_MS = 900;
|
|
893
1216
|
var WEBSOCKET_OPEN = 1;
|
|
894
1217
|
var RELAY_COOKIE_NAME = "remote_codex_relay_session";
|
|
895
1218
|
var threadAccessSchema = z.enum(["read", "control"]);
|
|
@@ -931,6 +1254,17 @@ var updateShareSchema = z.object({
|
|
|
931
1254
|
var setEnabledSchema = z.object({
|
|
932
1255
|
enabled: z.boolean()
|
|
933
1256
|
});
|
|
1257
|
+
var adminResetPasswordSchema = z.object({
|
|
1258
|
+
password: z.string().min(8)
|
|
1259
|
+
});
|
|
1260
|
+
var adminQuerySchema = z.object({
|
|
1261
|
+
days: z.coerce.number().int().positive().max(365).optional()
|
|
1262
|
+
});
|
|
1263
|
+
var updateRegistrationSettingsSchema = z.object({
|
|
1264
|
+
enabled: z.boolean().optional(),
|
|
1265
|
+
registrationPassword: z.string().nullable().optional(),
|
|
1266
|
+
approvalRequired: z.boolean().optional()
|
|
1267
|
+
});
|
|
934
1268
|
var updateAccountSchema = z.object({
|
|
935
1269
|
username: z.string().trim().min(3).optional()
|
|
936
1270
|
});
|
|
@@ -1027,6 +1361,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1027
1361
|
if (config2.registrationEnabledConfigured) {
|
|
1028
1362
|
store.setRegistrationEnabled(config2.registrationEnabled);
|
|
1029
1363
|
}
|
|
1364
|
+
store.ensureRegistrationPassword(config2.registrationPassword);
|
|
1030
1365
|
store.seedAdmin({
|
|
1031
1366
|
username: config2.adminUsername,
|
|
1032
1367
|
email: config2.adminEmail,
|
|
@@ -1064,21 +1399,35 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1064
1399
|
});
|
|
1065
1400
|
app2.post("/relay/auth/register", async (request, reply) => {
|
|
1066
1401
|
const body = registerSchema.parse(request.body ?? {});
|
|
1067
|
-
|
|
1402
|
+
const settings = store.registrationSettings();
|
|
1403
|
+
if (settings.registrationPassword && body.registrationPassword !== settings.registrationPassword) {
|
|
1068
1404
|
reply.status(403).send({
|
|
1069
1405
|
code: "forbidden",
|
|
1070
1406
|
message: "Invalid registration password."
|
|
1071
1407
|
});
|
|
1072
1408
|
return;
|
|
1073
1409
|
}
|
|
1074
|
-
const
|
|
1410
|
+
const registerInput = {
|
|
1411
|
+
email: body.email,
|
|
1412
|
+
username: body.username,
|
|
1413
|
+
password: body.password
|
|
1414
|
+
};
|
|
1415
|
+
if (settings.approvalRequired) {
|
|
1416
|
+
reply.status(202);
|
|
1417
|
+
return {
|
|
1418
|
+
pendingApproval: true,
|
|
1419
|
+
request: store.requestRegistrationApproval(registerInput)
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1075
1422
|
const result = store.register(registerInput);
|
|
1423
|
+
store.recordUserSeen(result.session.user.id);
|
|
1076
1424
|
attachRelayCookie(reply, result.token);
|
|
1077
1425
|
return result;
|
|
1078
1426
|
});
|
|
1079
1427
|
app2.post("/relay/auth/login", async (request, reply) => {
|
|
1080
1428
|
const body = loginSchema.parse(request.body ?? {});
|
|
1081
1429
|
const result = store.login(body);
|
|
1430
|
+
store.recordUserSeen(result.session.user.id);
|
|
1082
1431
|
attachRelayCookie(reply, result.token);
|
|
1083
1432
|
return result;
|
|
1084
1433
|
});
|
|
@@ -1109,7 +1458,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1109
1458
|
if (!user) {
|
|
1110
1459
|
return;
|
|
1111
1460
|
}
|
|
1112
|
-
return store.portalSummary(user.id, connectionStatus(state));
|
|
1461
|
+
return enrichPortalSummary(store.portalSummary(user.id, connectionStatus(state)), state);
|
|
1113
1462
|
});
|
|
1114
1463
|
app2.get("/relay/access", async (request, reply) => {
|
|
1115
1464
|
const user = requireRelayUser(request, reply, store);
|
|
@@ -1186,15 +1535,27 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1186
1535
|
if (!user) {
|
|
1187
1536
|
return;
|
|
1188
1537
|
}
|
|
1189
|
-
|
|
1538
|
+
const query = adminQuerySchema.parse(request.query ?? {});
|
|
1539
|
+
const baseSummary = store.adminSummary(connectionStatus(state), {
|
|
1540
|
+
...query.days !== void 0 ? { conversationWindowDays: query.days } : {}
|
|
1541
|
+
});
|
|
1542
|
+
return enrichAdminSummary(baseSummary, state, store, query.days);
|
|
1190
1543
|
});
|
|
1191
1544
|
app2.patch("/relay/admin/settings/registration", async (request, reply) => {
|
|
1192
1545
|
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1193
1546
|
if (!user) {
|
|
1194
1547
|
return;
|
|
1195
1548
|
}
|
|
1196
|
-
const body =
|
|
1197
|
-
|
|
1549
|
+
const body = updateRegistrationSettingsSchema.parse(request.body ?? {});
|
|
1550
|
+
const settings = store.updateRegistrationSettings({
|
|
1551
|
+
...body.enabled !== void 0 ? { enabled: body.enabled } : {},
|
|
1552
|
+
...body.registrationPassword !== void 0 ? { registrationPassword: body.registrationPassword } : {},
|
|
1553
|
+
...body.approvalRequired !== void 0 ? { approvalRequired: body.approvalRequired } : {}
|
|
1554
|
+
});
|
|
1555
|
+
return {
|
|
1556
|
+
registrationEnabled: settings.enabled,
|
|
1557
|
+
settings
|
|
1558
|
+
};
|
|
1198
1559
|
});
|
|
1199
1560
|
app2.patch("/relay/admin/users/:userId", async (request, reply) => {
|
|
1200
1561
|
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
@@ -1205,6 +1566,40 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1205
1566
|
const body = setEnabledSchema.parse(request.body ?? {});
|
|
1206
1567
|
return store.setUserEnabled(userId, body.enabled);
|
|
1207
1568
|
});
|
|
1569
|
+
app2.delete("/relay/admin/users/:userId", async (request, reply) => {
|
|
1570
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1571
|
+
if (!user) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const { userId } = z.object({ userId: z.string().uuid() }).parse(request.params);
|
|
1575
|
+
store.deleteUser(userId);
|
|
1576
|
+
return { id: userId };
|
|
1577
|
+
});
|
|
1578
|
+
app2.post("/relay/admin/users/:userId/reset-password", async (request, reply) => {
|
|
1579
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1580
|
+
if (!user) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const { userId } = z.object({ userId: z.string().uuid() }).parse(request.params);
|
|
1584
|
+
const body = adminResetPasswordSchema.parse(request.body ?? {});
|
|
1585
|
+
return store.adminResetUserPassword(userId, body.password);
|
|
1586
|
+
});
|
|
1587
|
+
app2.post("/relay/admin/registrations/:requestId/approve", async (request, reply) => {
|
|
1588
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1589
|
+
if (!user) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
|
|
1593
|
+
return store.approvePendingRegistration(user.id, requestId);
|
|
1594
|
+
});
|
|
1595
|
+
app2.post("/relay/admin/registrations/:requestId/reject", async (request, reply) => {
|
|
1596
|
+
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1597
|
+
if (!user) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const { requestId } = z.object({ requestId: z.string().uuid() }).parse(request.params);
|
|
1601
|
+
return store.rejectPendingRegistration(user.id, requestId);
|
|
1602
|
+
});
|
|
1208
1603
|
app2.all("/relay/devices/:deviceId/api/*", async (request, reply) => {
|
|
1209
1604
|
const user = requireRelayUser(request, reply, store);
|
|
1210
1605
|
if (!user) {
|
|
@@ -1297,7 +1692,8 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1297
1692
|
clientSockets: /* @__PURE__ */ new Map(),
|
|
1298
1693
|
connected: true,
|
|
1299
1694
|
connectedAt,
|
|
1300
|
-
lastHeartbeatAt: connectedAt
|
|
1695
|
+
lastHeartbeatAt: connectedAt,
|
|
1696
|
+
ipAddress: relayClientIp(request)
|
|
1301
1697
|
};
|
|
1302
1698
|
state.supervisors.set(deviceId, connection);
|
|
1303
1699
|
socket.send(
|
|
@@ -1479,6 +1875,19 @@ async function forwardRelayHttp(input) {
|
|
|
1479
1875
|
if (access.kind === "shared") {
|
|
1480
1876
|
input.store.recordShareAccess(access.share, input.user);
|
|
1481
1877
|
}
|
|
1878
|
+
const conversationEvent = conversationEventFromRequest(
|
|
1879
|
+
input.request.method,
|
|
1880
|
+
input.targetPath,
|
|
1881
|
+
input.request.body
|
|
1882
|
+
);
|
|
1883
|
+
if (conversationEvent) {
|
|
1884
|
+
input.store.recordConversationEvent({
|
|
1885
|
+
userId: input.user.id,
|
|
1886
|
+
deviceId: input.deviceId,
|
|
1887
|
+
threadId: conversationEvent.threadId,
|
|
1888
|
+
workspaceId: conversationEvent.workspaceId
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1482
1891
|
const supervisor = input.state.supervisors.get(input.deviceId);
|
|
1483
1892
|
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
1484
1893
|
input.reply.status(503).send({
|
|
@@ -1653,6 +2062,14 @@ function requireRelayUser(request, reply, store, options = {}) {
|
|
|
1653
2062
|
});
|
|
1654
2063
|
return null;
|
|
1655
2064
|
}
|
|
2065
|
+
if (!options.admin && session.user.role === "admin") {
|
|
2066
|
+
reply.status(403).send({
|
|
2067
|
+
code: "forbidden",
|
|
2068
|
+
message: "Use the relay admin panel for this account."
|
|
2069
|
+
});
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
store.recordUserSeen(session.user.id);
|
|
1656
2073
|
request.relayUser = session.user;
|
|
1657
2074
|
return session.user;
|
|
1658
2075
|
}
|
|
@@ -1677,11 +2094,199 @@ function connectionStatus(state) {
|
|
|
1677
2094
|
statuses.set(deviceId, {
|
|
1678
2095
|
connected: true,
|
|
1679
2096
|
connectedAt: supervisor.connectedAt,
|
|
1680
|
-
lastHeartbeatAt: supervisor.lastHeartbeatAt
|
|
2097
|
+
lastHeartbeatAt: supervisor.lastHeartbeatAt,
|
|
2098
|
+
ipAddress: supervisor.ipAddress ?? null
|
|
1681
2099
|
});
|
|
1682
2100
|
}
|
|
1683
2101
|
return statuses;
|
|
1684
2102
|
}
|
|
2103
|
+
async function enrichAdminSummary(summary, state, store, conversationWindowDays) {
|
|
2104
|
+
const workspacesByDeviceId = /* @__PURE__ */ new Map();
|
|
2105
|
+
const threadsByDeviceId = /* @__PURE__ */ new Map();
|
|
2106
|
+
await Promise.all(
|
|
2107
|
+
summary.devices.map(async (device) => {
|
|
2108
|
+
const supervisor = state.supervisors.get(device.id);
|
|
2109
|
+
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const [workspaces, threads] = await Promise.all([
|
|
2113
|
+
fetchRelayWorkspaces(supervisor, device.id),
|
|
2114
|
+
fetchRelayThreads(supervisor, device.id)
|
|
2115
|
+
]);
|
|
2116
|
+
workspacesByDeviceId.set(device.id, workspaces);
|
|
2117
|
+
const workspaceLabelById = new Map(workspaces.map((workspace) => [workspace.id, workspace.label]));
|
|
2118
|
+
threadsByDeviceId.set(
|
|
2119
|
+
device.id,
|
|
2120
|
+
threads.map((thread) => ({
|
|
2121
|
+
...thread,
|
|
2122
|
+
workspaceLabel: thread.workspaceId ? workspaceLabelById.get(thread.workspaceId) ?? thread.workspaceLabel : null
|
|
2123
|
+
}))
|
|
2124
|
+
);
|
|
2125
|
+
})
|
|
2126
|
+
);
|
|
2127
|
+
const enriched = store.adminSummary(connectionStatus(state), {
|
|
2128
|
+
metadata: {
|
|
2129
|
+
workspacesByDeviceId,
|
|
2130
|
+
threadsByDeviceId
|
|
2131
|
+
},
|
|
2132
|
+
...conversationWindowDays !== void 0 ? { conversationWindowDays } : {}
|
|
2133
|
+
});
|
|
2134
|
+
return {
|
|
2135
|
+
...enriched,
|
|
2136
|
+
shares: enriched.shares.map((share) => {
|
|
2137
|
+
const thread = threadsByDeviceId.get(share.deviceId)?.find((item) => item.id === share.threadId);
|
|
2138
|
+
const workspace = share.workspaceId ? workspacesByDeviceId.get(share.deviceId)?.find((item) => item.id === share.workspaceId) : null;
|
|
2139
|
+
return {
|
|
2140
|
+
...share,
|
|
2141
|
+
threadTitle: thread?.title ?? share.threadTitle,
|
|
2142
|
+
workspaceLabel: workspace?.label ?? thread?.workspaceLabel ?? share.workspaceLabel
|
|
2143
|
+
};
|
|
2144
|
+
})
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
async function fetchRelayWorkspaces(supervisor, deviceId) {
|
|
2148
|
+
const payload = await forwardSupervisorJson(supervisor, deviceId, "/api/workspaces");
|
|
2149
|
+
const rows = Array.isArray(payload) ? payload : [];
|
|
2150
|
+
const workspaces = [];
|
|
2151
|
+
for (const workspace of rows.filter(isObject)) {
|
|
2152
|
+
const id = stringField(workspace, "id");
|
|
2153
|
+
const label = stringField(workspace, "label");
|
|
2154
|
+
if (!id || !label) {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
workspaces.push({
|
|
2158
|
+
id,
|
|
2159
|
+
label,
|
|
2160
|
+
absPath: stringField(workspace, "absPath")
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
return workspaces.slice(0, 50);
|
|
2164
|
+
}
|
|
2165
|
+
async function fetchRelayThreads(supervisor, deviceId) {
|
|
2166
|
+
const payload = await forwardSupervisorJson(supervisor, deviceId, "/api/threads");
|
|
2167
|
+
const rows = Array.isArray(payload) ? payload : [];
|
|
2168
|
+
const threads = [];
|
|
2169
|
+
for (const thread of rows.filter(isObject)) {
|
|
2170
|
+
const id = stringField(thread, "id");
|
|
2171
|
+
if (!id) {
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
threads.push({
|
|
2175
|
+
id,
|
|
2176
|
+
title: stringField(thread, "title") ?? "Untitled thread",
|
|
2177
|
+
workspaceId: stringField(thread, "workspaceId"),
|
|
2178
|
+
workspaceLabel: null,
|
|
2179
|
+
status: stringField(thread, "status"),
|
|
2180
|
+
updatedAt: stringField(thread, "updatedAt") ?? stringField(thread, "createdAt")
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
return threads.slice(0, 80);
|
|
2184
|
+
}
|
|
2185
|
+
async function enrichPortalSummary(portal, state) {
|
|
2186
|
+
const threadCache = /* @__PURE__ */ new Map();
|
|
2187
|
+
const workspaceCache = /* @__PURE__ */ new Map();
|
|
2188
|
+
const enrichShare = async (share) => {
|
|
2189
|
+
const supervisor = state.supervisors.get(share.deviceId);
|
|
2190
|
+
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2191
|
+
return share;
|
|
2192
|
+
}
|
|
2193
|
+
const threadCacheKey = `${share.deviceId}:${share.threadId}`;
|
|
2194
|
+
let threadTitlePromise = threadCache.get(threadCacheKey);
|
|
2195
|
+
if (!threadTitlePromise) {
|
|
2196
|
+
threadTitlePromise = fetchRelayThreadTitle(supervisor, share.deviceId, share.threadId);
|
|
2197
|
+
threadCache.set(threadCacheKey, threadTitlePromise);
|
|
2198
|
+
}
|
|
2199
|
+
let workspaceLabelPromise = Promise.resolve(null);
|
|
2200
|
+
if (share.workspaceId) {
|
|
2201
|
+
const workspaceCacheKey = `${share.deviceId}:${share.workspaceId}`;
|
|
2202
|
+
const cached = workspaceCache.get(workspaceCacheKey);
|
|
2203
|
+
if (cached) {
|
|
2204
|
+
workspaceLabelPromise = cached;
|
|
2205
|
+
} else {
|
|
2206
|
+
workspaceLabelPromise = fetchRelayWorkspaceLabel(
|
|
2207
|
+
supervisor,
|
|
2208
|
+
share.deviceId,
|
|
2209
|
+
share.workspaceId
|
|
2210
|
+
);
|
|
2211
|
+
workspaceCache.set(workspaceCacheKey, workspaceLabelPromise);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
const [threadTitle, workspaceLabel] = await Promise.all([
|
|
2215
|
+
threadTitlePromise,
|
|
2216
|
+
workspaceLabelPromise
|
|
2217
|
+
]);
|
|
2218
|
+
return {
|
|
2219
|
+
...share,
|
|
2220
|
+
threadTitle,
|
|
2221
|
+
workspaceLabel
|
|
2222
|
+
};
|
|
2223
|
+
};
|
|
2224
|
+
const [sharedWithMe, sharedByMe] = await Promise.all([
|
|
2225
|
+
Promise.all(portal.sharedWithMe.map(enrichShare)),
|
|
2226
|
+
Promise.all(portal.sharedByMe.map(enrichShare))
|
|
2227
|
+
]);
|
|
2228
|
+
return {
|
|
2229
|
+
...portal,
|
|
2230
|
+
sharedWithMe,
|
|
2231
|
+
sharedByMe
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
async function fetchRelayThreadTitle(supervisor, deviceId, threadId) {
|
|
2235
|
+
const payload = await forwardSupervisorJson(
|
|
2236
|
+
supervisor,
|
|
2237
|
+
deviceId,
|
|
2238
|
+
`/api/threads/${encodeURIComponent(threadId)}?limit=1`
|
|
2239
|
+
);
|
|
2240
|
+
const thread = isObject(payload) && isObject(payload.thread) ? payload.thread : payload;
|
|
2241
|
+
return stringField(thread, "title");
|
|
2242
|
+
}
|
|
2243
|
+
async function fetchRelayWorkspaceLabel(supervisor, deviceId, workspaceId) {
|
|
2244
|
+
const payload = await forwardSupervisorJson(supervisor, deviceId, `/api/workspaces/${encodeURIComponent(workspaceId)}`);
|
|
2245
|
+
return stringField(payload, "label");
|
|
2246
|
+
}
|
|
2247
|
+
async function forwardSupervisorJson(supervisor, deviceId, targetPath) {
|
|
2248
|
+
try {
|
|
2249
|
+
const response = await supervisor.requestBroker.forward(
|
|
2250
|
+
supervisor.socket,
|
|
2251
|
+
{
|
|
2252
|
+
type: "relay.request",
|
|
2253
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2254
|
+
requestId: randomUUID(),
|
|
2255
|
+
deviceId,
|
|
2256
|
+
payload: {
|
|
2257
|
+
method: "GET",
|
|
2258
|
+
path: targetPath,
|
|
2259
|
+
headers: {},
|
|
2260
|
+
body: null
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
{ timeoutMs: RELAY_PORTAL_METADATA_TIMEOUT_MS }
|
|
2264
|
+
);
|
|
2265
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
const body = relayJsonBody(response);
|
|
2269
|
+
return JSON.parse(body);
|
|
2270
|
+
} catch {
|
|
2271
|
+
return null;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function relayJsonBody(response) {
|
|
2275
|
+
if (response.bodyEncoding === "base64") {
|
|
2276
|
+
return Buffer.from(response.body, "base64").toString("utf8");
|
|
2277
|
+
}
|
|
2278
|
+
return response.body;
|
|
2279
|
+
}
|
|
2280
|
+
function isObject(value) {
|
|
2281
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2282
|
+
}
|
|
2283
|
+
function stringField(value, field) {
|
|
2284
|
+
if (!isObject(value)) {
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
const fieldValue = value[field];
|
|
2288
|
+
return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue : null;
|
|
2289
|
+
}
|
|
1685
2290
|
function relayAccessDto(access) {
|
|
1686
2291
|
return {
|
|
1687
2292
|
kind: access.kind,
|
|
@@ -1713,6 +2318,39 @@ function workspaceIdFromPath(pathValue) {
|
|
|
1713
2318
|
const match = /^\/api\/workspaces\/([^/?#]+)/.exec(pathname);
|
|
1714
2319
|
return match ? decodeURIComponent(match[1]) : null;
|
|
1715
2320
|
}
|
|
2321
|
+
function conversationEventFromRequest(method, pathValue, body) {
|
|
2322
|
+
if (method.toUpperCase() !== "POST") {
|
|
2323
|
+
return null;
|
|
2324
|
+
}
|
|
2325
|
+
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
2326
|
+
if (pathname === "/api/threads/start") {
|
|
2327
|
+
return {
|
|
2328
|
+
threadId: null,
|
|
2329
|
+
workspaceId: isObject(body) && typeof body.workspaceId === "string" ? body.workspaceId : null
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
const promptMatch = /^\/api\/threads\/([^/?#]+)\/prompt$/.exec(pathname);
|
|
2333
|
+
if (promptMatch) {
|
|
2334
|
+
return {
|
|
2335
|
+
threadId: decodeURIComponent(promptMatch[1]),
|
|
2336
|
+
workspaceId: null
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
function relayClientIp(request) {
|
|
2342
|
+
const forwarded = firstHeaderValue(request.headers["cf-connecting-ip"]) ?? firstHeaderValue(request.headers["x-real-ip"]) ?? firstHeaderValue(request.headers["x-forwarded-for"]);
|
|
2343
|
+
if (forwarded) {
|
|
2344
|
+
return forwarded.split(",")[0]?.trim() || forwarded;
|
|
2345
|
+
}
|
|
2346
|
+
return request.ip || null;
|
|
2347
|
+
}
|
|
2348
|
+
function firstHeaderValue(value) {
|
|
2349
|
+
if (Array.isArray(value)) {
|
|
2350
|
+
return value[0];
|
|
2351
|
+
}
|
|
2352
|
+
return value;
|
|
2353
|
+
}
|
|
1716
2354
|
function isAllowedForRelayAccess(access, method, pathValue) {
|
|
1717
2355
|
if (access.kind === "owner") {
|
|
1718
2356
|
return true;
|
|
@@ -1751,16 +2389,36 @@ function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
|
|
|
1751
2389
|
if (access.threadAccess !== "control") {
|
|
1752
2390
|
return false;
|
|
1753
2391
|
}
|
|
2392
|
+
const controlReadPatterns = [
|
|
2393
|
+
new RegExp(`^/api/threads/${escapedThreadId}/fork-turns$`)
|
|
2394
|
+
];
|
|
2395
|
+
if (methodName === "GET") {
|
|
2396
|
+
return controlReadPatterns.some((pattern) => pattern.test(pathname));
|
|
2397
|
+
}
|
|
1754
2398
|
const controlPatterns = [
|
|
1755
2399
|
new RegExp(`^/api/threads/${escapedThreadId}/goal$`),
|
|
1756
2400
|
new RegExp(`^/api/threads/${escapedThreadId}/resume$`),
|
|
1757
2401
|
new RegExp(`^/api/threads/${escapedThreadId}/prompt$`),
|
|
1758
2402
|
new RegExp(`^/api/threads/${escapedThreadId}/interrupt$`),
|
|
2403
|
+
new RegExp(`^/api/threads/${escapedThreadId}/compact$`),
|
|
2404
|
+
new RegExp(`^/api/threads/${escapedThreadId}/fork$`),
|
|
2405
|
+
new RegExp(`^/api/threads/${escapedThreadId}/hooks$`),
|
|
2406
|
+
new RegExp(`^/api/threads/${escapedThreadId}/hooks/trust$`),
|
|
2407
|
+
new RegExp(`^/api/threads/${escapedThreadId}/hooks/untrust$`),
|
|
1759
2408
|
new RegExp(`^/api/threads/${escapedThreadId}/requests/[^/]+/respond$`)
|
|
1760
2409
|
];
|
|
1761
2410
|
if (methodName === "PATCH") {
|
|
2411
|
+
return [
|
|
2412
|
+
new RegExp(`^/api/threads/${escapedThreadId}/goal$`),
|
|
2413
|
+
new RegExp(`^/api/threads/${escapedThreadId}/settings$`)
|
|
2414
|
+
].some((pattern) => pattern.test(pathname));
|
|
2415
|
+
}
|
|
2416
|
+
if (methodName === "DELETE") {
|
|
1762
2417
|
return new RegExp(`^/api/threads/${escapedThreadId}/goal$`).test(pathname);
|
|
1763
2418
|
}
|
|
2419
|
+
if (methodName === "PUT") {
|
|
2420
|
+
return new RegExp(`^/api/threads/${escapedThreadId}/hooks$`).test(pathname);
|
|
2421
|
+
}
|
|
1764
2422
|
if (methodName === "POST") {
|
|
1765
2423
|
return controlPatterns.some((pattern) => pattern.test(pathname));
|
|
1766
2424
|
}
|