openmausbot 0.1.90 → 0.1.92
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/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +128 -15
- package/dist-server/container-mcp.js +277 -45
- package/dist-server/drivers/agents-proxy.js +16 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +46703 -39955
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4099 -1217
- package/dist-server/pair-cli.js +4099 -1217
- package/dist-server/server/boat.js +67 -22
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +136 -26
- package/dist-server/server/cloud-home.js +64 -5
- package/dist-server/server/cloud-lending.js +167 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/config.js +215 -17
- package/dist-server/server/container-computer.js +3 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/agents-catalog.js +22 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +313 -34
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1753 -263
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +25 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routes/desktop-viewer.js +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +79 -124
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +20 -11
- package/dist-server/server/vps-computer.js +79 -12
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +25 -3
- package/dist-server/server/workspace-backup.js +28 -8
- package/dist-server/server/workspace.js +65 -15
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +278 -45
- package/dist-server/workspace-backup.worker.js +184 -38
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -41,6 +41,7 @@ export function createBotMemoryRoutes(deps) {
|
|
|
41
41
|
return async ({ req, res, url, path, method, auth, json, readBody }) => {
|
|
42
42
|
/** scratch for route matches, shared by every `path.match` below */
|
|
43
43
|
let m = null;
|
|
44
|
+
const write = (botId, change) => deps.ownersWrite ? deps.ownersWrite(auth, botId, change) : change();
|
|
44
45
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
|
|
45
46
|
if (m && method === "GET") {
|
|
46
47
|
if (!deps.bot(m[1]))
|
|
@@ -49,7 +50,9 @@ export function createBotMemoryRoutes(deps) {
|
|
|
49
50
|
const overview = memoryOverview(m[1]);
|
|
50
51
|
// `text` and `truncated` ride along one release for clients of the
|
|
51
52
|
// old whole-file shape; the panel reads the file through /memory/file
|
|
52
|
-
|
|
53
|
+
const review = deps.lendingReview?.needed(m[1]);
|
|
54
|
+
return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated,
|
|
55
|
+
...(review ? { lendingReview: review } : {}) });
|
|
53
56
|
}
|
|
54
57
|
catch (error) {
|
|
55
58
|
return replyMemoryError(res, error);
|
|
@@ -65,7 +68,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
65
68
|
if (!parsed.success)
|
|
66
69
|
return json(res, 400, { error: "text must be a string" });
|
|
67
70
|
try {
|
|
68
|
-
const
|
|
71
|
+
const botId = m[1];
|
|
72
|
+
const { doc } = write(botId, () => journalMemoryWrite(botId, MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" }));
|
|
69
73
|
return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
|
|
70
74
|
}
|
|
71
75
|
catch (error) {
|
|
@@ -76,6 +80,24 @@ export function createBotMemoryRoutes(deps) {
|
|
|
76
80
|
return replyMemoryError(res, error);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
83
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/reviewed$/);
|
|
84
|
+
if (m && method === "POST" && deps.lendingReview) {
|
|
85
|
+
if (!deps.bot(m[1]))
|
|
86
|
+
return json(res, 404, { error: "no such bot" });
|
|
87
|
+
if (!deps.lendingReview.owner(auth)) {
|
|
88
|
+
return json(res, 403, { error: "Only the owner of this Cloud can review this bot's memory, from one of their own devices." });
|
|
89
|
+
}
|
|
90
|
+
// Bound to what the owner was shown: the token names the files exactly
|
|
91
|
+
// as the panel read them. Anything changed since is shown again.
|
|
92
|
+
const parsed = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) }).safeParse(await readBody(req));
|
|
93
|
+
if (!parsed.success)
|
|
94
|
+
return json(res, 400, { error: "token is required: the lendingReview token the Memory panel was shown" });
|
|
95
|
+
if (!deps.lendingReview.accept(m[1], parsed.data.token)) {
|
|
96
|
+
return json(res, 409, { error: "This bot's memory changed since you looked. Review it again.", code: "changed",
|
|
97
|
+
lendingReview: deps.lendingReview.needed(m[1]) });
|
|
98
|
+
}
|
|
99
|
+
return json(res, 200, { ok: true, lendingReview: false });
|
|
100
|
+
}
|
|
79
101
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
|
|
80
102
|
if (m && method === "GET") {
|
|
81
103
|
if (!deps.bot(m[1]))
|
|
@@ -96,11 +118,12 @@ export function createBotMemoryRoutes(deps) {
|
|
|
96
118
|
if (!parsed.success)
|
|
97
119
|
return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
|
|
98
120
|
try {
|
|
99
|
-
const
|
|
121
|
+
const botId = m[1];
|
|
122
|
+
const { doc, entry } = write(botId, () => journalMemoryWrite(botId, parsed.data.path, parsed.data.text, {
|
|
100
123
|
actor: "person",
|
|
101
124
|
via: "ui",
|
|
102
125
|
expectedHash: parsed.data.expectedHash,
|
|
103
|
-
});
|
|
126
|
+
}));
|
|
104
127
|
return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
105
128
|
}
|
|
106
129
|
catch (error) {
|
|
@@ -112,7 +135,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
112
135
|
return json(res, 404, { error: "no such bot" });
|
|
113
136
|
const file = url.searchParams.get("path") ?? "";
|
|
114
137
|
try {
|
|
115
|
-
const
|
|
138
|
+
const botId = m[1];
|
|
139
|
+
const entry = write(botId, () => journalMemoryDelete(botId, file, { actor: "person", via: "ui" }));
|
|
116
140
|
return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
117
141
|
}
|
|
118
142
|
catch (error) {
|
|
@@ -134,7 +158,8 @@ export function createBotMemoryRoutes(deps) {
|
|
|
134
158
|
if (!deps.bot(m[1]))
|
|
135
159
|
return json(res, 404, { error: "no such bot" });
|
|
136
160
|
await flushMemoryJournal(m[1]);
|
|
137
|
-
const
|
|
161
|
+
const botId = m[1], changeId = m[2];
|
|
162
|
+
const result = write(botId, () => revertMemoryChange(botId, changeId));
|
|
138
163
|
if (!result.ok)
|
|
139
164
|
return json(res, result.status, { error: result.error });
|
|
140
165
|
return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PASS } from "./table.js";
|
|
2
|
+
const MAX_KEY_LENGTH = 512;
|
|
3
|
+
export function createDeciderRoutes(deps) {
|
|
4
|
+
return async ({ req, res, path, method, json, readBody }) => {
|
|
5
|
+
if (path !== "/api/decider/test" || method !== "POST")
|
|
6
|
+
return PASS;
|
|
7
|
+
const body = await readBody(req, 8192);
|
|
8
|
+
if (body?.key !== undefined && typeof body.key !== "string")
|
|
9
|
+
return json(res, 400, { error: "key must be a string" });
|
|
10
|
+
const draft = typeof body?.key === "string" ? body.key.trim() : "";
|
|
11
|
+
if (draft.length > MAX_KEY_LENGTH)
|
|
12
|
+
return json(res, 400, { error: "That does not look like an API key." });
|
|
13
|
+
res.setHeader("cache-control", "no-store");
|
|
14
|
+
const result = await deps.decider.testKey(draft ? { key: draft } : {});
|
|
15
|
+
if (result.ok)
|
|
16
|
+
return json(res, 200, { ok: true, latencyMs: result.latencyMs });
|
|
17
|
+
return json(res, 200, { ok: false, reason: result.reason, ...(result.status ? { status: result.status } : {}) });
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Only the RFB WebSocket crosses the container boundary. The viewer page is
|
|
2
|
+
// bundled with the app: serving container-controlled JavaScript on the app's
|
|
3
|
+
// origin would give a bot access to the person's authenticated workspace.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { request, ServerResponse } from "node:http";
|
|
6
|
+
import { Socket } from "node:net";
|
|
7
|
+
import { isSameOrigin } from "../request-auth.js";
|
|
8
|
+
import { PASS } from "./table.js";
|
|
9
|
+
const ROUTE = /^\/api\/desktop-viewer\/(local\/(?:shared|bot-[a-f0-9]{64}|pool-\d+)|vps\/[\w-]+)(\/websockify)?$/;
|
|
10
|
+
const HANDSHAKE_MS = 10_000;
|
|
11
|
+
const RECHECK_MS = 5_000;
|
|
12
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
13
|
+
export function desktopViewerUrl(target, threadId) {
|
|
14
|
+
const params = new URLSearchParams({ target, ...(threadId ? { threadId } : {}) });
|
|
15
|
+
return `/desktop-viewer#${params}`;
|
|
16
|
+
}
|
|
17
|
+
export function createDesktopViewer(deps) {
|
|
18
|
+
const upgrades = new Map();
|
|
19
|
+
let stopped = false;
|
|
20
|
+
/** Run upgrades through the very same authentication, hosted-workspace and
|
|
21
|
+
* maintenance gates as HTTP. Only this route can detach the response socket. */
|
|
22
|
+
function attach(server, handle) {
|
|
23
|
+
server.on("upgrade", (req, socket, head) => {
|
|
24
|
+
if (stopped || !(socket instanceof Socket)) {
|
|
25
|
+
socket.destroy();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const res = new ServerResponse(req);
|
|
29
|
+
res.assignSocket(socket);
|
|
30
|
+
// Rejected upgrades end this socket; do not let HTTP clients pool it.
|
|
31
|
+
res.setHeader("connection", "close");
|
|
32
|
+
socket.on("error", () => socket.destroy());
|
|
33
|
+
res.once("finish", () => socket.end(() => socket.destroy()));
|
|
34
|
+
let path;
|
|
35
|
+
try {
|
|
36
|
+
path = new URL(req.url ?? "", "http://localhost").pathname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
res.writeHead(400).end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!ROUTE.exec(path)?.[2]) {
|
|
43
|
+
res.writeHead(404).end();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Keep reading while auth/inspection awaits, so a closed tab's FIN is
|
|
47
|
+
// observed. Bound and preserve any eagerly sent WebSocket bytes.
|
|
48
|
+
const upgrade = { socket, head, release: () => socket.off("data", buffer), close: () => socket.destroy() };
|
|
49
|
+
const buffer = (data) => {
|
|
50
|
+
if (upgrade.head.length + data.length > 64 * 1024)
|
|
51
|
+
socket.destroy();
|
|
52
|
+
else
|
|
53
|
+
upgrade.head = Buffer.concat([upgrade.head, data]);
|
|
54
|
+
};
|
|
55
|
+
socket.on("data", buffer);
|
|
56
|
+
socket.once("end", () => socket.destroy());
|
|
57
|
+
socket.once("close", () => { upgrade.release(); upgrades.delete(req); });
|
|
58
|
+
upgrades.set(req, upgrade);
|
|
59
|
+
const timeout = setTimeout(() => socket.destroy(), HANDSHAKE_MS);
|
|
60
|
+
timeout.unref();
|
|
61
|
+
socket.once("close", () => clearTimeout(timeout));
|
|
62
|
+
void handle(req, res).catch(() => {
|
|
63
|
+
if (!res.headersSent)
|
|
64
|
+
res.writeHead(500).end();
|
|
65
|
+
else
|
|
66
|
+
socket.destroy();
|
|
67
|
+
}).finally(() => clearTimeout(timeout));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const route = async ({ req, res, path, method, auth, json }) => {
|
|
71
|
+
const match = ROUTE.exec(path);
|
|
72
|
+
if (!match)
|
|
73
|
+
return PASS;
|
|
74
|
+
res.setHeader("cache-control", "private, no-store");
|
|
75
|
+
if (method !== "GET")
|
|
76
|
+
return json(res, 405, { error: "method not allowed" });
|
|
77
|
+
// Also enforce at this boundary; the central gate defaults these paths
|
|
78
|
+
// to admin, like the existing Local VM status and control endpoints.
|
|
79
|
+
if (!auth.scopes.includes("admin") || !isSameOrigin(req))
|
|
80
|
+
return json(res, 403, { error: "forbidden" });
|
|
81
|
+
const target = deps.target(match[1]);
|
|
82
|
+
if (!target)
|
|
83
|
+
return json(res, 404, { error: "Desktop not found" });
|
|
84
|
+
const upgrade = upgrades.get(req);
|
|
85
|
+
if (upgrade && auth.kind === "session")
|
|
86
|
+
upgrade.owner = auth.session.id;
|
|
87
|
+
let connection;
|
|
88
|
+
try {
|
|
89
|
+
connection = await target.resolve();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 502;
|
|
93
|
+
return json(res, typeof status === "number" && status >= 400 && status < 600 ? status : 502, { error: "The desktop is not available. Open it again from OpenMausBot." });
|
|
94
|
+
}
|
|
95
|
+
// Inspection may outlive a closed tab or the handshake deadline.
|
|
96
|
+
if (res.destroyed || upgrade?.socket.destroyed)
|
|
97
|
+
return;
|
|
98
|
+
const live = () => deps.live(auth) && deps.target(match[1])?.key === target.key && (connection.live?.() ?? true);
|
|
99
|
+
if (!live())
|
|
100
|
+
return json(res, 401, { error: "Viewer access expired" });
|
|
101
|
+
if (!Number.isInteger(connection.port) || connection.port < 1 || connection.port > 65535) {
|
|
102
|
+
return json(res, 409, { error: "The desktop viewer is not available." });
|
|
103
|
+
}
|
|
104
|
+
if (!match[2])
|
|
105
|
+
return json(res, 200, { password: connection.password });
|
|
106
|
+
if (!upgrade)
|
|
107
|
+
return json(res, 426, { error: "WebSocket upgrade required" });
|
|
108
|
+
const key = req.headers["sec-websocket-key"];
|
|
109
|
+
if (req.headers.upgrade?.toLowerCase() !== "websocket" || req.headers["sec-websocket-version"] !== "13"
|
|
110
|
+
|| typeof key !== "string" || !/^[A-Za-z0-9+/]{22}==$/.test(key)) {
|
|
111
|
+
return json(res, 400, { error: "Invalid WebSocket handshake" });
|
|
112
|
+
}
|
|
113
|
+
// Fixed host and path, port from an inspected managed container. Never
|
|
114
|
+
// forward cookies, bearer tokens, query parameters or forwarded headers.
|
|
115
|
+
const upstream = request({
|
|
116
|
+
hostname: "127.0.0.1", port: connection.port, path: "/websockify",
|
|
117
|
+
headers: { Upgrade: "websocket", Connection: "Upgrade", "Sec-WebSocket-Key": key, "Sec-WebSocket-Version": "13" },
|
|
118
|
+
});
|
|
119
|
+
const { socket } = upgrade;
|
|
120
|
+
let peer;
|
|
121
|
+
let recheck;
|
|
122
|
+
const release = connection.retain?.();
|
|
123
|
+
let closed = false;
|
|
124
|
+
const timeout = setTimeout(() => fail(), HANDSHAKE_MS);
|
|
125
|
+
timeout.unref();
|
|
126
|
+
upgrade.close = () => {
|
|
127
|
+
if (closed)
|
|
128
|
+
return;
|
|
129
|
+
closed = true;
|
|
130
|
+
release?.();
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
clearInterval(recheck);
|
|
133
|
+
upstream.destroy();
|
|
134
|
+
peer?.destroy();
|
|
135
|
+
socket.destroy();
|
|
136
|
+
};
|
|
137
|
+
const fail = () => {
|
|
138
|
+
if (closed)
|
|
139
|
+
return;
|
|
140
|
+
if (peer || socket.destroyed) {
|
|
141
|
+
upgrade.close();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!res.headersSent && !socket.destroyed)
|
|
145
|
+
json(res, 502, { error: "Could not connect to the desktop viewer" });
|
|
146
|
+
upstream.destroy();
|
|
147
|
+
};
|
|
148
|
+
socket.once("close", upgrade.close);
|
|
149
|
+
upstream.once("error", fail);
|
|
150
|
+
upstream.once("response", (answer) => { answer.resume(); fail(); });
|
|
151
|
+
upstream.once("upgrade", (answer, remote, remoteHead) => {
|
|
152
|
+
const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
153
|
+
if (!live() || socket.destroyed || answer.headers["sec-websocket-accept"] !== accept
|
|
154
|
+
|| answer.headers.upgrade?.toLowerCase() !== "websocket") {
|
|
155
|
+
remote.destroy();
|
|
156
|
+
fail();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
peer = remote;
|
|
161
|
+
remote.once("error", upgrade.close);
|
|
162
|
+
remote.once("close", upgrade.close);
|
|
163
|
+
upgrade.release();
|
|
164
|
+
res.detachSocket(socket);
|
|
165
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
166
|
+
if (remoteHead.length)
|
|
167
|
+
socket.write(remoteHead);
|
|
168
|
+
if (upgrade.head.length)
|
|
169
|
+
remote.write(upgrade.head);
|
|
170
|
+
upgrade.head = Buffer.alloc(0);
|
|
171
|
+
socket.pipe(remote).pipe(socket);
|
|
172
|
+
connection.touch?.();
|
|
173
|
+
recheck = setInterval(() => {
|
|
174
|
+
if (!live())
|
|
175
|
+
upgrade.close();
|
|
176
|
+
else
|
|
177
|
+
connection.touch?.();
|
|
178
|
+
}, RECHECK_MS);
|
|
179
|
+
recheck.unref();
|
|
180
|
+
});
|
|
181
|
+
upstream.end();
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
route, attach,
|
|
185
|
+
closeForOwner: (owner) => { for (const upgrade of upgrades.values())
|
|
186
|
+
if (upgrade.owner === owner)
|
|
187
|
+
upgrade.close(); },
|
|
188
|
+
closeAll: () => {
|
|
189
|
+
stopped = true;
|
|
190
|
+
for (const upgrade of upgrades.values())
|
|
191
|
+
upgrade.close();
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -468,7 +468,7 @@ function normalizedOperation(manager, botId, validated, now) {
|
|
|
468
468
|
...(validated.forBot ? { forBot: validated.forBot } : {}),
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
|
-
function asSchedule(schedule, now) {
|
|
471
|
+
export function asSchedule(schedule, now) {
|
|
472
472
|
if (schedule.type === "cron")
|
|
473
473
|
return { ...schedule };
|
|
474
474
|
if (schedule.type === "once")
|
|
@@ -1293,7 +1293,7 @@ export class RoutineManager {
|
|
|
1293
1293
|
? run.groupId
|
|
1294
1294
|
? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
|
|
1295
1295
|
: null
|
|
1296
|
-
: this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook");
|
|
1296
|
+
: this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook", run.routineId);
|
|
1297
1297
|
if (!task) {
|
|
1298
1298
|
this.failRun(run, run.target === "room-goal"
|
|
1299
1299
|
? "Could not create a room task for this goal"
|
|
@@ -72,9 +72,18 @@ const sessionSchema = z.object({
|
|
|
72
72
|
email: z.string().max(320).optional(),
|
|
73
73
|
/** Set only by the internal verified-portal issuance path, never by email or pairing input. */
|
|
74
74
|
membershipAuthority: z.literal("portal").optional(),
|
|
75
|
+
/** A browser sign-in (PairingCode `browser`): accepted only as its browser's cookie, never as a bearer token. */
|
|
76
|
+
cookieOnly: z.literal(true).optional(),
|
|
77
|
+
/** Whose OMB Cloud a browser sign-in signed in to (its PairingCode `owner`). */
|
|
78
|
+
owner: z.string().max(254).optional(),
|
|
75
79
|
});
|
|
76
80
|
const fileSchema = z.object({ version: z.literal(1), sessions: z.array(sessionSchema) });
|
|
77
81
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
82
|
+
/** A device's name as the sessions list shows it: what a client sent, less
|
|
83
|
+
* control and bidirectional-formatting characters. */
|
|
84
|
+
function deviceLabel(value) {
|
|
85
|
+
return (value ?? "").replace(/[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu, "").trim();
|
|
86
|
+
}
|
|
78
87
|
function sameDigest(a, b) {
|
|
79
88
|
const left = Buffer.from(a, "hex");
|
|
80
89
|
const right = Buffer.from(b, "hex");
|
|
@@ -130,6 +139,8 @@ function publicSession(record) {
|
|
|
130
139
|
};
|
|
131
140
|
if (record.email)
|
|
132
141
|
view.email = record.email;
|
|
142
|
+
if (record.owner)
|
|
143
|
+
view.owner = record.owner;
|
|
133
144
|
return view;
|
|
134
145
|
}
|
|
135
146
|
export class SessionRegistry {
|
|
@@ -144,6 +155,8 @@ export class SessionRegistry {
|
|
|
144
155
|
options;
|
|
145
156
|
openMarker;
|
|
146
157
|
closed = false;
|
|
158
|
+
/** Set on an OMB Cloud home (requireAdmin): only admin scope, anywhere. */
|
|
159
|
+
adminOnly = null;
|
|
147
160
|
// No parameter properties: the server runs this file under Node's
|
|
148
161
|
// strip-only TypeScript mode, which only erases types.
|
|
149
162
|
constructor(options) {
|
|
@@ -252,6 +265,20 @@ export class SessionRegistry {
|
|
|
252
265
|
for (const listener of this.onRevoked)
|
|
253
266
|
listener(sessionId);
|
|
254
267
|
}
|
|
268
|
+
/** From now on no pairing window, sign-in or session without admin scope
|
|
269
|
+
* is opened, redeemed, issued or accepted; `refusal` says why (an OMB
|
|
270
|
+
* Cloud home is personal: server/cloud-owner.ts). Sessions stored without
|
|
271
|
+
* it stop authenticating; the caller revokes them. */
|
|
272
|
+
requireAdmin(refusal) {
|
|
273
|
+
this.adminOnly = refusal;
|
|
274
|
+
}
|
|
275
|
+
personal(scopes) {
|
|
276
|
+
return this.adminOnly !== null && !scopes.includes("admin");
|
|
277
|
+
}
|
|
278
|
+
refusePersonal(scopes) {
|
|
279
|
+
if (this.personal(scopes))
|
|
280
|
+
throw Object.assign(new Error(this.adminOnly), { status: 403, code: "cloud_personal" });
|
|
281
|
+
}
|
|
255
282
|
// ── pairing ────────────────────────────────────────────────────────────
|
|
256
283
|
openPairing(input = {}) {
|
|
257
284
|
this.prune();
|
|
@@ -259,6 +286,7 @@ export class SessionRegistry {
|
|
|
259
286
|
const code = generatePairingCode();
|
|
260
287
|
const credential = generatePairingCredential();
|
|
261
288
|
const scopes = input.scopes?.length ? [...new Set(input.scopes)] : [...SCOPES];
|
|
289
|
+
this.refusePersonal(scopes);
|
|
262
290
|
const pairing = {
|
|
263
291
|
id: randomUUID(),
|
|
264
292
|
codeHash: sha256(code),
|
|
@@ -267,6 +295,7 @@ export class SessionRegistry {
|
|
|
267
295
|
label: (input.label ?? "").trim().slice(0, 80),
|
|
268
296
|
createdAt: now,
|
|
269
297
|
expiresAt: now + (input.ttlMs ?? PAIRING_CODE_TTL_MS),
|
|
298
|
+
...(input.browser ? { browser: true, ...(input.owner ? { owner: input.owner } : {}) } : {}),
|
|
270
299
|
};
|
|
271
300
|
this.pairings.push(pairing);
|
|
272
301
|
return { id: pairing.id, code, credential, expiresAt: pairing.expiresAt };
|
|
@@ -275,6 +304,17 @@ export class SessionRegistry {
|
|
|
275
304
|
this.prune();
|
|
276
305
|
return this.pairings.map((p) => ({ id: p.id, label: p.label, scopes: [...p.scopes], createdAt: p.createdAt, expiresAt: p.expiresAt }));
|
|
277
306
|
}
|
|
307
|
+
/** What a browser sign-in's page shows before anything is redeemed: whose
|
|
308
|
+
* Cloud it signs in to. Only a browser sign-in's 256-bit credential can
|
|
309
|
+
* match, so this consumes nothing and counts toward no lockout. */
|
|
310
|
+
previewBrowserSignIn(credential) {
|
|
311
|
+
this.prune();
|
|
312
|
+
if (!isPairingCredential(credential))
|
|
313
|
+
return null;
|
|
314
|
+
const presented = sha256(credential);
|
|
315
|
+
const pairing = this.pairings.find((p) => p.browser === true && sameDigest(p.credentialHash, presented));
|
|
316
|
+
return pairing ? { owner: pairing.owner ?? null, expiresAt: pairing.expiresAt } : null;
|
|
317
|
+
}
|
|
278
318
|
cancelPairing(id) {
|
|
279
319
|
const before = this.pairings.length;
|
|
280
320
|
this.pairings = this.pairings.filter((p) => p.id !== id);
|
|
@@ -313,7 +353,8 @@ export class SessionRegistry {
|
|
|
313
353
|
* the lockout (an IP); `label` names the device in the sessions list. */
|
|
314
354
|
/** `label` is what the client asked to be called; the code's own label
|
|
315
355
|
* (set by whoever minted it) comes next; `fallbackLabel` (derived from the
|
|
316
|
-
* user agent) last.
|
|
356
|
+
* user agent) last. `browser`: this is a browser sign-in (PairingCode
|
|
357
|
+
* `browser`), which redeems only a window opened for one. */
|
|
317
358
|
exchange(input) {
|
|
318
359
|
this.prune();
|
|
319
360
|
const now = this.now();
|
|
@@ -323,8 +364,9 @@ export class SessionRegistry {
|
|
|
323
364
|
const presented = isPairingCredential(input.code)
|
|
324
365
|
? sha256(input.code)
|
|
325
366
|
: sha256(normalizePairingCode(input.code));
|
|
367
|
+
const browser = input.browser === true;
|
|
326
368
|
const attemptId = typeof input.attemptId === "string" && /^[\w-]{8,64}$/.test(input.attemptId) ? input.attemptId : null;
|
|
327
|
-
const replay = attemptId ? this.replays.find((r) => r.attemptId === attemptId && sameDigest(r.codeHash, presented)) : undefined;
|
|
369
|
+
const replay = attemptId ? this.replays.find((r) => r.attemptId === attemptId && r.browser === browser && sameDigest(r.codeHash, presented)) : undefined;
|
|
328
370
|
if (replay)
|
|
329
371
|
return replay.result;
|
|
330
372
|
const lock = this.lockState(input.source);
|
|
@@ -332,31 +374,35 @@ export class SessionRegistry {
|
|
|
332
374
|
const seconds = Math.ceil(lock.retryAfterMs / 1000);
|
|
333
375
|
return { ok: false, status: 429, error: `too many failed pairing attempts from your address; try again in ${seconds}s` };
|
|
334
376
|
}
|
|
335
|
-
const index = this.pairings.findIndex((p) =>
|
|
377
|
+
const index = this.pairings.findIndex((p) => (p.browser === true) === browser &&
|
|
378
|
+
(sameDigest(p.credentialHash, presented) || (!p.browser && sameDigest(p.codeHash, presented))));
|
|
336
379
|
if (index < 0) {
|
|
337
380
|
this.recordFailure(input.source);
|
|
338
381
|
return { ok: false, status: 401, error: "pairing code is wrong or has expired; create a new one on the server" };
|
|
339
382
|
}
|
|
340
383
|
const [pairing] = this.pairings.splice(index, 1); // single use
|
|
341
384
|
this.failures.delete(input.source);
|
|
385
|
+
if (this.personal(pairing.scopes))
|
|
386
|
+
return { ok: false, status: 403, error: this.adminOnly };
|
|
342
387
|
const token = `omb_sess_${randomBytes(32).toString("base64url")}`;
|
|
343
388
|
const record = {
|
|
344
389
|
id: randomUUID(),
|
|
345
390
|
tokenHash: sha256(token),
|
|
346
|
-
label: (input.label
|
|
391
|
+
label: (deviceLabel(input.label) || deviceLabel(pairing.label) || deviceLabel(input.fallbackLabel) || "Unnamed device").slice(0, 80),
|
|
347
392
|
scopes: [...pairing.scopes],
|
|
348
393
|
createdAt: now,
|
|
349
394
|
lastSeenAt: now,
|
|
350
395
|
// The absolute cap applies from the first term, so a TTL configured
|
|
351
396
|
// longer than the cap does not hand out a session the cap forbids.
|
|
352
397
|
expiresAt: now + Math.min(SESSION_TTL_MS, SESSION_MAX_AGE_MS),
|
|
398
|
+
...(pairing.browser ? { cookieOnly: true, ...(pairing.owner ? { owner: pairing.owner } : {}) } : {}),
|
|
353
399
|
};
|
|
354
400
|
this.sessions.push(record);
|
|
355
401
|
this.lastSeenWrites.set(record.id, now); // the exchange itself was the first sighting
|
|
356
402
|
this.persist();
|
|
357
403
|
const result = { ok: true, token, session: publicSession(record) };
|
|
358
404
|
if (attemptId)
|
|
359
|
-
this.replays.push({ codeHash: presented, attemptId, result, expiresAt: now + EXCHANGE_REPLAY_MS });
|
|
405
|
+
this.replays.push({ codeHash: presented, attemptId, browser, result, expiresAt: now + EXCHANGE_REPLAY_MS });
|
|
360
406
|
return result;
|
|
361
407
|
}
|
|
362
408
|
/** A session from a verified account sign-in (server/account-signin.ts)
|
|
@@ -372,13 +418,14 @@ export class SessionRegistry {
|
|
|
372
418
|
return this.issueAccount({ label: "Hosted workspace", email: input.email, userId: `portal:${input.grant}`, scopes: input.scopes }, "portal");
|
|
373
419
|
}
|
|
374
420
|
issueAccount(input, membershipAuthority) {
|
|
421
|
+
this.refusePersonal(input.scopes);
|
|
375
422
|
this.prune();
|
|
376
423
|
const now = this.now();
|
|
377
424
|
const token = `omb_sess_${randomBytes(32).toString("base64url")}`;
|
|
378
425
|
const record = {
|
|
379
426
|
id: randomUUID(),
|
|
380
427
|
tokenHash: sha256(token),
|
|
381
|
-
label: (input.label
|
|
428
|
+
label: (deviceLabel(input.label) || "Unnamed device").slice(0, 80),
|
|
382
429
|
scopes: [...new Set(input.scopes)],
|
|
383
430
|
createdAt: now,
|
|
384
431
|
lastSeenAt: now,
|
|
@@ -457,7 +504,7 @@ export class SessionRegistry {
|
|
|
457
504
|
const hash = sha256(token);
|
|
458
505
|
const now = this.now();
|
|
459
506
|
const record = this.sessions.find((s) => sameDigest(s.tokenHash, hash));
|
|
460
|
-
if (!record || record.expiresAt <= now)
|
|
507
|
+
if (!record || record.expiresAt <= now || this.personal(record.scopes))
|
|
461
508
|
return null;
|
|
462
509
|
const lastWrite = this.lastSeenWrites.get(record.id) ?? 0;
|
|
463
510
|
if (now - lastWrite >= LAST_SEEN_WRITE_INTERVAL_MS) {
|
|
@@ -493,7 +540,7 @@ export class SessionRegistry {
|
|
|
493
540
|
/** Still valid right now (prunes expiry first). */
|
|
494
541
|
isLive(sessionId) {
|
|
495
542
|
this.prune();
|
|
496
|
-
return this.sessions.some((s) => s.id === sessionId);
|
|
543
|
+
return this.sessions.some((s) => s.id === sessionId && !this.personal(s.scopes));
|
|
497
544
|
}
|
|
498
545
|
list() {
|
|
499
546
|
this.prune();
|
|
@@ -529,6 +576,6 @@ export class SessionRegistry {
|
|
|
529
576
|
this.tickets.delete(hash);
|
|
530
577
|
const now = this.now();
|
|
531
578
|
const record = this.sessions.find((s) => s.id === entry.sessionId);
|
|
532
|
-
return record && record.expiresAt > now ? record : null;
|
|
579
|
+
return record && record.expiresAt > now && !this.personal(record.scopes) ? record : null;
|
|
533
580
|
}
|
|
534
581
|
}
|