flowchart-sequence-designer 1.0.1 → 1.2.0

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/index.js CHANGED
@@ -9,7 +9,15 @@ var Model = class _Model {
9
9
  * @param variant Optional UI variant (flowchart models only).
10
10
  */
11
11
  constructor(type, title, variant) {
12
- this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
12
+ this.data = {
13
+ type,
14
+ ...variant ? { variant } : {},
15
+ title,
16
+ nodes: [],
17
+ edges: [],
18
+ actors: [],
19
+ messages: []
20
+ };
13
21
  }
14
22
  /**
15
23
  * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
@@ -45,7 +53,8 @@ var Model = class _Model {
45
53
  updateNode(id, patch) {
46
54
  const node = this.data.nodes.find((n) => n.id === id);
47
55
  if (!node) throw new Error(`Node "${id}" not found`);
48
- Object.assign(node, patch);
56
+ const { __proto__, constructor, ...safe } = patch;
57
+ Object.assign(node, safe);
49
58
  return this;
50
59
  }
51
60
  /**
@@ -85,15 +94,35 @@ var Model = class _Model {
85
94
  const errors = [];
86
95
  const nodeIds = /* @__PURE__ */ new Set();
87
96
  for (const n of this.data.nodes) {
88
- if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
97
+ if (nodeIds.has(n.id))
98
+ errors.push({
99
+ kind: "duplicate-node-id",
100
+ id: n.id,
101
+ message: `Duplicate node id "${n.id}"`
102
+ });
89
103
  nodeIds.add(n.id);
90
104
  }
91
105
  const edgeIds = /* @__PURE__ */ new Set();
92
106
  for (const e of this.data.edges) {
93
- if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
107
+ if (edgeIds.has(e.id))
108
+ errors.push({
109
+ kind: "duplicate-edge-id",
110
+ id: e.id,
111
+ message: `Duplicate edge id "${e.id}"`
112
+ });
94
113
  edgeIds.add(e.id);
95
- if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
96
- if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
114
+ if (!nodeIds.has(e.from))
115
+ errors.push({
116
+ kind: "dangling-from",
117
+ id: e.id,
118
+ message: `Edge "${e.id}" references unknown source node "${e.from}"`
119
+ });
120
+ if (!nodeIds.has(e.to))
121
+ errors.push({
122
+ kind: "dangling-to",
123
+ id: e.id,
124
+ message: `Edge "${e.id}" references unknown target node "${e.to}"`
125
+ });
97
126
  }
98
127
  return errors;
99
128
  }
@@ -302,7 +331,9 @@ function computeLayout(model) {
302
331
  const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
303
332
  return { node: n, w, h };
304
333
  });
