remote-codex 0.11.28 → 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 +771 -48
- package/apps/supervisor-api/dist/index.js +9 -2
- 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-BnLdOiXc.js → thread-ui-C0VPL4Uk.js} +34 -36
- 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-Dpf8YNxk.js +0 -5
- package/apps/supervisor-web/dist/assets/index-IJffwc2e.css +0 -1
|
@@ -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 (
|
|
@@ -697,7 +859,9 @@ var RelayStore = class _RelayStore {
|
|
|
697
859
|
device_id TEXT NOT NULL REFERENCES relay_devices(id) ON DELETE CASCADE,
|
|
698
860
|
device_name TEXT,
|
|
699
861
|
thread_id TEXT NOT NULL,
|
|
862
|
+
thread_title TEXT,
|
|
700
863
|
workspace_id TEXT,
|
|
864
|
+
workspace_label TEXT,
|
|
701
865
|
label TEXT,
|
|
702
866
|
thread_access TEXT NOT NULL DEFAULT 'control',
|
|
703
867
|
workspace_access TEXT NOT NULL DEFAULT 'none',
|
|
@@ -715,11 +879,51 @@ var RelayStore = class _RelayStore {
|
|
|
715
879
|
share_id TEXT NOT NULL REFERENCES relay_shares(id) ON DELETE CASCADE,
|
|
716
880
|
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
717
881
|
username TEXT NOT NULL,
|
|
882
|
+
kind TEXT NOT NULL DEFAULT 'access',
|
|
718
883
|
accessed_at TEXT NOT NULL
|
|
719
884
|
);
|
|
720
885
|
|
|
721
886
|
CREATE INDEX IF NOT EXISTS relay_share_access_events_share_idx ON relay_share_access_events(share_id, accessed_at DESC);
|
|
722
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
|
+
|
|
723
927
|
CREATE TABLE IF NOT EXISTS relay_conversation_events (
|
|
724
928
|
id TEXT PRIMARY KEY,
|
|
725
929
|
user_id TEXT NOT NULL REFERENCES relay_users(id) ON DELETE CASCADE,
|
|
@@ -747,10 +951,17 @@ var RelayStore = class _RelayStore {
|
|
|
747
951
|
`);
|
|
748
952
|
this.ensureColumn("relay_users", "last_seen_at", "TEXT");
|
|
749
953
|
this.ensureColumn("relay_devices", "token", "TEXT");
|
|
954
|
+
this.ensureColumn("relay_shares", "thread_title", "TEXT");
|
|
750
955
|
this.ensureColumn("relay_shares", "workspace_id", "TEXT");
|
|
956
|
+
this.ensureColumn("relay_shares", "workspace_label", "TEXT");
|
|
751
957
|
this.ensureColumn("relay_shares", "thread_access", "TEXT NOT NULL DEFAULT 'control'");
|
|
752
958
|
this.ensureColumn("relay_shares", "workspace_access", "TEXT NOT NULL DEFAULT 'none'");
|
|
753
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'");
|
|
754
965
|
}
|
|
755
966
|
ensureColumn(table, column, definition) {
|
|
756
967
|
const columns = this.sqlite.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -934,9 +1145,9 @@ var RelayStore = class _RelayStore {
|
|
|
934
1145
|
`
|
|
935
1146
|
INSERT INTO relay_shares (
|
|
936
1147
|
id, owner_user_id, owner_username, target_user_id, target_username,
|
|
937
|
-
device_id, device_name, thread_id, workspace_id, label,
|
|
1148
|
+
device_id, device_name, thread_id, thread_title, workspace_id, workspace_label, label,
|
|
938
1149
|
thread_access, workspace_access, created_at, revoked_at, expires_at
|
|
939
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1150
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
940
1151
|
`
|
|
941
1152
|
).run(
|
|
942
1153
|
share.id,
|
|
@@ -947,7 +1158,9 @@ var RelayStore = class _RelayStore {
|
|
|
947
1158
|
share.deviceId,
|
|
948
1159
|
share.deviceName,
|
|
949
1160
|
share.threadId,
|
|
1161
|
+
share.threadTitle,
|
|
950
1162
|
share.workspaceId,
|
|
1163
|
+
share.workspaceLabel,
|
|
951
1164
|
share.label,
|
|
952
1165
|
share.threadAccess,
|
|
953
1166
|
share.workspaceAccess,
|
|
@@ -956,6 +1169,60 @@ var RelayStore = class _RelayStore {
|
|
|
956
1169
|
share.expiresAt
|
|
957
1170
|
);
|
|
958
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
|
+
}
|
|
1206
|
+
updateShareMetadata(shareId, input) {
|
|
1207
|
+
const threadTitle = normalizeOptionalMetadata(input.threadTitle);
|
|
1208
|
+
const workspaceLabel = normalizeOptionalMetadata(input.workspaceLabel);
|
|
1209
|
+
if (threadTitle === void 0 && workspaceLabel === void 0) {
|
|
1210
|
+
return this.rowToShare(
|
|
1211
|
+
this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ?").get(shareId)
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
this.sqlite.prepare(
|
|
1215
|
+
`
|
|
1216
|
+
UPDATE relay_shares
|
|
1217
|
+
SET thread_title = COALESCE(?, thread_title),
|
|
1218
|
+
workspace_label = COALESCE(?, workspace_label)
|
|
1219
|
+
WHERE id = ?
|
|
1220
|
+
`
|
|
1221
|
+
).run(threadTitle ?? null, workspaceLabel ?? null, shareId);
|
|
1222
|
+
return this.rowToShare(
|
|
1223
|
+
this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ?").get(shareId)
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
959
1226
|
getShareAccessEvents(shareId) {
|
|
960
1227
|
return this.sqlite.prepare(
|
|
961
1228
|
`
|
|
@@ -969,6 +1236,24 @@ var RelayStore = class _RelayStore {
|
|
|
969
1236
|
shareId: row.share_id,
|
|
970
1237
|
userId: row.user_id,
|
|
971
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),
|
|
972
1257
|
accessedAt: row.accessed_at
|
|
973
1258
|
}));
|
|
974
1259
|
}
|
|
@@ -995,6 +1280,33 @@ var RelayStore = class _RelayStore {
|
|
|
995
1280
|
this.sqlite.prepare("SELECT * FROM relay_shares WHERE id = ?").get(shareId)
|
|
996
1281
|
);
|
|
997
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
|
+
}
|
|
998
1310
|
getUser(id) {
|
|
999
1311
|
return this.rowToUser(this.sqlite.prepare("SELECT * FROM relay_users WHERE id = ?").get(id));
|
|
1000
1312
|
}
|
|
@@ -1031,6 +1343,107 @@ var RelayStore = class _RelayStore {
|
|
|
1031
1343
|
const rows = options.includeRevoked ? this.sqlite.prepare(sql).all() : this.sqlite.prepare(sql).all((/* @__PURE__ */ new Date()).toISOString());
|
|
1032
1344
|
return rows.map((row) => this.rowToShare(row)).filter((share) => Boolean(share));
|
|
1033
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
|
+
}
|
|
1034
1447
|
conversationCountsByUser(days) {
|
|
1035
1448
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1036
1449
|
const rows = this.sqlite.prepare(
|
|
@@ -1129,12 +1542,41 @@ var RelayStore = class _RelayStore {
|
|
|
1129
1542
|
deviceId: row.device_id,
|
|
1130
1543
|
deviceName: row.device_name ?? "Remote Codex device",
|
|
1131
1544
|
threadId: row.thread_id,
|
|
1132
|
-
threadTitle: null,
|
|
1545
|
+
threadTitle: row.thread_title ?? null,
|
|
1133
1546
|
workspaceId: row.workspace_id ?? null,
|
|
1134
|
-
workspaceLabel: null,
|
|
1547
|
+
workspaceLabel: row.workspace_label ?? null,
|
|
1548
|
+
label: row.label,
|
|
1549
|
+
threadAccess: normalizeThreadAccess(row.thread_access),
|
|
1550
|
+
workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
|
|
1551
|
+
createdAt: row.created_at,
|
|
1552
|
+
revokedAt: row.revoked_at,
|
|
1553
|
+
expiresAt: row.expires_at ?? null,
|
|
1554
|
+
lastAccessedAt: null,
|
|
1555
|
+
lastAccessedByUsername: null,
|
|
1556
|
+
accessEvents: []
|
|
1557
|
+
};
|
|
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),
|
|
1135
1576
|
label: row.label,
|
|
1136
1577
|
threadAccess: normalizeThreadAccess(row.thread_access),
|
|
1137
1578
|
workspaceAccess: normalizeWorkspaceAccess(row.workspace_access),
|
|
1579
|
+
canCreateThreads: Boolean(row.can_create_threads),
|
|
1138
1580
|
createdAt: row.created_at,
|
|
1139
1581
|
revokedAt: row.revoked_at,
|
|
1140
1582
|
expiresAt: row.expires_at ?? null,
|
|
@@ -1179,6 +1621,100 @@ function normalizeWorkspaceAccess(value) {
|
|
|
1179
1621
|
}
|
|
1180
1622
|
return "none";
|
|
1181
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
|
+
}
|
|
1182
1718
|
function normalizeExpiresAt(value) {
|
|
1183
1719
|
if (!value) {
|
|
1184
1720
|
return null;
|
|
@@ -1186,6 +1722,12 @@ function normalizeExpiresAt(value) {
|
|
|
1186
1722
|
const timestamp = Date.parse(value);
|
|
1187
1723
|
return Number.isNaN(timestamp) ? null : new Date(timestamp).toISOString();
|
|
1188
1724
|
}
|
|
1725
|
+
function normalizeOptionalMetadata(value) {
|
|
1726
|
+
if (value === void 0) {
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
return value?.trim() || void 0;
|
|
1730
|
+
}
|
|
1189
1731
|
function normalizeConversationWindowDays(value) {
|
|
1190
1732
|
if (!Number.isFinite(value ?? NaN)) {
|
|
1191
1733
|
return 7;
|
|
@@ -1220,6 +1762,8 @@ var WEBSOCKET_OPEN = 1;
|
|
|
1220
1762
|
var RELAY_COOKIE_NAME = "remote_codex_relay_session";
|
|
1221
1763
|
var threadAccessSchema = z.enum(["read", "control"]);
|
|
1222
1764
|
var workspaceAccessSchema = z.enum(["none", "read", "write"]);
|
|
1765
|
+
var grantScopeSchema = z.enum(["thread", "workspace", "device"]);
|
|
1766
|
+
var workspaceScopeSchema = z.enum(["all", "selected"]);
|
|
1223
1767
|
var loginSchema = z.object({
|
|
1224
1768
|
identifier: z.string().trim().min(1),
|
|
1225
1769
|
password: z.string().min(1)
|
|
@@ -1254,6 +1798,40 @@ var updateShareSchema = z.object({
|
|
|
1254
1798
|
workspaceAccess: workspaceAccessSchema.optional(),
|
|
1255
1799
|
expiresAt: z.string().datetime().nullable().optional()
|
|
1256
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
|
+
});
|
|
1257
1835
|
var setEnabledSchema = z.object({
|
|
1258
1836
|
enabled: z.boolean()
|
|
1259
1837
|
});
|
|
@@ -1461,7 +2039,7 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1461
2039
|
if (!user) {
|
|
1462
2040
|
return;
|
|
1463
2041
|
}
|
|
1464
|
-
return enrichPortalSummary(store.portalSummary(user.id, connectionStatus(state)), state);
|
|
2042
|
+
return enrichPortalSummary(store.portalSummary(user.id, connectionStatus(state)), state, store);
|
|
1465
2043
|
});
|
|
1466
2044
|
app2.get("/relay/access", async (request, reply) => {
|
|
1467
2045
|
const user = requireRelayUser(request, reply, store);
|
|
@@ -1533,6 +2111,44 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1533
2111
|
const { shareId } = z.object({ shareId: z.string().uuid() }).parse(request.params);
|
|
1534
2112
|
return store.revokeShare(user.id, shareId);
|
|
1535
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
|
+
});
|
|
1536
2152
|
app2.get("/relay/admin", async (request, reply) => {
|
|
1537
2153
|
const user = requireRelayUser(request, reply, store, { admin: true });
|
|
1538
2154
|
if (!user) {
|
|
@@ -1719,6 +2335,18 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1719
2335
|
}
|
|
1720
2336
|
if (parsed.type === "relay.server.message") {
|
|
1721
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
|
+
}
|
|
1722
2350
|
if (clientConnection && clientConnection.socket.readyState === WEBSOCKET_OPEN && shouldForwardSocketEvent(parsed.payload, clientConnection.threadId)) {
|
|
1723
2351
|
clientConnection.socket.send(JSON.stringify(parsed.payload));
|
|
1724
2352
|
}
|
|
@@ -1766,9 +2394,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1766
2394
|
return;
|
|
1767
2395
|
}
|
|
1768
2396
|
if (access.kind === "shared") {
|
|
1769
|
-
store
|
|
2397
|
+
recordRelayAccess(store, access, session.user, threadId ? "open_thread" : "open_device");
|
|
1770
2398
|
}
|
|
1771
|
-
connectRelayWebsocket(supervisor, socket, threadId, access);
|
|
2399
|
+
connectRelayWebsocket(supervisor, socket, store, session.user, deviceId, threadId, access);
|
|
1772
2400
|
}
|
|
1773
2401
|
});
|
|
1774
2402
|
realtimeApp.route({
|
|
@@ -1799,9 +2427,9 @@ function buildRelayServer(config2, options = {}) {
|
|
|
1799
2427
|
return;
|
|
1800
2428
|
}
|
|
1801
2429
|
if (access.kind === "shared") {
|
|
1802
|
-
store
|
|
2430
|
+
recordRelayAccess(store, access, session.user, threadId ? "open_thread" : "open_device");
|
|
1803
2431
|
}
|
|
1804
|
-
connectRelayWebsocket(supervisor, socket, threadId, access);
|
|
2432
|
+
connectRelayWebsocket(supervisor, socket, store, session.user, deviceId, threadId, access);
|
|
1805
2433
|
}
|
|
1806
2434
|
});
|
|
1807
2435
|
});
|
|
@@ -1900,7 +2528,13 @@ async function forwardRelayHttp(input) {
|
|
|
1900
2528
|
return;
|
|
1901
2529
|
}
|
|
1902
2530
|
if (access?.kind === "shared") {
|
|
1903
|
-
|
|
2531
|
+
const accessEventKind = relayAccessEventKindFromRequest(
|
|
2532
|
+
input.request.method,
|
|
2533
|
+
input.targetPath
|
|
2534
|
+
);
|
|
2535
|
+
if (accessEventKind) {
|
|
2536
|
+
recordRelayAccess(input.store, access, input.user, accessEventKind);
|
|
2537
|
+
}
|
|
1904
2538
|
}
|
|
1905
2539
|
const conversationEvent = conversationEventFromRequest(
|
|
1906
2540
|
input.request.method,
|
|
@@ -1983,9 +2617,9 @@ function relayResponseBody(response) {
|
|
|
1983
2617
|
}
|
|
1984
2618
|
return response.body;
|
|
1985
2619
|
}
|
|
1986
|
-
function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
2620
|
+
function connectRelayWebsocket(supervisor, socket, store, user, deviceId, threadId, access) {
|
|
1987
2621
|
const clientId = randomUUID();
|
|
1988
|
-
supervisor.clientSockets.set(clientId, { socket, threadId, access });
|
|
2622
|
+
supervisor.clientSockets.set(clientId, { socket, threadId, deviceId, user, access });
|
|
1989
2623
|
sendToSupervisor(supervisor, {
|
|
1990
2624
|
type: "relay.client.connected",
|
|
1991
2625
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1998,7 +2632,19 @@ function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
|
1998
2632
|
} catch {
|
|
1999
2633
|
return;
|
|
2000
2634
|
}
|
|
2001
|
-
|
|
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") {
|
|
2002
2648
|
socket.close(1008, "Shared read-only session cannot control supervisor.");
|
|
2003
2649
|
return;
|
|
2004
2650
|
}
|
|
@@ -2018,6 +2664,9 @@ function connectRelayWebsocket(supervisor, socket, threadId, access) {
|
|
|
2018
2664
|
});
|
|
2019
2665
|
});
|
|
2020
2666
|
}
|
|
2667
|
+
function threadIdFromSocketPayload(payload) {
|
|
2668
|
+
return isObject(payload) && typeof payload.threadId === "string" ? payload.threadId : null;
|
|
2669
|
+
}
|
|
2021
2670
|
function sendToSupervisor(supervisor, message) {
|
|
2022
2671
|
if (supervisor.socket.readyState === WEBSOCKET_OPEN) {
|
|
2023
2672
|
supervisor.socket.send(JSON.stringify(message));
|
|
@@ -2233,13 +2882,16 @@ async function fetchRelayThreads(supervisor, deviceId) {
|
|
|
2233
2882
|
}
|
|
2234
2883
|
return threads.slice(0, 80);
|
|
2235
2884
|
}
|
|
2236
|
-
async function enrichPortalSummary(portal, state) {
|
|
2885
|
+
async function enrichPortalSummary(portal, state, store) {
|
|
2237
2886
|
const threadCache = /* @__PURE__ */ new Map();
|
|
2238
2887
|
const workspaceCache = /* @__PURE__ */ new Map();
|
|
2239
2888
|
const enrichShare = async (share) => {
|
|
2240
2889
|
const supervisor = state.supervisors.get(share.deviceId);
|
|
2241
2890
|
if (!supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
|
|
2242
|
-
return
|
|
2891
|
+
return {
|
|
2892
|
+
...share,
|
|
2893
|
+
threadTitle: stableShareThreadTitle(share)
|
|
2894
|
+
};
|
|
2243
2895
|
}
|
|
2244
2896
|
const threadCacheKey = `${share.deviceId}:${share.threadId}`;
|
|
2245
2897
|
let threadTitlePromise = threadCache.get(threadCacheKey);
|
|
@@ -2266,10 +2918,16 @@ async function enrichPortalSummary(portal, state) {
|
|
|
2266
2918
|
threadTitlePromise,
|
|
2267
2919
|
workspaceLabelPromise
|
|
2268
2920
|
]);
|
|
2921
|
+
if (threadTitle || workspaceLabel) {
|
|
2922
|
+
store.updateShareMetadata(share.id, {
|
|
2923
|
+
threadTitle,
|
|
2924
|
+
workspaceLabel
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2269
2927
|
return {
|
|
2270
2928
|
...share,
|
|
2271
|
-
threadTitle,
|
|
2272
|
-
workspaceLabel
|
|
2929
|
+
threadTitle: threadTitle ?? stableShareThreadTitle(share),
|
|
2930
|
+
workspaceLabel: workspaceLabel ?? share.workspaceLabel
|
|
2273
2931
|
};
|
|
2274
2932
|
};
|
|
2275
2933
|
const [sharedWithMe, sharedByMe] = await Promise.all([
|
|
@@ -2295,6 +2953,14 @@ async function fetchRelayWorkspaceLabel(supervisor, deviceId, workspaceId) {
|
|
|
2295
2953
|
const payload = await forwardSupervisorJson(supervisor, deviceId, `/api/workspaces/${encodeURIComponent(workspaceId)}`);
|
|
2296
2954
|
return stringField(payload, "label");
|
|
2297
2955
|
}
|
|
2956
|
+
function stableShareThreadTitle(share) {
|
|
2957
|
+
const threadTitle = share.threadTitle?.trim();
|
|
2958
|
+
if (!threadTitle) {
|
|
2959
|
+
return null;
|
|
2960
|
+
}
|
|
2961
|
+
const label = share.label?.trim();
|
|
2962
|
+
return label && threadTitle === label ? null : threadTitle;
|
|
2963
|
+
}
|
|
2298
2964
|
async function forwardSupervisorJson(supervisor, deviceId, targetPath, options = {}) {
|
|
2299
2965
|
try {
|
|
2300
2966
|
const response = await supervisor.requestBroker.forward(
|
|
@@ -2341,12 +3007,26 @@ function stringField(value, field) {
|
|
|
2341
3007
|
function relayAccessDto(access) {
|
|
2342
3008
|
return {
|
|
2343
3009
|
kind: access.kind,
|
|
3010
|
+
grantId: access.grant?.id ?? null,
|
|
2344
3011
|
shareId: access.share?.id ?? null,
|
|
3012
|
+
scope: access.scope,
|
|
2345
3013
|
threadAccess: access.threadAccess,
|
|
2346
3014
|
workspaceAccess: access.workspaceAccess,
|
|
2347
|
-
workspaceId: access.workspaceId
|
|
3015
|
+
workspaceId: access.workspaceId,
|
|
3016
|
+
workspaceScope: access.workspaceScope,
|
|
3017
|
+
canCreateThreads: access.canCreateThreads
|
|
2348
3018
|
};
|
|
2349
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
|
+
}
|
|
2350
3030
|
function firstAccessibleConnectedDevice(state, store, userId, threadId) {
|
|
2351
3031
|
for (const deviceId of state.supervisors.keys()) {
|
|
2352
3032
|
if (store.canAccessDevice(userId, deviceId, threadId)) {
|
|
@@ -2366,6 +3046,9 @@ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
|
|
|
2366
3046
|
if (pathname === "/api/agent-runtimes") {
|
|
2367
3047
|
return true;
|
|
2368
3048
|
}
|
|
3049
|
+
if (pathname === "/api/plugins") {
|
|
3050
|
+
return true;
|
|
3051
|
+
}
|
|
2369
3052
|
return /^\/api\/agent-runtimes\/[^/]+\/(?:status|models)$/.test(pathname);
|
|
2370
3053
|
}
|
|
2371
3054
|
function threadIdFromPath(pathValue) {
|
|
@@ -2398,6 +3081,29 @@ function conversationEventFromRequest(method, pathValue, body) {
|
|
|
2398
3081
|
}
|
|
2399
3082
|
return null;
|
|
2400
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
|
+
}
|
|
2401
3107
|
function relayClientIp(request) {
|
|
2402
3108
|
const forwarded = firstHeaderValue(request.headers["cf-connecting-ip"]) ?? firstHeaderValue(request.headers["x-real-ip"]) ?? firstHeaderValue(request.headers["x-forwarded-for"]);
|
|
2403
3109
|
if (forwarded) {
|
|
@@ -2417,6 +3123,12 @@ function isAllowedForRelayAccess(access, method, pathValue) {
|
|
|
2417
3123
|
}
|
|
2418
3124
|
const pathname = new URL(pathValue, "http://relay.local").pathname;
|
|
2419
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
|
+
}
|
|
2420
3132
|
const threadId = threadIdFromPath(pathValue);
|
|
2421
3133
|
if (threadId) {
|
|
2422
3134
|
return isAllowedSharedThreadPath(access, methodName, pathname, threadId);
|
|
@@ -2425,10 +3137,18 @@ function isAllowedForRelayAccess(access, method, pathValue) {
|
|
|
2425
3137
|
if (workspaceId) {
|
|
2426
3138
|
return isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId);
|
|
2427
3139
|
}
|
|
3140
|
+
if (access.scope === "device") {
|
|
3141
|
+
if (methodName === "GET" && (pathname === "/api/threads" || pathname === "/api/workspaces")) {
|
|
3142
|
+
return true;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2428
3145
|
return false;
|
|
2429
3146
|
}
|
|
2430
3147
|
function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
|
|
2431
|
-
if (access.kind !== "shared"
|
|
3148
|
+
if (access.kind !== "shared") {
|
|
3149
|
+
return false;
|
|
3150
|
+
}
|
|
3151
|
+
if (access.scope !== "device" && access.grant.threadId !== threadId) {
|
|
2432
3152
|
return false;
|
|
2433
3153
|
}
|
|
2434
3154
|
const escapedThreadId = escapeRegExp(encodeURIComponent(threadId));
|
|
@@ -2485,7 +3205,10 @@ function isAllowedSharedThreadPath(access, methodName, pathname, threadId) {
|
|
|
2485
3205
|
return false;
|
|
2486
3206
|
}
|
|
2487
3207
|
function isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId) {
|
|
2488
|
-
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) {
|
|
2489
3212
|
return false;
|
|
2490
3213
|
}
|
|
2491
3214
|
const escapedWorkspaceId = escapeRegExp(encodeURIComponent(workspaceId));
|