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