305
- const allPositioned = sized.every((s) => typeof s.node.x === "number" && typeof s.node.y === "number");
334
+ const allPositioned = sized.every(
335
+ (s) => typeof s.node.x === "number" && typeof s.node.y === "number"
336
+ );
306
337
  if (allPositioned) {
307
338
  for (const s of sized) {
308
339
  boxes.set(s.node.id, { x: s.node.x, y: s.node.y, w: s.w, h: s.h });
@@ -352,7 +383,15 @@ function computeLayout(model) {
352
383
  return boxes;
353
384
  }
354
385
  function escapeXML(s) {
355
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
386
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
387
+ }
388
+ function sanitizeForSVG(s) {
389
+ let clean = s;
390
+ clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
391
+ clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
392
+ clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
393
+ clean = clean.replace(/\x00/g, "");
394
+ return escapeXML(clean);
356
395
  }
357
396
  var COLORS = {
358
397
  bg: "#fafbfc",
@@ -371,8 +410,8 @@ function renderStandardNode(node, box) {
371
410
  const cx = box.x + box.w / 2;
372
411
  const cy = box.y + box.h / 2;
373
412
  const shape = node.shape ?? "rectangle";
374
- const label = `<text x="${cx}" y="${cy + 4.5}" text-anchor="middle" font-family="ui-sans-serif,system-ui,-apple-system,sans-serif" font-size="13" font-weight="500" fill="${COLORS.text}">${escapeXML(node.label)}</text>`;
375
- let shapeEl = "";
413
+ const label = `<text x="${cx}" y="${cy + 4.5}" text-anchor="middle" font-family="ui-sans-serif,system-ui,-apple-system,sans-serif" font-size="13" font-weight="500" fill="${COLORS.text}">${sanitizeForSVG(node.label)}</text>`;
414
+ let shapeEl;
376
415
  if (shape === "diamond") {
377
416
  const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
378
417
  shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
@@ -392,17 +431,37 @@ function renderQuestionNode(node, box) {
392
431
  const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
393
432
  const x = box.x, y = box.y, w = box.w, h = box.h;
394
433
  const parts = [];
395
- parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`);
396
- parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
397
- parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
398
- parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
399
- parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
400
- parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
401
- parts.push(`<text x="${x + 50}" y="${y + 27}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="700" fill="${COLORS.textSub}" letter-spacing="0.6">QUESTION</text>`);
402
- parts.push(`<text x="${x + 50}" y="${y + 42}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="13" font-weight="700" fill="${COLORS.text}">${escapeXML(node.label)}</text>`);
403
- parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
434
+ parts.push(
435
+ `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
436
+ );
437
+ parts.push(
438
+ `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
439
+ );
440
+ parts.push(
441
+ `<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
442
+ );
443
+ parts.push(
444
+ `<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
445
+ );
446
+ parts.push(
447
+ `<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
448
+ );
449
+ parts.push(
450
+ `<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
451
+ );
452
+ parts.push(
453
+ `<text x="${x + 50}" y="${y + 27}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="9" font-weight="700" fill="${COLORS.textSub}" letter-spacing="0.6">QUESTION</text>`
454
+ );
455
+ parts.push(
456
+ `<text x="${x + 50}" y="${y + 42}" font-family="ui-sans-serif,system-ui,sans-serif" font-size="13" font-weight="700" fill="${COLORS.text}">${sanitizeForSVG(node.label)}</text>`
457
+ );
458
+ parts.push(
459
+ `<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
460
+ );
404
461
  if (answers.length === 0) {
405
- parts.push(`<text x="${x + w / 2}" y="${y + Q_BASE_H + 22}" text-anchor="middle" font-size="10" fill="${COLORS.amber}" opacity="0.4" font-weight="600">No answers yet</text>`);
462
+ parts.push(
463
+ `<text x="${x + w / 2}" y="${y + Q_BASE_H + 22}" text-anchor="middle" font-size="10" fill="${COLORS.amber}" opacity="0.4" font-weight="600">No answers yet</text>`
464
+ );
406
465
  } else {
407
466
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
408
467
  answers.forEach((ans, i) => {
@@ -415,10 +474,18 @@ function renderQuestionNode(node, box) {
415
474
  const letter = i < 26 ? letters[i] : `${i + 1}`;
416
475
  const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
417
476
  const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
418
- parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
419
- parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
420
- parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
421
- parts.push(`<text x="${cx}" y="${cardY + 46}" text-anchor="middle" font-size="11" font-weight="500" fill="#374151" font-family="ui-sans-serif,system-ui,sans-serif">${escapeXML(displayAns)}</text>`);
477
+ parts.push(
478
+ `<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
479
+ );
480
+ parts.push(
481
+ `<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
482
+ );
483
+ parts.push(
484
+ `<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
485
+ );
486
+ parts.push(
487
+ `<text x="${cx}" y="${cardY + 46}" text-anchor="middle" font-size="11" font-weight="500" fill="#374151" font-family="ui-sans-serif,system-ui,sans-serif">${sanitizeForSVG(displayAns)}</text>`
488
+ );
422
489
  });
423
490
  }
424
491
  return parts.join("");
@@ -456,7 +523,7 @@ function renderEdge(edge, boxes, variant, nodes) {
456
523
  const midY = (y1 + y2) / 2;
457
524
  const labelW = estimateTextW(edge.label, 7) + 14;
458
525
  out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
459
- out += `<text x="${midX}" y="${midY + 2}" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="11" fill="${COLORS.text}">${escapeXML(edge.label)}</text>`;
526
+ out += `<text x="${midX}" y="${midY + 2}" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="11" fill="${COLORS.text}">${sanitizeForSVG(edge.label)}</text>`;
460
527
  }
461
528
  return out;
462
529
  }
@@ -482,7 +549,7 @@ function toSVG(model) {
482
549
  `</marker>`,
483
550
  `</defs>`
484
551
  ].join("");
485
- const titleEl = model.title ? `<text x="${width / 2}" y="22" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" font-weight="700" fill="${COLORS.text}">${escapeXML(model.title)}</text>` : "";
552
+ const titleEl = model.title ? `<text x="${width / 2}" y="22" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="15" font-weight="700" fill="${COLORS.text}">${sanitizeForSVG(model.title)}</text>` : "";
486
553
  const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
487
554
  const nodes = model.nodes.map((n) => {
488
555
  const b = boxes.get(n.id);
@@ -499,7 +566,9 @@ ${nodes}
499
566
  }
500
567
  async function toPNG(model) {
501
568
  if (typeof document === "undefined") {
502
- throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
569
+ throw new Error(
570
+ "toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
571
+ );
503
572
  }
504
573
  const svg = toSVG(model);
505
574
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -515,7 +584,10 @@ async function toPNG(model) {
515
584
  ctx.scale(scale, scale);
516
585
  ctx.drawImage(img, 0, 0);
517
586
  URL.revokeObjectURL(url);
518
- canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
587
+ canvas.toBlob(
588
+ (b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
589
+ "image/png"
590
+ );
519
591
  };
520
592
  img.onerror = () => {
521
593
  URL.revokeObjectURL(url);
@@ -612,7 +684,13 @@ var SequenceBuilder = class {
612
684
  this.model.addActor(from);
613
685
  this.model.addActor(to);
614
686
  const messages = this.model.toJSON().messages ?? [];
615
- this.model.addMessage({ id: nextId("m", messages), from, to, label, style: options.style ?? "solid" });
687
+ this.model.addMessage({
688
+ id: nextId("m", messages),
689
+ from,
690
+ to,
691
+ label,
692
+ style: options.style ?? "solid"
693
+ });
616
694
  return this;
617
695
  }
618
696
  /** Convenience for a `dashed`-style return message. */
@@ -640,6 +718,31 @@ function sequence(title) {
640
718
  return new SequenceBuilder(title);
641
719
  }
642
720
 
721
+ // src/core/sanitize.ts
722
+ var MAX_LABEL_LENGTH = 2e3;
723
+ var MAX_NODES = 500;
724
+ var MAX_EDGES = 2e3;
725
+ var MAX_ACTORS = 100;
726
+ var MAX_MESSAGES = 2e3;
727
+ var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
728
+ function sanitizeLabel(raw) {
729
+ let s = raw;
730
+ s = s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
731
+ s = s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
732
+ s = s.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
733
+ s = s.replace(/\bon[a-z]+\s*=/gi, "");
734
+ if (s.length > MAX_LABEL_LENGTH) {
735
+ s = s.slice(0, MAX_LABEL_LENGTH);
736
+ }
737
+ return s;
738
+ }
739
+ function sanitizeURL(url) {
740
+ const trimmed = url.trim();
741
+ if (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
742
+ if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) return trimmed;
743
+ return void 0;
744
+ }
745
+
643
746
  // src/importers/mermaid.ts
644
747
  function parseNodeDecl(raw) {
645
748
  const patterns = [
@@ -651,7 +754,7 @@ function parseNodeDecl(raw) {
651
754
  ];
652
755
  for (const [re, shape] of patterns) {
653
756
  const m = raw.match(re);
654
- if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
757
+ if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
655
758
  }
656
759
  return null;
657
760
  }
@@ -666,8 +769,11 @@ function parseFlowchart(lines) {
666
769
  const model = new Model("flowchart");
667
770
  const nodeMap = /* @__PURE__ */ new Map();
668
771
  const groupStack = [];
772
+ let edgeCount = 0;
669
773
  const ensureNode = (id, group) => {
670
774
  if (!nodeMap.has(id)) {
775
+ if (nodeMap.size >= MAX_NODES)
776
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
671
777
  nodeMap.set(id, true);
672
778
  const metadata = group ? { group } : void 0;
673
779
  model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
@@ -676,7 +782,8 @@ function parseFlowchart(lines) {
676
782
  for (const line of lines) {
677
783
  const trimmed = line.trim();
678
784
  if (!trimmed) continue;
679
- if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle ")) continue;
785
+ if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
786
+ continue;
680
787
  const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
681
788
  if (subgraphOpen) {
682
789
  groupStack.push(subgraphOpen[1]);
@@ -692,12 +799,15 @@ function parseFlowchart(lines) {
692
799
  const fromRaw = edgeMatch[1].trim();
693
800
  const connector = edgeMatch[2];
694
801
  const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
802
+ const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
695
803
  const toRaw = edgeMatch[4].trim();
696
804
  const style = detectStyle(connector);
697
805
  const arrowhead = detectArrowhead(connector);
698
806
  const fromNode = parseNodeDecl(fromRaw);
699
807
  const toNode = parseNodeDecl(toRaw);
700
808
  if (fromNode && !nodeMap.has(fromNode.id)) {
809
+ if (nodeMap.size >= MAX_NODES)
810
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
701
811
  nodeMap.set(fromNode.id, true);
702
812
  const metadata = currentGroup ? { group: currentGroup } : void 0;
703
813
  model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
@@ -705,19 +815,24 @@ function parseFlowchart(lines) {
705
815
  ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
706
816
  }
707
817
  if (toNode && !nodeMap.has(toNode.id)) {
818
+ if (nodeMap.size >= MAX_NODES)
819
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
708
820
  nodeMap.set(toNode.id, true);
709
821
  const metadata = currentGroup ? { group: currentGroup } : void 0;
710
822
  model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
711
823
  } else if (!toNode) {
712
824
  ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
713
825
  }
826
+ if (edgeCount >= MAX_EDGES)
827
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
828
+ edgeCount++;
714
829
  const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
715
830
  const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
716
831
  model.addEdge({
717
832
  id: nextId("e", model.toJSON().edges),
718
833
  from: fromId,
719
834
  to: toId,
720
- ...label ? { label } : {},
835
+ ...sanitizedLabel ? { label: sanitizedLabel } : {},
721
836
  style,
722
837
  ...arrowhead === "none" ? { arrowhead } : {}
723
838
  });
@@ -725,6 +840,8 @@ function parseFlowchart(lines) {
725
840
  }
726
841
  const nodeDecl = parseNodeDecl(trimmed);
727
842
  if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
843
+ if (nodeMap.size >= MAX_NODES)
844
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
728
845
  nodeMap.set(nodeDecl.id, true);
729
846
  const metadata = currentGroup ? { group: currentGroup } : void 0;
730
847
  model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
@@ -734,34 +851,56 @@ function parseFlowchart(lines) {
734
851
  }
735
852
  function parseSequence(lines, title) {
736
853
  const model = new Model("sequence", title);
854
+ let actorCount = 0;
855
+ let messageCount = 0;
856
+ const safeAddActor = (name) => {
857
+ const safeName = sanitizeLabel(name);
858
+ if (!model.toJSON().actors?.includes(safeName)) {
859
+ if (actorCount >= MAX_ACTORS)
860
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
861
+ actorCount++;
862
+ }
863
+ model.addActor(safeName);
864
+ return safeName;
865
+ };
737
866
  for (const line of lines) {
738
867
  const trimmed = line.trim();
739
868
  if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
740
869
  const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
741
870
  if (participantMatch) {
742
- model.addActor(participantMatch[1].trim());
871
+ safeAddActor(participantMatch[1].trim());
743
872
  continue;
744
873
  }
745
874
  const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
746
875
  if (actorMatch) {
747
- model.addActor(actorMatch[1].trim());
876
+ safeAddActor(actorMatch[1].trim());
748
877
  continue;
749
878
  }
750
879
  const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
751
880
  if (msgMatch) {
752
- const from = msgMatch[1].trim();
881
+ const from = safeAddActor(msgMatch[1].trim());
753
882
  const arrow = msgMatch[2];
754
- const to = msgMatch[3].trim();
755
- const label = msgMatch[4].trim();
756
- model.addActor(from);
757
- model.addActor(to);
883
+ const to = safeAddActor(msgMatch[3].trim());
884
+ const label = sanitizeLabel(msgMatch[4].trim());
885
+ if (messageCount >= MAX_MESSAGES)
886
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
887
+ messageCount++;
758
888
  const messages = model.toJSON().messages ?? [];
759
- model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
889
+ model.addMessage({
890
+ id: nextId("m", messages),
891
+ from,
892
+ to,
893
+ label,
894
+ style: arrow.startsWith("--") ? "dashed" : "solid"
895
+ });
760
896
  }
761
897
  }
762
898
  return model;
763
899
  }
764
900
  function fromMermaid(mermaid) {
901
+ if (mermaid.length > MAX_IMPORT_LENGTH) {
902
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
903
+ }
765
904
  const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
766
905
  const rawLines = cleaned.split("\n");
767
906
  let startIdx = 0;
@@ -793,10 +932,74 @@ function fromMermaid(mermaid) {
793
932
  }
794
933
 
795
934
  // src/importers/json.ts
935
+ function stripDangerousKeys(obj) {
936
+ if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
937
+ if (obj !== null && typeof obj === "object") {
938
+ const clean = {};
939
+ for (const [key, val] of Object.entries(obj)) {
940
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
941
+ clean[key] = stripDangerousKeys(val);
942
+ }
943
+ return clean;
944
+ }
945
+ return obj;
946
+ }
796
947
  function fromJSON(json) {
797
- const data = typeof json === "string" ? JSON.parse(json) : json;
798
- if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
799
- throw new Error("Invalid DiagramModel JSON");
948
+ if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
949
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
950
+ }
951
+ const raw = typeof json === "string" ? JSON.parse(json) : json;
952
+ const data = stripDangerousKeys(raw);
953
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
954
+ throw new Error("Invalid DiagramModel JSON: expected an object");
955
+ }
956
+ if (data.type !== "flowchart" && data.type !== "sequence") {
957
+ throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
958
+ }
959
+ if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
960
+ throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
961
+ }
962
+ if (data.nodes.length > MAX_NODES) {
963
+ throw new Error(
964
+ `Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
965
+ );
966
+ }
967
+ if (data.edges.length > MAX_EDGES) {
968
+ throw new Error(
969
+ `Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
970
+ );
971
+ }
972
+ if (data.actors && data.actors.length > MAX_ACTORS) {
973
+ throw new Error(
974
+ `Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
975
+ );
976
+ }
977
+ if (data.messages && data.messages.length > MAX_MESSAGES) {
978
+ throw new Error(
979
+ `Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
980
+ );
981
+ }
982
+ for (const node of data.nodes) {
983
+ if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
984
+ throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
985
+ }
986
+ node.label = sanitizeLabel(node.label);
987
+ }
988
+ for (const edge of data.edges) {
989
+ if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
990
+ throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
991
+ }
992
+ if (edge.label) edge.label = sanitizeLabel(edge.label);
993
+ }
994
+ if (data.actors) {
995
+ data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
996
+ }
997
+ if (data.messages) {
998
+ for (const msg of data.messages) {
999
+ if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
1000
+ msg.label = sanitizeLabel(msg.label);
1001
+ }
1002
+ }
800
1003
  }
801
1004
  return Model.fromData(data);
802
1005
  }
@@ -807,6 +1010,8 @@ export {
807
1010
  flowchart,
808
1011
  fromJSON,
809
1012
  fromMermaid,
1013
+ sanitizeLabel,
1014
+ sanitizeURL,
810
1015
  sequence,
811
1016
  toJSON,
812
1017
  toMermaid,