@rogatio/cli 5.8.2 → 5.9.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.
@@ -27,6 +27,7 @@ var LIMITS = Object.freeze({
27
27
  maxMockDelayMs: 3e4,
28
28
  maxMockFilePathLength: 2048,
29
29
  maxResponseBodyReplacements: 64,
30
+ maxResponseBodyBytes: 4 * 1024 * 1024,
30
31
  maxResponseBodyPatternLength: 2048,
31
32
  maxResponseBodyReplacementLength: 4096,
32
33
  maxRequestBodyBytes: 4 * 1024 * 1024,
@@ -213,6 +214,21 @@ var HTTP_METHODS = Object.freeze([
213
214
  "CONNECT",
214
215
  "TRACE"
215
216
  ]);
217
+ function hasLoneSurrogate(value) {
218
+ for (let i = 0; i < value.length; i += 1) {
219
+ const code = value.charCodeAt(i);
220
+ if (code >= 55296 && code <= 56319) {
221
+ if (i + 1 >= value.length) return true;
222
+ const next = value.charCodeAt(i + 1);
223
+ if (next < 56320 || next > 57343) return true;
224
+ } else if (code >= 56320 && code <= 57343) {
225
+ if (i === 0) return true;
226
+ const prev = value.charCodeAt(i - 1);
227
+ if (prev < 55296 || prev > 56319) return true;
228
+ }
229
+ }
230
+ return false;
231
+ }
216
232
 
217
233
  // packages/editor/src/ai-assist-panel.ts
218
234
  function createAIAssistPanel(_container, _editor, onApply, onClose) {
@@ -561,307 +577,261 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
561
577
  };
562
578
  }
563
579
 
564
- // packages/editor/src/rule-types/mock.ts
565
- function isRecord(value) {
566
- return typeof value === "object" && value !== null && !Array.isArray(value);
580
+ // packages/editor/src/rule-types/dom.ts
581
+ function normalizeOption(option) {
582
+ if (typeof option === "string") {
583
+ return { value: option, label: option };
584
+ }
585
+ return option;
567
586
  }
568
- function asMockAction(value) {
569
- if (!isRecord(value) || typeof value.status !== "number") return void 0;
570
- const action = value;
571
- if (action.headers !== void 0 && !Array.isArray(action.headers)) {
572
- return void 0;
587
+ function createSelect(document2, options, value, onChange) {
588
+ const select = document2.createElement("select");
589
+ for (const option of options) {
590
+ const { value: optionValue, label } = normalizeOption(option);
591
+ const opt = document2.createElement("option");
592
+ opt.value = optionValue;
593
+ opt.textContent = label;
594
+ if (optionValue === value) opt.selected = true;
595
+ select.append(opt);
573
596
  }
574
- return action;
597
+ select.addEventListener("change", () => onChange(select.value));
598
+ return select;
575
599
  }
576
- function stable(diagnostics) {
577
- return [...diagnostics].sort(
578
- (a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
600
+ function createTextInput(document2, value, onChange, maxLength) {
601
+ const input = document2.createElement("input");
602
+ input.type = "text";
603
+ input.value = value;
604
+ if (maxLength !== void 0) input.maxLength = maxLength;
605
+ input.addEventListener("input", () => onChange(input.value));
606
+ return input;
607
+ }
608
+
609
+ // packages/editor/src/rule-types/header.ts
610
+ var FORBIDDEN_REQUEST_HEADERS2 = Object.freeze([
611
+ "accept-charset",
612
+ "accept-encoding",
613
+ "access-control-request-headers",
614
+ "access-control-request-method",
615
+ "connection",
616
+ "content-length",
617
+ "cookie",
618
+ "cookie2",
619
+ "date",
620
+ "dnt",
621
+ "expect",
622
+ "host",
623
+ "keep-alive",
624
+ "origin",
625
+ "proxy-authenticate",
626
+ "proxy-authorization",
627
+ "te",
628
+ "trailer",
629
+ "transfer-encoding",
630
+ "upgrade",
631
+ "via"
632
+ ]);
633
+ var FORBIDDEN_RESPONSE_HEADERS2 = Object.freeze([
634
+ "connection",
635
+ "content-encoding",
636
+ "content-length",
637
+ "date",
638
+ "keep-alive",
639
+ "proxy-authenticate",
640
+ "proxy-authorization",
641
+ "set-cookie",
642
+ "set-cookie2",
643
+ "te",
644
+ "trailer",
645
+ "transfer-encoding",
646
+ "upgrade",
647
+ "via"
648
+ ]);
649
+ var FORBIDDEN_REQUEST_PREFIXES2 = Object.freeze(["proxy-", "sec-"]);
650
+ function isForbiddenHeader(name, direction) {
651
+ const normalized = name.toLowerCase();
652
+ const forbidden = direction === "request" ? FORBIDDEN_REQUEST_HEADERS2 : FORBIDDEN_RESPONSE_HEADERS2;
653
+ return forbidden.includes(normalized) || direction === "request" && FORBIDDEN_REQUEST_PREFIXES2.some(
654
+ (prefix) => normalized.startsWith(prefix)
579
655
  );
580
656
  }
581
- function hasControlOrColon(value) {
582
- for (let index = 0; index < value.length; index += 1) {
583
- const code = value.charCodeAt(index);
584
- if (code <= 31 || code === 127 || code === 58) return true;
657
+ var HEADER_DIRECTIONS = [
658
+ "request",
659
+ "response"
660
+ ];
661
+ var HEADER_OPERATIONS = [
662
+ "set",
663
+ "append",
664
+ "remove"
665
+ ];
666
+ var HEADER_EDITOR_OPERATIONS = Object.freeze(["set", "remove"]);
667
+ function operationOptions(current) {
668
+ if (current === "append") {
669
+ return ["set", "append", "remove"];
585
670
  }
586
- return false;
671
+ return HEADER_EDITOR_OPERATIONS;
587
672
  }
588
- function createMockRuleType() {
673
+ function createHeaderRuleType() {
589
674
  return {
590
- id: "mock",
591
- label: "Mock response",
592
- actionField: "mock",
675
+ id: "header",
676
+ label: "Header",
593
677
  matches(rule) {
594
- const type = rule.type;
595
- if (type === "mock") return true;
596
- if (type !== void 0) return false;
597
- return asMockAction(rule.mock) !== void 0;
678
+ return rule.type === "header";
679
+ },
680
+ defaultFields() {
681
+ return {
682
+ headerDirection: "request",
683
+ headerOperation: "set",
684
+ headerName: "",
685
+ headerValue: ""
686
+ };
687
+ },
688
+ mount(context) {
689
+ const document2 = context.document;
690
+ const fieldset = document2.createElement("fieldset");
691
+ const legend = document2.createElement("legend");
692
+ legend.textContent = "Header rule";
693
+ fieldset.append(legend);
694
+ const directionLabel = document2.createElement("label");
695
+ directionLabel.textContent = "Direction";
696
+ const directionValue = context.getField("headerDirection") ?? "request";
697
+ const directionSelect = createSelect(
698
+ document2,
699
+ HEADER_DIRECTIONS,
700
+ directionValue,
701
+ (value) => {
702
+ context.setField("headerDirection", value);
703
+ }
704
+ );
705
+ context.registerControl("/headerDirection", directionSelect);
706
+ directionLabel.append(directionSelect);
707
+ fieldset.append(directionLabel);
708
+ const operationLabel = document2.createElement("label");
709
+ operationLabel.textContent = "Operation";
710
+ let operationValue = context.getField("headerOperation") ?? "set";
711
+ const valueLabel = document2.createElement("label");
712
+ valueLabel.textContent = "Header value";
713
+ const syncValueVisibility = () => {
714
+ const showValue = operationValue === "set" || operationValue === "append";
715
+ valueLabel.hidden = !showValue;
716
+ };
717
+ const operationSelect = createSelect(
718
+ document2,
719
+ operationOptions(operationValue),
720
+ operationValue,
721
+ (value) => {
722
+ operationValue = value;
723
+ context.setField("headerOperation", value);
724
+ if (value === "remove") {
725
+ context.deleteField("headerValue");
726
+ }
727
+ syncValueVisibility();
728
+ }
729
+ );
730
+ context.registerControl("/headerOperation", operationSelect);
731
+ operationLabel.append(operationSelect);
732
+ fieldset.append(operationLabel);
733
+ const nameLabel = document2.createElement("label");
734
+ nameLabel.textContent = "Header name";
735
+ const nameValue = context.getField("headerName") ?? "";
736
+ const nameInput = createTextInput(
737
+ document2,
738
+ nameValue,
739
+ (value) => {
740
+ context.setField("headerName", value);
741
+ },
742
+ 256
743
+ );
744
+ context.registerControl("/headerName", nameInput);
745
+ nameLabel.append(nameInput);
746
+ fieldset.append(nameLabel);
747
+ const valueValue = context.getField("headerValue") ?? "";
748
+ const valueInput = createTextInput(
749
+ document2,
750
+ valueValue,
751
+ (value) => {
752
+ context.setField("headerValue", value);
753
+ },
754
+ 4096
755
+ );
756
+ context.registerControl("/headerValue", valueInput);
757
+ valueLabel.append(valueInput);
758
+ fieldset.append(valueLabel);
759
+ syncValueVisibility();
760
+ context.container.append(fieldset);
761
+ return {
762
+ destroy() {
763
+ directionSelect.removeEventListener("change", () => {
764
+ });
765
+ operationSelect.removeEventListener("change", () => {
766
+ });
767
+ nameInput.removeEventListener("input", () => {
768
+ });
769
+ valueInput.removeEventListener("input", () => {
770
+ });
771
+ }
772
+ };
598
773
  },
599
774
  validate(rule, rulePath) {
600
- const mock = asMockAction(rule.mock);
601
- if (mock === void 0) return [];
602
775
  const diagnostics = [];
603
- const status = mock.status;
604
- if (!Number.isInteger(status) || status < LIMITS.minMockStatus || status > LIMITS.maxMockStatus) {
776
+ const direction = rule.headerDirection;
777
+ const operation = rule.headerOperation;
778
+ const headerName = rule.headerName;
779
+ const headerValue = rule.headerValue;
780
+ if (!direction || !HEADER_DIRECTIONS.includes(direction)) {
605
781
  diagnostics.push({
606
- code: "editor.mock-status-range",
782
+ code: "schema.invalid-value",
607
783
  severity: "error",
608
- path: `${rulePath}/mock/status`,
609
- message: `Mock status must be an integer between ${LIMITS.minMockStatus} and ${LIMITS.maxMockStatus}.`
784
+ path: `${rulePath}/headerDirection`,
785
+ message: 'Direction must be "request" or "response".'
610
786
  });
611
787
  }
612
- const hasBody = typeof mock.body === "string";
613
- const hasFile = typeof mock.file === "string";
614
- if (hasBody === hasFile) {
788
+ if (!operation || !HEADER_OPERATIONS.includes(operation)) {
615
789
  diagnostics.push({
616
- code: "editor.mock-body-source",
790
+ code: "schema.invalid-value",
617
791
  severity: "error",
618
- path: `${rulePath}/mock`,
619
- message: "A mock rule must set exactly one of body or file."
792
+ path: `${rulePath}/headerOperation`,
793
+ message: 'Operation must be "set", "append", or "remove".'
620
794
  });
621
- } else if (hasBody) {
622
- const body = mock.body;
623
- if (body.length > LIMITS.maxMockInlineBodyLength) {
624
- diagnostics.push({
625
- code: "editor.mock-body-too-long",
626
- severity: "error",
627
- path: `${rulePath}/mock/body`,
628
- message: `Mock inline body must be at most ${LIMITS.maxMockInlineBodyLength} characters.`
629
- });
630
- }
631
- } else if (hasFile) {
632
- const file = mock.file;
633
- if (file.length === 0 || file.length > LIMITS.maxMockFilePathLength) {
634
- diagnostics.push({
635
- code: "editor.mock-file-path",
636
- severity: "error",
637
- path: `${rulePath}/mock/file`,
638
- message: `Mock file path must be 1-${LIMITS.maxMockFilePathLength} characters.`
639
- });
640
- }
641
795
  }
642
- if (mock.delayMs !== void 0 && (!Number.isInteger(mock.delayMs) || mock.delayMs < 0 || mock.delayMs > LIMITS.maxMockDelayMs)) {
796
+ if (typeof headerName !== "string" || headerName.length === 0 || headerName.length > 256) {
643
797
  diagnostics.push({
644
- code: "editor.mock-delay-range",
798
+ code: "schema.out-of-range",
645
799
  severity: "error",
646
- path: `${rulePath}/mock/delayMs`,
647
- message: `Mock delay must be an integer between 0 and ${LIMITS.maxMockDelayMs} milliseconds.`
800
+ path: `${rulePath}/headerName`,
801
+ message: "Header name must be 1-256 characters."
648
802
  });
649
- }
650
- if (mock.headers !== void 0) {
651
- if (mock.headers.length > LIMITS.maxMockHeadersPerRule) {
652
- diagnostics.push({
653
- code: "editor.mock-too-many-headers",
654
- severity: "error",
655
- path: `${rulePath}/mock/headers`,
656
- message: `A mock rule may define at most ${LIMITS.maxMockHeadersPerRule} headers.`
657
- });
658
- }
659
- mock.headers.forEach((header, index) => {
660
- const namePath = `${rulePath}/mock/headers/${index}/name`;
661
- const valuePath = `${rulePath}/mock/headers/${index}/value`;
662
- if (typeof header?.name !== "string" || header.name.length === 0 || header.name.length > LIMITS.maxMockHeaderNameLength) {
663
- diagnostics.push({
664
- code: "editor.mock-header-name",
665
- severity: "error",
666
- path: namePath,
667
- message: `Mock header name must be 1-${LIMITS.maxMockHeaderNameLength} characters.`
668
- });
669
- } else if (hasControlOrColon(header.name)) {
670
- diagnostics.push({
671
- code: "editor.mock-header-name",
672
- severity: "error",
673
- path: namePath,
674
- message: "Mock header name must not contain control characters or a colon."
675
- });
676
- }
677
- if (typeof header?.value !== "string" || header.value.length > LIMITS.maxMockHeaderValueLength) {
678
- diagnostics.push({
679
- code: "editor.mock-header-value",
680
- severity: "error",
681
- path: valuePath,
682
- message: `Mock header value must be at most ${LIMITS.maxMockHeaderValueLength} characters.`
683
- });
684
- }
803
+ } else if (direction && isForbiddenHeader(headerName, direction)) {
804
+ diagnostics.push({
805
+ code: "extension.forbidden-header",
806
+ severity: "error",
807
+ path: `${rulePath}/headerName`,
808
+ message: `Header "${headerName}" is forbidden for ${direction} headers.`
685
809
  });
686
810
  }
687
- return stable(diagnostics);
688
- },
689
- mount(context) {
690
- const { document: document2, container } = context;
691
- const getCurrent = () => asMockAction(context.getField("mock"));
692
- const setAction = (next) => {
693
- context.setField("mock", next);
694
- };
695
- const render = () => {
696
- const current = getCurrent();
697
- container.replaceChildren();
698
- if (current === void 0) return;
699
- const statusLabel = document2.createElement("label");
700
- statusLabel.textContent = "Status";
701
- const statusInput = document2.createElement("input");
702
- statusInput.type = "number";
703
- statusInput.min = String(LIMITS.minMockStatus);
704
- statusInput.max = String(LIMITS.maxMockStatus);
705
- statusInput.step = "1";
706
- statusInput.value = String(current.status);
707
- statusInput.dataset.editorField = "true";
708
- statusInput.addEventListener("input", () => {
709
- const c = getCurrent();
710
- if (c === void 0) return;
711
- setAction({ ...c, status: Number(statusInput.value) });
811
+ if ((operation === "set" || operation === "append") && typeof headerValue !== "string") {
812
+ diagnostics.push({
813
+ code: "schema.required",
814
+ severity: "error",
815
+ path: `${rulePath}/headerValue`,
816
+ message: "Header value is required for set and append operations."
712
817
  });
713
- context.registerControl("/mock/status", statusInput);
714
- statusLabel.append(statusInput);
715
- const delayLabel = document2.createElement("label");
716
- delayLabel.textContent = "Delay (ms)";
717
- const delayInput = document2.createElement("input");
718
- delayInput.type = "number";
719
- delayInput.min = "0";
720
- delayInput.max = String(LIMITS.maxMockDelayMs);
721
- delayInput.step = "1";
722
- delayInput.value = current.delayMs === void 0 ? "" : String(current.delayMs);
723
- delayInput.dataset.editorField = "true";
724
- delayInput.addEventListener("input", () => {
725
- const c = getCurrent();
726
- if (c === void 0) return;
727
- const raw = delayInput.value;
728
- const next = raw === "" ? { ...c, delayMs: void 0 } : { ...c, delayMs: Number(raw) };
729
- setAction(next);
818
+ } else if ((operation === "set" || operation === "append") && typeof headerValue === "string" && headerValue.length > 4096) {
819
+ diagnostics.push({
820
+ code: "schema.out-of-range",
821
+ severity: "error",
822
+ path: `${rulePath}/headerValue`,
823
+ message: "Header value must be at most 4096 characters."
730
824
  });
731
- context.registerControl("/mock/delayMs", delayInput);
732
- delayLabel.append(delayInput);
733
- const sourceLabel = document2.createElement("label");
734
- sourceLabel.textContent = "Body source";
735
- const sourceSelect = document2.createElement("select");
736
- sourceSelect.dataset.editorField = "true";
737
- const bodyOption = document2.createElement("option");
738
- bodyOption.value = "body";
739
- bodyOption.textContent = "Inline body";
740
- const fileOption = document2.createElement("option");
741
- fileOption.value = "file";
742
- fileOption.textContent = "File snapshot";
743
- sourceSelect.append(bodyOption, fileOption);
744
- sourceSelect.value = typeof current.file === "string" ? "file" : "body";
745
- context.registerControl("/mock/body-source", sourceSelect);
746
- const bodyArea = document2.createElement("textarea");
747
- bodyArea.value = typeof current.body === "string" ? current.body : "";
748
- bodyArea.dataset.editorField = "true";
749
- bodyArea.addEventListener("input", () => {
750
- const c = getCurrent();
751
- if (c === void 0) return;
752
- setAction({ ...c, body: bodyArea.value, file: void 0 });
825
+ }
826
+ if (operation === "remove" && headerValue !== void 0) {
827
+ diagnostics.push({
828
+ code: "schema.unexpected",
829
+ severity: "error",
830
+ path: `${rulePath}/headerValue`,
831
+ message: "Header value must not be provided for remove operation."
753
832
  });
754
- context.registerControl("/mock/body", bodyArea);
755
- const fileLabel = document2.createElement("label");
756
- fileLabel.textContent = "File path";
757
- const fileInput = document2.createElement("input");
758
- fileInput.type = "text";
759
- fileInput.value = typeof current.file === "string" ? current.file : "";
760
- fileInput.dataset.editorField = "true";
761
- fileInput.addEventListener("input", () => {
762
- const c = getCurrent();
763
- if (c === void 0) return;
764
- setAction({ ...c, body: void 0, file: fileInput.value });
765
- });
766
- context.registerControl("/mock/file", fileInput);
767
- fileLabel.append(fileInput);
768
- const syncSource = () => {
769
- const useFile = sourceSelect.value === "file";
770
- bodyArea.style.display = useFile ? "none" : "";
771
- fileLabel.style.display = useFile ? "" : "none";
772
- };
773
- sourceSelect.addEventListener("change", syncSource);
774
- syncSource();
775
- const headersFieldset = document2.createElement("fieldset");
776
- const headersLegend = document2.createElement("legend");
777
- headersLegend.textContent = "Response headers";
778
- headersFieldset.append(headersLegend);
779
- const renderHeaders = () => {
780
- const c = getCurrent();
781
- if (c === void 0) return;
782
- headersFieldset.querySelectorAll("[data-mock-header-row]").forEach((row) => {
783
- row.remove();
784
- });
785
- const headers = c.headers ?? [];
786
- headers.forEach((header, index) => {
787
- const row = document2.createElement("div");
788
- row.dataset.mockHeaderRow = String(index);
789
- const nameLabel = document2.createElement("label");
790
- nameLabel.textContent = "Name";
791
- const nameInput = document2.createElement("input");
792
- nameInput.type = "text";
793
- nameInput.value = header.name;
794
- nameInput.dataset.editorField = "true";
795
- nameInput.addEventListener("input", () => {
796
- const cc = getCurrent();
797
- if (cc === void 0) return;
798
- const next = (cc.headers ?? []).map(
799
- (h, i) => i === index ? { ...h, name: nameInput.value } : h
800
- );
801
- setAction({ ...cc, headers: next });
802
- });
803
- context.registerControl(`/mock/headers/${index}/name`, nameInput);
804
- nameLabel.append(nameInput);
805
- const valueLabel = document2.createElement("label");
806
- valueLabel.textContent = "Value";
807
- const valueInput = document2.createElement("input");
808
- valueInput.type = "text";
809
- valueInput.value = header.value;
810
- valueInput.dataset.editorField = "true";
811
- valueInput.addEventListener("input", () => {
812
- const cc = getCurrent();
813
- if (cc === void 0) return;
814
- const next = (cc.headers ?? []).map(
815
- (h, i) => i === index ? { ...h, value: valueInput.value } : h
816
- );
817
- setAction({ ...cc, headers: next });
818
- });
819
- context.registerControl(`/mock/headers/${index}/value`, valueInput);
820
- valueLabel.append(valueInput);
821
- const remove = document2.createElement("button");
822
- remove.type = "button";
823
- remove.textContent = "Remove";
824
- remove.dataset.editorField = "true";
825
- remove.addEventListener("click", () => {
826
- const cc = getCurrent();
827
- if (cc === void 0) return;
828
- const next = (cc.headers ?? []).filter((_, i) => i !== index);
829
- setAction({ ...cc, headers: next });
830
- });
831
- row.append(nameLabel, valueLabel, remove);
832
- headersFieldset.append(row);
833
- });
834
- const add = document2.createElement("button");
835
- add.type = "button";
836
- add.textContent = "Add header";
837
- add.dataset.editorField = "true";
838
- add.dataset.mockAddHeader = "true";
839
- add.addEventListener("click", () => {
840
- const cc = getCurrent();
841
- if (cc === void 0) return;
842
- setAction({
843
- ...cc,
844
- headers: [...cc.headers ?? [], { name: "", value: "" }]
845
- });
846
- });
847
- headersFieldset.append(add);
848
- };
849
- renderHeaders();
850
- container.append(
851
- statusLabel,
852
- delayLabel,
853
- sourceLabel,
854
- bodyArea,
855
- fileLabel,
856
- headersFieldset
857
- );
858
- };
859
- render();
860
- return { destroy() {
861
- } };
862
- },
863
- defaultAction() {
864
- return { status: 200, body: "" };
833
+ }
834
+ return diagnostics;
865
835
  }
866
836
  };
867
837
  }
@@ -870,17 +840,33 @@ function createMockRuleType() {
870
840
  var MAX_QUERY_NAME_LENGTH = 256;
871
841
  var MAX_QUERY_VALUE_LENGTH = 2048;
872
842
  var MAX_QUERY_PARAMS = 64;
873
- function isRecord2(value) {
843
+ var QUERY_OPERATIONS = Object.freeze(["set", "remove"]);
844
+ function isRecord(value) {
874
845
  return typeof value === "object" && value !== null && !Array.isArray(value);
875
846
  }
876
847
  function asQueryAction(value) {
877
- if (!isRecord2(value)) return void 0;
848
+ if (!isRecord(value)) return void 0;
878
849
  if (value.type !== "query") return void 0;
879
850
  const params = value.params;
880
851
  if (!Array.isArray(params)) return void 0;
881
852
  return value;
882
853
  }
883
- function stable2(diagnostics) {
854
+ function paramOperation(param) {
855
+ return param.operation ?? "set";
856
+ }
857
+ function buildSetParam(name, value) {
858
+ return { name, operation: "set", value };
859
+ }
860
+ function buildRemoveParam(name) {
861
+ return { name, operation: "remove" };
862
+ }
863
+ function normalizeParam(param) {
864
+ if (paramOperation(param) === "remove") {
865
+ return buildRemoveParam(param.name);
866
+ }
867
+ return buildSetParam(param.name, param.value ?? "");
868
+ }
869
+ function stable(diagnostics) {
884
870
  return [...diagnostics].sort(
885
871
  (a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
886
872
  );
@@ -889,7 +875,7 @@ var queryRuleType = {
889
875
  id: "query",
890
876
  label: "Query parameters",
891
877
  matches(rule) {
892
- return asQueryAction(rule.action) !== void 0;
878
+ return rule.type === "query";
893
879
  },
894
880
  validate(rule, rulePath) {
895
881
  const action = asQueryAction(rule.action);
@@ -902,7 +888,7 @@ var queryRuleType = {
902
888
  path: `${rulePath}/action/params`,
903
889
  message: "Query rules need at least one parameter."
904
890
  });
905
- return stable2(diagnostics);
891
+ return stable(diagnostics);
906
892
  }
907
893
  if (action.params.length > MAX_QUERY_PARAMS) {
908
894
  diagnostics.push({
@@ -916,6 +902,7 @@ var queryRuleType = {
916
902
  action.params.forEach((param, index) => {
917
903
  const namePath = `${rulePath}/action/params/${index}/name`;
918
904
  const valuePath = `${rulePath}/action/params/${index}/value`;
905
+ const operation = paramOperation(param);
919
906
  if (typeof param?.name !== "string" || param.name.length === 0) {
920
907
  diagnostics.push({
921
908
  code: "editor.query-param-name-required",
@@ -931,19 +918,28 @@ var queryRuleType = {
931
918
  message: "Query parameter name is too long."
932
919
  });
933
920
  }
934
- if (typeof param?.value !== "string" || param.value.length === 0) {
935
- diagnostics.push({
936
- code: "editor.query-param-value-required",
937
- severity: "error",
938
- path: valuePath,
939
- message: "Query parameter value is required."
940
- });
941
- } else if (param.value.length > MAX_QUERY_VALUE_LENGTH) {
921
+ if (operation === "set") {
922
+ if (typeof param?.value !== "string" || param.value.length === 0) {
923
+ diagnostics.push({
924
+ code: "editor.query-param-value-required",
925
+ severity: "error",
926
+ path: valuePath,
927
+ message: "Query parameter value is required."
928
+ });
929
+ } else if (param.value.length > MAX_QUERY_VALUE_LENGTH) {
930
+ diagnostics.push({
931
+ code: "editor.query-param-value-too-long",
932
+ severity: "error",
933
+ path: valuePath,
934
+ message: "Query parameter value is too long."
935
+ });
936
+ }
937
+ } else if (param?.value !== void 0) {
942
938
  diagnostics.push({
943
- code: "editor.query-param-value-too-long",
939
+ code: "schema.unexpected",
944
940
  severity: "error",
945
941
  path: valuePath,
946
- message: "Query parameter value is too long."
942
+ message: "Query parameter value must not be provided for remove operation."
947
943
  });
948
944
  }
949
945
  if (typeof param?.name === "string" && seen.has(param.name)) {
@@ -956,7 +952,7 @@ var queryRuleType = {
956
952
  }
957
953
  if (typeof param?.name === "string") seen.add(param.name);
958
954
  });
959
- return stable2(diagnostics);
955
+ return stable(diagnostics);
960
956
  },
961
957
  mount(context) {
962
958
  const { document: document2, container } = context;
@@ -971,6 +967,7 @@ var queryRuleType = {
971
967
  current.params.forEach((param, index) => {
972
968
  const row = document2.createElement("div");
973
969
  row.dataset.queryParamRow = String(index);
970
+ const operation = paramOperation(param);
974
971
  const nameLabel = document2.createElement("label");
975
972
  nameLabel.textContent = "Name";
976
973
  const nameInput = document2.createElement("input");
@@ -980,29 +977,57 @@ var queryRuleType = {
980
977
  nameInput.addEventListener("input", () => {
981
978
  const c = getCurrent();
982
979
  if (c === void 0) return;
983
- const params = c.params.map(
984
- (p, i) => i === index ? { ...p, name: nameInput.value } : p
985
- );
980
+ const params = c.params.map((p, i) => {
981
+ if (i !== index) return p;
982
+ return normalizeParam({ ...p, name: nameInput.value });
983
+ });
986
984
  setAction({ type: "query", params });
987
985
  });
988
986
  context.registerControl(`/action/params/${index}/name`, nameInput);
989
987
  nameLabel.append(nameInput);
988
+ const operationLabel = document2.createElement("label");
989
+ operationLabel.textContent = "Operation";
990
+ const operationSelect = createSelect(
991
+ document2,
992
+ QUERY_OPERATIONS,
993
+ operation,
994
+ (value) => {
995
+ const c = getCurrent();
996
+ if (c === void 0) return;
997
+ const params = c.params.map((p, i) => {
998
+ if (i !== index) return p;
999
+ if (value === "remove") {
1000
+ return buildRemoveParam(p.name);
1001
+ }
1002
+ return buildSetParam(p.name, p.value ?? "");
1003
+ });
1004
+ setAction({ type: "query", params });
1005
+ render();
1006
+ }
1007
+ );
1008
+ operationSelect.dataset.queryParamOperation = String(index);
1009
+ context.registerControl(
1010
+ `/action/params/${index}/operation`,
1011
+ operationSelect
1012
+ );
1013
+ operationLabel.append(operationSelect);
990
1014
  const valueLabel = document2.createElement("label");
991
1015
  valueLabel.textContent = "Value";
992
1016
  const valueInput = document2.createElement("input");
993
1017
  valueInput.type = "text";
994
- valueInput.value = param.value;
1018
+ valueInput.value = param.value ?? "";
995
1019
  valueInput.dataset.editorField = "true";
996
1020
  valueInput.addEventListener("input", () => {
997
1021
  const c = getCurrent();
998
1022
  if (c === void 0) return;
999
1023
  const params = c.params.map(
1000
- (p, i) => i === index ? { ...p, value: valueInput.value } : p
1024
+ (p, i) => i === index ? buildSetParam(p.name, valueInput.value) : p
1001
1025
  );
1002
1026
  setAction({ type: "query", params });
1003
1027
  });
1004
1028
  context.registerControl(`/action/params/${index}/value`, valueInput);
1005
1029
  valueLabel.append(valueInput);
1030
+ valueLabel.hidden = operation === "remove";
1006
1031
  const remove = document2.createElement("button");
1007
1032
  remove.type = "button";
1008
1033
  remove.textContent = "Remove";
@@ -1012,8 +1037,9 @@ var queryRuleType = {
1012
1037
  if (c === void 0) return;
1013
1038
  const params = c.params.filter((_, i) => i !== index);
1014
1039
  setAction({ type: "query", params });
1040
+ render();
1015
1041
  });
1016
- row.append(nameLabel, valueLabel, remove);
1042
+ row.append(nameLabel, operationLabel, valueLabel, remove);
1017
1043
  container.append(row);
1018
1044
  });
1019
1045
  const add = document2.createElement("button");
@@ -1025,8 +1051,9 @@ var queryRuleType = {
1025
1051
  if (c === void 0) return;
1026
1052
  setAction({
1027
1053
  type: "query",
1028
- params: [...c.params, { name: "", value: "" }]
1054
+ params: [...c.params, buildSetParam("", "")]
1029
1055
  });
1056
+ render();
1030
1057
  });
1031
1058
  container.append(add);
1032
1059
  };
@@ -1035,34 +1062,71 @@ var queryRuleType = {
1035
1062
  } };
1036
1063
  },
1037
1064
  defaultAction() {
1038
- return { type: "query", params: [{ name: "", value: "" }] };
1065
+ return {
1066
+ type: "query",
1067
+ params: [buildSetParam("", "")]
1068
+ };
1039
1069
  }
1040
1070
  };
1041
1071
 
1072
+ // packages/editor/src/rule-types/redirect.ts
1073
+ function createRedirectRuleType() {
1074
+ return {
1075
+ id: "redirect",
1076
+ label: "Redirect",
1077
+ actionField: "redirect",
1078
+ matches(rule) {
1079
+ return rule.type === "redirect";
1080
+ },
1081
+ defaultAction() {
1082
+ return { destination: "" };
1083
+ },
1084
+ mount(context) {
1085
+ const document2 = context.document;
1086
+ const label = document2.createElement("label");
1087
+ label.textContent = "Redirect destination URL";
1088
+ const input = document2.createElement("input");
1089
+ input.type = "text";
1090
+ input.maxLength = 2048;
1091
+ const existing = context.getField("redirect.destination");
1092
+ input.value = typeof existing === "string" ? existing : "";
1093
+ input.addEventListener("input", () => {
1094
+ context.setField("redirect.destination", input.value);
1095
+ });
1096
+ context.registerControl("/redirect/destination", input);
1097
+ label.append(input);
1098
+ context.container.append(label);
1099
+ return {
1100
+ destroy() {
1101
+ input.removeEventListener("input", () => {
1102
+ });
1103
+ }
1104
+ };
1105
+ },
1106
+ validate(rule, rulePath) {
1107
+ const redirect = rule.redirect;
1108
+ const destination = redirect !== null && typeof redirect === "object" && typeof redirect.destination === "string" ? redirect.destination : "";
1109
+ const urlRegex = typeof rule.urlRegex === "string" ? rule.urlRegex : "";
1110
+ const issues = validateRedirectDestination(destination, urlRegex);
1111
+ return issues.map((issue) => ({
1112
+ code: `schema.${issue.code}`,
1113
+ severity: "error",
1114
+ path: `${rulePath}/redirect/destination`,
1115
+ message: issue.message
1116
+ }));
1117
+ }
1118
+ };
1119
+ }
1120
+
1042
1121
  // packages/editor/src/rule-types/request-body.ts
1043
- function isRecord3(value) {
1122
+ function isRecord2(value) {
1044
1123
  return value !== null && typeof value === "object" && !Array.isArray(value);
1045
1124
  }
1046
1125
  function requestBodyOf(value) {
1047
- if (!isRecord3(value) || !("mode" in value)) return void 0;
1126
+ if (!isRecord2(value) || !("mode" in value)) return void 0;
1048
1127
  return value;
1049
1128
  }
1050
- function hasLoneSurrogate(value) {
1051
- for (let i = 0; i < value.length; i += 1) {
1052
- const code = value.charCodeAt(i);
1053
- if (code >= 55296 && code <= 56319) {
1054
- if (i + 1 >= value.length) return true;
1055
- const next = value.charCodeAt(i + 1);
1056
- if (next < 56320 || next > 57343) return true;
1057
- } else if (code >= 56320 && code <= 57343) {
1058
- if (i === 0) return true;
1059
- const prev = value.charCodeAt(i - 1);
1060
- if (prev < 55296 || prev > 56319) return true;
1061
- }
1062
- }
1063
- return false;
1064
- }
1065
- function stable3(values) {
1129
+ function stable2(values) {
1066
1130
  return [...values].sort(
1067
1131
  (left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)
1068
1132
  );
@@ -1178,7 +1242,7 @@ function createRequestBodyRuleType() {
1178
1242
  message: 'Request-body rules require exactly one resource type: "xmlhttprequest".'
1179
1243
  });
1180
1244
  }
1181
- return stable3(diagnostics);
1245
+ return stable2(diagnostics);
1182
1246
  },
1183
1247
  mount(context) {
1184
1248
  const { document: document2, container } = context;
@@ -1186,26 +1250,25 @@ function createRequestBodyRuleType() {
1186
1250
  const current = requestBodyOf(context.getField("requestBody"));
1187
1251
  container.replaceChildren();
1188
1252
  if (current === void 0) return;
1189
- const modeSelect = document2.createElement("select");
1190
- modeSelect.value = current.mode;
1191
- const replaceOption = document2.createElement("option");
1192
- replaceOption.value = "replace";
1193
- replaceOption.textContent = "Replace body";
1194
- const regexOption = document2.createElement("option");
1195
- regexOption.value = "regex";
1196
- regexOption.textContent = "Regex replace";
1197
- modeSelect.append(replaceOption, regexOption);
1198
- modeSelect.addEventListener("change", () => {
1199
- const next = { ...current, mode: modeSelect.value };
1200
- if (modeSelect.value === "replace") {
1201
- delete next.pattern;
1202
- delete next.replacement;
1203
- } else {
1204
- delete next.body;
1253
+ const modeSelect = createSelect(
1254
+ document2,
1255
+ [
1256
+ { value: "replace", label: "Replace body" },
1257
+ { value: "regex", label: "Regex replace" }
1258
+ ],
1259
+ current.mode,
1260
+ (mode) => {
1261
+ const next = { ...current, mode };
1262
+ if (mode === "replace") {
1263
+ delete next.pattern;
1264
+ delete next.replacement;
1265
+ } else {
1266
+ delete next.body;
1267
+ }
1268
+ context.setField("requestBody", next);
1269
+ render();
1205
1270
  }
1206
- context.setField("requestBody", next);
1207
- render();
1208
- });
1271
+ );
1209
1272
  context.registerControl("/requestBody/mode", modeSelect);
1210
1273
  container.append(modeSelect);
1211
1274
  if (current.mode === "replace") {
@@ -1252,71 +1315,135 @@ function createRequestBodyRuleType() {
1252
1315
  }
1253
1316
 
1254
1317
  // packages/editor/src/rule-types/response-body.ts
1255
- function isRecord4(value) {
1318
+ function isRecord3(value) {
1256
1319
  return value !== null && typeof value === "object" && !Array.isArray(value);
1257
1320
  }
1258
- function replacementsOf(value) {
1259
- if (!isRecord4(value) || !Array.isArray(value.replacements)) return void 0;
1260
- return value.replacements;
1321
+ function responseBodyOf(value) {
1322
+ if (!isRecord3(value)) return void 0;
1323
+ if (value.mode === "replace") {
1324
+ return { mode: "replace", body: value.body };
1325
+ }
1326
+ if (value.mode === "regex" && Array.isArray(value.replacements)) {
1327
+ return {
1328
+ mode: "regex",
1329
+ replacements: value.replacements
1330
+ };
1331
+ }
1332
+ if (Array.isArray(value.replacements)) {
1333
+ return {
1334
+ mode: "regex",
1335
+ replacements: value.replacements
1336
+ };
1337
+ }
1338
+ return void 0;
1261
1339
  }
1262
- function stable4(values) {
1340
+ function stable3(values) {
1263
1341
  return [...values].sort(
1264
1342
  (left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)
1265
1343
  );
1266
1344
  }
1345
+ function validateRegexReplacements(replacements, rulePath) {
1346
+ const diagnostics = [];
1347
+ if (replacements.length === 0 || replacements.length > LIMITS.maxResponseBodyReplacements) {
1348
+ diagnostics.push({
1349
+ code: "editor.response-body-replacements",
1350
+ severity: "error",
1351
+ path: `${rulePath}/responseBody/replacements`,
1352
+ message: `Response-body rules need 1-${LIMITS.maxResponseBodyReplacements} replacements.`
1353
+ });
1354
+ }
1355
+ replacements.forEach((entry, index) => {
1356
+ const patternPath = `${rulePath}/responseBody/replacements/${index}/pattern`;
1357
+ const replacementPath = `${rulePath}/responseBody/replacements/${index}/replacement`;
1358
+ try {
1359
+ if (typeof entry?.pattern !== "string" || entry.pattern.length === 0 || entry.pattern.length > LIMITS.maxResponseBodyPatternLength)
1360
+ throw new Error();
1361
+ new RegExp(entry.pattern, "u");
1362
+ } catch {
1363
+ diagnostics.push({
1364
+ code: "editor.response-body-pattern",
1365
+ severity: "error",
1366
+ path: patternPath,
1367
+ message: "Replacement pattern must be a valid bounded regular expression."
1368
+ });
1369
+ }
1370
+ if (typeof entry?.replacement !== "string" || entry.replacement.length > LIMITS.maxResponseBodyReplacementLength) {
1371
+ diagnostics.push({
1372
+ code: "editor.response-body-replacement",
1373
+ severity: "error",
1374
+ path: replacementPath,
1375
+ message: "Replacement text exceeds the permitted size."
1376
+ });
1377
+ }
1378
+ });
1379
+ return diagnostics;
1380
+ }
1267
1381
  function createResponseBodyRuleType() {
1268
1382
  return {
1269
1383
  id: "response-body",
1270
- label: "Response body rewrite",
1384
+ label: "Response body",
1271
1385
  actionField: "responseBody",
1272
1386
  matches(rule) {
1273
- return rule.type === "response-body" || replacementsOf(rule.responseBody) !== void 0;
1387
+ return rule.type === "response-body" || responseBodyOf(rule.responseBody) !== void 0;
1274
1388
  },
1275
1389
  validate(rule, rulePath) {
1276
- const replacements = replacementsOf(rule.responseBody);
1277
- if (replacements === void 0) return [];
1278
- const diagnostics = [];
1279
- if (replacements.length === 0 || replacements.length > LIMITS.maxResponseBodyReplacements) {
1280
- diagnostics.push({
1281
- code: "editor.response-body-replacements",
1282
- severity: "error",
1283
- path: `${rulePath}/responseBody/replacements`,
1284
- message: `Response-body rules need 1-${LIMITS.maxResponseBodyReplacements} replacements.`
1285
- });
1390
+ const raw = rule.responseBody;
1391
+ if (isRecord3(raw) && "mode" in raw && raw.mode !== "replace" && raw.mode !== "regex") {
1392
+ return stable3([
1393
+ {
1394
+ code: "editor.response-body-mode",
1395
+ severity: "error",
1396
+ path: `${rulePath}/responseBody/mode`,
1397
+ message: 'responseBody.mode must be "replace" or "regex"'
1398
+ }
1399
+ ]);
1286
1400
  }
1287
- replacements.forEach((entry, index) => {
1288
- const patternPath = `${rulePath}/responseBody/replacements/${index}/pattern`;
1289
- const replacementPath = `${rulePath}/responseBody/replacements/${index}/replacement`;
1290
- try {
1291
- if (typeof entry?.pattern !== "string" || entry.pattern.length === 0 || entry.pattern.length > LIMITS.maxResponseBodyPatternLength)
1292
- throw new Error();
1293
- new RegExp(entry.pattern, "u");
1294
- } catch {
1401
+ const action = responseBodyOf(raw);
1402
+ if (action === void 0) {
1403
+ if (isRecord3(raw) && raw.mode === "regex") {
1404
+ return stable3(validateRegexReplacements([], rulePath));
1405
+ }
1406
+ return [];
1407
+ }
1408
+ const diagnostics = [];
1409
+ if (action.mode === "replace") {
1410
+ if (typeof action.body !== "string") {
1295
1411
  diagnostics.push({
1296
- code: "editor.response-body-pattern",
1412
+ code: "editor.response-body-replace-body",
1297
1413
  severity: "error",
1298
- path: patternPath,
1299
- message: "Replacement pattern must be a valid bounded regular expression."
1414
+ path: `${rulePath}/responseBody/body`,
1415
+ message: "Replace mode requires a body string."
1300
1416
  });
1301
- }
1302
- if (typeof entry?.replacement !== "string" || entry.replacement.length > LIMITS.maxResponseBodyReplacementLength) {
1417
+ } else if (action.body.length > LIMITS.maxResponseBodyBytes) {
1303
1418
  diagnostics.push({
1304
- code: "editor.response-body-replacement",
1419
+ code: "editor.response-body-replace-body",
1305
1420
  severity: "error",
1306
- path: replacementPath,
1307
- message: "Replacement text exceeds the permitted size."
1421
+ path: `${rulePath}/responseBody/body`,
1422
+ message: `Replace body exceeds the maximum size of ${LIMITS.maxResponseBodyBytes} bytes.`
1423
+ });
1424
+ } else if (hasLoneSurrogate(action.body)) {
1425
+ diagnostics.push({
1426
+ code: "editor.response-body-lone-surrogate",
1427
+ severity: "error",
1428
+ path: `${rulePath}/responseBody/body`,
1429
+ message: "Replace body must not contain lone UTF-16 surrogates."
1308
1430
  });
1309
1431
  }
1310
- });
1311
- return stable4(diagnostics);
1432
+ } else {
1433
+ diagnostics.push(
1434
+ ...validateRegexReplacements(action.replacements, rulePath)
1435
+ );
1436
+ }
1437
+ return stable3(diagnostics);
1312
1438
  },
1313
1439
  mount(context) {
1314
1440
  const { document: document2, container } = context;
1315
- const render = () => {
1316
- const current = replacementsOf(context.getField("responseBody"));
1317
- container.replaceChildren();
1318
- if (current === void 0) return;
1319
- current.forEach((entry, index) => {
1441
+ const currentReplacements = () => {
1442
+ const action = responseBodyOf(context.getField("responseBody"));
1443
+ return action?.mode === "regex" ? action.replacements : [];
1444
+ };
1445
+ const renderRegexRows = (onUpdate) => {
1446
+ currentReplacements().forEach((entry, index) => {
1320
1447
  const row = document2.createElement("div");
1321
1448
  row.dataset.responseBodyReplacement = String(index);
1322
1449
  const pattern = document2.createElement("input");
@@ -1324,11 +1451,9 @@ function createResponseBodyRuleType() {
1324
1451
  pattern.value = entry.pattern;
1325
1452
  pattern.placeholder = "Pattern";
1326
1453
  pattern.addEventListener("input", () => {
1327
- const next = [
1328
- ...replacementsOf(context.getField("responseBody")) ?? []
1329
- ];
1454
+ const next = [...currentReplacements()];
1330
1455
  next[index] = { ...next[index], pattern: pattern.value };
1331
- context.setField("responseBody", { replacements: next });
1456
+ onUpdate(next);
1332
1457
  });
1333
1458
  context.registerControl(
1334
1459
  `/responseBody/replacements/${index}/pattern`,
@@ -1339,11 +1464,9 @@ function createResponseBodyRuleType() {
1339
1464
  replacement.value = entry.replacement;
1340
1465
  replacement.placeholder = "Replacement";
1341
1466
  replacement.addEventListener("input", () => {
1342
- const next = [
1343
- ...replacementsOf(context.getField("responseBody")) ?? []
1344
- ];
1467
+ const next = [...currentReplacements()];
1345
1468
  next[index] = { ...next[index], replacement: replacement.value };
1346
- context.setField("responseBody", { replacements: next });
1469
+ onUpdate(next);
1347
1470
  });
1348
1471
  context.registerControl(
1349
1472
  `/responseBody/replacements/${index}/replacement`,
@@ -1353,8 +1476,11 @@ function createResponseBodyRuleType() {
1353
1476
  remove.type = "button";
1354
1477
  remove.textContent = "Remove replacement";
1355
1478
  remove.addEventListener("click", () => {
1356
- const next = (replacementsOf(context.getField("responseBody")) ?? []).filter((_, itemIndex) => itemIndex !== index);
1357
- context.setField("responseBody", { replacements: next });
1479
+ onUpdate(
1480
+ currentReplacements().filter(
1481
+ (_, itemIndex) => itemIndex !== index
1482
+ )
1483
+ );
1358
1484
  render();
1359
1485
  });
1360
1486
  row.append(pattern, replacement, remove);
@@ -1364,29 +1490,81 @@ function createResponseBodyRuleType() {
1364
1490
  add.type = "button";
1365
1491
  add.textContent = "Add replacement";
1366
1492
  add.addEventListener("click", () => {
1367
- const next = [
1368
- ...replacementsOf(context.getField("responseBody")) ?? [],
1493
+ onUpdate([
1494
+ ...currentReplacements(),
1369
1495
  { pattern: "", replacement: "" }
1370
- ];
1371
- context.setField("responseBody", { replacements: next });
1496
+ ]);
1372
1497
  render();
1373
1498
  });
1374
1499
  container.append(add);
1375
1500
  };
1501
+ const render = () => {
1502
+ const current = responseBodyOf(context.getField("responseBody"));
1503
+ container.replaceChildren();
1504
+ if (current === void 0) return;
1505
+ const modeSelect = createSelect(
1506
+ document2,
1507
+ [
1508
+ { value: "replace", label: "Replace body" },
1509
+ { value: "regex", label: "Regex rewrite" }
1510
+ ],
1511
+ current.mode,
1512
+ (mode) => {
1513
+ if (mode === "replace") {
1514
+ context.setField("responseBody", { mode: "replace", body: "" });
1515
+ } else {
1516
+ context.setField("responseBody", {
1517
+ mode: "regex",
1518
+ replacements: [{ pattern: "", replacement: "" }]
1519
+ });
1520
+ }
1521
+ render();
1522
+ }
1523
+ );
1524
+ context.registerControl("/responseBody/mode", modeSelect);
1525
+ container.append(modeSelect);
1526
+ if (current.mode === "replace") {
1527
+ const bodyInput = document2.createElement("textarea");
1528
+ bodyInput.value = current.body ?? "";
1529
+ bodyInput.placeholder = "Replacement body";
1530
+ bodyInput.addEventListener("input", () => {
1531
+ context.setField("responseBody", {
1532
+ mode: "replace",
1533
+ body: bodyInput.value
1534
+ });
1535
+ });
1536
+ context.registerControl("/responseBody/body", bodyInput);
1537
+ container.append(bodyInput);
1538
+ } else {
1539
+ const stored = context.getField("responseBody");
1540
+ const writeRegex = (replacements) => {
1541
+ if (isRecord3(stored) && !("mode" in stored) && Array.isArray(stored.replacements)) {
1542
+ context.setField("responseBody", { replacements });
1543
+ } else {
1544
+ context.setField("responseBody", {
1545
+ mode: "regex",
1546
+ replacements
1547
+ });
1548
+ }
1549
+ };
1550
+ renderRegexRows(writeRegex);
1551
+ }
1552
+ };
1376
1553
  render();
1377
1554
  return { destroy() {
1378
1555
  } };
1379
1556
  },
1380
1557
  defaultAction() {
1381
- return { replacements: [{ pattern: "", replacement: "" }] };
1558
+ return { mode: "replace", body: "" };
1382
1559
  }
1383
1560
  };
1384
1561
  }
1385
1562
 
1386
1563
  // packages/editor/src/rule-types/index.ts
1387
1564
  var builtInRuleTypes = Object.freeze([
1565
+ createHeaderRuleType(),
1566
+ createRedirectRuleType(),
1388
1567
  queryRuleType,
1389
- createMockRuleType(),
1390
1568
  createResponseBodyRuleType(),
1391
1569
  createRequestBodyRuleType()
1392
1570
  ]);
@@ -1472,7 +1650,7 @@ var FORBIDDEN_EXTENSION_FIELDS = /* @__PURE__ */ new Set([
1472
1650
  var MAX_SNAPSHOT_ARRAY_LENGTH = 4096;
1473
1651
  var F2_MAX_URL_REGEX_LENGTH2 = 2048;
1474
1652
  var editorInstanceCount = 0;
1475
- function isRecord5(value) {
1653
+ function isRecord4(value) {
1476
1654
  return value !== null && typeof value === "object" && !Array.isArray(value);
1477
1655
  }
1478
1656
  function snapshotOwnData(value, ancestors = /* @__PURE__ */ new WeakSet()) {
@@ -1551,11 +1729,11 @@ function freezeSnapshot(value, seen = /* @__PURE__ */ new WeakSet()) {
1551
1729
  return value;
1552
1730
  }
1553
1731
  function asDraftProject(value) {
1554
- if (!isRecord5(value) || !Array.isArray(value.groups)) return void 0;
1732
+ if (!isRecord4(value) || !Array.isArray(value.groups)) return void 0;
1555
1733
  for (const group of value.groups) {
1556
- if (!isRecord5(group) || !Array.isArray(group.rules)) return void 0;
1734
+ if (!isRecord4(group) || !Array.isArray(group.rules)) return void 0;
1557
1735
  for (const rule of group.rules) {
1558
- if (!isRecord5(rule)) return void 0;
1736
+ if (!isRecord4(rule)) return void 0;
1559
1737
  }
1560
1738
  }
1561
1739
  return value;
@@ -1586,7 +1764,7 @@ function valueAtPath(root, path) {
1586
1764
  if (index === void 0 || !Object.hasOwn(current, index))
1587
1765
  return void 0;
1588
1766
  current = current[index];
1589
- } else if (isRecord5(current) && Object.hasOwn(current, segment)) {
1767
+ } else if (isRecord4(current) && Object.hasOwn(current, segment)) {
1590
1768
  current = current[segment];
1591
1769
  } else {
1592
1770
  return void 0;
@@ -1594,6 +1772,25 @@ function valueAtPath(root, path) {
1594
1772
  }
1595
1773
  return current;
1596
1774
  }
1775
+ var MATCHER_CREATABLE_FIELDS = ["description", "method", "type"];
1776
+ var ACTION_PAYLOAD_FIELDS = [
1777
+ "redirect",
1778
+ "action",
1779
+ "requestBody",
1780
+ "responseBody",
1781
+ "headerDirection",
1782
+ "headerOperation",
1783
+ "headerName",
1784
+ "headerValue"
1785
+ ];
1786
+ var CREATABLE_RULE_FIELDS = /* @__PURE__ */ new Set([
1787
+ ...MATCHER_CREATABLE_FIELDS,
1788
+ ...ACTION_PAYLOAD_FIELDS,
1789
+ "redactSensitiveInLogs"
1790
+ ]);
1791
+ function isBodyRuleType(type) {
1792
+ return type === "request-body" || type === "response-body";
1793
+ }
1597
1794
  function setValueAtPath(root, path, value) {
1598
1795
  const segments = decodePointer(path);
1599
1796
  if (!segments || segments.length === 0) return false;
@@ -1606,7 +1803,7 @@ function setValueAtPath(root, path, value) {
1606
1803
  return false;
1607
1804
  }
1608
1805
  current = current[childIndex];
1609
- } else if (isRecord5(current) && Object.hasOwn(current, segment)) {
1806
+ } else if (isRecord4(current) && Object.hasOwn(current, segment)) {
1610
1807
  current = current[segment];
1611
1808
  } else {
1612
1809
  return false;
@@ -1620,8 +1817,8 @@ function setValueAtPath(root, path, value) {
1620
1817
  current[index] = value;
1621
1818
  return true;
1622
1819
  }
1623
- if (!isRecord5(current)) return false;
1624
- if (!Object.hasOwn(current, finalSegment) && finalSegment !== "description" && finalSegment !== "method" && finalSegment !== "type" && finalSegment !== "action" && finalSegment !== "redirect" && finalSegment !== "mock") {
1820
+ if (!isRecord4(current)) return false;
1821
+ if (!Object.hasOwn(current, finalSegment) && !CREATABLE_RULE_FIELDS.has(finalSegment)) {
1625
1822
  return false;
1626
1823
  }
1627
1824
  if (Object.is(current[finalSegment], value)) return false;
@@ -1639,7 +1836,7 @@ function deleteValueAtPath(root, path) {
1639
1836
  const parentPath = pointer(...segments.slice(0, -1));
1640
1837
  const parent = valueAtPath(root, parentPath);
1641
1838
  const key = segments[segments.length - 1];
1642
- if (!isRecord5(parent) || !Object.hasOwn(parent, key)) return false;
1839
+ if (!isRecord4(parent) || !Object.hasOwn(parent, key)) return false;
1643
1840
  return delete parent[key];
1644
1841
  }
1645
1842
  function safeText(value, fallback = "") {
@@ -1675,7 +1872,7 @@ function normalizeDiagnostics(value) {
1675
1872
  }
1676
1873
  const diagnostics = [];
1677
1874
  for (const item of value) {
1678
- if (!isRecord5(item)) continue;
1875
+ if (!isRecord4(item)) continue;
1679
1876
  const code = item.code;
1680
1877
  const path = item.path;
1681
1878
  const message = item.message;
@@ -1688,12 +1885,11 @@ function normalizeDiagnostics(value) {
1688
1885
  function isValidExtensionName(name) {
1689
1886
  return name.length > 0 && !COMMON_RULE_FIELDS.has(name) && !FORBIDDEN_EXTENSION_FIELDS.has(name) && !hasControl(name);
1690
1887
  }
1691
- var ACTION_FIELDS = ["redirect", "action", "mock"];
1692
- function clearActionFields(rule, keep) {
1693
- if (!isRecord5(rule)) return false;
1888
+ function clearActionFields(rule, keep, keepFields) {
1889
+ if (!isRecord4(rule)) return false;
1694
1890
  let changed = false;
1695
- for (const field of ACTION_FIELDS) {
1696
- if (field === keep) continue;
1891
+ for (const field of ACTION_PAYLOAD_FIELDS) {
1892
+ if (field === keep || keepFields?.has(field)) continue;
1697
1893
  if (Object.hasOwn(rule, field)) {
1698
1894
  delete rule[field];
1699
1895
  changed = true;
@@ -1706,9 +1902,9 @@ function extensionFieldParent(rule, name) {
1706
1902
  if (segments.length === 0) return void 0;
1707
1903
  let current = rule;
1708
1904
  for (let index = 0; index < segments.length - 1; index += 1) {
1709
- if (!isRecord5(current)) return void 0;
1905
+ if (!isRecord4(current)) return void 0;
1710
1906
  const next = current[segments[index]];
1711
- if (!isRecord5(next)) {
1907
+ if (!isRecord4(next)) {
1712
1908
  const created = {};
1713
1909
  Object.defineProperty(current, segments[index], {
1714
1910
  configurable: true,
@@ -1721,7 +1917,7 @@ function extensionFieldParent(rule, name) {
1721
1917
  current = next;
1722
1918
  }
1723
1919
  }
1724
- if (!isRecord5(current)) return void 0;
1920
+ if (!isRecord4(current)) return void 0;
1725
1921
  return { parent: current, key: segments[segments.length - 1] };
1726
1922
  }
1727
1923
  function toSearchText(value) {
@@ -2009,6 +2205,14 @@ var EditorControllerImpl = class {
2009
2205
  return;
2010
2206
  }
2011
2207
  const path = target.dataset.path;
2208
+ if (path && target instanceof HTMLInputElement && target.type === "checkbox" && !this.extensionControls.has(path)) {
2209
+ const changed = setValueAtPath(this.draft, path, target.checked);
2210
+ if (changed) {
2211
+ this.markChanged();
2212
+ this.render();
2213
+ }
2214
+ return;
2215
+ }
2012
2216
  if (!path || this.extensionControls.has(path)) return;
2013
2217
  if (this.updateCommonField(path, target.value)) this.render();
2014
2218
  };
@@ -2283,7 +2487,7 @@ var EditorControllerImpl = class {
2283
2487
  const ruleContainer = valueAtPath(this.draft, ruleContainerPath);
2284
2488
  clearActionFields(
2285
2489
  ruleContainer,
2286
- rawValue === "redirect" || rawValue === "mock" ? rawValue : void 0
2490
+ rawValue === "redirect" ? rawValue : void 0
2287
2491
  );
2288
2492
  this.markChanged();
2289
2493
  }
@@ -2319,16 +2523,30 @@ var EditorControllerImpl = class {
2319
2523
  return;
2320
2524
  }
2321
2525
  const extension = this.extensions.find((entry) => entry.id === typeId);
2322
- if (!extension?.defaultAction) return;
2323
- const actionField = extension.actionField ?? "action";
2526
+ if (!extension?.defaultAction && !extension?.defaultFields) return;
2324
2527
  const changedType = setValueAtPath(this.draft, `${rulePath}/type`, typeId);
2325
- const changedAction = setValueAtPath(
2326
- this.draft,
2327
- `${rulePath}/${actionField}`,
2328
- extension.defaultAction()
2329
- );
2330
- const cleared = clearActionFields(ruleContainer, actionField);
2331
- if (changedType || changedAction || cleared) {
2528
+ let changedPayload = false;
2529
+ let keep;
2530
+ let keepFields;
2531
+ if (extension.defaultFields) {
2532
+ const fields = extension.defaultFields();
2533
+ keepFields = new Set(Object.keys(fields));
2534
+ for (const [key, value] of Object.entries(fields)) {
2535
+ if (setValueAtPath(this.draft, `${rulePath}/${key}`, value)) {
2536
+ changedPayload = true;
2537
+ }
2538
+ }
2539
+ } else {
2540
+ const defaultAction = extension.defaultAction;
2541
+ if (defaultAction) {
2542
+ keep = extension.actionField ?? "action";
2543
+ if (setValueAtPath(this.draft, `${rulePath}/${keep}`, defaultAction())) {
2544
+ changedPayload = true;
2545
+ }
2546
+ }
2547
+ }
2548
+ const cleared = clearActionFields(ruleContainer, keep, keepFields);
2549
+ if (changedType || changedPayload || cleared) {
2332
2550
  this.markChanged();
2333
2551
  this.render();
2334
2552
  }
@@ -2580,7 +2798,7 @@ var EditorControllerImpl = class {
2580
2798
  if (ruleProposal.method) {
2581
2799
  rule.method = ruleProposal.method;
2582
2800
  }
2583
- const actionField = ruleProposal.kind === "redirect" || ruleProposal.kind === "mock" ? ruleProposal.kind : "action";
2801
+ const actionField = ruleProposal.kind === "redirect" ? ruleProposal.kind : "action";
2584
2802
  rule[actionField] = ruleProposal.action;
2585
2803
  group.rules.push(rule);
2586
2804
  this.markChanged();
@@ -3048,31 +3266,25 @@ var EditorControllerImpl = class {
3048
3266
  const group = this.groupById(this.route.groupId);
3049
3267
  if (!group) return;
3050
3268
  const index = this.groupIndex(this.route.groupId);
3051
- const name = displayName(group.name, "Unnamed group");
3052
3269
  this.commandBar.append(
3053
3270
  this.createCommandButton("Add rule", "add-rule", this.saving, {
3054
3271
  groupId: this.route.groupId
3055
3272
  }),
3056
3273
  this.createCommandButton(
3057
- `Move group ${name} up`,
3274
+ "Move group up",
3058
3275
  "move-group-up",
3059
3276
  this.saving || index <= 0,
3060
3277
  { groupId: this.route.groupId }
3061
3278
  ),
3062
3279
  this.createCommandButton(
3063
- `Move group ${name} down`,
3280
+ "Move group down",
3064
3281
  "move-group-down",
3065
3282
  this.saving || index >= this.draft.groups.length - 1,
3066
3283
  { groupId: this.route.groupId }
3067
3284
  ),
3068
- this.createCommandButton(
3069
- `Remove group ${name}`,
3070
- "remove-group",
3071
- this.saving,
3072
- {
3073
- groupId: this.route.groupId
3074
- }
3075
- )
3285
+ this.createCommandButton("Remove group", "remove-group", this.saving, {
3286
+ groupId: this.route.groupId
3287
+ })
3076
3288
  );
3077
3289
  }
3078
3290
  renderProject() {
@@ -3306,7 +3518,9 @@ var EditorControllerImpl = class {
3306
3518
  card.id = `rogatio-rule-${groupId}-${ruleId}`;
3307
3519
  card.tabIndex = -1;
3308
3520
  const heading = this.document.createElement("h3");
3521
+ heading.id = `${this.instanceId}-rule-title-${groupIndex}-${ruleIndex}`;
3309
3522
  heading.textContent = ruleName;
3523
+ card.setAttribute("aria-labelledby", heading.id);
3310
3524
  card.append(heading);
3311
3525
  const fields = this.document.createElement("fieldset");
3312
3526
  const legend = this.document.createElement("legend");
@@ -3318,23 +3532,18 @@ var EditorControllerImpl = class {
3318
3532
  id.type = "text";
3319
3533
  id.maxLength = 64;
3320
3534
  id.value = safeText(rule.id);
3321
- this.renderField(grid, `Rule ID for ${ruleName}`, `${rulePath}/id`, id);
3535
+ this.renderField(grid, "Rule ID", `${rulePath}/id`, id);
3322
3536
  const name = this.document.createElement("input");
3323
3537
  name.type = "text";
3324
3538
  name.maxLength = 100;
3325
3539
  name.value = safeText(rule.name);
3326
- this.renderField(
3327
- grid,
3328
- `Rule name for ${ruleName}`,
3329
- `${rulePath}/name`,
3330
- name
3331
- );
3540
+ this.renderField(grid, "Rule name", `${rulePath}/name`, name);
3332
3541
  const regex = this.document.createElement("textarea");
3333
3542
  regex.maxLength = F2_MAX_URL_REGEX_LENGTH2;
3334
3543
  regex.value = safeText(rule.urlRegex);
3335
3544
  this.renderField(
3336
3545
  grid,
3337
- `URL regular expression for ${ruleName}`,
3546
+ "URL regular expression",
3338
3547
  `${rulePath}/urlRegex`,
3339
3548
  regex
3340
3549
  );
@@ -3357,7 +3566,7 @@ var EditorControllerImpl = class {
3357
3566
  groupIndex,
3358
3567
  ruleIndex
3359
3568
  );
3360
- this.renderResourceTypes(card, rule, rulePath, ruleName);
3569
+ this.renderResourceTypes(card, rule, rulePath);
3361
3570
  const matcherFields = this.document.createElement("fieldset");
3362
3571
  const matcherLegend = this.document.createElement("legend");
3363
3572
  matcherLegend.textContent = "Request constraints";
@@ -3370,12 +3579,7 @@ var EditorControllerImpl = class {
3370
3579
  priority.max = "1000";
3371
3580
  priority.step = "1";
3372
3581
  priority.value = typeof rule.priority === "number" && Number.isFinite(rule.priority) ? String(rule.priority) : safeText(rule.priority);
3373
- this.renderField(
3374
- matcherGrid,
3375
- `Priority for ${ruleName}`,
3376
- `${rulePath}/priority`,
3377
- priority
3378
- );
3582
+ this.renderField(matcherGrid, "Priority", `${rulePath}/priority`, priority);
3379
3583
  const method = this.document.createElement("select");
3380
3584
  const anyMethod = this.document.createElement("option");
3381
3585
  anyMethod.value = "";
@@ -3388,21 +3592,35 @@ var EditorControllerImpl = class {
3388
3592
  method.append(option);
3389
3593
  }
3390
3594
  method.value = safeText(rule.method);
3391
- this.renderField(
3392
- matcherGrid,
3393
- `Method for ${ruleName}`,
3394
- `${rulePath}/method`,
3395
- method
3396
- );
3595
+ this.renderField(matcherGrid, "Method", `${rulePath}/method`, method);
3397
3596
  matcherFields.append(matcherGrid);
3398
3597
  card.append(matcherFields);
3598
+ if (!isBodyRuleType(rule.type)) {
3599
+ const redactFieldset = this.document.createElement("fieldset");
3600
+ const redactLegend = this.document.createElement("legend");
3601
+ redactLegend.textContent = "Match logging";
3602
+ redactFieldset.append(redactLegend);
3603
+ const redactGrid = this.document.createElement("div");
3604
+ redactGrid.dataset.editorFields = "true";
3605
+ const redactCheckbox = this.document.createElement("input");
3606
+ redactCheckbox.type = "checkbox";
3607
+ redactCheckbox.checked = rule.redactSensitiveInLogs === true;
3608
+ this.renderField(
3609
+ redactGrid,
3610
+ "Redact sensitive fields in logs",
3611
+ `${rulePath}/redactSensitiveInLogs`,
3612
+ redactCheckbox
3613
+ );
3614
+ redactFieldset.append(redactGrid);
3615
+ card.append(redactFieldset);
3616
+ }
3399
3617
  if (this.extensions.length > 0) {
3400
3618
  const currentType = this.extensions.find(
3401
3619
  (e) => e.matches(rule)
3402
3620
  )?.id ?? safeText(rule.type);
3403
3621
  const typeFieldset = this.document.createElement("fieldset");
3404
3622
  const typeLegend = this.document.createElement("legend");
3405
- typeLegend.textContent = `Rule type for ${ruleName}`;
3623
+ typeLegend.textContent = "Rule type";
3406
3624
  const typeField = this.document.createElement("div");
3407
3625
  typeField.dataset.editorField = "true";
3408
3626
  const typeLabel = this.document.createElement("label");
@@ -3440,26 +3658,21 @@ var EditorControllerImpl = class {
3440
3658
  actions.dataset.ruleActions = "true";
3441
3659
  actions.append(
3442
3660
  this.createCommandButton(
3443
- `Move rule ${ruleName} up`,
3661
+ "Move rule up",
3444
3662
  "move-rule-up",
3445
3663
  this.saving || ruleIndex <= 0,
3446
3664
  { groupId, ruleId }
3447
3665
  ),
3448
3666
  this.createCommandButton(
3449
- `Move rule ${ruleName} down`,
3667
+ "Move rule down",
3450
3668
  "move-rule-down",
3451
3669
  this.saving || ruleIndex >= group.rules.length - 1,
3452
3670
  { groupId, ruleId }
3453
3671
  ),
3454
- this.createCommandButton(
3455
- `Remove rule ${ruleName}`,
3456
- "remove-rule",
3457
- this.saving,
3458
- {
3459
- groupId,
3460
- ruleId
3461
- }
3462
- )
3672
+ this.createCommandButton("Remove rule", "remove-rule", this.saving, {
3673
+ groupId,
3674
+ ruleId
3675
+ })
3463
3676
  );
3464
3677
  card.append(actions);
3465
3678
  return card;
@@ -3505,10 +3718,10 @@ var EditorControllerImpl = class {
3505
3718
  );
3506
3719
  parent.append(fieldset);
3507
3720
  }
3508
- renderResourceTypes(parent, rule, rulePath, ruleName) {
3721
+ renderResourceTypes(parent, rule, rulePath) {
3509
3722
  const fieldset = this.document.createElement("fieldset");
3510
3723
  const legend = this.document.createElement("legend");
3511
- legend.textContent = `Resource types for ${ruleName}`;
3724
+ legend.textContent = "Resource types";
3512
3725
  fieldset.append(legend);
3513
3726
  const checks = this.document.createElement("div");
3514
3727
  checks.dataset.editorChecks = "true";
@@ -3886,10 +4099,10 @@ var EditorControllerImpl = class {
3886
4099
  }
3887
4100
  };
3888
4101
  function isSaveSuccess(value) {
3889
- return isRecord5(value) && value.ok === true;
4102
+ return isRecord4(value) && value.ok === true;
3890
4103
  }
3891
4104
  function saveFailureDiagnostic(value) {
3892
- if (!isRecord5(value)) {
4105
+ if (!isRecord4(value)) {
3893
4106
  return diagnostic(
3894
4107
  "editor.save-failed",
3895
4108
  "",
@@ -3936,282 +4149,11 @@ function createEditor(options) {
3936
4149
  const extensions = normalizeExtensions(options.ruleTypes);
3937
4150
  return new EditorControllerImpl(options, project, extensions);
3938
4151
  }
3939
-
3940
- // packages/editor/src/rule-types/header.ts
3941
- var FORBIDDEN_REQUEST_HEADERS2 = Object.freeze([
3942
- "accept-charset",
3943
- "accept-encoding",
3944
- "access-control-request-headers",
3945
- "access-control-request-method",
3946
- "connection",
3947
- "content-length",
3948
- "cookie",
3949
- "cookie2",
3950
- "date",
3951
- "dnt",
3952
- "expect",
3953
- "host",
3954
- "keep-alive",
3955
- "origin",
3956
- "proxy-authenticate",
3957
- "proxy-authorization",
3958
- "te",
3959
- "trailer",
3960
- "transfer-encoding",
3961
- "upgrade",
3962
- "via"
3963
- ]);
3964
- var FORBIDDEN_RESPONSE_HEADERS2 = Object.freeze([
3965
- "connection",
3966
- "content-encoding",
3967
- "content-length",
3968
- "date",
3969
- "keep-alive",
3970
- "proxy-authenticate",
3971
- "proxy-authorization",
3972
- "set-cookie",
3973
- "set-cookie2",
3974
- "te",
3975
- "trailer",
3976
- "transfer-encoding",
3977
- "upgrade",
3978
- "via"
3979
- ]);
3980
- var FORBIDDEN_REQUEST_PREFIXES2 = Object.freeze(["proxy-", "sec-"]);
3981
- function isForbiddenHeader(name, direction) {
3982
- const normalized = name.toLowerCase();
3983
- const forbidden = direction === "request" ? FORBIDDEN_REQUEST_HEADERS2 : FORBIDDEN_RESPONSE_HEADERS2;
3984
- return forbidden.includes(normalized) || direction === "request" && FORBIDDEN_REQUEST_PREFIXES2.some(
3985
- (prefix) => normalized.startsWith(prefix)
3986
- );
3987
- }
3988
- var HEADER_DIRECTIONS = [
3989
- "request",
3990
- "response"
3991
- ];
3992
- var HEADER_OPERATIONS = [
3993
- "set",
3994
- "append",
3995
- "remove"
3996
- ];
3997
- function createSelect(document2, options, value, onChange) {
3998
- const select = document2.createElement("select");
3999
- for (const option of options) {
4000
- const opt = document2.createElement("option");
4001
- opt.value = option;
4002
- opt.textContent = option;
4003
- if (option === value) opt.selected = true;
4004
- select.append(opt);
4005
- }
4006
- select.addEventListener("change", () => onChange(select.value));
4007
- return select;
4008
- }
4009
- function createInput(document2, value, onChange, maxLength) {
4010
- const input = document2.createElement("input");
4011
- input.type = "text";
4012
- input.value = value;
4013
- if (maxLength !== void 0) input.maxLength = maxLength;
4014
- input.addEventListener("input", () => onChange(input.value));
4015
- return input;
4016
- }
4017
- function createHeaderRuleType() {
4018
- return {
4019
- id: "header",
4020
- label: "Header",
4021
- matches(rule) {
4022
- return rule.type === "header";
4023
- },
4024
- mount(context) {
4025
- const document2 = context.document;
4026
- const fieldset = document2.createElement("fieldset");
4027
- const legend = document2.createElement("legend");
4028
- legend.textContent = "Header rule";
4029
- fieldset.append(legend);
4030
- const directionLabel = document2.createElement("label");
4031
- directionLabel.textContent = "Direction";
4032
- const directionValue = context.getField("headerDirection") ?? "request";
4033
- const directionSelect = createSelect(
4034
- document2,
4035
- HEADER_DIRECTIONS,
4036
- directionValue,
4037
- (value) => {
4038
- context.setField("headerDirection", value);
4039
- }
4040
- );
4041
- context.registerControl("/headerDirection", directionSelect);
4042
- directionLabel.append(directionSelect);
4043
- fieldset.append(directionLabel);
4044
- const operationLabel = document2.createElement("label");
4045
- operationLabel.textContent = "Operation";
4046
- const operationValue = context.getField("headerOperation") ?? "set";
4047
- const operationSelect = createSelect(
4048
- document2,
4049
- HEADER_OPERATIONS,
4050
- operationValue,
4051
- (value) => {
4052
- context.setField("headerOperation", value);
4053
- }
4054
- );
4055
- context.registerControl("/headerOperation", operationSelect);
4056
- operationLabel.append(operationSelect);
4057
- fieldset.append(operationLabel);
4058
- const nameLabel = document2.createElement("label");
4059
- nameLabel.textContent = "Header name";
4060
- const nameValue = context.getField("headerName") ?? "";
4061
- const nameInput = createInput(
4062
- document2,
4063
- nameValue,
4064
- (value) => {
4065
- context.setField("headerName", value);
4066
- },
4067
- 256
4068
- );
4069
- context.registerControl("/headerName", nameInput);
4070
- nameLabel.append(nameInput);
4071
- fieldset.append(nameLabel);
4072
- const valueLabel = document2.createElement("label");
4073
- valueLabel.textContent = "Header value";
4074
- const valueValue = context.getField("headerValue") ?? "";
4075
- const valueInput = createInput(
4076
- document2,
4077
- valueValue,
4078
- (value) => {
4079
- context.setField("headerValue", value);
4080
- },
4081
- 4096
4082
- );
4083
- context.registerControl("/headerValue", valueInput);
4084
- valueLabel.append(valueInput);
4085
- fieldset.append(valueLabel);
4086
- context.container.append(fieldset);
4087
- return {
4088
- destroy() {
4089
- directionSelect.removeEventListener("change", () => {
4090
- });
4091
- operationSelect.removeEventListener("change", () => {
4092
- });
4093
- nameInput.removeEventListener("input", () => {
4094
- });
4095
- valueInput.removeEventListener("input", () => {
4096
- });
4097
- }
4098
- };
4099
- },
4100
- validate(rule, rulePath) {
4101
- const diagnostics = [];
4102
- const direction = rule.headerDirection;
4103
- const operation = rule.headerOperation;
4104
- const headerName = rule.headerName;
4105
- const headerValue = rule.headerValue;
4106
- if (!direction || !HEADER_DIRECTIONS.includes(direction)) {
4107
- diagnostics.push({
4108
- code: "schema.invalid-value",
4109
- severity: "error",
4110
- path: `${rulePath}/headerDirection`,
4111
- message: 'Direction must be "request" or "response".'
4112
- });
4113
- }
4114
- if (!operation || !HEADER_OPERATIONS.includes(operation)) {
4115
- diagnostics.push({
4116
- code: "schema.invalid-value",
4117
- severity: "error",
4118
- path: `${rulePath}/headerOperation`,
4119
- message: 'Operation must be "set", "append", or "remove".'
4120
- });
4121
- }
4122
- if (typeof headerName !== "string" || headerName.length === 0 || headerName.length > 256) {
4123
- diagnostics.push({
4124
- code: "schema.out-of-range",
4125
- severity: "error",
4126
- path: `${rulePath}/headerName`,
4127
- message: "Header name must be 1-256 characters."
4128
- });
4129
- } else if (direction && isForbiddenHeader(headerName, direction)) {
4130
- diagnostics.push({
4131
- code: "extension.forbidden-header",
4132
- severity: "error",
4133
- path: `${rulePath}/headerName`,
4134
- message: `Header "${headerName}" is forbidden for ${direction} headers.`
4135
- });
4136
- }
4137
- if ((operation === "set" || operation === "append") && typeof headerValue !== "string") {
4138
- diagnostics.push({
4139
- code: "schema.required",
4140
- severity: "error",
4141
- path: `${rulePath}/headerValue`,
4142
- message: "Header value is required for set and append operations."
4143
- });
4144
- } else if ((operation === "set" || operation === "append") && typeof headerValue === "string" && headerValue.length > 4096) {
4145
- diagnostics.push({
4146
- code: "schema.out-of-range",
4147
- severity: "error",
4148
- path: `${rulePath}/headerValue`,
4149
- message: "Header value must be at most 4096 characters."
4150
- });
4151
- }
4152
- if (operation === "remove" && headerValue !== void 0) {
4153
- diagnostics.push({
4154
- code: "schema.unexpected",
4155
- severity: "error",
4156
- path: `${rulePath}/headerValue`,
4157
- message: "Header value must not be provided for remove operation."
4158
- });
4159
- }
4160
- return diagnostics;
4161
- }
4162
- };
4163
- }
4164
-
4165
- // packages/editor/src/rule-types/redirect.ts
4166
- function createRedirectRuleType() {
4167
- return {
4168
- id: "redirect",
4169
- label: "Redirect",
4170
- matches(rule) {
4171
- return rule.type === "redirect";
4172
- },
4173
- mount(context) {
4174
- const document2 = context.document;
4175
- const label = document2.createElement("label");
4176
- label.textContent = "Redirect destination URL";
4177
- const input = document2.createElement("input");
4178
- input.type = "text";
4179
- input.maxLength = 2048;
4180
- const existing = context.getField("redirect.destination");
4181
- input.value = typeof existing === "string" ? existing : "";
4182
- input.addEventListener("input", () => {
4183
- context.setField("redirect.destination", input.value);
4184
- });
4185
- context.registerControl("/redirect/destination", input);
4186
- label.append(input);
4187
- context.container.append(label);
4188
- return {
4189
- destroy() {
4190
- input.removeEventListener("input", () => {
4191
- });
4192
- }
4193
- };
4194
- },
4195
- validate(rule, rulePath) {
4196
- const redirect = rule.redirect;
4197
- const destination = redirect !== null && typeof redirect === "object" && typeof redirect.destination === "string" ? redirect.destination : "";
4198
- const urlRegex = typeof rule.urlRegex === "string" ? rule.urlRegex : "";
4199
- const issues = validateRedirectDestination(destination, urlRegex);
4200
- return issues.map((issue) => ({
4201
- code: `schema.${issue.code}`,
4202
- severity: "error",
4203
- path: `${rulePath}/redirect/destination`,
4204
- message: issue.message
4205
- }));
4206
- }
4207
- };
4208
- }
4209
4152
  export {
4210
4153
  EditorInitializationError,
4211
4154
  builtInRuleTypes,
4212
4155
  createEditor,
4213
4156
  createHeaderRuleType,
4214
- createMockRuleType,
4215
4157
  createRedirectRuleType,
4216
4158
  createRequestBodyRuleType,
4217
4159
  createResponseBodyRuleType,