flowchart-sequence-designer 1.1.0 → 1.2.1

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.cjs CHANGED
@@ -26,6 +26,8 @@ __export(src_exports, {
26
26
  flowchart: () => flowchart,
27
27
  fromJSON: () => fromJSON,
28
28
  fromMermaid: () => fromMermaid,
29
+ sanitizeLabel: () => sanitizeLabel,
30
+ sanitizeURL: () => sanitizeURL,
29
31
  sequence: () => sequence,
30
32
  toJSON: () => toJSON,
31
33
  toMermaid: () => toMermaid,
@@ -46,7 +48,15 @@ var Model = class _Model {
46
48
  * @param variant Optional UI variant (flowchart models only).
47
49
  */
48
50
  constructor(type, title, variant) {
49
- this.data = { type, ...variant ? { variant } : {}, title, nodes: [], edges: [], actors: [], messages: [] };
51
+ this.data = {
52
+ type,
53
+ ...variant ? { variant } : {},
54
+ title,
55
+ nodes: [],
56
+ edges: [],
57
+ actors: [],
58
+ messages: []
59
+ };
50
60
  }
51
61
  /**
52
62
  * Rehydrate a `Model` from a previously serialized `DiagramModel`. The
@@ -82,7 +92,8 @@ var Model = class _Model {
82
92
  updateNode(id, patch) {
83
93
  const node = this.data.nodes.find((n) => n.id === id);
84
94
  if (!node) throw new Error(`Node "${id}" not found`);
85
- Object.assign(node, patch);
95
+ const { __proto__, constructor, ...safe } = patch;
96
+ Object.assign(node, safe);
86
97
  return this;
87
98
  }
88
99
  /**
@@ -122,15 +133,35 @@ var Model = class _Model {
122
133
  const errors = [];
123
134
  const nodeIds = /* @__PURE__ */ new Set();
124
135
  for (const n of this.data.nodes) {
125
- if (nodeIds.has(n.id)) errors.push({ kind: "duplicate-node-id", id: n.id, message: `Duplicate node id "${n.id}"` });
136
+ if (nodeIds.has(n.id))
137
+ errors.push({
138
+ kind: "duplicate-node-id",
139
+ id: n.id,
140
+ message: `Duplicate node id "${n.id}"`
141
+ });
126
142
  nodeIds.add(n.id);
127
143
  }
128
144
  const edgeIds = /* @__PURE__ */ new Set();
129
145
  for (const e of this.data.edges) {
130
- if (edgeIds.has(e.id)) errors.push({ kind: "duplicate-edge-id", id: e.id, message: `Duplicate edge id "${e.id}"` });
146
+ if (edgeIds.has(e.id))
147
+ errors.push({
148
+ kind: "duplicate-edge-id",
149
+ id: e.id,
150
+ message: `Duplicate edge id "${e.id}"`
151
+ });
131
152
  edgeIds.add(e.id);
132
- if (!nodeIds.has(e.from)) errors.push({ kind: "dangling-from", id: e.id, message: `Edge "${e.id}" references unknown source node "${e.from}"` });
133
- if (!nodeIds.has(e.to)) errors.push({ kind: "dangling-to", id: e.id, message: `Edge "${e.id}" references unknown target node "${e.to}"` });
153
+ if (!nodeIds.has(e.from))
154
+ errors.push({
155
+ kind: "dangling-from",
156
+ id: e.id,
157
+ message: `Edge "${e.id}" references unknown source node "${e.from}"`
158
+ });
159
+ if (!nodeIds.has(e.to))
160
+ errors.push({
161
+ kind: "dangling-to",
162
+ id: e.id,
163
+ message: `Edge "${e.id}" references unknown target node "${e.to}"`
164
+ });
134
165
  }
135
166
  return errors;
136
167
  }
@@ -339,7 +370,9 @@ function computeLayout(model) {
339
370
  const h = isQuestion(n, model.variant) ? questionNodeH(n.metadata?.answers ?? []) : NODE_H;
340
371
  return { node: n, w, h };
341
372
  });
342
- const allPositioned = sized.every((s) => typeof s.node.x === "number" && typeof s.node.y === "number");
373
+ const allPositioned = sized.every(
374
+ (s) => typeof s.node.x === "number" && typeof s.node.y === "number"
375
+ );
343
376
  if (allPositioned) {
344
377
  for (const s of sized) {
345
378
  boxes.set(s.node.id, { x: s.node.x, y: s.node.y, w: s.w, h: s.h });
@@ -389,7 +422,15 @@ function computeLayout(model) {
389
422
  return boxes;
390
423
  }
391
424
  function escapeXML(s) {
392
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
425
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
426
+ }
427
+ function sanitizeForSVG(s) {
428
+ let clean = s;
429
+ clean = clean.replace(/<\/?[a-zA-Z][^>]*>/g, "");
430
+ clean = clean.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
431
+ clean = clean.replace(/\bon[a-z]+\s*=/gi, "");
432
+ clean = clean.replace(/\x00/g, "");
433
+ return escapeXML(clean);
393
434
  }
394
435
  var COLORS = {
395
436
  bg: "#fafbfc",
@@ -408,8 +449,8 @@ function renderStandardNode(node, box) {
408
449
  const cx = box.x + box.w / 2;
409
450
  const cy = box.y + box.h / 2;
410
451
  const shape = node.shape ?? "rectangle";
411
- 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>`;
412
- let shapeEl = "";
452
+ 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>`;
453
+ let shapeEl;
413
454
  if (shape === "diamond") {
414
455
  const pts = `${cx},${box.y} ${box.x + box.w},${cy} ${cx},${box.y + box.h} ${box.x},${cy}`;
415
456
  shapeEl = `<polygon points="${pts}" fill="${COLORS.nodeFill}" stroke="${COLORS.nodeStroke}" stroke-width="1.25" filter="url(#nodeShadow)"/>`;
@@ -429,17 +470,37 @@ function renderQuestionNode(node, box) {
429
470
  const clipId = `qhdr-${node.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
430
471
  const x = box.x, y = box.y, w = box.w, h = box.h;
431
472
  const parts = [];
432
- 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)"/>`);
433
- parts.push(`<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`);
434
- parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`);
435
- parts.push(`<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`);
436
- parts.push(`<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`);
437
- parts.push(`<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`);
438
- 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>`);
439
- 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>`);
440
- parts.push(`<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
473
+ parts.push(
474
+ `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="${COLORS.nodeFill}" stroke="${COLORS.amberLine}" stroke-width="1.5" filter="url(#nodeShadow)"/>`
475
+ );
476
+ parts.push(
477
+ `<defs><clipPath id="${clipId}"><rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" rx="14"/></clipPath></defs>`
478
+ );
479
+ parts.push(
480
+ `<rect x="${x}" y="${y}" width="${w}" height="${Q_BASE_H}" fill="${COLORS.amberSoft}" clip-path="url(#${clipId})"/>`
481
+ );
482
+ parts.push(
483
+ `<rect x="${x}" y="${y}" width="4" height="${Q_BASE_H}" rx="2" fill="${COLORS.amber}"/>`
484
+ );
485
+ parts.push(
486
+ `<rect x="${x + 12}" y="${y + 14}" width="28" height="28" rx="8" fill="${COLORS.amber}"/>`
487
+ );
488
+ parts.push(
489
+ `<text x="${x + 26}" y="${y + 33}" text-anchor="middle" font-size="15" font-weight="900" fill="white">?</text>`
490
+ );
491
+ parts.push(
492
+ `<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>`
493
+ );
494
+ parts.push(
495
+ `<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>`
496
+ );
497
+ parts.push(
498
+ `<line x1="${x}" y1="${y + Q_BASE_H}" x2="${x + w}" y2="${y + Q_BASE_H}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
499
+ );
441
500
  if (answers.length === 0) {
442
- 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>`);
501
+ parts.push(
502
+ `<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>`
503
+ );
443
504
  } else {
444
505
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
445
506
  answers.forEach((ans, i) => {
@@ -452,10 +513,18 @@ function renderQuestionNode(node, box) {
452
513
  const letter = i < 26 ? letters[i] : `${i + 1}`;
453
514
  const maxChars = Math.max(2, Math.floor((cW - 20) / 7.5));
454
515
  const displayAns = ans.length > maxChars ? ans.slice(0, maxChars - 1) + "\u2026" : ans;
455
- parts.push(`<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`);
456
- parts.push(`<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`);
457
- parts.push(`<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${escapeXML(letter)}</text>`);
458
- 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>`);
516
+ parts.push(
517
+ `<rect x="${cardX}" y="${cardY}" width="${cW}" height="${cardH}" rx="8" fill="${COLORS.amberCardBg}" stroke="${COLORS.amberLine}" stroke-width="1"/>`
518
+ );
519
+ parts.push(
520
+ `<rect x="${cx - 11}" y="${cardY + 7}" width="22" height="22" rx="6" fill="#fef3c7"/>`
521
+ );
522
+ parts.push(
523
+ `<text x="${cx}" y="${cardY + 22}" text-anchor="middle" font-size="10" font-weight="800" fill="${COLORS.amber}">${sanitizeForSVG(letter)}</text>`
524
+ );
525
+ parts.push(
526
+ `<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>`
527
+ );
459
528
  });
460
529
  }
461
530
  return parts.join("");
@@ -493,7 +562,7 @@ function renderEdge(edge, boxes, variant, nodes) {
493
562
  const midY = (y1 + y2) / 2;
494
563
  const labelW = estimateTextW(edge.label, 7) + 14;
495
564
  out += `<rect x="${midX - labelW / 2}" y="${midY - 11}" width="${labelW}" height="18" rx="9" fill="${COLORS.bg}" stroke="${COLORS.nodeStroke}" stroke-width="1"/>`;
496
- 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>`;
565
+ 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>`;
497
566
  }
498
567
  return out;
499
568
  }
@@ -519,7 +588,7 @@ function toSVG(model) {
519
588
  `</marker>`,
520
589
  `</defs>`
521
590
  ].join("");
522
- 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>` : "";
591
+ 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>` : "";
523
592
  const edges = model.edges.map((e) => renderEdge(e, boxes, model.variant, model.nodes)).join("\n");
524
593
  const nodes = model.nodes.map((n) => {
525
594
  const b = boxes.get(n.id);
@@ -536,7 +605,9 @@ ${nodes}
536
605
  }
537
606
  async function toPNG(model) {
538
607
  if (typeof document === "undefined") {
539
- throw new Error("toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js.");
608
+ throw new Error(
609
+ "toPNG requires a browser environment. For Node/Bun server use, pipe toSVG() through @resvg/resvg-js."
610
+ );
540
611
  }
541
612
  const svg = toSVG(model);
542
613
  const blob = new Blob([svg], { type: "image/svg+xml" });
@@ -552,7 +623,10 @@ async function toPNG(model) {
552
623
  ctx.scale(scale, scale);
553
624
  ctx.drawImage(img, 0, 0);
554
625
  URL.revokeObjectURL(url);
555
- canvas.toBlob((b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")), "image/png");
626
+ canvas.toBlob(
627
+ (b) => b ? resolve(b) : reject(new Error("Canvas toBlob failed")),
628
+ "image/png"
629
+ );
556
630
  };
557
631
  img.onerror = () => {
558
632
  URL.revokeObjectURL(url);
@@ -649,7 +723,13 @@ var SequenceBuilder = class {
649
723
  this.model.addActor(from);
650
724
  this.model.addActor(to);
651
725
  const messages = this.model.toJSON().messages ?? [];
652
- this.model.addMessage({ id: nextId("m", messages), from, to, label, style: options.style ?? "solid" });
726
+ this.model.addMessage({
727
+ id: nextId("m", messages),
728
+ from,
729
+ to,
730
+ label,
731
+ style: options.style ?? "solid"
732
+ });
653
733
  return this;
654
734
  }
655
735
  /** Convenience for a `dashed`-style return message. */
@@ -677,6 +757,31 @@ function sequence(title) {
677
757
  return new SequenceBuilder(title);
678
758
  }
679
759
 
760
+ // src/core/sanitize.ts
761
+ var MAX_LABEL_LENGTH = 2e3;
762
+ var MAX_NODES = 500;
763
+ var MAX_EDGES = 2e3;
764
+ var MAX_ACTORS = 100;
765
+ var MAX_MESSAGES = 2e3;
766
+ var MAX_IMPORT_LENGTH = 2 * 1024 * 1024;
767
+ function sanitizeLabel(raw) {
768
+ let s = raw;
769
+ s = s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
770
+ s = s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
771
+ s = s.replace(/\b(?:javascript|data|vbscript)\s*:/gi, "");
772
+ s = s.replace(/\bon[a-z]+\s*=/gi, "");
773
+ if (s.length > MAX_LABEL_LENGTH) {
774
+ s = s.slice(0, MAX_LABEL_LENGTH);
775
+ }
776
+ return s;
777
+ }
778
+ function sanitizeURL(url) {
779
+ const trimmed = url.trim();
780
+ if (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
781
+ if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) return trimmed;
782
+ return void 0;
783
+ }
784
+
680
785
  // src/importers/mermaid.ts
681
786
  function parseNodeDecl(raw) {
682
787
  const patterns = [
@@ -688,7 +793,7 @@ function parseNodeDecl(raw) {
688
793
  ];
689
794
  for (const [re, shape] of patterns) {
690
795
  const m = raw.match(re);
691
- if (m) return { id: m[1], label: m[2].replace(/^["']|["']$/g, ""), shape };
796
+ if (m) return { id: m[1], label: sanitizeLabel(m[2].replace(/^["']|["']$/g, "")), shape };
692
797
  }
693
798
  return null;
694
799
  }
@@ -703,8 +808,11 @@ function parseFlowchart(lines) {
703
808
  const model = new Model("flowchart");
704
809
  const nodeMap = /* @__PURE__ */ new Map();
705
810
  const groupStack = [];
811
+ let edgeCount = 0;
706
812
  const ensureNode = (id, group) => {
707
813
  if (!nodeMap.has(id)) {
814
+ if (nodeMap.size >= MAX_NODES)
815
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
708
816
  nodeMap.set(id, true);
709
817
  const metadata = group ? { group } : void 0;
710
818
  model.addNode({ id, label: id, shape: "rectangle", ...metadata ? { metadata } : {} });
@@ -713,7 +821,8 @@ function parseFlowchart(lines) {
713
821
  for (const line of lines) {
714
822
  const trimmed = line.trim();
715
823
  if (!trimmed) continue;
716
- 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;
824
+ if (trimmed.startsWith("%%") || trimmed.startsWith("graph") || trimmed.startsWith("flowchart") || trimmed.startsWith("click ") || trimmed.startsWith("classDef ") || trimmed.startsWith("class ") || trimmed.startsWith("style ") || trimmed.startsWith("linkStyle "))
825
+ continue;
717
826
  const subgraphOpen = trimmed.match(/^subgraph\s+(\S+)/i);
718
827
  if (subgraphOpen) {
719
828
  groupStack.push(subgraphOpen[1]);
@@ -729,12 +838,15 @@ function parseFlowchart(lines) {
729
838
  const fromRaw = edgeMatch[1].trim();
730
839
  const connector = edgeMatch[2];
731
840
  const label = edgeMatch[3]?.replace(/^["']|["']$/g, "");
841
+ const sanitizedLabel = label ? sanitizeLabel(label) : void 0;
732
842
  const toRaw = edgeMatch[4].trim();
733
843
  const style = detectStyle(connector);
734
844
  const arrowhead = detectArrowhead(connector);
735
845
  const fromNode = parseNodeDecl(fromRaw);
736
846
  const toNode = parseNodeDecl(toRaw);
737
847
  if (fromNode && !nodeMap.has(fromNode.id)) {
848
+ if (nodeMap.size >= MAX_NODES)
849
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
738
850
  nodeMap.set(fromNode.id, true);
739
851
  const metadata = currentGroup ? { group: currentGroup } : void 0;
740
852
  model.addNode({ ...fromNode, ...metadata ? { metadata } : {} });
@@ -742,19 +854,24 @@ function parseFlowchart(lines) {
742
854
  ensureNode(fromRaw.replace(/\W.*/, ""), currentGroup);
743
855
  }
744
856
  if (toNode && !nodeMap.has(toNode.id)) {
857
+ if (nodeMap.size >= MAX_NODES)
858
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
745
859
  nodeMap.set(toNode.id, true);
746
860
  const metadata = currentGroup ? { group: currentGroup } : void 0;
747
861
  model.addNode({ ...toNode, ...metadata ? { metadata } : {} });
748
862
  } else if (!toNode) {
749
863
  ensureNode(toRaw.replace(/\W.*/, ""), currentGroup);
750
864
  }
865
+ if (edgeCount >= MAX_EDGES)
866
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_EDGES} edges`);
867
+ edgeCount++;
751
868
  const fromId = fromNode?.id ?? fromRaw.replace(/\W.*/, "");
752
869
  const toId = toNode?.id ?? toRaw.replace(/\W.*/, "");
753
870
  model.addEdge({
754
871
  id: nextId("e", model.toJSON().edges),
755
872
  from: fromId,
756
873
  to: toId,
757
- ...label ? { label } : {},
874
+ ...sanitizedLabel ? { label: sanitizedLabel } : {},
758
875
  style,
759
876
  ...arrowhead === "none" ? { arrowhead } : {}
760
877
  });
@@ -762,6 +879,8 @@ function parseFlowchart(lines) {
762
879
  }
763
880
  const nodeDecl = parseNodeDecl(trimmed);
764
881
  if (nodeDecl && !nodeMap.has(nodeDecl.id)) {
882
+ if (nodeMap.size >= MAX_NODES)
883
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_NODES} nodes`);
765
884
  nodeMap.set(nodeDecl.id, true);
766
885
  const metadata = currentGroup ? { group: currentGroup } : void 0;
767
886
  model.addNode({ ...nodeDecl, ...metadata ? { metadata } : {} });
@@ -771,34 +890,56 @@ function parseFlowchart(lines) {
771
890
  }
772
891
  function parseSequence(lines, title) {
773
892
  const model = new Model("sequence", title);
893
+ let actorCount = 0;
894
+ let messageCount = 0;
895
+ const safeAddActor = (name) => {
896
+ const safeName = sanitizeLabel(name);
897
+ if (!model.toJSON().actors?.includes(safeName)) {
898
+ if (actorCount >= MAX_ACTORS)
899
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_ACTORS} actors`);
900
+ actorCount++;
901
+ }
902
+ model.addActor(safeName);
903
+ return safeName;
904
+ };
774
905
  for (const line of lines) {
775
906
  const trimmed = line.trim();
776
907
  if (!trimmed || trimmed.startsWith("sequenceDiagram") || trimmed.startsWith("%%")) continue;
777
908
  const participantMatch = trimmed.match(/^participant\s+(.+)$/i);
778
909
  if (participantMatch) {
779
- model.addActor(participantMatch[1].trim());
910
+ safeAddActor(participantMatch[1].trim());
780
911
  continue;
781
912
  }
782
913
  const actorMatch = trimmed.match(/^actor\s+(.+)$/i);
783
914
  if (actorMatch) {
784
- model.addActor(actorMatch[1].trim());
915
+ safeAddActor(actorMatch[1].trim());
785
916
  continue;
786
917
  }
787
918
  const msgMatch = trimmed.match(/^(.+?)\s*(-->>|->>|-->|->)\s*(.+?):\s*(.+)$/);
788
919
  if (msgMatch) {
789
- const from = msgMatch[1].trim();
920
+ const from = safeAddActor(msgMatch[1].trim());
790
921
  const arrow = msgMatch[2];
791
- const to = msgMatch[3].trim();
792
- const label = msgMatch[4].trim();
793
- model.addActor(from);
794
- model.addActor(to);
922
+ const to = safeAddActor(msgMatch[3].trim());
923
+ const label = sanitizeLabel(msgMatch[4].trim());
924
+ if (messageCount >= MAX_MESSAGES)
925
+ throw new Error(`Import aborted: diagram exceeds the maximum of ${MAX_MESSAGES} messages`);
926
+ messageCount++;
795
927
  const messages = model.toJSON().messages ?? [];
796
- model.addMessage({ id: nextId("m", messages), from, to, label, style: arrow.startsWith("--") ? "dashed" : "solid" });
928
+ model.addMessage({
929
+ id: nextId("m", messages),
930
+ from,
931
+ to,
932
+ label,
933
+ style: arrow.startsWith("--") ? "dashed" : "solid"
934
+ });
797
935
  }
798
936
  }
799
937
  return model;
800
938
  }
801
939
  function fromMermaid(mermaid) {
940
+ if (mermaid.length > MAX_IMPORT_LENGTH) {
941
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
942
+ }
802
943
  const cleaned = mermaid.replace(/mermaid\.initialize\([\s\S]*?\)\s*;?/g, "");
803
944
  const rawLines = cleaned.split("\n");
804
945
  let startIdx = 0;
@@ -830,10 +971,74 @@ function fromMermaid(mermaid) {
830
971
  }
831
972
 
832
973
  // src/importers/json.ts
974
+ function stripDangerousKeys(obj) {
975
+ if (Array.isArray(obj)) return obj.map(stripDangerousKeys);
976
+ if (obj !== null && typeof obj === "object") {
977
+ const clean = {};
978
+ for (const [key, val] of Object.entries(obj)) {
979
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
980
+ clean[key] = stripDangerousKeys(val);
981
+ }
982
+ return clean;
983
+ }
984
+ return obj;
985
+ }
833
986
  function fromJSON(json) {
834
- const data = typeof json === "string" ? JSON.parse(json) : json;
835
- if (!data.type || !Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
836
- throw new Error("Invalid DiagramModel JSON");
987
+ if (typeof json === "string" && json.length > MAX_IMPORT_LENGTH) {
988
+ throw new Error(`Import aborted: input exceeds the maximum of ${MAX_IMPORT_LENGTH} characters`);
989
+ }
990
+ const raw = typeof json === "string" ? JSON.parse(json) : json;
991
+ const data = stripDangerousKeys(raw);
992
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
993
+ throw new Error("Invalid DiagramModel JSON: expected an object");
994
+ }
995
+ if (data.type !== "flowchart" && data.type !== "sequence") {
996
+ throw new Error(`Invalid DiagramModel JSON: unknown type "${data.type}"`);
997
+ }
998
+ if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
999
+ throw new Error("Invalid DiagramModel JSON: nodes and edges must be arrays");
1000
+ }
1001
+ if (data.nodes.length > MAX_NODES) {
1002
+ throw new Error(
1003
+ `Import aborted: diagram has ${data.nodes.length} nodes, maximum is ${MAX_NODES}`
1004
+ );
1005
+ }
1006
+ if (data.edges.length > MAX_EDGES) {
1007
+ throw new Error(
1008
+ `Import aborted: diagram has ${data.edges.length} edges, maximum is ${MAX_EDGES}`
1009
+ );
1010
+ }
1011
+ if (data.actors && data.actors.length > MAX_ACTORS) {
1012
+ throw new Error(
1013
+ `Import aborted: diagram has ${data.actors.length} actors, maximum is ${MAX_ACTORS}`
1014
+ );
1015
+ }
1016
+ if (data.messages && data.messages.length > MAX_MESSAGES) {
1017
+ throw new Error(
1018
+ `Import aborted: diagram has ${data.messages.length} messages, maximum is ${MAX_MESSAGES}`
1019
+ );
1020
+ }
1021
+ for (const node of data.nodes) {
1022
+ if (typeof node !== "object" || node === null || typeof node.id !== "string" || typeof node.label !== "string") {
1023
+ throw new Error("Invalid DiagramModel JSON: each node must have string id and label");
1024
+ }
1025
+ node.label = sanitizeLabel(node.label);
1026
+ }
1027
+ for (const edge of data.edges) {
1028
+ if (typeof edge !== "object" || edge === null || typeof edge.id !== "string" || typeof edge.from !== "string" || typeof edge.to !== "string") {
1029
+ throw new Error("Invalid DiagramModel JSON: each edge must have string id, from, and to");
1030
+ }
1031
+ if (edge.label) edge.label = sanitizeLabel(edge.label);
1032
+ }
1033
+ if (data.actors) {
1034
+ data.actors = data.actors.map((a) => typeof a === "string" ? sanitizeLabel(a) : a);
1035
+ }
1036
+ if (data.messages) {
1037
+ for (const msg of data.messages) {
1038
+ if (typeof msg === "object" && msg !== null && typeof msg.label === "string") {
1039
+ msg.label = sanitizeLabel(msg.label);
1040
+ }
1041
+ }
837
1042
  }
838
1043
  return Model.fromData(data);
839
1044
  }
@@ -845,6 +1050,8 @@ function fromJSON(json) {
845
1050
  flowchart,
846
1051
  fromJSON,
847
1052
  fromMermaid,
1053
+ sanitizeLabel,
1054
+ sanitizeURL,
848
1055
  sequence,
849
1056
  toJSON,
850
1057
  toMermaid,