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.
Files changed (83) hide show
  1. package/package.json +2 -2
  2. package/tui/src/node-sdk-transport.js +25 -0
  3. package/ui/core/src/controller.js +34 -8
  4. package/ui/core/src/history.js +5 -1
  5. package/ui/core/src/index.js +1 -0
  6. package/ui/core/src/portal-links.js +46 -0
  7. package/ui/core/src/reducer.js +88 -2
  8. package/ui/core/src/selectors.js +2 -0
  9. package/ui/react/src/web-app.js +358 -36
  10. package/web/README.md +28 -0
  11. package/web/api/canvas-plane.js +116 -0
  12. package/web/api/ws.js +120 -5
  13. package/web/config.js +60 -0
  14. package/web/dist/assets/{abnfDiagram-VRR7QNED-Hsly1fgf.js → abnfDiagram-VRR7QNED-kpxu_PEa.js} +1 -1
  15. package/web/dist/assets/{arc-DiQLL9vM.js → arc-BnVQmtB5.js} +1 -1
  16. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-B8ry8WNZ.js → architectureDiagram-ZJ3FMSHR-CDynQ_6_.js} +1 -1
  17. package/web/dist/assets/{blockDiagram-677ZJIJ3-Z4PcukIo.js → blockDiagram-677ZJIJ3-BUpYaSDb.js} +1 -1
  18. package/web/dist/assets/{c4Diagram-LMCZKHZV-Cahkc0gc.js → c4Diagram-LMCZKHZV-ZL8eskDB.js} +1 -1
  19. package/web/dist/assets/channel-Bi5A6sHR.js +1 -0
  20. package/web/dist/assets/{chunk-2Q5K7J3B-CtZuWH4J.js → chunk-2Q5K7J3B-o0Nsyp1T.js} +1 -1
  21. package/web/dist/assets/{chunk-32BRIVSS-C0M390-k.js → chunk-32BRIVSS-CH5CSNRP.js} +1 -1
  22. package/web/dist/assets/{chunk-5VM5RSS4-ClvX9u5j.js → chunk-5VM5RSS4-C9wwtPWL.js} +1 -1
  23. package/web/dist/assets/{chunk-EX3LRPZG-BVAet5KG.js → chunk-EX3LRPZG--F9yFbhA.js} +1 -1
  24. package/web/dist/assets/{chunk-JWPE2WC7-BccmZmrr.js → chunk-JWPE2WC7-BPIstyWL.js} +1 -1
  25. package/web/dist/assets/{chunk-MOJQB5TN-BN5MKe94.js → chunk-MOJQB5TN-5Dm2u-0k.js} +1 -1
  26. package/web/dist/assets/{chunk-RYQCIY6F-DfWw-ACP.js → chunk-RYQCIY6F-Dbtf9iQK.js} +1 -1
  27. package/web/dist/assets/{chunk-V7JOEXUC-BgUEoLnN.js → chunk-V7JOEXUC-DrZEBJ5l.js} +1 -1
  28. package/web/dist/assets/{chunk-VR4S4FIN-k1eKfFNA.js → chunk-VR4S4FIN-AqAUynqK.js} +1 -1
  29. package/web/dist/assets/{chunk-XXDRQBXY-DPyV10yR.js → chunk-XXDRQBXY-DQlFaqCy.js} +1 -1
  30. package/web/dist/assets/classDiagram-OUVF2IWQ-D8dCxI54.js +1 -0
  31. package/web/dist/assets/classDiagram-v2-EOCWNBFH-D8dCxI54.js +1 -0
  32. package/web/dist/assets/{cose-bilkent-JH36ORCC-Bt_rpukK.js → cose-bilkent-JH36ORCC-RAtPG1Ou.js} +1 -1
  33. package/web/dist/assets/{cynefin-VYW2F7L2-Z8eMzFMv.js → cynefin-VYW2F7L2-CbeHaUWZ.js} +1 -1
  34. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BJSNawFi.js → cynefinDiagram-TSTJHNR4-B6paqF5Y.js} +1 -1
  35. package/web/dist/assets/{dagre-VKFMJZFB-D6dNl8jc.js → dagre-VKFMJZFB-DziTBO2K.js} +1 -1
  36. package/web/dist/assets/{diagram-FQU43EPY-Cy1nbGsU.js → diagram-FQU43EPY-CPRUiLcr.js} +1 -1
  37. package/web/dist/assets/{diagram-G47NLZAW-CmrbuA_d.js → diagram-G47NLZAW-CIMVdcL1.js} +1 -1
  38. package/web/dist/assets/{diagram-NH7WQ7WH-PoLNQE2j.js → diagram-NH7WQ7WH-CHPFllxR.js} +1 -1
  39. package/web/dist/assets/{diagram-OA4YK3LP-BZwNFcSV.js → diagram-OA4YK3LP-DwS6unBJ.js} +1 -1
  40. package/web/dist/assets/{diagram-WEI45ONY-pKw0b_fN.js → diagram-WEI45ONY-DKWgkObI.js} +1 -1
  41. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-UNctuQCV.js → ebnfDiagram-CCIWWBDH-zAVWV6-D.js} +1 -1
  42. package/web/dist/assets/{erDiagram-Q63AITRT-DxIETMif.js → erDiagram-Q63AITRT-D0tYX4h0.js} +1 -1
  43. package/web/dist/assets/{flowDiagram-23GEKE2U-C06IdNKV.js → flowDiagram-23GEKE2U-Cfq4I7R0.js} +1 -1
  44. package/web/dist/assets/{ganttDiagram-NO4QXBWP-fU7aGyP1.js → ganttDiagram-NO4QXBWP-wgBAHr5I.js} +1 -1
  45. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-9UKkPJm6.js → gitGraphDiagram-IHSO6WYX-BTIPX5PR.js} +1 -1
  46. package/web/dist/assets/index-OHj-gGHp.js +24 -0
  47. package/web/dist/assets/index-eTH1FBtN.css +1 -0
  48. package/web/dist/assets/{infoDiagram-FWYZ7A6U-C6F06SQJ.js → infoDiagram-FWYZ7A6U-CRLiQ87D.js} +1 -1
  49. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CB-O9NfO.js → ishikawaDiagram-FXEZZL3T-CwzwUis1.js} +1 -1
  50. package/web/dist/assets/{journeyDiagram-5HDEW3XC-B-8MOf-H.js → journeyDiagram-5HDEW3XC-B9pqMeFw.js} +1 -1
  51. package/web/dist/assets/{kanban-definition-HUTT4EX6-DX0XA3hI.js → kanban-definition-HUTT4EX6-CZ5Xky4p.js} +1 -1
  52. package/web/dist/assets/{linear-D60PrjTW.js → linear-BLZ2eH4a.js} +1 -1
  53. package/web/dist/assets/{mermaid.core-bonzwapf.js → mermaid.core-CPN1IVjz.js} +4 -4
  54. package/web/dist/assets/{mindmap-definition-LN4V7U3C-Y-6Dum33.js → mindmap-definition-LN4V7U3C-MdC8d2tc.js} +1 -1
  55. package/web/dist/assets/{papaparse.min-Cas1JfRG.js → papaparse.min-CtrVJupf.js} +1 -1
  56. package/web/dist/assets/{pegDiagram-2B236MQR-_paO0yDw.js → pegDiagram-2B236MQR-B2DqAc-A.js} +1 -1
  57. package/web/dist/assets/{pieDiagram-ENE6RG2P-xT1_Gpn4.js → pieDiagram-ENE6RG2P-C3QAoXZh.js} +1 -1
  58. package/web/dist/assets/pilotswarm-CF1nmpMy.js +97 -0
  59. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-1usdLFOF.js → quadrantDiagram-ABIIQ3AL-BoUr5qFH.js} +1 -1
  60. package/web/dist/assets/{railroadDiagram-RFXS5EU6-R3lwHOz9.js → railroadDiagram-RFXS5EU6-bZzE4KFN.js} +1 -1
  61. package/web/dist/assets/{requirementDiagram-TGXJPOKE-BL8J1__Z.js → requirementDiagram-TGXJPOKE-B2ENAqLS.js} +1 -1
  62. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-Dz6--0pl.js → sankeyDiagram-HTMAVEWB-AJZ3h46Z.js} +1 -1
  63. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CG2Xpkct.js → sequenceDiagram-DBY2YBRQ-QUf0VVE7.js} +1 -1
  64. package/web/dist/assets/{sizeCapture-X5ZJPWSS-BPbYfjxj.js → sizeCapture-X5ZJPWSS-Cv9RcUy6.js} +1 -1
  65. package/web/dist/assets/{stateDiagram-2N3HPSRC-46x_y_aA.js → stateDiagram-2N3HPSRC-hDiye45t.js} +1 -1
  66. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Bu4ufvaU.js +1 -0
  67. package/web/dist/assets/{swimlanes-5IMT3BWC-SXdMo4zJ.js → swimlanes-5IMT3BWC-hSk_OF85.js} +2 -2
  68. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTFePb1Y.js +8 -0
  69. package/web/dist/assets/{timeline-definition-FHXFAJF6-CGgT-b38.js → timeline-definition-FHXFAJF6-ccOZEJ5e.js} +1 -1
  70. package/web/dist/assets/{vennDiagram-L72KCM5P-BGS3xBa_.js → vennDiagram-L72KCM5P-Bk5DLH_V.js} +1 -1
  71. package/web/dist/assets/{wardleyDiagram-EHGQE667-m2U3wEZJ.js → wardleyDiagram-EHGQE667-C20AX8ot.js} +1 -1
  72. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DI6tQysg.js → xychartDiagram-FW5EYKEG-DaHqmi-R.js} +1 -1
  73. package/web/dist/index.html +3 -3
  74. package/web/runtime.js +71 -0
  75. package/web/server.js +51 -2
  76. package/web/dist/assets/channel-BHLBPE50.js +0 -1
  77. package/web/dist/assets/classDiagram-OUVF2IWQ-rj9D2TSL.js +0 -1
  78. package/web/dist/assets/classDiagram-v2-EOCWNBFH-rj9D2TSL.js +0 -1
  79. package/web/dist/assets/index-B8nGlHwb.js +0 -24
  80. package/web/dist/assets/index-DtL0lbis.css +0 -1
  81. package/web/dist/assets/pilotswarm-BT6tOknQ.js +0 -96
  82. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C2tnolSo.js +0 -1
  83. 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
- const auth = await authenticateToken(extractToken(req), req);
25
- if (!auth.ok) {
26
- ws.close(auth.status === 403 ? 4403 : 4401, auth.error || (auth.status === 403 ? "Forbidden" : "Unauthorized"));
27
- return;
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
+ }
@@ -1 +1 @@
1
- import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-BN5MKe94.js";import{p as f}from"./chunk-JWPE2WC7-BccmZmrr.js";import{_ as n,l as o}from"./mermaid.core-bonzwapf.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-Z8eMzFMv.js";import"./pilotswarm-BT6tOknQ.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
+ 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-bonzwapf.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};
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};