remote-codex 0.11.29 → 0.11.30
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 +713 -39
- package/apps/supervisor-web/dist/assets/{core-oEok_Crl.js → core-DODVy7wn.js} +1 -1
- package/apps/supervisor-web/dist/assets/{graph-vendor-DVPtkh3h.js → graph-vendor-DVQUpZ8C.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-BnpZn_3_.js +6 -0
- package/apps/supervisor-web/dist/assets/index-CJFMmjP5.css +1 -0
- package/apps/supervisor-web/dist/assets/{markdown-vendor-BQJfKm05.js → markdown-vendor-RZk8L7-L.js} +1 -1
- package/apps/supervisor-web/dist/assets/{react-vendor-CgLzZcV4.js → react-vendor-Dfg_6BLf.js} +1 -1
- package/apps/supervisor-web/dist/assets/{terminal-vendor-B365Go3Z.js → terminal-vendor-C5bTa-Ka.js} +1 -1
- package/apps/supervisor-web/dist/assets/{thread-ui-DXUs5Aqt.js → thread-ui-C0VPL4Uk.js} +32 -32
- package/apps/supervisor-web/dist/assets/{ui-vendor-CeKGesq3.js → ui-vendor-CuR8GHb0.js} +1 -1
- package/apps/supervisor-web/dist/index.html +8 -8
- package/package.json +1 -1
- package/packages/shared/src/index.ts +82 -0
- package/apps/supervisor-web/dist/assets/index-FOSXdyuj.css +0 -1
- package/apps/supervisor-web/dist/assets/index-fi_pAm4J.js +0 -6
|
@@ -284,6 +284,82 @@ var RelayStore = class _RelayStore {
|
|
|
284
284
|
this.insertShare(share);
|
|
285
285
|
return share;
|
|
286
286
|
}
|
|
287
|
+
createGrant(ownerUserId, input) {
|
|
288
|
+
const owner = this.requireUser(ownerUserId);
|
|
289
|
+
const device = this.getDevice(input.deviceId);
|
|
290
|
+
if (!device || device.ownerUserId !== ownerUserId) {
|
|
291
|
+
throw new RelayStoreError(404, "not_found", "Device was not found.");
|
|
292
|
+
}
|
|
293
|
+
const target = this.getUserByIdentifier(input.targetIdentifier);
|
|
294
|
+
if (!target || !target.enabled) {
|
|
295
|
+
throw new RelayStoreError(404, "not_found", "Target user was not found.");
|
|
296
|
+
}
|
|
297
|
+
if (target.id === ownerUserId) {
|
|
298
|
+
throw new RelayStoreError(400, "bad_request", "You cannot share access with yourself.");
|
|
299
|
+
}
|
|
300
|
+
const scope = normalizeShareScope(input.scope);
|
|
301
|
+
const threadId = scope === "thread" ? input.threadId?.trim() || null : null;
|
|
302
|
+
const workspaceId = scope === "workspace" ? input.workspaceId?.trim() || null : null;
|
|
303
|
+
if (scope === "thread" && !threadId) {
|
|
304
|
+
throw new RelayStoreError(400, "bad_request", "threadId is required for thread grants.");
|
|
305
|
+
}
|
|
306
|
+
if (scope === "workspace" && !workspaceId) {
|
|
307
|
+
throw new RelayStoreError(400, "bad_request", "workspaceId is required for workspace grants.");
|
|
308
|
+
}
|
|
309
|
+
const existing = this.rowToGrant(
|
|
310
|
+
this.sqlite.prepare(
|
|
311
|
+
`
|
|
312
|
+
SELECT * FROM relay_access_grants
|
|
313
|
+
WHERE owner_user_id = ?
|
|
314
|
+
AND target_user_id = ?
|
|
315
|
+
AND device_id = ?
|
|
316
|
+
AND scope = ?
|
|
317
|
+
AND COALESCE(thread_id, '') = COALESCE(?, '')
|
|
318
|
+
AND COALESCE(workspace_id, '') = COALESCE(?, '')
|
|
319
|
+
AND revoked_at IS NULL
|
|
320
|
+
`
|
|
321
|
+
).get(ownerUserId, target.id, input.deviceId, scope, threadId, workspaceId)
|
|
322
|
+
);
|
|
323
|
+
if (existing) {
|
|
324
|
+
return this.updateGrantRecord(existing.id, {
|
|
325
|
+
label: input.label?.trim() || null,
|
|
326
|
+
workspaceScope: normalizeWorkspaceScope(input.workspaceScope),
|
|
327
|
+
workspaceIds: normalizeWorkspaceIds(input.workspaceIds),
|
|
328
|
+
threadAccess: normalizeThreadAccess(input.threadAccess),
|
|
329
|
+
workspaceAccess: normalizeWorkspaceAccess(input.workspaceAccess),
|
|
330
|
+
canCreateThreads: Boolean(input.canCreateThreads),
|
|
331
|
+
expiresAt: normalizeExpiresAt(input.expiresAt)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const grant = {
|
|
335
|
+
id: crypto.randomUUID(),
|
|
336
|
+
ownerUserId,
|
|
337
|
+
ownerUsername: owner.username,
|
|
338
|
+
targetUserId: target.id,
|
|
339
|
+
targetUsername: target.username,
|
|
340
|
+
deviceId: input.deviceId,
|
|
341
|
+
deviceName: device.name,
|
|
342
|
+
scope,
|
|
343
|
+
threadId,
|
|
344
|
+
threadTitle: null,
|
|
345
|
+
workspaceId,
|
|
346
|
+
workspaceLabel: null,
|
|
347
|
+
workspaceScope: normalizeWorkspaceScope(input.workspaceScope),
|
|
348
|
+
workspaceIds: normalizeWorkspaceIds(input.workspaceIds),
|
|
349
|
+
label: input.label?.trim() || null,
|
|
350
|
+
threadAccess: normalizeThreadAccess(input.threadAccess),
|
|
351
|
+
workspaceAccess: normalizeWorkspaceAccess(input.workspaceAccess),
|
|
352
|
+
canCreateThreads: Boolean(input.canCreateThreads),
|
|
353
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
354
|
+
revokedAt: null,
|
|
355
|
+
expiresAt: normalizeExpiresAt(input.expiresAt),
|
|
356
|
+
lastAccessedAt: null,
|
|
357
|
+
lastAccessedByUsername: null,
|
|
358
|
+
accessEvents: []
|
|
359
|
+
};
|
|
360
|
+
this.insertGrant(grant);
|
|
361
|
+
return grant;
|
|
362
|
+
}
|
|
287
363
|
updateShare(userId, shareId, input) {
|
|
288
364
|
const share = this.rowToShare(
|
|
289
365
|
this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ? AND owner_user_id = ? AND revoked_at IS NULL").get(shareId, userId)
|
|
@@ -312,15 +388,49 @@ var RelayStore = class _RelayStore {
|
|
|
312
388
|
this.sqlite.prepare("UPDATE relay_shares SET revoked_at = ? WHERE id = ?").run(revokedAt, shareId);
|
|
313
389
|
return { ...share, revokedAt };
|
|
314
390
|
}
|
|
391
|
+
updateGrant(userId, grantId, input) {
|
|
392
|
+
const grant = this.rowToGrant(
|
|
393
|
+
this.sqlite.prepare("SELECT * FROM relay_access_grants WHERE id = ? AND owner_user_id = ? AND revoked_at IS NULL").get(grantId, userId)
|
|
394
|
+
);
|
|
395
|
+
if (!grant) {
|
|
396
|
+
throw new RelayStoreError(404, "not_found", "Grant was not found.");
|
|
397
|
+
}
|
|
398
|
+
return this.publicGrant(
|
|
399
|
+
this.updateGrantRecord(grantId, {
|
|
400
|
+
label: input.label !== void 0 ? input.label?.trim() || null : grant.label,
|
|
401
|
+
workspaceScope: input.workspaceScope !== void 0 ? normalizeWorkspaceScope(input.workspaceScope) : grant.workspaceScope,
|
|
402
|
+
workspaceIds: input.workspaceIds !== void 0 ? normalizeWorkspaceIds(input.workspaceIds) : grant.workspaceIds,
|
|
403
|
+
threadAccess: input.threadAccess !== void 0 ? normalizeThreadAccess(input.threadAccess) : grant.threadAccess,
|
|
404
|
+
workspaceAccess: input.workspaceAccess !== void 0 ? normalizeWorkspaceAccess(input.workspaceAccess) : grant.workspaceAccess,
|
|
405
|
+
canCreateThreads: input.canCreateThreads !== void 0 ? Boolean(input.canCreateThreads) : grant.canCreateThreads,
|
|
406
|
+
expiresAt: input.expiresAt !== void 0 ? normalizeExpiresAt(input.expiresAt) : grant.expiresAt
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
revokeGrant(userId, grantId) {
|
|
411
|
+
const grant = this.rowToGrant(
|
|
412
|
+
this.sqlite.prepare("SELECT * FROM relay_access_grants WHERE id = ? AND owner_user_id = ?").get(grantId, userId)
|
|
413
|
+
);
|
|
414
|
+
if (!grant) {
|
|
415
|
+
throw new RelayStoreError(404, "not_found", "Grant was not found.");
|
|
416
|
+
}
|
|
417
|
+
const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
418
|
+
this.sqlite.prepare("UPDATE relay_access_grants SET revoked_at = ? WHERE id = ?").run(revokedAt, grantId);
|
|
419
|
+
return { ...grant, revokedAt };
|
|
420
|
+
}
|
|
315
421
|
effectiveAccess(userId, deviceId, scope = {}) {
|
|
316
422
|
const owned = this.sqlite.prepare("SELECT 1 FROM relay_devices WHERE id = ? AND owner_user_id = ?").get(deviceId, userId);
|
|
317
423
|
if (owned) {
|
|
318
424
|
return {
|
|
319
425
|
kind: "owner",
|
|
320
426
|
share: null,
|
|
427
|
+
grant: null,
|
|
428
|
+
scope: "owner",
|
|
321
429
|
threadAccess: "control",
|
|
322
430
|
workspaceAccess: "write",
|
|
323
|
-
workspaceId: null
|
|
431
|
+
workspaceId: null,
|
|
432
|
+
workspaceScope: null,
|
|
433
|
+
canCreateThreads: true
|
|
324
434
|
};
|
|
325
435
|
}
|
|
326
436
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -339,19 +449,25 @@ var RelayStore = class _RelayStore {
|
|
|
339
449
|
`
|
|
340
450
|
).get(userId, deviceId, scope.threadId, now)
|
|
341
451
|
);
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
452
|
+
if (share) {
|
|
453
|
+
if (scope.workspaceId && (!share.workspaceId || share.workspaceId !== scope.workspaceId || share.workspaceAccess === "none")) {
|
|
454
|
+
const grant3 = this.findBestGrant(userId, deviceId, scope);
|
|
455
|
+
return grant3 ? this.accessFromGrant(grant3) : null;
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
kind: "shared",
|
|
459
|
+
share,
|
|
460
|
+
grant: this.grantFromShare(share),
|
|
461
|
+
scope: "thread",
|
|
462
|
+
threadAccess: share.threadAccess,
|
|
463
|
+
workspaceAccess: share.workspaceAccess,
|
|
464
|
+
workspaceId: share.workspaceId,
|
|
465
|
+
workspaceScope: "selected",
|
|
466
|
+
canCreateThreads: false
|
|
467
|
+
};
|
|
347
468
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
share,
|
|
351
|
-
threadAccess: share.threadAccess,
|
|
352
|
-
workspaceAccess: share.workspaceAccess,
|
|
353
|
-
workspaceId: share.workspaceId
|
|
354
|
-
};
|
|
469
|
+
const grant2 = this.findBestGrant(userId, deviceId, scope);
|
|
470
|
+
return grant2 ? this.accessFromGrant(grant2) : null;
|
|
355
471
|
}
|
|
356
472
|
if (scope.workspaceId) {
|
|
357
473
|
const share = this.rowToShare(
|
|
@@ -371,18 +487,24 @@ var RelayStore = class _RelayStore {
|
|
|
371
487
|
`
|
|
372
488
|
).get(userId, deviceId, scope.workspaceId, now)
|
|
373
489
|
);
|
|
374
|
-
if (
|
|
375
|
-
return
|
|
490
|
+
if (share) {
|
|
491
|
+
return {
|
|
492
|
+
kind: "shared",
|
|
493
|
+
share,
|
|
494
|
+
grant: this.grantFromShare(share),
|
|
495
|
+
scope: "thread",
|
|
496
|
+
threadAccess: share.threadAccess,
|
|
497
|
+
workspaceAccess: share.workspaceAccess,
|
|
498
|
+
workspaceId: share.workspaceId,
|
|
499
|
+
workspaceScope: "selected",
|
|
500
|
+
canCreateThreads: false
|
|
501
|
+
};
|
|
376
502
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
share,
|
|
380
|
-
threadAccess: share.threadAccess,
|
|
381
|
-
workspaceAccess: share.workspaceAccess,
|
|
382
|
-
workspaceId: share.workspaceId
|
|
383
|
-
};
|
|
503
|
+
const grant2 = this.findBestGrant(userId, deviceId, scope);
|
|
504
|
+
return grant2 ? this.accessFromGrant(grant2) : null;
|
|
384
505
|
}
|
|
385
|
-
|
|
506
|
+
const grant = this.findBestGrant(userId, deviceId, scope);
|
|
507
|
+
return grant ? this.accessFromGrant(grant) : null;
|
|
386
508
|
}
|
|
387
509
|
canAccessDevice(userId, deviceId, threadId) {
|
|
388
510
|
return Boolean(
|
|
@@ -396,11 +518,22 @@ var RelayStore = class _RelayStore {
|
|
|
396
518
|
const devices = this.getDevicesByOwner(userId);
|
|
397
519
|
const sharedWithMe = this.getSharesByTarget(userId);
|
|
398
520
|
const sharedByMe = this.getSharesByOwner(userId);
|
|
521
|
+
const grantsWithMe = this.getGrantsByTarget(userId);
|
|
522
|
+
const grantsByMe = this.getGrantsByOwner(userId);
|
|
399
523
|
return {
|
|
400
524
|
user: this.publicUser(user),
|
|
401
525
|
devices: devices.map((device) => this.publicDevice(device, connectedDevices.get(device.id) ?? null)),
|
|
402
526
|
sharedWithMe: sharedWithMe.map((share) => this.publicShare(share)),
|
|
403
|
-
sharedByMe: sharedByMe.map((share) => this.publicShare(share))
|
|
527
|
+
sharedByMe: sharedByMe.map((share) => this.publicShare(share)),
|
|
528
|
+
sharedDevicesWithMe: grantsWithMe.filter((grant) => grant.scope === "device").map((grant) => this.publicGrant(grant)),
|
|
529
|
+
sharedThreadsWithMe: [
|
|
530
|
+
...sharedWithMe.map((share) => this.publicGrant(this.grantFromShare(share))),
|
|
531
|
+
...grantsWithMe.filter((grant) => grant.scope !== "device").map((grant) => this.publicGrant(grant))
|
|
532
|
+
],
|
|
533
|
+
grantsByMe: [
|
|
534
|
+
...sharedByMe.map((share) => this.publicGrant(this.grantFromShare(share))),
|
|
535
|
+
...grantsByMe.map((grant) => this.publicGrant(grant))
|
|
536
|
+
]
|
|
404
537
|
};
|
|
405
538
|
}
|
|
406
539
|
sharedThreadsForDevice(userId, deviceId) {
|
|
@@ -628,7 +761,7 @@ var RelayStore = class _RelayStore {
|
|
|
628
761
|
threads: metadata?.threadsByDeviceId?.get(device.id) ?? []
|
|
629
762
|
};
|
|
630
763
|
}
|
|
631
|
-
recordShareAccess(share, user) {
|
|
764
|
+
recordShareAccess(share, user, kind = "access") {
|
|
632
765
|
if (share.revokedAt || share.expiresAt && share.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
|
|
633
766
|
return;
|
|
634
767
|
}
|
|
@@ -636,10 +769,23 @@ var RelayStore = class _RelayStore {
|
|
|
636
769
|
this.sqlite.prepare(
|
|
637
770
|
`
|
|
638
771
|
INSERT INTO relay_share_access_events (
|
|
639
|
-
id, share_id, user_id, username, accessed_at
|
|
640
|
-
) VALUES (?, ?, ?, ?, ?)
|
|
772
|
+
id, share_id, user_id, username, kind, accessed_at
|
|
773
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
641
774
|
`
|
|
642
|
-
).run(crypto.randomUUID(), share.id, user.id, user.username, accessedAt);
|
|
775
|
+
).run(crypto.randomUUID(), share.id, user.id, user.username, kind, accessedAt);
|
|
776
|
+
}
|
|
777
|
+
recordGrantAccess(grant, user, kind = "access") {
|
|
778
|
+
if (grant.revokedAt || grant.expiresAt && grant.expiresAt <= (/* @__PURE__ */ new Date()).toISOString()) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const accessedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
782
|
+
this.sqlite.prepare(
|
|
783
|
+
`
|
|
784
|
+
INSERT INTO relay_access_grant_events (
|
|
785
|
+
id, grant_id, user_id, username, kind, accessed_at
|
|
786
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
787
|
+
`
|
|
788
|
+
).run(crypto.randomUUID(), grant.id, user.id, user.username, kind, accessedAt);
|
|
643
789
|
}
|
|
644
790
|
publicShare(share) {
|
|
645
791
|
const owner = this.getUser(share.ownerUserId);
|
|
@@ -657,6 +803,22 @@ var RelayStore = class _RelayStore {
|
|
|
657
803
|
accessEvents
|
|
658
804
|
};
|
|
659
805
|
}
|
|
806
|
+
publicGrant(grant) {
|
|
807
|
+
const owner = this.getUser(grant.ownerUserId);
|
|
808
|
+
const target = this.getUser(grant.targetUserId);
|
|
809
|
+
const device = this.getDevice(grant.deviceId);
|
|
810
|
+
const accessEvents = this.getGrantAccessEvents(grant.id);
|
|
811
|
+
const lastAccess = accessEvents[0] ?? null;
|
|
812
|
+
return {
|
|
813
|
+
...grant,
|
|
814
|
+
ownerUsername: grant.ownerUsername ?? owner?.username ?? "unknown",
|
|
815
|
+
targetUsername: grant.targetUsername ?? target?.username ?? "unknown",
|
|
816
|
+
deviceName: grant.deviceName ?? device?.name ?? "Remote Codex device",
|
|
817
|
+
lastAccessedAt: lastAccess?.accessedAt ?? null,
|
|
818
|
+
lastAccessedByUsername: lastAccess?.username ?? null,
|
|
819
|
+
accessEvents
|
|
820
|
+
};
|
|
821
|
+
}
|
|
660
822
|
migrate() {
|
|
661
823
|
this.sqlite.exec(`
|
|
662
824
|
CREATE TABLE IF NOT EXISTS relay_settings (
|
|
@@ -717,11 +879,51 @@ var RelayStore = class _RelayStore {
|
|
|
717
879
|
share_id TEXT NOT NULL REFERENCES relay_shares(id) ON DELETE CASCADE,
|
|
718
880
|
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
719
881
|
username TEXT NOT NULL,
|
|
882
|
+
kind TEXT NOT NULL DEFAULT 'access',
|
|
720
883
|
accessed_at TEXT NOT NULL
|
|
721
884
|
);
|
|
722
885
|
|
|
723
886
|
CREATE INDEX IF NOT EXISTS relay_share_access_events_share_idx ON relay_share_access_events(share_id, accessed_at DESC);
|
|
724
887
|
|
|
888
|
+
CREATE TABLE IF NOT EXISTS relay_access_grants (
|
|
889
|
+
id TEXT PRIMARY KEY,
|
|
890
|
+
owner_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
891
|
+
owner_username TEXT,
|
|
892
|
+
target_user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
893
|
+
target_username TEXT,
|
|
894
|
+
device_id TEXT NOT NULL REFERENCES relay_devices(id) ON DELETE CASCADE,
|
|
895
|
+
device_name TEXT,
|
|
896
|
+
scope TEXT NOT NULL CHECK (scope IN ('thread', 'workspace', 'device')),
|
|
897
|
+
thread_id TEXT,
|
|
898
|
+
thread_title TEXT,
|
|
899
|
+
workspace_id TEXT,
|
|
900
|
+
workspace_label TEXT,
|
|
901
|
+
workspace_scope TEXT NOT NULL DEFAULT 'all',
|
|
902
|
+
workspace_ids TEXT NOT NULL DEFAULT '[]',
|
|
903
|
+
label TEXT,
|
|
904
|
+
thread_access TEXT NOT NULL DEFAULT 'control',
|
|
905
|
+
workspace_access TEXT NOT NULL DEFAULT 'none',
|
|
906
|
+
can_create_threads INTEGER NOT NULL DEFAULT 0,
|
|
907
|
+
created_at TEXT NOT NULL,
|
|
908
|
+
revoked_at TEXT,
|
|
909
|
+
expires_at TEXT
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
CREATE INDEX IF NOT EXISTS relay_access_grants_owner_idx ON relay_access_grants(owner_user_id);
|
|
913
|
+
CREATE INDEX IF NOT EXISTS relay_access_grants_target_idx ON relay_access_grants(target_user_id);
|
|
914
|
+
CREATE INDEX IF NOT EXISTS relay_access_grants_device_scope_idx ON relay_access_grants(device_id, scope);
|
|
915
|
+
|
|
916
|
+
CREATE TABLE IF NOT EXISTS relay_access_grant_events (
|
|
917
|
+
id TEXT PRIMARY KEY,
|
|
918
|
+
grant_id TEXT NOT NULL REFERENCES relay_access_grants(id) ON DELETE CASCADE,
|
|
919
|
+
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
920
|
+
username TEXT NOT NULL,
|
|
921
|
+
kind TEXT NOT NULL DEFAULT 'access',
|
|
922
|
+
accessed_at TEXT NOT NULL
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
CREATE INDEX IF NOT EXISTS relay_access_grant_events_grant_idx ON relay_access_grant_events(grant_id, accessed_at DESC);
|
|
926
|
+
|
|
725
927
|
CREATE TABLE IF NOT EXISTS relay_conversation_events (
|
|
726
928
|
id TEXT PRIMARY KEY,
|
|
727
929
|
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
@@ -755,6 +957,11 @@ var RelayStore = class _RelayStore {
|
|
|
755
957
|
this.ensureColumn("relay_shares", "thread_access", "TEXT NOT NULL DEFAULT 'control'");
|
|
756
958
|
this.ensureColumn("relay_shares", "workspace_access", "TEXT NOT NULL DEFAULT 'none'");
|
|
757
959
|
this.ensureColumn("relay_shares", "expires_at", "TEXT");
|
|
960
|
+
this.ensureColumn("relay_access_grants", "workspace_scope", "TEXT NOT NULL DEFAULT 'all'");
|
|
961
|
+
this.ensureColumn("relay_access_grants", "workspace_ids", "TEXT NOT NULL DEFAULT '[]'");
|
|
962
|
+
this.ensureColumn("relay_access_grants", "can_create_threads", "INTEGER NOT NULL DEFAULT 0");
|
|
963
|
+
this.ensureColumn("relay_share_access_events", "kind", "TEXT NOT NULL DEFAULT 'access'");
|
|
964
|
+
this.ensureColumn("relay_access_grant_events", "kind", "TEXT NOT NULL DEFAULT 'access'");
|
|
758
965
|
}
|
|
759
966
|
ensureColumn(table, column, definition) {
|
|
760
967
|
const columns = this.sqlite.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -962,6 +1169,40 @@ var RelayStore = class _RelayStore {
|
|
|
962
1169
|
share.expiresAt
|
|
963
1170
|
);
|
|
964
1171
|
}
|
|
1172
|
+
insertGrant(grant) {
|
|
1173
|
+
this.sqlite.prepare(
|
|
1174
|
+
`
|
|
1175
|
+
INSERT INTO relay_access_grants (
|
|
1176
|
+
id, owner_user_id, owner_username, target_user_id, target_username,
|
|
1177
|
+
device_id, device_name, scope, thread_id, thread_title, workspace_id,
|
|
1178
|
+
workspace_label, workspace_scope, workspace_ids, label, thread_access,
|
|
1179
|
+
workspace_access, can_create_threads, created_at, revoked_at, expires_at
|
|
1180
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1181
|
+
`
|
|
1182
|
+
).run(
|
|
1183
|
+
grant.id,
|
|
1184
|
+
grant.ownerUserId,
|
|
1185
|
+
grant.ownerUsername,
|
|
1186
|
+
grant.targetUserId,
|
|
1187
|
+
grant.targetUsername,
|
|
1188
|
+
grant.deviceId,
|
|
1189
|
+
grant.deviceName,
|
|
1190
|
+
grant.scope,
|
|
1191
|
+
grant.threadId,
|
|
1192
|
+
grant.threadTitle,
|
|
1193
|
+
grant.workspaceId,
|
|
1194
|
+
grant.workspaceLabel,
|
|
1195
|
+
grant.workspaceScope,
|
|
1196
|
+
JSON.stringify(grant.workspaceIds),
|
|
1197
|
+
grant.label,
|
|
1198
|
+
grant.threadAccess,
|
|
1199
|
+
grant.workspaceAccess,
|
|
1200
|
+
grant.canCreateThreads ? 1 : 0,
|
|
1201
|
+
grant.createdAt,
|
|
1202
|
+
grant.revokedAt,
|
|
1203
|
+
grant.expiresAt
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
965
1206
|
updateShareMetadata(shareId, input) {
|
|
966
1207
|
const threadTitle = normalizeOptionalMetadata(input.threadTitle);
|
|
967
1208
|
const workspaceLabel = normalizeOptionalMetadata(input.workspaceLabel);
|
|
@@ -995,6 +1236,24 @@ var RelayStore = class _RelayStore {
|
|
|
995
1236
|
shareId: row.share_id,
|
|
996
1237
|
userId: row.user_id,
|
|
997
1238
|
username: row.username,
|
|
1239
|
+
kind: normalizeAccessEventKind(row.kind),
|
|
1240
|
+
accessedAt: row.accessed_at
|
|
1241
|
+
}));
|
|
1242
|
+
}
|
|
1243
|
+
getGrantAccessEvents(grantId) {
|
|
1244
|
+
return this.sqlite.prepare(
|
|
1245
|
+
`
|
|
1246
|
+
SELECT * FROM relay_access_grant_events
|
|
1247
|
+
WHERE grant_id = ?
|
|
1248
|
+
ORDER BY accessed_at DESC
|
|
1249
|
+
LIMIT 8
|
|
1250
|
+
`
|
|
1251
|
+
).all(grantId).map((row) => ({
|
|
1252
|
+
id: row.id,
|
|
1253
|
+
grantId: row.grant_id,
|
|
1254
|
+
userId: row.user_id,
|
|
1255
|
+
username: row.username,
|
|
1256
|
+
kind: normalizeAccessEventKind(row.kind),
|
|
998
1257
|
accessedAt: row.accessed_at
|
|
999
1258
|
}));
|
|
1000
1259
|
}
|
|
@@ -1021,6 +1280,33 @@ var RelayStore = class _RelayStore {
|
|
|
1021
1280
|
this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ?").get(shareId)
|
|
1022
1281
|
);
|
|
1023
1282
|
}
|
|
1283
|
+
updateGrantRecord(grantId, input) {
|
|
1284
|
+
this.sqlite.prepare(
|
|
1285
|
+
`
|
|
1286
|
+
UPDATE relay_access_grants
|
|
1287
|
+
SET label = ?,
|
|
1288
|
+
workspace_scope = ?,
|
|
1289
|
+
workspace_ids = ?,
|
|
1290
|
+
thread_access = ?,
|
|
1291
|
+
workspace_access = ?,
|
|
1292
|
+
can_create_threads = ?,
|
|
1293
|
+
expires_at = ?
|
|
1294
|
+
WHERE id = ?
|
|
1295
|
+
`
|
|
1296
|
+
).run(
|
|
1297
|
+
input.label,
|
|
1298
|
+
input.workspaceScope,
|
|
1299
|
+
JSON.stringify(input.workspaceIds),
|
|
1300
|
+
input.threadAccess,
|
|
1301
|
+
input.workspaceAccess,
|
|
1302
|
+
input.canCreateThreads ? 1 : 0,
|
|
1303
|
+
input.expiresAt,
|
|
1304
|
+
grantId
|
|
1305
|
+
);
|
|
1306
|
+
return this.rowToGrant(
|
|
1307
|
+
this.sqlite.prepare("SELECT * FROM relay_access_grants WHERE id = ?").get(grantId)
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1024
1310
|
getUser(id) {
|
|
1025
1311
|
return this.rowToUser(this.sqlite.prepare("SELECT * FROM relay_users WHERE id = ?").get(id));
|
|
1026
1312
|
}
|
|
@@ -1057,6 +1343,107 @@ var RelayStore = class _RelayStore {
|
|
|
1057
1343
|
const rows = options.includeRevoked ? this.sqlite.prepare(sql).all() : this.sqlite.prepare(sql).all((/* @__PURE__ */ new Date()).toISOString());
|
|
1058
1344
|
return rows.map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1059
1345
|
}
|
|
1346
|
+
getGrantsByOwner(ownerUserId) {
|
|
1347
|
+
return this.sqlite.prepare("SELECT * FROM relay_access_grants WHERE owner_user_id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY created_at ASC").all(ownerUserId, (/* @__PURE__ */ new Date()).toISOString()).map((row) => this.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1348
|
+
}
|
|
1349
|
+
getGrantsByTarget(targetUserId) {
|
|
1350
|
+
return this.sqlite.prepare("SELECT * FROM relay_access_grants 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.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1351
|
+
}
|
|
1352
|
+
findBestGrant(userId, deviceId, scope) {
|
|
1353
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1354
|
+
const rows = this.sqlite.prepare(
|
|
1355
|
+
`
|
|
1356
|
+
SELECT * FROM relay_access_grants
|
|
1357
|
+
WHERE target_user_id = ?
|
|
1358
|
+
AND device_id = ?
|
|
1359
|
+
AND revoked_at IS NULL
|
|
1360
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
1361
|
+
ORDER BY
|
|
1362
|
+
CASE scope
|
|
1363
|
+
WHEN 'thread' THEN 3
|
|
1364
|
+
WHEN 'workspace' THEN 2
|
|
1365
|
+
WHEN 'device' THEN 1
|
|
1366
|
+
ELSE 0
|
|
1367
|
+
END DESC,
|
|
1368
|
+
CASE thread_access WHEN 'control' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC,
|
|
1369
|
+
CASE workspace_access WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC,
|
|
1370
|
+
can_create_threads DESC,
|
|
1371
|
+
created_at DESC
|
|
1372
|
+
`
|
|
1373
|
+
).all(userId, deviceId, now);
|
|
1374
|
+
const grants = rows.map((row) => this.rowToGrant(row)).filter((grant) => Boolean(grant));
|
|
1375
|
+
const matchingGrants = grants.filter((grant) => this.grantMatchesScope(grant, scope));
|
|
1376
|
+
return matchingGrants.length > 0 ? mergeMatchingGrants(matchingGrants, scope) : null;
|
|
1377
|
+
}
|
|
1378
|
+
grantMatchesScope(grant, scope) {
|
|
1379
|
+
if (grant.scope === "thread") {
|
|
1380
|
+
if (!scope.threadId || grant.threadId !== scope.threadId) {
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (grant.scope === "workspace") {
|
|
1386
|
+
if (!scope.workspaceId || grant.workspaceId !== scope.workspaceId || grant.workspaceAccess === "none") {
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
if (grant.scope === "device") {
|
|
1392
|
+
if (scope.workspaceId && grant.workspaceAccess === "none") {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
accessFromGrant(grant) {
|
|
1400
|
+
return {
|
|
1401
|
+
kind: "shared",
|
|
1402
|
+
share: null,
|
|
1403
|
+
grant,
|
|
1404
|
+
scope: grant.scope,
|
|
1405
|
+
threadAccess: grant.threadAccess,
|
|
1406
|
+
workspaceAccess: grant.workspaceAccess,
|
|
1407
|
+
workspaceId: grant.workspaceId,
|
|
1408
|
+
workspaceScope: grant.workspaceScope,
|
|
1409
|
+
canCreateThreads: grant.canCreateThreads
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
grantFromShare(share) {
|
|
1413
|
+
return {
|
|
1414
|
+
id: share.id,
|
|
1415
|
+
ownerUserId: share.ownerUserId,
|
|
1416
|
+
ownerUsername: share.ownerUsername,
|
|
1417
|
+
targetUserId: share.targetUserId,
|
|
1418
|
+
targetUsername: share.targetUsername,
|
|
1419
|
+
deviceId: share.deviceId,
|
|
1420
|
+
deviceName: share.deviceName,
|
|
1421
|
+
scope: "thread",
|
|
1422
|
+
threadId: share.threadId,
|
|
1423
|
+
threadTitle: share.threadTitle,
|
|
1424
|
+
workspaceId: share.workspaceId,
|
|
1425
|
+
workspaceLabel: share.workspaceLabel,
|
|
1426
|
+
workspaceScope: "selected",
|
|
1427
|
+
workspaceIds: share.workspaceId ? [share.workspaceId] : [],
|
|
1428
|
+
label: share.label,
|
|
1429
|
+
threadAccess: share.threadAccess,
|
|
1430
|
+
workspaceAccess: share.workspaceAccess,
|
|
1431
|
+
canCreateThreads: false,
|
|
1432
|
+
createdAt: share.createdAt,
|
|
1433
|
+
revokedAt: share.revokedAt,
|
|
1434
|
+
expiresAt: share.expiresAt,
|
|
1435
|
+
lastAccessedAt: share.lastAccessedAt,
|
|
1436
|
+
lastAccessedByUsername: share.lastAccessedByUsername,
|
|
1437
|
+
accessEvents: share.accessEvents.map((event) => ({
|
|
1438
|
+
id: event.id,
|
|
1439
|
+
grantId: event.shareId,
|
|
1440
|
+
userId: event.userId,
|
|
1441
|
+
username: event.username,
|
|
1442
|
+
kind: event.kind,
|
|
1443
|
+
accessedAt: event.accessedAt
|
|
1444
|
+
}))
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1060
1447
|
conversationCountsByUser(days) {
|
|
1061
1448
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1062
1449
|
const rows = this.sqlite.prepare(
|
|
@@ -1169,6 +1556,35 @@ var RelayStore = class _RelayStore {
|
|
|
1169
1556
|
accessEvents: []
|
|
1170
1557
|
};
|
|
1171
1558
|
}
|
|
1559
|
+
rowToGrant(row) {
|
|
1560
|
+
if (!row) return null;
|
|
1561
|
+
return {
|
|
1562
|
+
id: row.id,
|
|
1563
|
+
ownerUserId: row.owner_user_id,
|
|
1564
|
+
ownerUsername: row.owner_username ?? "unknown",
|
|
1565
|
+
targetUserId: row.target_user_id,
|
|
1566
|
+
targetUsername: row.target_username ?? "unknown",
|
|
1567
|
+
deviceId: row.device_id,
|
|
1568
|
+
deviceName: row.device_name ?? "Remote Codex device",
|
|
1569
|
+
scope: normalizeShareScope(row.scope),
|
|
1570
|
+
threadId: row.thread_id ?? null,
|
|
1571
|
+
threadTitle: row.thread_title ?? null,
|
|
1572
|
+
workspaceId: row.workspace_id ?? null,
|
|
1573
|
+
workspaceLabel: row.workspace_label ?? null,
|
|
1574
|
+
workspaceScope: normalizeWorkspaceScope(row.workspace_scope),
|
|
1575
|
+
workspaceIds: parseWorkspaceIds(row.workspace_ids),
|
|
1576
|
+
label: row.label,
|
|
1577
|
+
threadAccess: normalizeThreadAccess(row.thread_access),
|
|
1578
|
+
workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
|
|
1579
|
+
canCreateThreads: Boolean(row.can_create_threads),
|
|
1580
|
+
createdAt: row.created_at,
|
|
1581
|
+
revokedAt: row.revoked_at,
|
|
1582
|
+
expiresAt: row.expires_at ?? null,
|
|
1583
|
+
lastAccessedAt: null,
|
|
1584
|
+
lastAccessedByUsername: null,
|
|
1585
|
+
accessEvents: []
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1172
1588
|
rowToPendingRegistration(row) {
|
|
1173
1589
|
if (!row) return null;
|
|
1174
1590
|
return {
|
|
@@ -1205,6 +1621,100 @@ function normalizeWorkspaceAccess(value) {
|
|
|
1205
1621
|
}
|
|
1206
1622
|
return "none";
|
|
1207
1623
|
}
|
|
1624
|
+
function normalizeShareScope(value) {
|
|
1625
|
+
if (value === "workspace" || value === "device") {
|
|
1626
|
+
return value;
|
|
1627
|
+
}
|
|
1628
|
+
return "thread";
|
|
1629
|
+
}
|
|
1630
|
+
function normalizeWorkspaceScope(value) {
|
|
1631
|
+
return value === "selected" ? "selected" : "all";
|
|
1632
|
+
}
|
|
1633
|
+
function normalizeWorkspaceIds(values) {
|
|
1634
|
+
if (!Array.isArray(values)) {
|
|
1635
|
+
return [];
|
|
1636
|
+
}
|
|
1637
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean))).sort();
|
|
1638
|
+
}
|
|
1639
|
+
function mergeMatchingGrants(grants, scope) {
|
|
1640
|
+
const sorted = [...grants].sort(compareGrantCapability);
|
|
1641
|
+
const grantScope = mergedGrantScope(sorted);
|
|
1642
|
+
const representative = sorted.find((grant) => grant.scope === grantScope) ?? sorted[0];
|
|
1643
|
+
const threadAccess = sorted.reduce(
|
|
1644
|
+
(best, grant) => maxThreadAccess(best, grant.threadAccess),
|
|
1645
|
+
"read"
|
|
1646
|
+
);
|
|
1647
|
+
const workspaceAccess = sorted.reduce(
|
|
1648
|
+
(best, grant) => maxWorkspaceAccess(best, grant.workspaceAccess),
|
|
1649
|
+
"none"
|
|
1650
|
+
);
|
|
1651
|
+
return {
|
|
1652
|
+
...representative,
|
|
1653
|
+
scope: grantScope,
|
|
1654
|
+
threadId: grantScope === "thread" ? scope.threadId ?? representative.threadId : null,
|
|
1655
|
+
workspaceId: grantScope === "workspace" ? scope.workspaceId ?? representative.workspaceId : null,
|
|
1656
|
+
threadAccess,
|
|
1657
|
+
workspaceAccess,
|
|
1658
|
+
canCreateThreads: sorted.some((grant) => grant.canCreateThreads)
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function mergedGrantScope(grants) {
|
|
1662
|
+
if (grants.some((grant) => grant.scope === "device")) {
|
|
1663
|
+
return "device";
|
|
1664
|
+
}
|
|
1665
|
+
if (grants.some((grant) => grant.scope === "workspace")) {
|
|
1666
|
+
return "workspace";
|
|
1667
|
+
}
|
|
1668
|
+
return "thread";
|
|
1669
|
+
}
|
|
1670
|
+
function maxThreadAccess(left, right) {
|
|
1671
|
+
return threadAccessScore(right) > threadAccessScore(left) ? right : left;
|
|
1672
|
+
}
|
|
1673
|
+
function maxWorkspaceAccess(left, right) {
|
|
1674
|
+
return workspaceAccessScore(right) > workspaceAccessScore(left) ? right : left;
|
|
1675
|
+
}
|
|
1676
|
+
function compareGrantCapability(left, right) {
|
|
1677
|
+
return threadAccessScore(right.threadAccess) - threadAccessScore(left.threadAccess) || workspaceAccessScore(right.workspaceAccess) - workspaceAccessScore(left.workspaceAccess) || Number(right.canCreateThreads) - Number(left.canCreateThreads) || grantScopeScore(right.scope) - grantScopeScore(left.scope) || right.createdAt.localeCompare(left.createdAt);
|
|
1678
|
+
}
|
|
1679
|
+
function threadAccessScore(value) {
|
|
1680
|
+
return value === "control" ? 2 : value === "read" ? 1 : 0;
|
|
1681
|
+
}
|
|
1682
|
+
function workspaceAccessScore(value) {
|
|
1683
|
+
return value === "write" ? 2 : value === "read" ? 1 : 0;
|
|
1684
|
+
}
|
|
1685
|
+
function grantScopeScore(value) {
|
|
1686
|
+
if (value === "thread") {
|
|
1687
|
+
return 3;
|
|
1688
|
+
}
|
|
1689
|
+
if (value === "workspace") {
|
|
1690
|
+
return 2;
|
|
1691
|
+
}
|
|
1692
|
+
return 1;
|
|
1693
|
+
}
|
|
1694
|
+
function normalizeAccessEventKind(value) {
|
|
1695
|
+
switch (value) {
|
|
1696
|
+
case "open_device":
|
|
1697
|
+
case "open_thread":
|
|
1698
|
+
case "create_thread":
|
|
1699
|
+
case "send_prompt":
|
|
1700
|
+
case "read_workspace_file":
|
|
1701
|
+
case "write_workspace_file":
|
|
1702
|
+
return value;
|
|
1703
|
+
default:
|
|
1704
|
+
return "access";
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function parseWorkspaceIds(value) {
|
|
1708
|
+
if (!value) {
|
|
1709
|
+
return [];
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const parsed = JSON.parse(value);
|
|
1713
|
+
return normalizeWorkspaceIds(Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []);
|
|
1714
|
+
} catch {
|
|
1715
|
+
return [];
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1208
1718
|
function normalizeExpiresAt(value) {
|
|
1209
1719
|
if (!value) {
|
|
1210
1720
|
return null;
|
|
@@ -1252,6 +1762,8 @@ var WEBSOCKET_OPEN = 1;
|
|
|
1252
1762
|
var RELAY_COOKIE_NAME = "remote_codex_relay_session";
|
|
1253
1763
|
var threadAccessSchema = z.enum(["read", "control"]);
|
|
1254
1764
|
var workspaceAccessSchema = z.enum(["none", "read", "write"]);
|
|
1765
|
+
var grantScopeSchema = z.enum(["thread", "workspace", "device"]);
|
|
1766
|
+
var workspaceScopeSchema = z.enum(["all", "selected"]);
|
|
1255
1767
|
var loginSchema = z.object({
|
|
1256
1768
|
identifier: z.string().trim().min(1),
|
|
1257
1769
|
password: z.string().min(1)
|
|
@@ -1286,6 +1798,40 @@ var updateShareSchema = z.object({
|
|
|
1286
1798
|
workspaceAccess: workspaceAccessSchema.optional(),
|
|
1287
1799
|
expiresAt: z.string().datetime().nullable().optional()
|
|
1288
1800
|
});
|
|
1801
|
+
var createGrantSchema = z.object({
|
|
1802
|
+
targetIdentifier: z.string().trim().min(1).optional(),
|
|
1803
|
+
targetUsername: z.string().trim().min(3).optional(),
|
|
1804
|
+
deviceId: z.string().uuid(),
|
|
1805
|
+
scope: grantScopeSchema,
|
|
1806
|
+
threadId: z.string().trim().min(1).nullable().optional(),
|
|
1807
|
+
workspaceId: z.string().uuid().nullable().optional(),
|
|
1808
|
+
workspaceScope: workspaceScopeSchema.default("all"),
|
|
1809
|
+
workspaceIds: z.array(z.string().uuid()).default([]),
|
|
1810
|
+
label: z.string().trim().min(1).max(160).nullable().optional(),
|
|
1811
|
+
threadAccess: threadAccessSchema.default("control"),
|
|
1812
|
+
workspaceAccess: workspaceAccessSchema.default("none"),
|
|
1813
|
+
canCreateThreads: z.boolean().optional(),
|
|
1814
|
+
expiresAt: z.string().datetime().nullable().optional()
|
|
1815
|
+
}).refine((input) => input.targetIdentifier || input.targetUsername, {
|
|
1816
|
+
message: "targetIdentifier is required.",
|
|
1817
|
+
path: ["targetIdentifier"]
|
|
1818
|
+
}).refine((input) => input.scope !== "thread" || Boolean(input.threadId), {
|
|
1819
|
+
message: "threadId is required for thread grants.",
|
|
1820
|
+
path: ["threadId"]
|
|
1821
|
+
}).refine((input) => input.scope !== "workspace" || Boolean(input.workspaceId), {
|
|
1822
|
+
message: "workspaceId is required for workspace grants.",
|
|
1823
|
+
path: ["workspaceId"]
|
|
1824
|
+
});
|
|
1825
|
+
var updateGrantSchema = z.object({
|
|
1826
|
+
workspaceId: z.string().uuid().nullable().optional(),
|
|
1827
|
+
workspaceScope: workspaceScopeSchema.optional(),
|
|
1828
|
+
workspaceIds: z.array(z.string().uuid()).optional(),
|
|
1829
|
+
label: z.string().trim().min(1).max(160).nullable().optional(),
|
|
1830
|
+
threadAccess: threadAccessSchema.optional(),
|
|
1831
|
+
workspaceAccess: workspaceAccessSchema.optional(),
|
|
1832
|
+
canCreateThreads: z.boolean().optional(),
|
|
1833
|
+
expiresAt: z.string().datetime().nullable().optional()
|
|
1834
|
+
});
|
|
1289
1835
|
var setEnabledSchema = z.object({
|
|
1290
1836
|
enabled: z.boolean()
|
|
1291
1837
|
});
|
|
@@ -1565,6 +2111,44 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1565
2111
|
const { shareId } = z.object({ shareId: z.string().uuid() }).parse(request.params);
|
|
1566
2112
|
return store.revokeShare(user.id, shareId);
|
|
1567
2113
|
});
|
|
2114
|
+
app2.post("/relay/grants", async (request, reply) => {
|
|
2115
|
+
const user = requireRelayUser(request, reply, store);
|
|
2116
|
+
if (!user) {
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const body = createGrantSchema.parse(request.body ?? {});
|
|
2120
|
+
return store.createGrant(user.id, {
|
|
2121
|
+
targetIdentifier: body.targetIdentifier ?? body.targetUsername,
|
|
2122
|
+
deviceId: body.deviceId,
|
|
2123
|
+
scope: body.scope,
|
|
2124
|
+
threadId: body.threadId ?? null,
|
|
2125
|
+
workspaceId: body.workspaceId ?? null,
|
|
2126
|
+
workspaceScope: body.workspaceScope,
|
|
2127
|
+
workspaceIds: body.workspaceIds,
|
|
2128
|
+
label: body.label ?? null,
|
|
2129
|
+
threadAccess: body.threadAccess,
|
|
2130
|
+
workspaceAccess: body.workspaceAccess,
|
|
2131
|
+
canCreateThreads: body.canCreateThreads ?? false,
|
|
2132
|
+
expiresAt: body.expiresAt ?? null
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
app2.patch("/relay/grants/:grantId", async (request, reply) => {
|
|
2136
|
+
const user = requireRelayUser(request, reply, store);
|
|
2137
|
+
if (!user) {
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const { grantId } = z.object({ grantId: z.string().uuid() }).parse(request.params);
|
|
2141
|
+
const body = updateGrantSchema.parse(request.body ?? {});
|
|
2142
|
+
return store.updateGrant(user.id, grantId, body);
|
|
2143
|
+
});
|
|
2144
|
+
app2.delete("/relay/grants/:grantId", async (request, reply) => {
|
|
2145
|
+
const user = requireRelayUser(request, reply, store);
|
|
2146
|
+
if (!user) {
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const { grantId } = z.object({ grantId: z.string().uuid() }).parse(request.params);
|
|
2150
|
+
return store.revokeGrant(user.id, grantId);
|
|
2151
|
+
});
|
|
1568
2152
|
app2.get("/relay/admin", async (request, reply) => {
|
|
1569
2153
|
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1570
2154
|
if (!user) {
|
|
@@ -1751,6 +2335,18 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1751
2335
|
}
|
|
1752
2336
|
if (parsed.type === "relay.server.message") {
|
|
1753
2337
|
const clientConnection = connection.clientSockets.get(parsed.clientId);
|
|
2338
|
+
if (clientConnection) {
|
|
2339
|
+
const eventThreadId = threadIdFromSocketPayload(parsed.payload);
|
|
2340
|
+
const freshAccess = store.effectiveAccess(clientConnection.user.id, clientConnection.deviceId, {
|
|
2341
|
+
threadId: clientConnection.threadId ?? eventThreadId
|
|
2342
|
+
});
|
|
2343
|
+
if (!freshAccess) {
|
|
2344
|
+
connection.clientSockets.delete(parsed.clientId);
|
|
2345
|
+
clientConnection.socket.close(1008, "Shared access is no longer allowed.");
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
clientConnection.access = freshAccess;
|
|
2349
|
+
}
|
|
1754
2350
|
if (clientConnection && clientConnection.socket.readyState === WEBSOCKET_OPEN && shouldForwardSocketEvent(parsed.payload, clientConnection.threadId)) {
|
|
1755
2351
|
clientConnection.socket.send(JSON.stringify(parsed.payload));
|
|
1756
2352
|
}
|
|
@@ -1798,9 +2394,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1798
2394
|
return;
|
|
1799
2395
|
}
|
|
1800
2396
|
if (access.kind === "shared") {
|
|
1801
|
-
store
|
|
2397
|
+
recordRelayAccess(store, access, session.user, threadId ? "open_thread" : "open_device");
|
|
1802
2398
|
}
|
|
1803
|
-
connectRelayWebsocket(supervisor, socket, threadId, access);
|
|
2399
|
+
connectRelayWebsocket(supervisor, socket, store, session.user, deviceId, threadId, access);
|
|
1804
2400
|
}
|
|
1805
2401
|
});
|
|
1806
2402
|
realtimeApp.route({
|
|
@@ -1831,9 +2427,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1831
2427
|
return;
|
|
1832
2428
|
}
|
|
1833
2429
|
if (access.kind === "shared") {
|
|
1834
|
-
store
|
|
2430
|
+
recordRelayAccess(store, access, session.user, threadId ? "open_thread" : "open_device");
|
|
1835
2431
|
}
|
|
1836
|
-
connectRelayWebsocket(supervisor, socket, threadId, access);
|
|
2432
|
+
connectRelayWebsocket(supervisor, socket, store, session.user, deviceId, threadId, access);
|
|
1837
2433
|
}
|
|
1838
2434
|
});
|
|
1839
2435
|
});
|
|
@@ -1932,7 +2528,13 @@ async function forwardRelayHttp(input) {
|
|
|
1932
2528
|
return;
|
|
1933
2529
|
}
|
|
1934
2530
|
if (access?.kind === "shared") {
|
|
1935
|
-
|
|
2531
|
+
const accessEventKind = relayAccessEventKindFromRequest(
|
|
2532
|
+
input.request.method,
|
|
2533
|
+
input.targetPath
|
|
2534
|
+
);
|
|
2535
|
+
if (accessEventKind) {
|
|
2536
|
+
recordRelayAccess(input.store, access, input.user, accessEventKind);
|
|
2537
|
+
}
|
|
1936
2538
|
}
|
|
1937
2539
|
const conversationEvent = conversationEventFromRequest(
|
|
1938
2540
|
input.request.method,
|
|
@@ -2015,9 +2617,9 @@ function relayResponseBody(response) {
|
|
|
2015
2617
|
}
|
|
2016
2618
|
return response.body;
|
|
2017
2619
|
}
|
|
2018
|
-
function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
2620
|
+
function connectRelayWebsocket(supervisor, socket, store, user, deviceId, threadId, access) {
|
|
2019
2621
|
const clientId = randomUUID();
|
|
2020
|
-
supervisor.clientSockets.set(clientId, { socket, threadId, access });
|
|
2622
|
+
supervisor.clientSockets.set(clientId, { socket, threadId, deviceId, user, access });
|
|
2021
2623
|
sendToSupervisor(supervisor, {
|
|
2022
2624
|
type: "relay.client.connected",
|
|
2023
2625
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2030,7 +2632,19 @@ function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
|
2030
2632
|
} catch {
|
|
2031
2633
|
return;
|
|
2032
2634
|
}
|
|
2033
|
-
|
|
2635
|
+
const freshAccess = store.effectiveAccess(user.id, deviceId, {
|
|
2636
|
+
threadId: threadId ?? threadIdFromSocketPayload(payload)
|
|
2637
|
+
});
|
|
2638
|
+
if (!freshAccess) {
|
|
2639
|
+
supervisor.clientSockets.delete(clientId);
|
|
2640
|
+
socket.close(1008, "Shared access is no longer allowed.");
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const clientConnection = supervisor.clientSockets.get(clientId);
|
|
2644
|
+
if (clientConnection) {
|
|
2645
|
+
clientConnection.access = freshAccess;
|
|
2646
|
+
}
|
|
2647
|
+
if (freshAccess.kind === "shared" && freshAccess.threadAccess !== "control") {
|
|
2034
2648
|
socket.close(1008, "Shared read-only session cannot control supervisor.");
|
|
2035
2649
|
return;
|
|
2036
2650
|
}
|
|
@@ -2050,6 +2664,9 @@ function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
|
2050
2664
|
});
|
|
2051
2665
|
});
|
|
2052
2666
|
}
|
|
2667
|
+
function threadIdFromSocketPayload(payload) {
|
|
2668
|
+
return isObject(payload) && typeof payload.threadId === "string" ? payload.threadId : null;
|
|
2669
|
+
}
|
|
2053
2670
|
function sendToSupervisor(supervisor, message) {
|
|
2054
2671
|
if (supervisor.socket.readyState === WEBSOCKET_OPEN) {
|
|
2055
2672
|
supervisor.socket.send(JSON.stringify(message));
|
|
@@ -2390,12 +3007,26 @@ function stringField(value, field) {
|
|
|
2390
3007
|
function relayAccessDto(access) {
|
|
2391
3008
|
return {
|
|
2392
3009
|
kind: access.kind,
|
|
3010
|
+
grantId: access.grant?.id ?? null,
|
|
2393
3011
|
shareId: access.share?.id ?? null,
|
|
3012
|
+
scope: access.scope,
|
|
2394
3013
|
threadAccess: access.threadAccess,
|
|
2395
3014
|
workspaceAccess: access.workspaceAccess,
|
|
2396
|
-
workspaceId: access.workspaceId
|
|
3015
|
+
workspaceId: access.workspaceId,
|
|
3016
|
+
workspaceScope: access.workspaceScope,
|
|
3017
|
+
canCreateThreads: access.canCreateThreads
|
|
2397
3018
|
};
|
|
2398
3019
|
}
|
|
3020
|
+
function recordRelayAccess(store, access, user, kind) {
|
|
3021
|
+
if (access.kind !== "shared") {
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
if (access.share) {
|
|
3025
|
+
store.recordShareAccess(access.share, user, kind);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
store.recordGrantAccess(access.grant, user, kind);
|
|
3029
|
+
}
|
|
2399
3030
|
function firstAccessibleConnectedDevice(state, store, userId, threadId) {
|
|
2400
3031
|
for (const deviceId of state.supervisors.keys()) {
|
|
2401
3032
|
if (store.canAccessDevice(userId, deviceId, threadId)) {
|
|
@@ -2415,6 +3046,9 @@ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
|
|
|
2415
3046
|
if (pathname === "/api/agent-runtimes") {
|
|
2416
3047
|
return true;
|
|
2417
3048
|
}
|
|
3049
|
+
if (pathname === "/api/plugins") {
|
|
3050
|
+
return true;
|
|
3051
|
+
}
|
|
2418
3052
|
return /^\/api\/agent-runtimes\/[^/]+\/(?:status|models)$/.test(pathname);
|
|
2419
3053
|
}
|
|
2420
3054
|
function threadIdFromPath(pathValue) {
|
|
@@ -2447,6 +3081,29 @@ function conversationEventFromRequest(method, pathValue, body) {
|
|
|
2447
3081
|
}
|
|
2448
3082
|
return null;
|
|
2449
3083
|
}
|
|
3084
|
+
function relayAccessEventKindFromRequest(method, pathValue) {
|
|
3085
|
+
const methodName = method.toUpperCase();
|
|
3086
|
+
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
3087
|
+
if (methodName === "POST" && pathname === "/api/threads/start") {
|
|
3088
|
+
return "create_thread";
|
|
3089
|
+
}
|
|
3090
|
+
if (methodName === "POST" && /^\/api\/threads\/[^/]+\/prompt$/.test(pathname)) {
|
|
3091
|
+
return "send_prompt";
|
|
3092
|
+
}
|
|
3093
|
+
if (methodName === "GET" && /^\/api\/threads\/[^/]+$/.test(pathname)) {
|
|
3094
|
+
return "open_thread";
|
|
3095
|
+
}
|
|
3096
|
+
if (methodName === "GET" && /^\/api\/workspaces\/[^/]+$/.test(pathname)) {
|
|
3097
|
+
return "open_device";
|
|
3098
|
+
}
|
|
3099
|
+
if (methodName === "GET" && /^\/api\/workspaces\/[^/]+\/(?:files\/(?:tree|preview|raw|download)|artifacts(?:\/[^/]+(?:\/download)?)?)$/.test(pathname)) {
|
|
3100
|
+
return "read_workspace_file";
|
|
3101
|
+
}
|
|
3102
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(methodName) && /^\/api\/workspaces\/[^/]+\/files(?:\/(?:upload|move))?$/.test(pathname)) {
|
|
3103
|
+
return "write_workspace_file";
|
|
3104
|
+
}
|
|
3105
|
+
return null;
|
|
3106
|
+
}
|
|
2450
3107
|
function relayClientIp(request) {
|
|
2451
3108
|
const forwarded = firstHeaderValue(request.headers["cf-connecting-ip"]) ?? firstHeaderValue(request.headers["x-real-ip"]) ?? firstHeaderValue(request.headers["x-forwarded-for"]);
|
|
2452
3109
|
if (forwarded) {
|
|
@@ -2466,6 +3123,12 @@ function isAllowedForRelayAccess(access, method, pathValue) {
|
|
|
2466
3123
|
}
|
|
2467
3124
|
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
2468
3125
|
const methodName = method.toUpperCase();
|
|
3126
|
+
if (isAllowedSharedRuntimeMetadataRequest(methodName, pathname)) {
|
|
3127
|
+
return true;
|
|
3128
|
+
}
|
|
3129
|
+
if (access.scope === "device" && methodName === "POST" && pathname === "/api/threads/start") {
|
|
3130
|
+
return access.canCreateThreads && access.threadAccess === "control";
|
|
3131
|
+
}
|
|
2469
3132
|
const threadId = threadIdFromPath(pathValue);
|
|
2470
3133
|
if (threadId) {
|
|
2471
3134
|
return isAllowedSharedThreadPath(access, methodName, pathname, threadId);
|
|
@@ -2474,10 +3137,18 @@ function isAllowedForRelayAccess(access, method, pathValue) {
|
|
|
2474
3137
|
if (workspaceId) {
|
|
2475
3138
|
return isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId);
|
|
2476
3139
|
}
|
|
3140
|
+
if (access.scope === "device") {
|
|
3141
|
+
if (methodName === "GET" && (pathname === "/api/threads" || pathname === "/api/workspaces")) {
|
|
3142
|
+
return true;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2477
3145
|
return false;
|
|
2478
3146
|
}
|
|
2479
3147
|
function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
|
|
2480
|
-
if (access.kind !== "shared"
|
|
3148
|
+
if (access.kind !== "shared") {
|
|
3149
|
+
return false;
|
|
3150
|
+
}
|
|
3151
|
+
if (access.scope !== "device" && access.grant.threadId !== threadId) {
|
|
2481
3152
|
return false;
|
|
2482
3153
|
}
|
|
2483
3154
|
const escapedThreadId = escapeRegExp(encodeURIComponent(threadId));
|
|
@@ -2534,7 +3205,10 @@ function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
|
|
|
2534
3205
|
return false;
|
|
2535
3206
|
}
|
|
2536
3207
|
function isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId) {
|
|
2537
|
-
if (access.kind !== "shared" || access.workspaceAccess === "none"
|
|
3208
|
+
if (access.kind !== "shared" || access.workspaceAccess === "none") {
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
if (access.scope !== "device" && access.workspaceId !== workspaceId) {
|
|
2538
3212
|
return false;
|
|
2539
3213
|
}
|
|
2540
3214
|
const escapedWorkspaceId = escapeRegExp(encodeURIComponent(workspaceId));
|