pilotswarm 0.5.39 → 0.5.41
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/package.json +2 -2
- package/tui/src/node-sdk-transport.js +25 -0
- package/ui/core/src/controller.js +34 -8
- package/ui/core/src/history.js +5 -1
- package/ui/core/src/index.js +1 -0
- package/ui/core/src/portal-links.js +46 -0
- package/ui/core/src/reducer.js +88 -2
- package/ui/core/src/selectors.js +2 -0
- package/ui/react/src/web-app.js +358 -36
- package/web/README.md +28 -0
- package/web/api/canvas-plane.js +116 -0
- package/web/api/ws.js +120 -5
- package/web/config.js +60 -0
- package/web/dist/assets/{abnfDiagram-VRR7QNED-Hsly1fgf.js → abnfDiagram-VRR7QNED-kpxu_PEa.js} +1 -1
- package/web/dist/assets/{arc-DiQLL9vM.js → arc-BnVQmtB5.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js → architectureDiagram-ZJ3FMSHR-CDynQ_6_.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Z4PcukIo.js → blockDiagram-677ZJIJ3-BUpYaSDb.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cahkc0gc.js → c4Diagram-LMCZKHZV-ZL8eskDB.js} +1 -1
- package/web/dist/assets/channel-Bi5A6sHR.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CtZuWH4J.js → chunk-2Q5K7J3B-o0Nsyp1T.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C0M390-k.js → chunk-32BRIVSS-CH5CSNRP.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-ClvX9u5j.js → chunk-5VM5RSS4-C9wwtPWL.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-BVAet5KG.js → chunk-EX3LRPZG--F9yFbhA.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BccmZmrr.js → chunk-JWPE2WC7-BPIstyWL.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BN5MKe94.js → chunk-MOJQB5TN-5Dm2u-0k.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-DfWw-ACP.js → chunk-RYQCIY6F-Dbtf9iQK.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-BgUEoLnN.js → chunk-V7JOEXUC-DrZEBJ5l.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-k1eKfFNA.js → chunk-VR4S4FIN-AqAUynqK.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DPyV10yR.js → chunk-XXDRQBXY-DQlFaqCy.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D8dCxI54.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D8dCxI54.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Bt_rpukK.js → cose-bilkent-JH36ORCC-RAtPG1Ou.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Z8eMzFMv.js → cynefin-VYW2F7L2-CbeHaUWZ.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BJSNawFi.js → cynefinDiagram-TSTJHNR4-B6paqF5Y.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-D6dNl8jc.js → dagre-VKFMJZFB-DziTBO2K.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-Cy1nbGsU.js → diagram-FQU43EPY-CPRUiLcr.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CmrbuA_d.js → diagram-G47NLZAW-CIMVdcL1.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-PoLNQE2j.js → diagram-NH7WQ7WH-CHPFllxR.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-BZwNFcSV.js → diagram-OA4YK3LP-DwS6unBJ.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-pKw0b_fN.js → diagram-WEI45ONY-DKWgkObI.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-UNctuQCV.js → ebnfDiagram-CCIWWBDH-zAVWV6-D.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DxIETMif.js → erDiagram-Q63AITRT-D0tYX4h0.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-C06IdNKV.js → flowDiagram-23GEKE2U-Cfq4I7R0.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-fU7aGyP1.js → ganttDiagram-NO4QXBWP-wgBAHr5I.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-9UKkPJm6.js → gitGraphDiagram-IHSO6WYX-BTIPX5PR.js} +1 -1
- package/web/dist/assets/index-OHj-gGHp.js +24 -0
- package/web/dist/assets/index-eTH1FBtN.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-C6F06SQJ.js → infoDiagram-FWYZ7A6U-CRLiQ87D.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CB-O9NfO.js → ishikawaDiagram-FXEZZL3T-CwzwUis1.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-B-8MOf-H.js → journeyDiagram-5HDEW3XC-B9pqMeFw.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DX0XA3hI.js → kanban-definition-HUTT4EX6-CZ5Xky4p.js} +1 -1
- package/web/dist/assets/{linear-D60PrjTW.js → linear-BLZ2eH4a.js} +1 -1
- package/web/dist/assets/{mermaid.core-bonzwapf.js → mermaid.core-CPN1IVjz.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-Y-6Dum33.js → mindmap-definition-LN4V7U3C-MdC8d2tc.js} +1 -1
- package/web/dist/assets/{papaparse.min-Cas1JfRG.js → papaparse.min-CtrVJupf.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-_paO0yDw.js → pegDiagram-2B236MQR-B2DqAc-A.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-xT1_Gpn4.js → pieDiagram-ENE6RG2P-C3QAoXZh.js} +1 -1
- package/web/dist/assets/pilotswarm-CF1nmpMy.js +97 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-1usdLFOF.js → quadrantDiagram-ABIIQ3AL-BoUr5qFH.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-R3lwHOz9.js → railroadDiagram-RFXS5EU6-bZzE4KFN.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BL8J1__Z.js → requirementDiagram-TGXJPOKE-B2ENAqLS.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Dz6--0pl.js → sankeyDiagram-HTMAVEWB-AJZ3h46Z.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CG2Xpkct.js → sequenceDiagram-DBY2YBRQ-QUf0VVE7.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BPbYfjxj.js → sizeCapture-X5ZJPWSS-Cv9RcUy6.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-46x_y_aA.js → stateDiagram-2N3HPSRC-hDiye45t.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Bu4ufvaU.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-SXdMo4zJ.js → swimlanes-5IMT3BWC-hSk_OF85.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTFePb1Y.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-CGgT-b38.js → timeline-definition-FHXFAJF6-ccOZEJ5e.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BGS3xBa_.js → vennDiagram-L72KCM5P-Bk5DLH_V.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-m2U3wEZJ.js → wardleyDiagram-EHGQE667-C20AX8ot.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DI6tQysg.js → xychartDiagram-FW5EYKEG-DaHqmi-R.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +71 -0
- package/web/server.js +51 -2
- package/web/dist/assets/channel-BHLBPE50.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +0 -1
- package/web/dist/assets/index-B8nGlHwb.js +0 -24
- package/web/dist/assets/index-DtL0lbis.css +0 -1
- package/web/dist/assets/pilotswarm-BT6tOknQ.js +0 -96
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Dh6Sn__t.js +0 -8
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canvas-plane relay: one LISTEN connection to the CMS primary, fanned
|
|
5
|
+
* out to WebSocket subscribers by session id.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately a PURE fan-out. The NOTIFY payload already carries everything
|
|
8
|
+
* a viewer needs ({slot, seq, kind} and, for small patches, the patch
|
|
9
|
+
* itself); when it does not (big patch, PUT, doc), the ping is a pointer and
|
|
10
|
+
* the BROWSER fetches the snapshot through the authorized REST path
|
|
11
|
+
* (getCanvasLive). No queries here, no state worth losing, so any number of
|
|
12
|
+
* relay processes scale with zero coordination — NOTIFY broadcasts to all.
|
|
13
|
+
*
|
|
14
|
+
* Connection discipline (the dispatcher-freeze lesson): TCP keepalive on the
|
|
15
|
+
* client, a 30 s liveness probe, and reconnect-with-backoff. Subscriptions
|
|
16
|
+
* live in process memory, so a reconnect needs no re-reads — browsers detect
|
|
17
|
+
* any missed seq and resync themselves.
|
|
18
|
+
*/
|
|
19
|
+
export function createCanvasPlane({
|
|
20
|
+
connectionString = process.env.DATABASE_URL,
|
|
21
|
+
schema = process.env.PILOTSWARM_CMS_SCHEMA || "copilot_sessions",
|
|
22
|
+
channel = "pilotswarm_canvas_live",
|
|
23
|
+
} = {}) {
|
|
24
|
+
const subscribers = new Map(); // sessionId -> Set<cb>
|
|
25
|
+
let client = null;
|
|
26
|
+
let stopped = false;
|
|
27
|
+
let probeTimer = null;
|
|
28
|
+
let reconnectDelay = 1_000;
|
|
29
|
+
|
|
30
|
+
const available = Boolean(connectionString);
|
|
31
|
+
|
|
32
|
+
async function connect() {
|
|
33
|
+
if (stopped || !available) return;
|
|
34
|
+
const next = new pg.Client({ connectionString, keepAlive: true });
|
|
35
|
+
try {
|
|
36
|
+
await next.connect();
|
|
37
|
+
await next.query(`LISTEN ${channel}`);
|
|
38
|
+
} catch {
|
|
39
|
+
try { await next.end(); } catch { /* already dead */ }
|
|
40
|
+
scheduleReconnect();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
client = next;
|
|
44
|
+
reconnectDelay = 1_000;
|
|
45
|
+
next.on("notification", (msg) => {
|
|
46
|
+
let payload;
|
|
47
|
+
try {
|
|
48
|
+
payload = JSON.parse(msg.payload || "");
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Multi-schema deployments (and test suites) share the channel;
|
|
53
|
+
// the payload names its schema so we never cross streams.
|
|
54
|
+
if (payload?.schema && payload.schema !== schema) return;
|
|
55
|
+
const handlers = subscribers.get(payload?.sessionId);
|
|
56
|
+
if (!handlers) return;
|
|
57
|
+
const update = {
|
|
58
|
+
slot: Number(payload.slot),
|
|
59
|
+
seq: Number(payload.seq),
|
|
60
|
+
kind: payload.kind === "doc" ? "doc" : "data",
|
|
61
|
+
...(payload.patch && typeof payload.patch === "object" ? { patch: payload.patch } : {}),
|
|
62
|
+
};
|
|
63
|
+
for (const cb of handlers) {
|
|
64
|
+
try { cb(update); } catch { /* one bad socket must not stop the fan-out */ }
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const onGone = () => {
|
|
68
|
+
if (client === next) client = null;
|
|
69
|
+
scheduleReconnect();
|
|
70
|
+
};
|
|
71
|
+
next.on("error", onGone);
|
|
72
|
+
next.on("end", onGone);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scheduleReconnect() {
|
|
76
|
+
if (stopped) return;
|
|
77
|
+
const delay = reconnectDelay;
|
|
78
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 15_000);
|
|
79
|
+
setTimeout(() => { void connect(); }, delay);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
available,
|
|
84
|
+
async start() {
|
|
85
|
+
if (!available) return;
|
|
86
|
+
await connect();
|
|
87
|
+
// Liveness probe: a LISTEN connection can die silently behind
|
|
88
|
+
// NAT/pools. A failed probe triggers the error path → reconnect.
|
|
89
|
+
probeTimer = setInterval(() => {
|
|
90
|
+
if (client) client.query("SELECT 1").catch(() => { /* error handler reconnects */ });
|
|
91
|
+
}, 30_000);
|
|
92
|
+
if (probeTimer.unref) probeTimer.unref();
|
|
93
|
+
},
|
|
94
|
+
subscribe(sessionId, cb) {
|
|
95
|
+
const key = String(sessionId || "").trim();
|
|
96
|
+
if (!key) return () => {};
|
|
97
|
+
if (!subscribers.has(key)) subscribers.set(key, new Set());
|
|
98
|
+
subscribers.get(key).add(cb);
|
|
99
|
+
return () => {
|
|
100
|
+
const handlers = subscribers.get(key);
|
|
101
|
+
if (!handlers) return;
|
|
102
|
+
handlers.delete(cb);
|
|
103
|
+
if (handlers.size === 0) subscribers.delete(key);
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
async stop() {
|
|
107
|
+
stopped = true;
|
|
108
|
+
if (probeTimer) clearInterval(probeTimer);
|
|
109
|
+
subscribers.clear();
|
|
110
|
+
if (client) {
|
|
111
|
+
try { await client.end(); } catch { /* going down anyway */ }
|
|
112
|
+
client = null;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
package/web/api/ws.js
CHANGED
|
@@ -21,10 +21,48 @@ function isSafeThemeId(value) {
|
|
|
21
21
|
|
|
22
22
|
export function createConnectionHandler(runtime, { allowThemeMessages = false } = {}) {
|
|
23
23
|
return async function handleConnection(ws, req) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Canvas share tokens FIRST: presenting `?canvasShare=<token>` makes
|
|
25
|
+
// this a token-bearer connection, full stop — it never falls back to
|
|
26
|
+
// portal auth (on no-auth deployments anonymous auth SUCCEEDS, and a
|
|
27
|
+
// fallback would silently upgrade a link bearer to a full-auth
|
|
28
|
+
// connection — or, just as bad, break the share protocol by routing
|
|
29
|
+
// it through the authed branch). A token connection may do exactly
|
|
30
|
+
// one thing: subscribe to its canvas's plane pings, slot-filtered.
|
|
31
|
+
// An invalid token closes 4401, indistinguishable from missing auth.
|
|
32
|
+
const shareToken = new URL(req.url || "/", "http://localhost").searchParams.get("canvasShare");
|
|
33
|
+
let shareScope = null;
|
|
34
|
+
let shareRevalidateTimer = null;
|
|
35
|
+
let auth = null;
|
|
36
|
+
if (shareToken) {
|
|
37
|
+
if (typeof runtime.resolveCanvasShareToken === "function") {
|
|
38
|
+
shareScope = await runtime.resolveCanvasShareToken(shareToken).catch(() => null);
|
|
39
|
+
}
|
|
40
|
+
if (!shareScope) {
|
|
41
|
+
ws.close(4401, "Unauthorized");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Revocation has a bounded latency for OPEN sockets: the owner's
|
|
45
|
+
// reset/remove replaces the hash row, and this re-validation
|
|
46
|
+
// notices within the interval and closes the connection. Without
|
|
47
|
+
// it, an already-connected bearer would keep receiving pings for
|
|
48
|
+
// the connection's whole lifetime after the link died.
|
|
49
|
+
const revalidateMs = Math.max(1, Number(process.env.PILOTSWARM_SHARE_REVALIDATE_MS) || 60_000);
|
|
50
|
+
shareRevalidateTimer = setInterval(() => {
|
|
51
|
+
runtime.resolveCanvasShareToken(shareToken)
|
|
52
|
+
.then((scope) => {
|
|
53
|
+
if (!scope || scope.sessionId !== shareScope.sessionId || scope.slot !== shareScope.slot) {
|
|
54
|
+
ws.close(4403, "Link no longer valid");
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.catch(() => { /* transient — next tick retries */ });
|
|
58
|
+
}, revalidateMs);
|
|
59
|
+
if (shareRevalidateTimer.unref) shareRevalidateTimer.unref();
|
|
60
|
+
} else {
|
|
61
|
+
auth = await authenticateToken(extractToken(req), req);
|
|
62
|
+
if (!auth.ok) {
|
|
63
|
+
ws.close(auth.status === 403 ? 4403 : 4401, auth.error || (auth.status === 403 ? "Forbidden" : "Unauthorized"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
28
66
|
}
|
|
29
67
|
// Same sign-in role capture as the HTTP `requireAuth` path. This is
|
|
30
68
|
// the OTHER authenticated entry point, and it must not be forgotten:
|
|
@@ -32,9 +70,10 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
32
70
|
// otherwise never re-confirm its role, and the worker — which expires
|
|
33
71
|
// stale observations — would quietly drop that user's agent sessions
|
|
34
72
|
// to non-admin while the portal still showed them as admin.
|
|
35
|
-
runtime.noteSignInRole?.(auth);
|
|
73
|
+
if (!shareScope && auth) runtime.noteSignInRole?.(auth);
|
|
36
74
|
|
|
37
75
|
const sessionSubscriptions = new Map();
|
|
76
|
+
const canvasSubscriptions = new Map();
|
|
38
77
|
let logUnsubscribe = null;
|
|
39
78
|
|
|
40
79
|
const send = (message) => {
|
|
@@ -54,6 +93,39 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
54
93
|
}
|
|
55
94
|
|
|
56
95
|
const type = String(message?.type || "");
|
|
96
|
+
if (shareScope) {
|
|
97
|
+
// The whole share-scope protocol: subscribe to THE canvas the
|
|
98
|
+
// token names. No session events, no logs, no other sessions.
|
|
99
|
+
if (type === "subscribeCanvas") {
|
|
100
|
+
// The bearer does not know (and is never told) the
|
|
101
|
+
// session id — the token IS the address. An absent or
|
|
102
|
+
// matching id subscribes the token's canvas; anything
|
|
103
|
+
// else is refused.
|
|
104
|
+
const requested = String(message?.sessionId || "").trim();
|
|
105
|
+
const sessionId = requested || shareScope.sessionId;
|
|
106
|
+
if (sessionId !== shareScope.sessionId || canvasSubscriptions.has(sessionId)) return;
|
|
107
|
+
const plane = runtime.canvasPlane;
|
|
108
|
+
if (!plane?.available) {
|
|
109
|
+
send({ type: "error", scope: "canvas", sessionId, error: "canvas plane unavailable" });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const unsubscribe = plane.subscribe(sessionId, (update) => {
|
|
113
|
+
if (Number(update?.slot) !== shareScope.slot) return;
|
|
114
|
+
send({ type: "canvasLive", sessionId, ...update });
|
|
115
|
+
});
|
|
116
|
+
canvasSubscriptions.set(sessionId, unsubscribe);
|
|
117
|
+
send({ type: "subscribedCanvas", sessionId });
|
|
118
|
+
} else if (type === "unsubscribeCanvas") {
|
|
119
|
+
const unsubscribe = canvasSubscriptions.get(shareScope.sessionId);
|
|
120
|
+
if (unsubscribe) {
|
|
121
|
+
unsubscribe();
|
|
122
|
+
canvasSubscriptions.delete(shareScope.sessionId);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
send({ type: "error", scope: "share", error: "share connections may only subscribe to their canvas" });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
57
129
|
if (type === "subscribeSession") {
|
|
58
130
|
const sessionId = String(message?.sessionId || "").trim();
|
|
59
131
|
if (!sessionId || sessionSubscriptions.has(sessionId)) return;
|
|
@@ -85,6 +157,42 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
85
157
|
return;
|
|
86
158
|
}
|
|
87
159
|
|
|
160
|
+
if (type === "subscribeCanvas") {
|
|
161
|
+
const sessionId = String(message?.sessionId || "").trim();
|
|
162
|
+
if (!sessionId || canvasSubscriptions.has(sessionId)) return;
|
|
163
|
+
const plane = runtime.canvasPlane;
|
|
164
|
+
if (!plane?.available) {
|
|
165
|
+
send({ type: "error", scope: "canvas", sessionId, error: "canvas plane unavailable" });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await runtime.start();
|
|
170
|
+
// A canvas subscription is a session content read — the
|
|
171
|
+
// exact predicate live events use.
|
|
172
|
+
if (typeof runtime.authorizeSessionSubscribe === "function") {
|
|
173
|
+
await runtime.authorizeSessionSubscribe(sessionId, auth);
|
|
174
|
+
}
|
|
175
|
+
const unsubscribe = plane.subscribe(sessionId, (update) => {
|
|
176
|
+
send({ type: "canvasLive", sessionId, ...update });
|
|
177
|
+
});
|
|
178
|
+
canvasSubscriptions.set(sessionId, unsubscribe);
|
|
179
|
+
send({ type: "subscribedCanvas", sessionId });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
send({ type: "error", scope: "canvas", sessionId, error: error?.message || String(error) });
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (type === "unsubscribeCanvas") {
|
|
187
|
+
const sessionId = String(message?.sessionId || "").trim();
|
|
188
|
+
const unsubscribe = canvasSubscriptions.get(sessionId);
|
|
189
|
+
if (unsubscribe) {
|
|
190
|
+
unsubscribe();
|
|
191
|
+
canvasSubscriptions.delete(sessionId);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
88
196
|
if (type === "subscribeLogs") {
|
|
89
197
|
if (logUnsubscribe) return;
|
|
90
198
|
try {
|
|
@@ -117,12 +225,19 @@ export function createConnectionHandler(runtime, { allowThemeMessages = false }
|
|
|
117
225
|
});
|
|
118
226
|
|
|
119
227
|
ws.on("close", () => {
|
|
228
|
+
if (shareRevalidateTimer) clearInterval(shareRevalidateTimer);
|
|
120
229
|
for (const unsubscribe of sessionSubscriptions.values()) {
|
|
121
230
|
try {
|
|
122
231
|
unsubscribe();
|
|
123
232
|
} catch {}
|
|
124
233
|
}
|
|
125
234
|
sessionSubscriptions.clear();
|
|
235
|
+
for (const unsubscribe of canvasSubscriptions.values()) {
|
|
236
|
+
try {
|
|
237
|
+
unsubscribe();
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
canvasSubscriptions.clear();
|
|
126
241
|
if (logUnsubscribe) {
|
|
127
242
|
try {
|
|
128
243
|
logUnsubscribe();
|
package/web/config.js
CHANGED
|
@@ -18,3 +18,63 @@ export function getPortalAssetFile(assetName) {
|
|
|
18
18
|
if (!key) return null;
|
|
19
19
|
return getPortalBundle().assetFiles?.[key] || null;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* PORTAL_LINK_ORIGINS: named entry points for link generation.
|
|
24
|
+
*
|
|
25
|
+
* Format: comma-separated `Label=Origin` pairs, e.g.
|
|
26
|
+
* PORTAL_LINK_ORIGINS="Corporate=https://corp.example.com,Private/VPN=https://vpn.example.com"
|
|
27
|
+
*
|
|
28
|
+
* When two or more are configured, every link-producing surface (session
|
|
29
|
+
* copy-link, artifact links, the canvas share dialog) offers one labeled
|
|
30
|
+
* variant per origin — a sender on one network path can hand a recipient
|
|
31
|
+
* on another a link that actually works. This is the platform version of
|
|
32
|
+
* waldemort's dual-session-links deploy patch; deployments plug in with
|
|
33
|
+
* this one env var and delete the patch.
|
|
34
|
+
*
|
|
35
|
+
* Validation is FAIL-LOUD at startup: a malformed value should stop the
|
|
36
|
+
* portal visibly, not silently produce broken links. Rules: 2..6 entries
|
|
37
|
+
* to activate (0 or absent = feature off; exactly 1 is refused as almost
|
|
38
|
+
* certainly a mistake), labels nonempty/unique/<=40 chars, origins bare
|
|
39
|
+
* (no path/query/hash), https required except localhost, all distinct.
|
|
40
|
+
*/
|
|
41
|
+
export function parsePortalLinkOrigins(raw = process.env.PORTAL_LINK_ORIGINS) {
|
|
42
|
+
const value = String(raw || "").trim();
|
|
43
|
+
if (!value) return [];
|
|
44
|
+
const entries = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
45
|
+
if (entries.length === 1) {
|
|
46
|
+
throw new Error("PORTAL_LINK_ORIGINS with a single entry is refused — one origin means the feature is off; unset the variable or configure at least two.");
|
|
47
|
+
}
|
|
48
|
+
if (entries.length > 6) {
|
|
49
|
+
throw new Error("PORTAL_LINK_ORIGINS supports at most 6 entries.");
|
|
50
|
+
}
|
|
51
|
+
const seenLabels = new Set();
|
|
52
|
+
const seenOrigins = new Set();
|
|
53
|
+
return entries.map((entry) => {
|
|
54
|
+
const eq = entry.indexOf("=");
|
|
55
|
+
if (eq <= 0) throw new Error(`PORTAL_LINK_ORIGINS entry "${entry}" must be Label=Origin.`);
|
|
56
|
+
const label = entry.slice(0, eq).trim();
|
|
57
|
+
const originRaw = entry.slice(eq + 1).trim();
|
|
58
|
+
if (!label || label.length > 40) throw new Error(`PORTAL_LINK_ORIGINS label "${label}" must be 1-40 characters.`);
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = new URL(originRaw);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`PORTAL_LINK_ORIGINS origin "${originRaw}" is not a valid URL.`);
|
|
64
|
+
}
|
|
65
|
+
const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
66
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
|
|
67
|
+
throw new Error(`PORTAL_LINK_ORIGINS origin "${originRaw}" must be https (http only for localhost).`);
|
|
68
|
+
}
|
|
69
|
+
if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
|
|
70
|
+
throw new Error(`PORTAL_LINK_ORIGINS origin "${originRaw}" must be a bare origin — no path, query, hash, or credentials.`);
|
|
71
|
+
}
|
|
72
|
+
const origin = url.origin;
|
|
73
|
+
const labelKey = label.toLowerCase();
|
|
74
|
+
if (seenLabels.has(labelKey)) throw new Error(`PORTAL_LINK_ORIGINS label "${label}" is duplicated.`);
|
|
75
|
+
if (seenOrigins.has(origin)) throw new Error(`PORTAL_LINK_ORIGINS origin "${origin}" is duplicated.`);
|
|
76
|
+
seenLabels.add(labelKey);
|
|
77
|
+
seenOrigins.add(origin);
|
|
78
|
+
return { label, origin };
|
|
79
|
+
});
|
|
80
|
+
}
|
package/web/dist/assets/{abnfDiagram-VRR7QNED-Hsly1fgf.js → abnfDiagram-VRR7QNED-kpxu_PEa.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-
|
|
1
|
+
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-5Dm2u-0k.js";import{p as f}from"./chunk-JWPE2WC7-BPIstyWL.js";import{_ as n,l as o}from"./mermaid.core-CPN1IVjz.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-CbeHaUWZ.js";import"./pilotswarm-CF1nmpMy.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Y as ln,$ as an,a0 as F,a1 as q,a2 as j,a3 as un,a4 as y,a5 as tn,a6 as J,a7 as _,a8 as rn,a9 as o,aa as on,ab as sn,ac as fn}from"./mermaid.core-
|
|
1
|
+
import{Y as ln,$ as an,a0 as F,a1 as q,a2 as j,a3 as un,a4 as y,a5 as tn,a6 as J,a7 as _,a8 as rn,a9 as o,aa as on,ab as sn,ac as fn}from"./mermaid.core-CPN1IVjz.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,z,a){var O=I-l,i=D-h,n=z-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function U(l,h,I,D,v,A,z){var a=l-I,O=h-D,i=(z?A:-A)/J(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,B=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*J(on(0,T*T*R-P*P)),Y=(P*g-m*S)/R,$=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=Y-B,e=$-t,r=w-B,C=p-t;return x*x+e*e>r*r+C*C&&(Y=w,$=p),{cx:Y,cy:$,x01:-n,y01:-d,x11:Y*(v/T-1),y11:$*(v/T-1)}}function hn(){var l=cn,h=yn,I=j(0),D=null,v=gn,A=dn,z=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=A.apply(this,arguments)-un,B=rn(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(B>tn-y)a.moveTo(s*F(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*F(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=B,S=B,Y=z.apply(this,arguments)/2,$=Y>y&&(D?+D.apply(this,arguments):J(u*u+s*s)),w=_(rn(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if($>y){var C=sn($/u*q(Y)),K=sn($/s*q(Y));(P-=C*2)>y?(C*=t?1:-1,R+=C,T-=C):(P=0,R=T=(f+c)/2),(S-=K*2)>y?(K*=t?1:-1,m+=K,g-=K):(S=0,m=g=(f+c)/2)}var G=s*F(m),H=s*q(m),L=u*F(T),M=u*q(T);if(w>y){var N=s*F(g),Q=s*q(g),V=u*F(R),W=u*q(R),E;if(B<an)if(E=pn(G,H,V,W,N,Q,L,M)){var X=G-E[0],Z=H-E[1],b=N-E[0],k=Q-E[1],nn=1/q(fn((X*b+Z*k)/(J(X*X+Z*Z)*J(b*b+k*k)))/2),en=J(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=U(V,W,G,H,s,x,t),r=U(N,Q,L,M,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(G,H),a.arc(0,0,s,m,g,!t)):a.moveTo(G,H),!(u>y)||!(P>y)?a.lineTo(L,M):p>y?(e=U(L,M,N,Q,u,-p,t),r=U(G,H,V,W,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-an/2;return[F(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:j(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:j(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:j(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:j(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:j(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:j(+n),i):A},i.padAngle=function(n){return arguments.length?(z=typeof n=="function"?n:j(+n),i):z},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|