@stackwright-pro/openapi 0.1.0 → 0.1.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.d.mts +331 -2
- package/dist/index.d.ts +331 -2
- package/dist/index.js +1025 -201
- package/dist/index.mjs +1022 -201
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -107,8 +107,8 @@ var require_compat_dotall_s_transform = __commonJS({
|
|
|
107
107
|
this._hasUFlag = ast.flags.includes("u");
|
|
108
108
|
return true;
|
|
109
109
|
},
|
|
110
|
-
Char: function Char(
|
|
111
|
-
var node =
|
|
110
|
+
Char: function Char(path3) {
|
|
111
|
+
var node = path3.node;
|
|
112
112
|
if (node.kind !== "meta" || node.value !== ".") {
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
@@ -118,7 +118,7 @@ var require_compat_dotall_s_transform = __commonJS({
|
|
|
118
118
|
toValue = "\\u{10FFFF}";
|
|
119
119
|
toSymbol = "\u{10FFFF}";
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
path3.replace({
|
|
122
122
|
type: "CharacterClass",
|
|
123
123
|
expressions: [{
|
|
124
124
|
type: "ClassRange",
|
|
@@ -163,8 +163,8 @@ var require_compat_named_capturing_groups_transform = __commonJS({
|
|
|
163
163
|
getExtra: function getExtra() {
|
|
164
164
|
return this._groupNames;
|
|
165
165
|
},
|
|
166
|
-
Group: function Group(
|
|
167
|
-
var node =
|
|
166
|
+
Group: function Group(path3) {
|
|
167
|
+
var node = path3.node;
|
|
168
168
|
if (!node.name) {
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
@@ -172,8 +172,8 @@ var require_compat_named_capturing_groups_transform = __commonJS({
|
|
|
172
172
|
delete node.name;
|
|
173
173
|
delete node.nameRaw;
|
|
174
174
|
},
|
|
175
|
-
Backreference: function Backreference(
|
|
176
|
-
var node =
|
|
175
|
+
Backreference: function Backreference(path3) {
|
|
176
|
+
var node = path3.node;
|
|
177
177
|
if (node.kind !== "name") {
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
@@ -2015,8 +2015,8 @@ var require_node_path = __commonJS({
|
|
|
2015
2015
|
value: function _rebuildIndex(parent, property) {
|
|
2016
2016
|
var parentPath = NodePath2.getForNode(parent);
|
|
2017
2017
|
for (var i = 0; i < parent[property].length; i++) {
|
|
2018
|
-
var
|
|
2019
|
-
|
|
2018
|
+
var path3 = NodePath2.getForNode(parent[property][i], parentPath, property, i);
|
|
2019
|
+
path3.index = i;
|
|
2020
2020
|
}
|
|
2021
2021
|
}
|
|
2022
2022
|
/**
|
|
@@ -2088,8 +2088,8 @@ var require_node_path = __commonJS({
|
|
|
2088
2088
|
*/
|
|
2089
2089
|
}, {
|
|
2090
2090
|
key: "hasEqualSource",
|
|
2091
|
-
value: function hasEqualSource(
|
|
2092
|
-
return JSON.stringify(this.node, jsonSkipLoc) === JSON.stringify(
|
|
2091
|
+
value: function hasEqualSource(path3) {
|
|
2092
|
+
return JSON.stringify(this.node, jsonSkipLoc) === JSON.stringify(path3.node, jsonSkipLoc);
|
|
2093
2093
|
}
|
|
2094
2094
|
/**
|
|
2095
2095
|
* JSON-encodes a node skipping location.
|
|
@@ -2140,18 +2140,18 @@ var require_node_path = __commonJS({
|
|
|
2140
2140
|
if (!NodePath2.registry.has(node)) {
|
|
2141
2141
|
NodePath2.registry.set(node, new NodePath2(node, parentPath, prop, index == -1 ? null : index));
|
|
2142
2142
|
}
|
|
2143
|
-
var
|
|
2143
|
+
var path3 = NodePath2.registry.get(node);
|
|
2144
2144
|
if (parentPath !== null) {
|
|
2145
|
-
|
|
2146
|
-
|
|
2145
|
+
path3.parentPath = parentPath;
|
|
2146
|
+
path3.parent = path3.parentPath.node;
|
|
2147
2147
|
}
|
|
2148
2148
|
if (prop !== null) {
|
|
2149
|
-
|
|
2149
|
+
path3.property = prop;
|
|
2150
2150
|
}
|
|
2151
2151
|
if (index >= 0) {
|
|
2152
|
-
|
|
2152
|
+
path3.index = index;
|
|
2153
2153
|
}
|
|
2154
|
-
return
|
|
2154
|
+
return path3;
|
|
2155
2155
|
}
|
|
2156
2156
|
/**
|
|
2157
2157
|
* Initializes the NodePath registry. The registry is a map from
|
|
@@ -2631,8 +2631,8 @@ var require_char_surrogate_pair_to_single_unicode_transform = __commonJS({
|
|
|
2631
2631
|
shouldRun: function shouldRun(ast) {
|
|
2632
2632
|
return ast.flags.includes("u");
|
|
2633
2633
|
},
|
|
2634
|
-
Char: function Char(
|
|
2635
|
-
var node =
|
|
2634
|
+
Char: function Char(path3) {
|
|
2635
|
+
var node = path3.node;
|
|
2636
2636
|
if (node.kind !== "unicode" || !node.isSurrogatePair || isNaN(node.codePoint)) {
|
|
2637
2637
|
return;
|
|
2638
2638
|
}
|
|
@@ -2654,8 +2654,8 @@ var require_char_code_to_simple_char_transform = __commonJS({
|
|
|
2654
2654
|
var DIGIT_0_CP = "0".codePointAt(0);
|
|
2655
2655
|
var DIGIT_9_CP = "9".codePointAt(0);
|
|
2656
2656
|
module.exports = {
|
|
2657
|
-
Char: function Char(
|
|
2658
|
-
var node =
|
|
2657
|
+
Char: function Char(path3) {
|
|
2658
|
+
var node = path3.node, parent = path3.parent;
|
|
2659
2659
|
if (isNaN(node.codePoint) || node.kind === "simple") {
|
|
2660
2660
|
return;
|
|
2661
2661
|
}
|
|
@@ -2678,7 +2678,7 @@ var require_char_code_to_simple_char_transform = __commonJS({
|
|
|
2678
2678
|
if (needsEscape(symbol, parent.type)) {
|
|
2679
2679
|
newChar.escaped = true;
|
|
2680
2680
|
}
|
|
2681
|
-
|
|
2681
|
+
path3.replace(newChar);
|
|
2682
2682
|
}
|
|
2683
2683
|
};
|
|
2684
2684
|
function isSimpleRange(classRange) {
|
|
@@ -2713,8 +2713,8 @@ var require_char_case_insensitive_lowercase_transform = __commonJS({
|
|
|
2713
2713
|
shouldRun: function shouldRun(ast) {
|
|
2714
2714
|
return ast.flags.includes("i");
|
|
2715
2715
|
},
|
|
2716
|
-
Char: function Char(
|
|
2717
|
-
var node =
|
|
2716
|
+
Char: function Char(path3) {
|
|
2717
|
+
var node = path3.node, parent = path3.parent;
|
|
2718
2718
|
if (isNaN(node.codePoint)) {
|
|
2719
2719
|
return;
|
|
2720
2720
|
}
|
|
@@ -2779,11 +2779,11 @@ var require_char_class_remove_duplicates_transform = __commonJS({
|
|
|
2779
2779
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/char-class-remove-duplicates-transform.js"(exports, module) {
|
|
2780
2780
|
"use strict";
|
|
2781
2781
|
module.exports = {
|
|
2782
|
-
CharacterClass: function CharacterClass(
|
|
2783
|
-
var node =
|
|
2782
|
+
CharacterClass: function CharacterClass(path3) {
|
|
2783
|
+
var node = path3.node;
|
|
2784
2784
|
var sources = {};
|
|
2785
2785
|
for (var i = 0; i < node.expressions.length; i++) {
|
|
2786
|
-
var childPath =
|
|
2786
|
+
var childPath = path3.getChild(i);
|
|
2787
2787
|
var source = childPath.jsonEncode();
|
|
2788
2788
|
if (sources.hasOwnProperty(source)) {
|
|
2789
2789
|
childPath.remove();
|
|
@@ -2864,17 +2864,17 @@ var require_quantifiers_merge_transform = __commonJS({
|
|
|
2864
2864
|
var _require = require_utils();
|
|
2865
2865
|
var increaseQuantifierByOne = _require.increaseQuantifierByOne;
|
|
2866
2866
|
module.exports = {
|
|
2867
|
-
Repetition: function Repetition(
|
|
2868
|
-
var node =
|
|
2869
|
-
if (parent.type !== "Alternative" || !
|
|
2867
|
+
Repetition: function Repetition(path3) {
|
|
2868
|
+
var node = path3.node, parent = path3.parent;
|
|
2869
|
+
if (parent.type !== "Alternative" || !path3.index) {
|
|
2870
2870
|
return;
|
|
2871
2871
|
}
|
|
2872
|
-
var previousSibling =
|
|
2872
|
+
var previousSibling = path3.getPreviousSibling();
|
|
2873
2873
|
if (!previousSibling) {
|
|
2874
2874
|
return;
|
|
2875
2875
|
}
|
|
2876
2876
|
if (previousSibling.node.type === "Repetition") {
|
|
2877
|
-
if (!previousSibling.getChild().hasEqualSource(
|
|
2877
|
+
if (!previousSibling.getChild().hasEqualSource(path3.getChild())) {
|
|
2878
2878
|
return;
|
|
2879
2879
|
}
|
|
2880
2880
|
var _extractFromTo = extractFromTo(previousSibling.node.quantifier), previousSiblingFrom = _extractFromTo.from, previousSiblingTo = _extractFromTo.to;
|
|
@@ -2894,7 +2894,7 @@ var require_quantifiers_merge_transform = __commonJS({
|
|
|
2894
2894
|
}
|
|
2895
2895
|
previousSibling.remove();
|
|
2896
2896
|
} else {
|
|
2897
|
-
if (!previousSibling.hasEqualSource(
|
|
2897
|
+
if (!previousSibling.hasEqualSource(path3.getChild())) {
|
|
2898
2898
|
return;
|
|
2899
2899
|
}
|
|
2900
2900
|
increaseQuantifierByOne(node.quantifier);
|
|
@@ -2930,38 +2930,38 @@ var require_quantifier_range_to_symbol_transform = __commonJS({
|
|
|
2930
2930
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/quantifier-range-to-symbol-transform.js"(exports, module) {
|
|
2931
2931
|
"use strict";
|
|
2932
2932
|
module.exports = {
|
|
2933
|
-
Quantifier: function Quantifier(
|
|
2934
|
-
var node =
|
|
2933
|
+
Quantifier: function Quantifier(path3) {
|
|
2934
|
+
var node = path3.node;
|
|
2935
2935
|
if (node.kind !== "Range") {
|
|
2936
2936
|
return;
|
|
2937
2937
|
}
|
|
2938
|
-
rewriteOpenZero(
|
|
2939
|
-
rewriteOpenOne(
|
|
2940
|
-
rewriteExactOne(
|
|
2938
|
+
rewriteOpenZero(path3);
|
|
2939
|
+
rewriteOpenOne(path3);
|
|
2940
|
+
rewriteExactOne(path3);
|
|
2941
2941
|
}
|
|
2942
2942
|
};
|
|
2943
|
-
function rewriteOpenZero(
|
|
2944
|
-
var node =
|
|
2943
|
+
function rewriteOpenZero(path3) {
|
|
2944
|
+
var node = path3.node;
|
|
2945
2945
|
if (node.from !== 0 || node.to) {
|
|
2946
2946
|
return;
|
|
2947
2947
|
}
|
|
2948
2948
|
node.kind = "*";
|
|
2949
2949
|
delete node.from;
|
|
2950
2950
|
}
|
|
2951
|
-
function rewriteOpenOne(
|
|
2952
|
-
var node =
|
|
2951
|
+
function rewriteOpenOne(path3) {
|
|
2952
|
+
var node = path3.node;
|
|
2953
2953
|
if (node.from !== 1 || node.to) {
|
|
2954
2954
|
return;
|
|
2955
2955
|
}
|
|
2956
2956
|
node.kind = "+";
|
|
2957
2957
|
delete node.from;
|
|
2958
2958
|
}
|
|
2959
|
-
function rewriteExactOne(
|
|
2960
|
-
var node =
|
|
2959
|
+
function rewriteExactOne(path3) {
|
|
2960
|
+
var node = path3.node;
|
|
2961
2961
|
if (node.from !== 1 || node.to !== 1) {
|
|
2962
2962
|
return;
|
|
2963
2963
|
}
|
|
2964
|
-
|
|
2964
|
+
path3.parentPath.replace(path3.parentPath.node.expression);
|
|
2965
2965
|
}
|
|
2966
2966
|
}
|
|
2967
2967
|
});
|
|
@@ -2971,13 +2971,13 @@ var require_char_class_classranges_to_chars_transform = __commonJS({
|
|
|
2971
2971
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/char-class-classranges-to-chars-transform.js"(exports, module) {
|
|
2972
2972
|
"use strict";
|
|
2973
2973
|
module.exports = {
|
|
2974
|
-
ClassRange: function ClassRange(
|
|
2975
|
-
var node =
|
|
2974
|
+
ClassRange: function ClassRange(path3) {
|
|
2975
|
+
var node = path3.node;
|
|
2976
2976
|
if (node.from.codePoint === node.to.codePoint) {
|
|
2977
|
-
|
|
2977
|
+
path3.replace(node.from);
|
|
2978
2978
|
} else if (node.from.codePoint === node.to.codePoint - 1) {
|
|
2979
|
-
|
|
2980
|
-
|
|
2979
|
+
path3.getParent().insertChildAt(node.to, path3.index + 1);
|
|
2980
|
+
path3.replace(node.from);
|
|
2981
2981
|
}
|
|
2982
2982
|
}
|
|
2983
2983
|
};
|
|
@@ -3005,17 +3005,17 @@ var require_char_class_to_meta_transform = __commonJS({
|
|
|
3005
3005
|
this._hasIFlag = ast.flags.includes("i");
|
|
3006
3006
|
this._hasUFlag = ast.flags.includes("u");
|
|
3007
3007
|
},
|
|
3008
|
-
CharacterClass: function CharacterClass(
|
|
3009
|
-
rewriteNumberRanges(
|
|
3010
|
-
rewriteWordRanges(
|
|
3011
|
-
rewriteWhitespaceRanges(
|
|
3008
|
+
CharacterClass: function CharacterClass(path3) {
|
|
3009
|
+
rewriteNumberRanges(path3);
|
|
3010
|
+
rewriteWordRanges(path3, this._hasIFlag, this._hasUFlag);
|
|
3011
|
+
rewriteWhitespaceRanges(path3);
|
|
3012
3012
|
}
|
|
3013
3013
|
};
|
|
3014
|
-
function rewriteNumberRanges(
|
|
3015
|
-
var node =
|
|
3014
|
+
function rewriteNumberRanges(path3) {
|
|
3015
|
+
var node = path3.node;
|
|
3016
3016
|
node.expressions.forEach(function(expression, i) {
|
|
3017
3017
|
if (isFullNumberRange(expression)) {
|
|
3018
|
-
|
|
3018
|
+
path3.getChild(i).replace({
|
|
3019
3019
|
type: "Char",
|
|
3020
3020
|
value: "\\d",
|
|
3021
3021
|
kind: "meta"
|
|
@@ -3023,8 +3023,8 @@ var require_char_class_to_meta_transform = __commonJS({
|
|
|
3023
3023
|
}
|
|
3024
3024
|
});
|
|
3025
3025
|
}
|
|
3026
|
-
function rewriteWordRanges(
|
|
3027
|
-
var node =
|
|
3026
|
+
function rewriteWordRanges(path3, hasIFlag, hasUFlag) {
|
|
3027
|
+
var node = path3.node;
|
|
3028
3028
|
var numberPath = null;
|
|
3029
3029
|
var lowerCasePath = null;
|
|
3030
3030
|
var upperCasePath = null;
|
|
@@ -3033,17 +3033,17 @@ var require_char_class_to_meta_transform = __commonJS({
|
|
|
3033
3033
|
var u212aPath = null;
|
|
3034
3034
|
node.expressions.forEach(function(expression, i) {
|
|
3035
3035
|
if (isMetaChar(expression, "\\d")) {
|
|
3036
|
-
numberPath =
|
|
3036
|
+
numberPath = path3.getChild(i);
|
|
3037
3037
|
} else if (isLowerCaseRange(expression)) {
|
|
3038
|
-
lowerCasePath =
|
|
3038
|
+
lowerCasePath = path3.getChild(i);
|
|
3039
3039
|
} else if (isUpperCaseRange(expression)) {
|
|
3040
|
-
upperCasePath =
|
|
3040
|
+
upperCasePath = path3.getChild(i);
|
|
3041
3041
|
} else if (isUnderscore(expression)) {
|
|
3042
|
-
underscorePath =
|
|
3042
|
+
underscorePath = path3.getChild(i);
|
|
3043
3043
|
} else if (hasIFlag && hasUFlag && isCodePoint(expression, 383)) {
|
|
3044
|
-
u017fPath =
|
|
3044
|
+
u017fPath = path3.getChild(i);
|
|
3045
3045
|
} else if (hasIFlag && hasUFlag && isCodePoint(expression, 8490)) {
|
|
3046
|
-
u212aPath =
|
|
3046
|
+
u212aPath = path3.getChild(i);
|
|
3047
3047
|
}
|
|
3048
3048
|
});
|
|
3049
3049
|
if (numberPath && (lowerCasePath && upperCasePath || hasIFlag && (lowerCasePath || upperCasePath)) && underscorePath && (!hasUFlag || !hasIFlag || u017fPath && u212aPath)) {
|
|
@@ -3080,8 +3080,8 @@ var require_char_class_to_meta_transform = __commonJS({
|
|
|
3080
3080
|
})), [function(node) {
|
|
3081
3081
|
return node.type === "ClassRange" && isCodePoint(node.from, 8192) && isCodePoint(node.to, 8202);
|
|
3082
3082
|
}]);
|
|
3083
|
-
function rewriteWhitespaceRanges(
|
|
3084
|
-
var node =
|
|
3083
|
+
function rewriteWhitespaceRanges(path3) {
|
|
3084
|
+
var node = path3.node;
|
|
3085
3085
|
if (node.expressions.length < whitespaceRangeTests.length || !whitespaceRangeTests.every(function(test) {
|
|
3086
3086
|
return node.expressions.some(function(expression) {
|
|
3087
3087
|
return test(expression);
|
|
@@ -3098,9 +3098,9 @@ var require_char_class_to_meta_transform = __commonJS({
|
|
|
3098
3098
|
node.expressions.map(function(expression, i) {
|
|
3099
3099
|
return whitespaceRangeTests.some(function(test) {
|
|
3100
3100
|
return test(expression);
|
|
3101
|
-
}) ?
|
|
3102
|
-
}).filter(Boolean).forEach(function(
|
|
3103
|
-
return
|
|
3101
|
+
}) ? path3.getChild(i) : void 0;
|
|
3102
|
+
}).filter(Boolean).forEach(function(path4) {
|
|
3103
|
+
return path4.remove();
|
|
3104
3104
|
});
|
|
3105
3105
|
}
|
|
3106
3106
|
function isFullNumberRange(node) {
|
|
@@ -3133,9 +3133,9 @@ var require_char_class_to_single_char_transform = __commonJS({
|
|
|
3133
3133
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/char-class-to-single-char-transform.js"(exports, module) {
|
|
3134
3134
|
"use strict";
|
|
3135
3135
|
module.exports = {
|
|
3136
|
-
CharacterClass: function CharacterClass(
|
|
3137
|
-
var node =
|
|
3138
|
-
if (node.expressions.length !== 1 || !hasAppropriateSiblings(
|
|
3136
|
+
CharacterClass: function CharacterClass(path3) {
|
|
3137
|
+
var node = path3.node;
|
|
3138
|
+
if (node.expressions.length !== 1 || !hasAppropriateSiblings(path3) || !isAppropriateChar(node.expressions[0])) {
|
|
3139
3139
|
return;
|
|
3140
3140
|
}
|
|
3141
3141
|
var _node$expressions$ = node.expressions[0], value = _node$expressions$.value, kind = _node$expressions$.kind, escaped = _node$expressions$.escaped;
|
|
@@ -3145,7 +3145,7 @@ var require_char_class_to_single_char_transform = __commonJS({
|
|
|
3145
3145
|
}
|
|
3146
3146
|
value = getInverseMeta(value);
|
|
3147
3147
|
}
|
|
3148
|
-
|
|
3148
|
+
path3.replace({
|
|
3149
3149
|
type: "Char",
|
|
3150
3150
|
value,
|
|
3151
3151
|
kind,
|
|
@@ -3164,8 +3164,8 @@ var require_char_class_to_single_char_transform = __commonJS({
|
|
|
3164
3164
|
function getInverseMeta(value) {
|
|
3165
3165
|
return /[dws]/.test(value) ? value.toUpperCase() : value.toLowerCase();
|
|
3166
3166
|
}
|
|
3167
|
-
function hasAppropriateSiblings(
|
|
3168
|
-
var parent =
|
|
3167
|
+
function hasAppropriateSiblings(path3) {
|
|
3168
|
+
var parent = path3.parent, index = path3.index;
|
|
3169
3169
|
if (parent.type !== "Alternative") {
|
|
3170
3170
|
return true;
|
|
3171
3171
|
}
|
|
@@ -3196,18 +3196,18 @@ var require_char_escape_unescape_transform = __commonJS({
|
|
|
3196
3196
|
init: function init(ast) {
|
|
3197
3197
|
this._hasXFlag = ast.flags.includes("x");
|
|
3198
3198
|
},
|
|
3199
|
-
Char: function Char(
|
|
3200
|
-
var node =
|
|
3199
|
+
Char: function Char(path3) {
|
|
3200
|
+
var node = path3.node;
|
|
3201
3201
|
if (!node.escaped) {
|
|
3202
3202
|
return;
|
|
3203
3203
|
}
|
|
3204
|
-
if (shouldUnescape(
|
|
3204
|
+
if (shouldUnescape(path3, this._hasXFlag)) {
|
|
3205
3205
|
delete node.escaped;
|
|
3206
3206
|
}
|
|
3207
3207
|
}
|
|
3208
3208
|
};
|
|
3209
|
-
function shouldUnescape(
|
|
3210
|
-
var value =
|
|
3209
|
+
function shouldUnescape(path3, hasXFlag) {
|
|
3210
|
+
var value = path3.node.value, index = path3.index, parent = path3.parent;
|
|
3211
3211
|
if (parent.type !== "CharacterClass" && parent.type !== "ClassRange") {
|
|
3212
3212
|
return !preservesEscape(value, index, parent, hasXFlag);
|
|
3213
3213
|
}
|
|
@@ -3296,8 +3296,8 @@ var require_char_class_classranges_merge_transform = __commonJS({
|
|
|
3296
3296
|
init: function init(ast) {
|
|
3297
3297
|
this._hasIUFlags = ast.flags.includes("i") && ast.flags.includes("u");
|
|
3298
3298
|
},
|
|
3299
|
-
CharacterClass: function CharacterClass(
|
|
3300
|
-
var node =
|
|
3299
|
+
CharacterClass: function CharacterClass(path3) {
|
|
3300
|
+
var node = path3.node;
|
|
3301
3301
|
var expressions = node.expressions;
|
|
3302
3302
|
var metas = [];
|
|
3303
3303
|
expressions.forEach(function(expression2) {
|
|
@@ -3515,8 +3515,8 @@ var require_disjunction_remove_duplicates_transform = __commonJS({
|
|
|
3515
3515
|
var disjunctionToList = _require.disjunctionToList;
|
|
3516
3516
|
var listToDisjunction = _require.listToDisjunction;
|
|
3517
3517
|
module.exports = {
|
|
3518
|
-
Disjunction: function Disjunction(
|
|
3519
|
-
var node =
|
|
3518
|
+
Disjunction: function Disjunction(path3) {
|
|
3519
|
+
var node = path3.node;
|
|
3520
3520
|
var uniqueNodesMap = {};
|
|
3521
3521
|
var parts = disjunctionToList(node).filter(function(part) {
|
|
3522
3522
|
var encoded = part ? NodePath.getForNode(part).jsonEncode() : "null";
|
|
@@ -3526,7 +3526,7 @@ var require_disjunction_remove_duplicates_transform = __commonJS({
|
|
|
3526
3526
|
uniqueNodesMap[encoded] = part;
|
|
3527
3527
|
return true;
|
|
3528
3528
|
});
|
|
3529
|
-
|
|
3529
|
+
path3.replace(listToDisjunction(parts));
|
|
3530
3530
|
}
|
|
3531
3531
|
};
|
|
3532
3532
|
}
|
|
@@ -3537,8 +3537,8 @@ var require_group_single_chars_to_char_class = __commonJS({
|
|
|
3537
3537
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/group-single-chars-to-char-class.js"(exports, module) {
|
|
3538
3538
|
"use strict";
|
|
3539
3539
|
module.exports = {
|
|
3540
|
-
Disjunction: function Disjunction(
|
|
3541
|
-
var node =
|
|
3540
|
+
Disjunction: function Disjunction(path3) {
|
|
3541
|
+
var node = path3.node, parent = path3.parent;
|
|
3542
3542
|
if (!handlers[parent.type]) {
|
|
3543
3543
|
return;
|
|
3544
3544
|
}
|
|
@@ -3552,20 +3552,20 @@ var require_group_single_chars_to_char_class = __commonJS({
|
|
|
3552
3552
|
return charset.get(key);
|
|
3553
3553
|
})
|
|
3554
3554
|
};
|
|
3555
|
-
handlers[parent.type](
|
|
3555
|
+
handlers[parent.type](path3.getParent(), characterClass);
|
|
3556
3556
|
}
|
|
3557
3557
|
};
|
|
3558
3558
|
var handlers = {
|
|
3559
|
-
RegExp: function RegExp2(
|
|
3560
|
-
var node =
|
|
3559
|
+
RegExp: function RegExp2(path3, characterClass) {
|
|
3560
|
+
var node = path3.node;
|
|
3561
3561
|
node.body = characterClass;
|
|
3562
3562
|
},
|
|
3563
|
-
Group: function Group(
|
|
3564
|
-
var node =
|
|
3563
|
+
Group: function Group(path3, characterClass) {
|
|
3564
|
+
var node = path3.node;
|
|
3565
3565
|
if (node.capturing) {
|
|
3566
3566
|
node.expression = characterClass;
|
|
3567
3567
|
} else {
|
|
3568
|
-
|
|
3568
|
+
path3.replace(characterClass);
|
|
3569
3569
|
}
|
|
3570
3570
|
}
|
|
3571
3571
|
};
|
|
@@ -3599,16 +3599,16 @@ var require_remove_empty_group_transform = __commonJS({
|
|
|
3599
3599
|
"../../node_modules/.pnpm/regexp-tree@0.1.27/node_modules/regexp-tree/dist/optimizer/transforms/remove-empty-group-transform.js"(exports, module) {
|
|
3600
3600
|
"use strict";
|
|
3601
3601
|
module.exports = {
|
|
3602
|
-
Group: function Group(
|
|
3603
|
-
var node =
|
|
3604
|
-
var childPath =
|
|
3602
|
+
Group: function Group(path3) {
|
|
3603
|
+
var node = path3.node, parent = path3.parent;
|
|
3604
|
+
var childPath = path3.getChild();
|
|
3605
3605
|
if (node.capturing || childPath) {
|
|
3606
3606
|
return;
|
|
3607
3607
|
}
|
|
3608
3608
|
if (parent.type === "Repetition") {
|
|
3609
|
-
|
|
3609
|
+
path3.getParent().replace(node);
|
|
3610
3610
|
} else if (parent.type !== "RegExp") {
|
|
3611
|
-
|
|
3611
|
+
path3.remove();
|
|
3612
3612
|
}
|
|
3613
3613
|
}
|
|
3614
3614
|
};
|
|
@@ -3630,13 +3630,13 @@ var require_ungroup_transform = __commonJS({
|
|
|
3630
3630
|
}
|
|
3631
3631
|
}
|
|
3632
3632
|
module.exports = {
|
|
3633
|
-
Group: function Group(
|
|
3634
|
-
var node =
|
|
3635
|
-
var childPath =
|
|
3633
|
+
Group: function Group(path3) {
|
|
3634
|
+
var node = path3.node, parent = path3.parent;
|
|
3635
|
+
var childPath = path3.getChild();
|
|
3636
3636
|
if (node.capturing || !childPath) {
|
|
3637
3637
|
return;
|
|
3638
3638
|
}
|
|
3639
|
-
if (!hasAppropriateSiblings(
|
|
3639
|
+
if (!hasAppropriateSiblings(path3)) {
|
|
3640
3640
|
return;
|
|
3641
3641
|
}
|
|
3642
3642
|
if (childPath.node.type === "Disjunction" && parent.type !== "RegExp") {
|
|
@@ -3646,20 +3646,20 @@ var require_ungroup_transform = __commonJS({
|
|
|
3646
3646
|
return;
|
|
3647
3647
|
}
|
|
3648
3648
|
if (childPath.node.type === "Alternative") {
|
|
3649
|
-
var parentPath =
|
|
3649
|
+
var parentPath = path3.getParent();
|
|
3650
3650
|
if (parentPath.node.type === "Alternative") {
|
|
3651
3651
|
parentPath.replace({
|
|
3652
3652
|
type: "Alternative",
|
|
3653
|
-
expressions: [].concat(_toConsumableArray(parent.expressions.slice(0,
|
|
3653
|
+
expressions: [].concat(_toConsumableArray(parent.expressions.slice(0, path3.index)), _toConsumableArray(childPath.node.expressions), _toConsumableArray(parent.expressions.slice(path3.index + 1)))
|
|
3654
3654
|
});
|
|
3655
3655
|
}
|
|
3656
3656
|
} else {
|
|
3657
|
-
|
|
3657
|
+
path3.replace(childPath.node);
|
|
3658
3658
|
}
|
|
3659
3659
|
}
|
|
3660
3660
|
};
|
|
3661
|
-
function hasAppropriateSiblings(
|
|
3662
|
-
var parent =
|
|
3661
|
+
function hasAppropriateSiblings(path3) {
|
|
3662
|
+
var parent = path3.parent, index = path3.index;
|
|
3663
3663
|
if (parent.type !== "Alternative") {
|
|
3664
3664
|
return true;
|
|
3665
3665
|
}
|
|
@@ -3696,22 +3696,22 @@ var require_combine_repeating_patterns_transform = __commonJS({
|
|
|
3696
3696
|
var _require = require_utils();
|
|
3697
3697
|
var increaseQuantifierByOne = _require.increaseQuantifierByOne;
|
|
3698
3698
|
module.exports = {
|
|
3699
|
-
Alternative: function Alternative(
|
|
3700
|
-
var node =
|
|
3699
|
+
Alternative: function Alternative(path3) {
|
|
3700
|
+
var node = path3.node;
|
|
3701
3701
|
var index = 1;
|
|
3702
3702
|
while (index < node.expressions.length) {
|
|
3703
|
-
var child =
|
|
3704
|
-
index = Math.max(1, combineRepeatingPatternLeft(
|
|
3703
|
+
var child = path3.getChild(index);
|
|
3704
|
+
index = Math.max(1, combineRepeatingPatternLeft(path3, child, index));
|
|
3705
3705
|
if (index >= node.expressions.length) {
|
|
3706
3706
|
break;
|
|
3707
3707
|
}
|
|
3708
|
-
child =
|
|
3709
|
-
index = Math.max(1, combineWithPreviousRepetition(
|
|
3708
|
+
child = path3.getChild(index);
|
|
3709
|
+
index = Math.max(1, combineWithPreviousRepetition(path3, child, index));
|
|
3710
3710
|
if (index >= node.expressions.length) {
|
|
3711
3711
|
break;
|
|
3712
3712
|
}
|
|
3713
|
-
child =
|
|
3714
|
-
index = Math.max(1, combineRepetitionWithPrevious(
|
|
3713
|
+
child = path3.getChild(index);
|
|
3714
|
+
index = Math.max(1, combineRepetitionWithPrevious(path3, child, index));
|
|
3715
3715
|
index++;
|
|
3716
3716
|
}
|
|
3717
3717
|
}
|
|
@@ -5842,8 +5842,8 @@ var SchemaResolver = class {
|
|
|
5842
5842
|
* @param responseCode - Response code (default: '200')
|
|
5843
5843
|
* @returns Schema object or undefined
|
|
5844
5844
|
*/
|
|
5845
|
-
getResponseSchema(
|
|
5846
|
-
const pathItem = this.document.paths?.[
|
|
5845
|
+
getResponseSchema(path3, method, responseCode = "200") {
|
|
5846
|
+
const pathItem = this.document.paths?.[path3];
|
|
5847
5847
|
if (!pathItem) return void 0;
|
|
5848
5848
|
const operation = pathItem[method];
|
|
5849
5849
|
if (!operation?.responses) return void 0;
|
|
@@ -5869,14 +5869,14 @@ var SchemaResolver = class {
|
|
|
5869
5869
|
*/
|
|
5870
5870
|
getAllEndpoints() {
|
|
5871
5871
|
const endpoints = [];
|
|
5872
|
-
for (const [
|
|
5872
|
+
for (const [path3, pathItem] of Object.entries(this.document.paths || {})) {
|
|
5873
5873
|
if (!pathItem) continue;
|
|
5874
5874
|
const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
5875
5875
|
for (const method of methods) {
|
|
5876
5876
|
const operation = pathItem[method];
|
|
5877
5877
|
if (operation) {
|
|
5878
5878
|
endpoints.push({
|
|
5879
|
-
path:
|
|
5879
|
+
path: path3,
|
|
5880
5880
|
method,
|
|
5881
5881
|
operationId: operation.operationId,
|
|
5882
5882
|
summary: operation.summary,
|
|
@@ -5900,20 +5900,31 @@ var ZodSchemaGenerator = class {
|
|
|
5900
5900
|
* @returns Zod schema code as string
|
|
5901
5901
|
*/
|
|
5902
5902
|
generate(schema, options = {}) {
|
|
5903
|
-
const { schemaName = "GeneratedSchema" } = options;
|
|
5903
|
+
const { schemaName = "GeneratedSchema", bare = false } = options;
|
|
5904
5904
|
const zodSchemaCode = this.schemaToZod(schema);
|
|
5905
|
-
|
|
5906
|
-
import isSafe from 'safe-regex';
|
|
5907
|
-
|
|
5908
|
-
export const ${schemaName} = ${zodSchemaCode};
|
|
5905
|
+
const body = `export const ${schemaName} = ${zodSchemaCode};
|
|
5909
5906
|
|
|
5910
5907
|
export type ${schemaName}Type = z.infer<typeof ${schemaName}>;
|
|
5911
5908
|
`;
|
|
5909
|
+
if (bare) {
|
|
5910
|
+
return body;
|
|
5911
|
+
}
|
|
5912
|
+
return `import { z } from 'zod';
|
|
5913
|
+
import isSafe from 'safe-regex';
|
|
5914
|
+
|
|
5915
|
+
${body}`;
|
|
5912
5916
|
}
|
|
5913
5917
|
/**
|
|
5914
5918
|
* Convert OpenAPI schema to Zod schema code
|
|
5915
5919
|
*/
|
|
5916
5920
|
schemaToZod(schema, depth = 0) {
|
|
5921
|
+
if ("$ref" in schema && schema.$ref) {
|
|
5922
|
+
const refPath = schema.$ref;
|
|
5923
|
+
console.warn(
|
|
5924
|
+
`\u26A0\uFE0F Unresolved $ref encountered: ${refPath} \u2014 generating z.unknown()`
|
|
5925
|
+
);
|
|
5926
|
+
return `z.unknown() /* unresolved $ref: ${refPath} */`;
|
|
5927
|
+
}
|
|
5917
5928
|
const isNullable = "nullable" in schema && schema.nullable === true;
|
|
5918
5929
|
let baseSchema = this.schemaTypeToZod(schema);
|
|
5919
5930
|
if (schema.description) {
|
|
@@ -5942,6 +5953,12 @@ export type ${schemaName}Type = z.infer<typeof ${schemaName}>;
|
|
|
5942
5953
|
return this.handleComposition(schema.oneOf, "discriminatedUnion");
|
|
5943
5954
|
}
|
|
5944
5955
|
const type = schema.type;
|
|
5956
|
+
if (!type && schema.properties) {
|
|
5957
|
+
return this.objectToZod(schema);
|
|
5958
|
+
}
|
|
5959
|
+
if (!type && schema.additionalProperties !== void 0) {
|
|
5960
|
+
return this.objectToZod(schema);
|
|
5961
|
+
}
|
|
5945
5962
|
switch (type) {
|
|
5946
5963
|
case "string":
|
|
5947
5964
|
return this.stringToZod(schema);
|
|
@@ -6196,7 +6213,8 @@ var CollectionProviderGenerator = class {
|
|
|
6196
6213
|
const {
|
|
6197
6214
|
providerName = this.generateProviderName(config),
|
|
6198
6215
|
baseUrl = this.getBaseUrl(),
|
|
6199
|
-
auth
|
|
6216
|
+
auth,
|
|
6217
|
+
bare = false
|
|
6200
6218
|
} = options;
|
|
6201
6219
|
const { endpoint, slugField, method = "get", name } = config;
|
|
6202
6220
|
const collectionName = name || this.sanitizePath(endpoint);
|
|
@@ -6206,9 +6224,7 @@ var CollectionProviderGenerator = class {
|
|
|
6206
6224
|
throw new Error(`No response schema found for ${method.toUpperCase()} ${endpoint}`);
|
|
6207
6225
|
}
|
|
6208
6226
|
const isArray = schema.type === "array";
|
|
6209
|
-
const itemSchema = isArray ? schema.items : schema;
|
|
6210
6227
|
const schemaName = `${this.capitalize(collectionName)}Schema`;
|
|
6211
|
-
const typeName = `${this.capitalize(collectionName)}`;
|
|
6212
6228
|
return this.generateProviderCode({
|
|
6213
6229
|
providerName,
|
|
6214
6230
|
collectionName,
|
|
@@ -6218,8 +6234,8 @@ var CollectionProviderGenerator = class {
|
|
|
6218
6234
|
baseUrl,
|
|
6219
6235
|
auth,
|
|
6220
6236
|
schemaName,
|
|
6221
|
-
|
|
6222
|
-
|
|
6237
|
+
isArray,
|
|
6238
|
+
bare
|
|
6223
6239
|
});
|
|
6224
6240
|
}
|
|
6225
6241
|
/**
|
|
@@ -6235,14 +6251,17 @@ var CollectionProviderGenerator = class {
|
|
|
6235
6251
|
baseUrl,
|
|
6236
6252
|
auth,
|
|
6237
6253
|
schemaName,
|
|
6238
|
-
|
|
6239
|
-
|
|
6254
|
+
isArray,
|
|
6255
|
+
bare
|
|
6240
6256
|
} = params;
|
|
6241
6257
|
const authHeader = this.generateAuthHeader(auth);
|
|
6242
|
-
|
|
6243
|
-
|
|
6258
|
+
const arraySchemaName = `${schemaName.replace(/Schema$/, "")}ArraySchema`;
|
|
6259
|
+
const validationSchema = isArray ? arraySchemaName : schemaName;
|
|
6260
|
+
const imports = bare ? "" : `import type { CollectionProvider, CollectionItem } from '@stackwright/collections';
|
|
6261
|
+
import { ${isArray ? arraySchemaName : schemaName} } from './schemas';
|
|
6244
6262
|
|
|
6245
|
-
|
|
6263
|
+
`;
|
|
6264
|
+
return `${imports}/**
|
|
6246
6265
|
* CollectionProvider for ${collectionName}
|
|
6247
6266
|
*
|
|
6248
6267
|
* Generated from OpenAPI endpoint: ${method.toUpperCase()} ${endpoint}
|
|
@@ -6293,7 +6312,7 @@ export class ${providerName} implements CollectionProvider {
|
|
|
6293
6312
|
const data = await response.json();
|
|
6294
6313
|
|
|
6295
6314
|
// Validate response with Zod schema
|
|
6296
|
-
const validated = ${
|
|
6315
|
+
const validated = ${validationSchema}.parse(data);
|
|
6297
6316
|
|
|
6298
6317
|
// Convert to CollectionItem format
|
|
6299
6318
|
return ${isArray ? "validated" : "[validated]"}.map((item: any) => this.toCollectionItem(item));
|
|
@@ -6404,8 +6423,8 @@ export class ${providerName} implements CollectionProvider {
|
|
|
6404
6423
|
/**
|
|
6405
6424
|
* Sanitize path to valid identifier
|
|
6406
6425
|
*/
|
|
6407
|
-
sanitizePath(
|
|
6408
|
-
return
|
|
6426
|
+
sanitizePath(path3) {
|
|
6427
|
+
return path3.replace(/^\//, "").replace(/\//g, "-").replace(/[{}:]/g, "").replace(/-+/g, "-");
|
|
6409
6428
|
}
|
|
6410
6429
|
/**
|
|
6411
6430
|
* Capitalize string
|
|
@@ -6430,10 +6449,12 @@ var ClientGenerator = class {
|
|
|
6430
6449
|
this.resolver = new SchemaResolver(document);
|
|
6431
6450
|
this.schemaMapping = schemaMapping;
|
|
6432
6451
|
this.requiredSchemas = /* @__PURE__ */ new Set();
|
|
6452
|
+
this.generatedRequestSchemas = /* @__PURE__ */ new Set();
|
|
6433
6453
|
}
|
|
6434
6454
|
resolver;
|
|
6435
6455
|
schemaMapping;
|
|
6436
6456
|
requiredSchemas;
|
|
6457
|
+
generatedRequestSchemas;
|
|
6437
6458
|
/**
|
|
6438
6459
|
* Generate typed API client code from OpenAPI document
|
|
6439
6460
|
*/
|
|
@@ -6451,6 +6472,7 @@ var ClientGenerator = class {
|
|
|
6451
6472
|
throw new Error("No endpoints found in OpenAPI document");
|
|
6452
6473
|
}
|
|
6453
6474
|
this.requiredSchemas.clear();
|
|
6475
|
+
this.generatedRequestSchemas.clear();
|
|
6454
6476
|
let code = this.generateImports(!!schemaMapping, validateResponses);
|
|
6455
6477
|
code += "\n";
|
|
6456
6478
|
if (schemaMapping) {
|
|
@@ -6525,6 +6547,7 @@ var ClientGenerator = class {
|
|
|
6525
6547
|
code += `export const ${schemaName} = ${schemaCode};
|
|
6526
6548
|
|
|
6527
6549
|
`;
|
|
6550
|
+
this.generatedRequestSchemas.add(schemaName);
|
|
6528
6551
|
}
|
|
6529
6552
|
}
|
|
6530
6553
|
return code;
|
|
@@ -6834,10 +6857,10 @@ ${paramSchemas.join(",\n")}
|
|
|
6834
6857
|
continue;
|
|
6835
6858
|
}
|
|
6836
6859
|
const typeName = this.getOperationTypeName(endpoint);
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6860
|
+
const requestSchemaName = mapping.requestSchema || typeName + "RequestSchema";
|
|
6861
|
+
if (this.generatedRequestSchemas.has(requestSchemaName)) {
|
|
6862
|
+
this.addRequiredSchema(requestSchemaName);
|
|
6863
|
+
code += "export type " + typeName + "Request = z.infer<typeof " + requestSchemaName + ">;\n";
|
|
6841
6864
|
}
|
|
6842
6865
|
this.addRequiredSchema(mapping.responseSchema);
|
|
6843
6866
|
code += `export type ${typeName}Response = z.infer<typeof schemas.${mapping.responseSchema}>;
|
|
@@ -7176,7 +7199,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7176
7199
|
* Generate a method for an endpoint with Zod validation (NEW)
|
|
7177
7200
|
*/
|
|
7178
7201
|
generateMethodWithValidation(endpoint, includeJsDoc, schemaMapping) {
|
|
7179
|
-
const { operation, path:
|
|
7202
|
+
const { operation, path: path3, method } = endpoint;
|
|
7180
7203
|
const methodName = this.getMethodName(endpoint);
|
|
7181
7204
|
const typeName = this.getOperationTypeName(endpoint);
|
|
7182
7205
|
const operationId = endpoint.operationId || methodName;
|
|
@@ -7236,7 +7259,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7236
7259
|
if (pathParams.length > 0) {
|
|
7237
7260
|
code += ` // Build URL with path parameters
|
|
7238
7261
|
`;
|
|
7239
|
-
code += ` let url = this.baseUrl + '${
|
|
7262
|
+
code += ` let url = this.baseUrl + '${path3}'
|
|
7240
7263
|
`;
|
|
7241
7264
|
for (const param of pathParams) {
|
|
7242
7265
|
code += ` .replace('{${param.name}}', encodeURIComponent(String(request.path.${param.name})))
|
|
@@ -7246,7 +7269,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7246
7269
|
|
|
7247
7270
|
`;
|
|
7248
7271
|
} else {
|
|
7249
|
-
code += ` const url = this.baseUrl + '${
|
|
7272
|
+
code += ` const url = this.baseUrl + '${path3}';
|
|
7250
7273
|
|
|
7251
7274
|
`;
|
|
7252
7275
|
}
|
|
@@ -7258,7 +7281,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7258
7281
|
code += ` if (request.query) {
|
|
7259
7282
|
`;
|
|
7260
7283
|
for (const param of queryParams) {
|
|
7261
|
-
code += ` if (request.query.${param.name}
|
|
7284
|
+
code += ` if (request.query.${param.name} != null) {
|
|
7262
7285
|
`;
|
|
7263
7286
|
code += ` searchParams.append('${param.name}', String(request.query.${param.name}));
|
|
7264
7287
|
`;
|
|
@@ -7284,7 +7307,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7284
7307
|
code += ` method: '${method.toUpperCase()}',
|
|
7285
7308
|
`;
|
|
7286
7309
|
if (hasBody) {
|
|
7287
|
-
code += ` body: request.body ? JSON.stringify(request.body) :
|
|
7310
|
+
code += ` body: request.body ? JSON.stringify(request.body) : null,
|
|
7288
7311
|
`;
|
|
7289
7312
|
}
|
|
7290
7313
|
if (headerParams.length > 0) {
|
|
@@ -7342,7 +7365,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7342
7365
|
* Generate a method for an endpoint (LEGACY)
|
|
7343
7366
|
*/
|
|
7344
7367
|
generateMethodLegacy(endpoint, includeJsDoc) {
|
|
7345
|
-
const { operation, path:
|
|
7368
|
+
const { operation, path: path3, method } = endpoint;
|
|
7346
7369
|
const methodName = this.getMethodName(endpoint);
|
|
7347
7370
|
const typeName = this.getOperationTypeName(endpoint);
|
|
7348
7371
|
const params = this.getOperationParameters(operation);
|
|
@@ -7378,7 +7401,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7378
7401
|
if (pathParams.length > 0) {
|
|
7379
7402
|
code += ` // Build URL with path parameters
|
|
7380
7403
|
`;
|
|
7381
|
-
code += ` let url = this.baseUrl + '${
|
|
7404
|
+
code += ` let url = this.baseUrl + '${path3}'
|
|
7382
7405
|
`;
|
|
7383
7406
|
for (const param of pathParams) {
|
|
7384
7407
|
code += ` .replace('{${param.name}}', encodeURIComponent(String(request.path.${param.name})))
|
|
@@ -7388,7 +7411,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7388
7411
|
|
|
7389
7412
|
`;
|
|
7390
7413
|
} else {
|
|
7391
|
-
code += ` const url = this.baseUrl + '${
|
|
7414
|
+
code += ` const url = this.baseUrl + '${path3}';
|
|
7392
7415
|
|
|
7393
7416
|
`;
|
|
7394
7417
|
}
|
|
@@ -7401,7 +7424,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7401
7424
|
code += ` if (request.query) {
|
|
7402
7425
|
`;
|
|
7403
7426
|
for (const param of queryParams) {
|
|
7404
|
-
code += ` if (request.query.${param.name}
|
|
7427
|
+
code += ` if (request.query.${param.name} != null) {
|
|
7405
7428
|
`;
|
|
7406
7429
|
code += ` searchParams.append('${param.name}', String(request.query.${param.name}));
|
|
7407
7430
|
`;
|
|
@@ -7425,7 +7448,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7425
7448
|
code += ` method: '${method.toUpperCase()}',
|
|
7426
7449
|
`;
|
|
7427
7450
|
if (hasBody) {
|
|
7428
|
-
code += ` body: request.body ? JSON.stringify(request.body) :
|
|
7451
|
+
code += ` body: request.body ? JSON.stringify(request.body) : null,
|
|
7429
7452
|
`;
|
|
7430
7453
|
}
|
|
7431
7454
|
const headerParams = params.filter((p) => p.in === "header");
|
|
@@ -7472,7 +7495,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7472
7495
|
const errorBody = await response.text().catch(() => null);
|
|
7473
7496
|
|
|
7474
7497
|
// \u2705 SECURITY FIX: Safe JSON parsing with fallback
|
|
7475
|
-
let parsedError: unknown
|
|
7498
|
+
let parsedError: unknown;
|
|
7476
7499
|
if (errorBody) {
|
|
7477
7500
|
try {
|
|
7478
7501
|
parsedError = JSON.parse(errorBody);
|
|
@@ -7494,7 +7517,7 @@ ${paramSchemas.join(",\n")}
|
|
|
7494
7517
|
|
|
7495
7518
|
// Handle empty responses (204 No Content, etc.)
|
|
7496
7519
|
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
7497
|
-
return
|
|
7520
|
+
return null as unknown as T;
|
|
7498
7521
|
}
|
|
7499
7522
|
|
|
7500
7523
|
// \u2705 SECURITY FIX: Validate Content-Type before parsing JSON
|
|
@@ -7520,14 +7543,14 @@ ${paramSchemas.join(",\n")}
|
|
|
7520
7543
|
*/
|
|
7521
7544
|
getAllEndpoints() {
|
|
7522
7545
|
const endpoints = [];
|
|
7523
|
-
for (const [
|
|
7546
|
+
for (const [path3, pathItem] of Object.entries(this.document.paths || {})) {
|
|
7524
7547
|
if (!pathItem) continue;
|
|
7525
7548
|
const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
7526
7549
|
for (const method of methods) {
|
|
7527
7550
|
const operation = pathItem[method];
|
|
7528
7551
|
if (operation) {
|
|
7529
7552
|
endpoints.push({
|
|
7530
|
-
path:
|
|
7553
|
+
path: path3,
|
|
7531
7554
|
method,
|
|
7532
7555
|
operationId: operation.operationId,
|
|
7533
7556
|
summary: operation.summary,
|
|
@@ -7537,7 +7560,32 @@ ${paramSchemas.join(",\n")}
|
|
|
7537
7560
|
}
|
|
7538
7561
|
}
|
|
7539
7562
|
}
|
|
7540
|
-
return endpoints;
|
|
7563
|
+
return this.deduplicateOperationIds(endpoints);
|
|
7564
|
+
}
|
|
7565
|
+
/**
|
|
7566
|
+
* Deduplicate colliding operationIds by appending a path-based suffix.
|
|
7567
|
+
*
|
|
7568
|
+
* Real-world specs (e.g. SAM.gov) reuse the same operationId across
|
|
7569
|
+
* versioned paths. We make each one unique so the generated client
|
|
7570
|
+
* has no duplicate method names.
|
|
7571
|
+
*/
|
|
7572
|
+
deduplicateOperationIds(endpoints) {
|
|
7573
|
+
const seen = /* @__PURE__ */ new Map();
|
|
7574
|
+
return endpoints.map((ep) => {
|
|
7575
|
+
if (!ep.operationId) return ep;
|
|
7576
|
+
const count = seen.get(ep.operationId) ?? 0;
|
|
7577
|
+
seen.set(ep.operationId, count + 1);
|
|
7578
|
+
if (count === 0) return ep;
|
|
7579
|
+
const suffix = this.sanitizePath(ep.path);
|
|
7580
|
+
return { ...ep, operationId: `${ep.operationId}_${suffix}` };
|
|
7581
|
+
});
|
|
7582
|
+
}
|
|
7583
|
+
/**
|
|
7584
|
+
* Turn an API path into a valid, readable identifier suffix.
|
|
7585
|
+
* e.g. '/entity-information/v4/download-entities' -> 'entityInformationV4DownloadEntities'
|
|
7586
|
+
*/
|
|
7587
|
+
sanitizePath(path3) {
|
|
7588
|
+
return path3.split("/").filter((seg) => seg && !seg.startsWith("{")).join("-").replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()).replace(/^[A-Z]/, (chr) => chr.toLowerCase());
|
|
7541
7589
|
}
|
|
7542
7590
|
/**
|
|
7543
7591
|
* Get parameters for an operation
|
|
@@ -7633,12 +7681,556 @@ var OpenAPICollectionProvider = class {
|
|
|
7633
7681
|
};
|
|
7634
7682
|
|
|
7635
7683
|
// src/prebuild/OpenAPIPlugin.ts
|
|
7684
|
+
import fs2 from "fs";
|
|
7685
|
+
import path2 from "path";
|
|
7686
|
+
|
|
7687
|
+
// src/utils/EndpointFilter.ts
|
|
7688
|
+
var EndpointFilter = class {
|
|
7689
|
+
include;
|
|
7690
|
+
exclude;
|
|
7691
|
+
constructor(filter) {
|
|
7692
|
+
if (filter?.include !== void 0 && filter.include.length > 0) {
|
|
7693
|
+
this.include = filter.include;
|
|
7694
|
+
} else {
|
|
7695
|
+
this.include = ["/**"];
|
|
7696
|
+
}
|
|
7697
|
+
this.exclude = filter?.exclude ?? [];
|
|
7698
|
+
}
|
|
7699
|
+
/**
|
|
7700
|
+
* Check if a path should be included in code generation.
|
|
7701
|
+
*
|
|
7702
|
+
* Logic:
|
|
7703
|
+
* 1. If path matches any exclude pattern → excluded
|
|
7704
|
+
* 2. If path matches any include pattern → included
|
|
7705
|
+
* 3. Otherwise → excluded (default deny)
|
|
7706
|
+
*/
|
|
7707
|
+
matches(path3) {
|
|
7708
|
+
const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
|
|
7709
|
+
for (const pattern of this.exclude) {
|
|
7710
|
+
if (this.matchPattern(normalizedPath, pattern)) {
|
|
7711
|
+
return false;
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
for (const pattern of this.include) {
|
|
7715
|
+
if (this.matchPattern(normalizedPath, pattern)) {
|
|
7716
|
+
return true;
|
|
7717
|
+
}
|
|
7718
|
+
}
|
|
7719
|
+
return false;
|
|
7720
|
+
}
|
|
7721
|
+
/**
|
|
7722
|
+
* Match path against a pattern.
|
|
7723
|
+
*/
|
|
7724
|
+
matchPattern(path3, pattern) {
|
|
7725
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
7726
|
+
if (normalizedPattern === "/") {
|
|
7727
|
+
return true;
|
|
7728
|
+
}
|
|
7729
|
+
const regex = this.globToRegex(normalizedPattern);
|
|
7730
|
+
if (regex.test(path3)) {
|
|
7731
|
+
return true;
|
|
7732
|
+
}
|
|
7733
|
+
if (!normalizedPattern.endsWith("*")) {
|
|
7734
|
+
const patternSegments = normalizedPattern.split("/");
|
|
7735
|
+
const pathSegments = path3.split("/");
|
|
7736
|
+
if (pathSegments.length >= patternSegments.length) {
|
|
7737
|
+
let matches = true;
|
|
7738
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
7739
|
+
const pSeg = patternSegments[i];
|
|
7740
|
+
if (pSeg === "*" || pSeg === "**") {
|
|
7741
|
+
continue;
|
|
7742
|
+
}
|
|
7743
|
+
if (pSeg.startsWith("{") && pSeg.endsWith("}")) {
|
|
7744
|
+
continue;
|
|
7745
|
+
}
|
|
7746
|
+
if (pSeg !== pathSegments[i]) {
|
|
7747
|
+
matches = false;
|
|
7748
|
+
break;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
if (matches) {
|
|
7752
|
+
return true;
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
return false;
|
|
7757
|
+
}
|
|
7758
|
+
/**
|
|
7759
|
+
* Convert a glob-like pattern to a RegExp.
|
|
7760
|
+
*
|
|
7761
|
+
* Pattern semantics:
|
|
7762
|
+
* - `*` = single path segment (no slashes), minimum 1 character
|
|
7763
|
+
* - `**` = multiple path segments (includes slashes), zero or more segments
|
|
7764
|
+
* - `{param}` = single path segment parameter
|
|
7765
|
+
* - Exact match segments are literal strings
|
|
7766
|
+
*/
|
|
7767
|
+
globToRegex(pattern) {
|
|
7768
|
+
let result = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
7769
|
+
result = result.replace(/\*\*/g, "\u27EADOUBLESTAR\u27EB");
|
|
7770
|
+
result = result.replace(/\*/g, "\u27EASTAR\u27EB");
|
|
7771
|
+
if (result.startsWith("\u27EADOUBLESTAR\u27EB")) {
|
|
7772
|
+
result = ".*" + result.slice("\u27EADOUBLESTAR\u27EB".length);
|
|
7773
|
+
} else if (result.endsWith("\u27EADOUBLESTAR\u27EB") && result.endsWith("/\u27EADOUBLESTAR\u27EB")) {
|
|
7774
|
+
result = result.slice(0, -"\u27EADOUBLESTAR\u27EB".length - 1) + "(?:/.*)?";
|
|
7775
|
+
} else {
|
|
7776
|
+
result = result.replace(/⟪DOUBLESTAR⟫/g, "(?:/.*)?");
|
|
7777
|
+
}
|
|
7778
|
+
result = result.replace(/⟪STAR⟫/g, "[^/]+");
|
|
7779
|
+
result = result.replace(/\{[^}]+\}/g, "[^/]+");
|
|
7780
|
+
return new RegExp(`^${result}$`);
|
|
7781
|
+
}
|
|
7782
|
+
};
|
|
7783
|
+
|
|
7784
|
+
// src/utils/ApprovedSpecsValidator.ts
|
|
7785
|
+
import crypto from "crypto";
|
|
7636
7786
|
import fs from "fs";
|
|
7787
|
+
import https from "https";
|
|
7788
|
+
import http from "http";
|
|
7789
|
+
import { URL as URL2 } from "url";
|
|
7637
7790
|
import path from "path";
|
|
7791
|
+
import { realpathSync } from "fs";
|
|
7792
|
+
import { isIP } from "net";
|
|
7793
|
+
var ApprovedSpecsValidator = class {
|
|
7794
|
+
allowlist;
|
|
7795
|
+
cache = /* @__PURE__ */ new Map();
|
|
7796
|
+
skipHashVerification;
|
|
7797
|
+
ALLOWED_DIRS;
|
|
7798
|
+
MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
7799
|
+
// 10MB - prevents memory exhaustion attacks
|
|
7800
|
+
/**
|
|
7801
|
+
* Create a new ApprovedSpecsValidator
|
|
7802
|
+
*
|
|
7803
|
+
* @param config - Security configuration from stackwright.yml
|
|
7804
|
+
* @param skipHashVerification - Skip hash check (for testing/development)
|
|
7805
|
+
*/
|
|
7806
|
+
constructor(config, skipHashVerification = false) {
|
|
7807
|
+
this.allowlist = config.allowlist || [];
|
|
7808
|
+
this.skipHashVerification = skipHashVerification;
|
|
7809
|
+
this.ALLOWED_DIRS = this.buildAllowedDirs();
|
|
7810
|
+
}
|
|
7811
|
+
/**
|
|
7812
|
+
* Build list of allowed directories for path traversal prevention.
|
|
7813
|
+
* Defaults to cwd, specs subdir, and .stackwright cache dir.
|
|
7814
|
+
*/
|
|
7815
|
+
buildAllowedDirs() {
|
|
7816
|
+
const cwd = process.cwd();
|
|
7817
|
+
return [cwd, path.join(cwd, "specs"), path.join(cwd, ".stackwright")];
|
|
7818
|
+
}
|
|
7819
|
+
/**
|
|
7820
|
+
* Validate that a file path is within allowed directories (path traversal prevention).
|
|
7821
|
+
* Uses realpathSync to resolve symlinks and prevent symlink traversal attacks.
|
|
7822
|
+
*
|
|
7823
|
+
* @param filePath - File path to validate
|
|
7824
|
+
* @returns true if path is allowed, false otherwise
|
|
7825
|
+
*/
|
|
7826
|
+
isPathAllowed(filePath) {
|
|
7827
|
+
try {
|
|
7828
|
+
const realPath = realpathSync(filePath);
|
|
7829
|
+
return this.ALLOWED_DIRS.some((dir) => {
|
|
7830
|
+
const realDir = realpathSync(dir);
|
|
7831
|
+
return realPath.startsWith(realDir + path.sep) || realPath === realDir;
|
|
7832
|
+
});
|
|
7833
|
+
} catch {
|
|
7834
|
+
return false;
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
/**
|
|
7838
|
+
* Check if a URL/path is a path traversal attempt.
|
|
7839
|
+
* This is checked BEFORE the allowlist to prevent bypassing path security.
|
|
7840
|
+
*
|
|
7841
|
+
* @param specUrl - URL or path to check
|
|
7842
|
+
* @returns true if this is a path traversal attempt
|
|
7843
|
+
*/
|
|
7844
|
+
isPathTraversalAttempt(specUrl) {
|
|
7845
|
+
if (specUrl.startsWith("file://")) {
|
|
7846
|
+
return true;
|
|
7847
|
+
}
|
|
7848
|
+
if (fs.existsSync(specUrl)) {
|
|
7849
|
+
return !this.isPathAllowed(specUrl);
|
|
7850
|
+
}
|
|
7851
|
+
if (!specUrl.startsWith("http://") && !specUrl.startsWith("https://")) {
|
|
7852
|
+
if (specUrl.includes("../") || specUrl.includes("..\\")) {
|
|
7853
|
+
return true;
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
return false;
|
|
7857
|
+
}
|
|
7858
|
+
/**
|
|
7859
|
+
* Validate that a hex string is a valid SHA-256 hash (64 hex characters).
|
|
7860
|
+
*
|
|
7861
|
+
* @param hash - String to validate
|
|
7862
|
+
* @returns true if valid SHA-256 hex format
|
|
7863
|
+
*/
|
|
7864
|
+
isValidHex(hash) {
|
|
7865
|
+
return /^[a-fA-F0-9]{64}$/.test(hash);
|
|
7866
|
+
}
|
|
7867
|
+
/**
|
|
7868
|
+
* Validate that a redirect URL is safe (SSRF protection).
|
|
7869
|
+
* Blocks:
|
|
7870
|
+
* - HTTPS → HTTP downgrades
|
|
7871
|
+
* - Private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x)
|
|
7872
|
+
* - Localhost and loopback addresses
|
|
7873
|
+
* - Cloud metadata endpoints
|
|
7874
|
+
* - IPv6 private/link-local addresses
|
|
7875
|
+
*
|
|
7876
|
+
* @param location - Redirect location URL
|
|
7877
|
+
* @param originalProtocol - Protocol of the original request
|
|
7878
|
+
* @returns true if redirect is safe, false if it should be blocked
|
|
7879
|
+
*/
|
|
7880
|
+
isRedirectSafe(location, originalProtocol) {
|
|
7881
|
+
try {
|
|
7882
|
+
const redirectUrl = new URL2(location);
|
|
7883
|
+
if (redirectUrl.protocol === "http:" && originalProtocol === "https:") {
|
|
7884
|
+
return false;
|
|
7885
|
+
}
|
|
7886
|
+
const blockedPatterns = [
|
|
7887
|
+
"localhost",
|
|
7888
|
+
"127.0.0.1",
|
|
7889
|
+
"::1",
|
|
7890
|
+
"0.0.0.0",
|
|
7891
|
+
"169.254.169.254",
|
|
7892
|
+
// AWS/GCP/Azure metadata
|
|
7893
|
+
".metadata.google.internal",
|
|
7894
|
+
// GCP metadata
|
|
7895
|
+
"metadata.google.internal",
|
|
7896
|
+
"metadata.internal"
|
|
7897
|
+
];
|
|
7898
|
+
for (const pattern of blockedPatterns) {
|
|
7899
|
+
if (redirectUrl.hostname.includes(pattern)) {
|
|
7900
|
+
return false;
|
|
7901
|
+
}
|
|
7902
|
+
}
|
|
7903
|
+
const ipv4Match = redirectUrl.hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
7904
|
+
if (ipv4Match) {
|
|
7905
|
+
const octets = ipv4Match.slice(1, 5).map(Number);
|
|
7906
|
+
const [first, second] = octets;
|
|
7907
|
+
if (first === 10) {
|
|
7908
|
+
return false;
|
|
7909
|
+
}
|
|
7910
|
+
if (first === 172 && second >= 16 && second <= 31) {
|
|
7911
|
+
return false;
|
|
7912
|
+
}
|
|
7913
|
+
if (first === 192 && second === 168) {
|
|
7914
|
+
return false;
|
|
7915
|
+
}
|
|
7916
|
+
if (first === 127) {
|
|
7917
|
+
return false;
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
const ipVersion = isIP(redirectUrl.hostname);
|
|
7921
|
+
if (ipVersion === 6) {
|
|
7922
|
+
const blockedPrefixes = [
|
|
7923
|
+
"::1",
|
|
7924
|
+
// Loopback
|
|
7925
|
+
"fe80:",
|
|
7926
|
+
// Link-local
|
|
7927
|
+
"fc00:",
|
|
7928
|
+
// Unique local
|
|
7929
|
+
"fd",
|
|
7930
|
+
// Unique local (short form)
|
|
7931
|
+
"::ffff:"
|
|
7932
|
+
// IPv4-mapped
|
|
7933
|
+
];
|
|
7934
|
+
if (blockedPrefixes.some((p) => redirectUrl.hostname.startsWith(p))) {
|
|
7935
|
+
return false;
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
return true;
|
|
7939
|
+
} catch {
|
|
7940
|
+
return false;
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
/**
|
|
7944
|
+
* Atomically check if path is allowed and read content if so.
|
|
7945
|
+
* Prevents TOCTOU race conditions by combining existence check,
|
|
7946
|
+
* symlink resolution, path validation, and file read in a single operation.
|
|
7947
|
+
*
|
|
7948
|
+
* @param filePath - File path to check and read
|
|
7949
|
+
* @returns Object with content if successful, or error message
|
|
7950
|
+
*/
|
|
7951
|
+
readAllowedFile(filePath) {
|
|
7952
|
+
try {
|
|
7953
|
+
fs.lstatSync(filePath);
|
|
7954
|
+
const realPath = fs.realpathSync(filePath);
|
|
7955
|
+
if (!this.isPathAllowed(realPath)) {
|
|
7956
|
+
return { error: "File path outside allowed directories (path traversal blocked)" };
|
|
7957
|
+
}
|
|
7958
|
+
return { content: fs.readFileSync(filePath, "utf8") };
|
|
7959
|
+
} catch (e) {
|
|
7960
|
+
const err = e;
|
|
7961
|
+
if (err.code === "ENOENT") {
|
|
7962
|
+
return { error: "File not found" };
|
|
7963
|
+
}
|
|
7964
|
+
return { error: String(e) };
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
7967
|
+
/**
|
|
7968
|
+
* Check if security enforcement is enabled
|
|
7969
|
+
*/
|
|
7970
|
+
isEnabled() {
|
|
7971
|
+
return this.allowlist.length > 0;
|
|
7972
|
+
}
|
|
7973
|
+
/**
|
|
7974
|
+
* Get the allowlist count
|
|
7975
|
+
*/
|
|
7976
|
+
getAllowlistCount() {
|
|
7977
|
+
return this.allowlist.length;
|
|
7978
|
+
}
|
|
7979
|
+
/**
|
|
7980
|
+
* Validate that a spec is on the approved list and matches expected hash.
|
|
7981
|
+
*
|
|
7982
|
+
* @param specUrl - URL or file path to the spec to validate
|
|
7983
|
+
* @returns ValidationResult indicating if the spec is approved
|
|
7984
|
+
*/
|
|
7985
|
+
async validate(specUrl) {
|
|
7986
|
+
if (this.isPathTraversalAttempt(specUrl)) {
|
|
7987
|
+
return {
|
|
7988
|
+
valid: false,
|
|
7989
|
+
errorCode: "DOWNLOAD_FAILED",
|
|
7990
|
+
specUrl,
|
|
7991
|
+
error: "File path outside allowed directories (path traversal blocked)"
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7994
|
+
const approved = this.allowlist.find((a) => this.urlsMatch(a.url, specUrl));
|
|
7995
|
+
if (!approved) {
|
|
7996
|
+
return {
|
|
7997
|
+
valid: false,
|
|
7998
|
+
errorCode: "SPEC_NOT_ON_ALLOWLIST",
|
|
7999
|
+
specUrl,
|
|
8000
|
+
error: this.formatAllowlistError(specUrl)
|
|
8001
|
+
};
|
|
8002
|
+
}
|
|
8003
|
+
if (this.skipHashVerification) {
|
|
8004
|
+
return { valid: true, specUrl };
|
|
8005
|
+
}
|
|
8006
|
+
const hashResult = await this.getHash(specUrl);
|
|
8007
|
+
if (hashResult.error) {
|
|
8008
|
+
return {
|
|
8009
|
+
valid: false,
|
|
8010
|
+
errorCode: "DOWNLOAD_FAILED",
|
|
8011
|
+
specUrl,
|
|
8012
|
+
error: `Failed to fetch spec '${specUrl}': ${hashResult.error}`
|
|
8013
|
+
};
|
|
8014
|
+
}
|
|
8015
|
+
if (!this.isValidHex(hashResult.hash)) {
|
|
8016
|
+
return {
|
|
8017
|
+
valid: false,
|
|
8018
|
+
errorCode: "DOWNLOAD_FAILED",
|
|
8019
|
+
specUrl,
|
|
8020
|
+
error: "Invalid hash format returned from download"
|
|
8021
|
+
};
|
|
8022
|
+
}
|
|
8023
|
+
if (!this.isValidHex(approved.sha256)) {
|
|
8024
|
+
return {
|
|
8025
|
+
valid: false,
|
|
8026
|
+
errorCode: "DOWNLOAD_FAILED",
|
|
8027
|
+
specUrl,
|
|
8028
|
+
error: "Invalid hash format in approved spec configuration"
|
|
8029
|
+
};
|
|
8030
|
+
}
|
|
8031
|
+
if (!crypto.timingSafeEqual(
|
|
8032
|
+
Buffer.from(hashResult.hash, "hex"),
|
|
8033
|
+
Buffer.from(approved.sha256, "hex")
|
|
8034
|
+
)) {
|
|
8035
|
+
return {
|
|
8036
|
+
valid: false,
|
|
8037
|
+
errorCode: "SPEC_MODIFIED",
|
|
8038
|
+
specUrl,
|
|
8039
|
+
error: this.formatHashMismatchError(approved, hashResult.hash)
|
|
8040
|
+
};
|
|
8041
|
+
}
|
|
8042
|
+
return { valid: true, specUrl };
|
|
8043
|
+
}
|
|
8044
|
+
/**
|
|
8045
|
+
* Validate multiple specs at once (batch validation).
|
|
8046
|
+
* Fails fast on first error.
|
|
8047
|
+
*
|
|
8048
|
+
* @param specUrls - Array of URLs/paths to validate
|
|
8049
|
+
* @returns Map of URL to ValidationResult
|
|
8050
|
+
*/
|
|
8051
|
+
async validateAll(specUrls) {
|
|
8052
|
+
const results = /* @__PURE__ */ new Map();
|
|
8053
|
+
for (const url of specUrls) {
|
|
8054
|
+
const result = await this.validate(url);
|
|
8055
|
+
results.set(url, result);
|
|
8056
|
+
if (!result.valid) {
|
|
8057
|
+
return results;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
return results;
|
|
8061
|
+
}
|
|
8062
|
+
/**
|
|
8063
|
+
* Validate all specs and return all results (non-fail-fast).
|
|
8064
|
+
*
|
|
8065
|
+
* @param specUrls - Array of URLs/paths to validate
|
|
8066
|
+
* @returns Map of URL to ValidationResult
|
|
8067
|
+
*/
|
|
8068
|
+
async validateAllComplete(specUrls) {
|
|
8069
|
+
const results = /* @__PURE__ */ new Map();
|
|
8070
|
+
for (const url of specUrls) {
|
|
8071
|
+
const result = await this.validate(url);
|
|
8072
|
+
results.set(url, result);
|
|
8073
|
+
}
|
|
8074
|
+
return results;
|
|
8075
|
+
}
|
|
8076
|
+
/**
|
|
8077
|
+
* Get content hash from cache or download.
|
|
8078
|
+
*/
|
|
8079
|
+
async getHash(url) {
|
|
8080
|
+
const cached = this.cache.get(url);
|
|
8081
|
+
if (cached) {
|
|
8082
|
+
return { hash: cached.hash };
|
|
8083
|
+
}
|
|
8084
|
+
const contentResult = await this.download(url);
|
|
8085
|
+
if (contentResult.error) {
|
|
8086
|
+
return { error: contentResult.error };
|
|
8087
|
+
}
|
|
8088
|
+
const content = contentResult.content;
|
|
8089
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
8090
|
+
this.cache.set(url, { content, hash });
|
|
8091
|
+
return { hash };
|
|
8092
|
+
}
|
|
8093
|
+
/**
|
|
8094
|
+
* Download content from URL or file.
|
|
8095
|
+
* Includes path traversal and SSRF protection.
|
|
8096
|
+
*/
|
|
8097
|
+
async download(url, originalProtocol = "") {
|
|
8098
|
+
if (fs.existsSync(url)) {
|
|
8099
|
+
return this.readAllowedFile(url);
|
|
8100
|
+
}
|
|
8101
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
8102
|
+
return { error: `Invalid URL protocol. Expected http:// or https://, got: ${url}` };
|
|
8103
|
+
}
|
|
8104
|
+
const parsed = new URL2(url);
|
|
8105
|
+
const requestProtocol = originalProtocol || parsed.protocol;
|
|
8106
|
+
return new Promise((resolve) => {
|
|
8107
|
+
const client = url.startsWith("https") ? https : http;
|
|
8108
|
+
const req = client.get(url, { timeout: 3e4 }, (res) => {
|
|
8109
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
8110
|
+
const location = res.headers.location;
|
|
8111
|
+
if (!this.isRedirectSafe(location, requestProtocol)) {
|
|
8112
|
+
resolve({ error: `Unsafe redirect target blocked: ${location}` });
|
|
8113
|
+
return;
|
|
8114
|
+
}
|
|
8115
|
+
this.download(location, requestProtocol).then(resolve);
|
|
8116
|
+
return;
|
|
8117
|
+
}
|
|
8118
|
+
if (res.statusCode !== 200) {
|
|
8119
|
+
resolve({ error: `HTTP ${res.statusCode}` });
|
|
8120
|
+
return;
|
|
8121
|
+
}
|
|
8122
|
+
let data = "";
|
|
8123
|
+
let size = 0;
|
|
8124
|
+
res.on("data", (chunk) => {
|
|
8125
|
+
size += chunk.length;
|
|
8126
|
+
if (size > this.MAX_RESPONSE_SIZE) {
|
|
8127
|
+
req.destroy();
|
|
8128
|
+
resolve({ error: `Response too large (>${this.MAX_RESPONSE_SIZE / 1024 / 1024}MB)` });
|
|
8129
|
+
return;
|
|
8130
|
+
}
|
|
8131
|
+
data += chunk;
|
|
8132
|
+
});
|
|
8133
|
+
res.on("end", () => resolve({ content: data }));
|
|
8134
|
+
res.on("error", (e) => resolve({ error: String(e) }));
|
|
8135
|
+
});
|
|
8136
|
+
req.on("error", (e) => resolve({ error: String(e) }));
|
|
8137
|
+
req.on("timeout", () => {
|
|
8138
|
+
req.destroy();
|
|
8139
|
+
resolve({ error: "Request timeout (30s)" });
|
|
8140
|
+
});
|
|
8141
|
+
});
|
|
8142
|
+
}
|
|
8143
|
+
/**
|
|
8144
|
+
* Check if two URLs match (handles trailing slashes, case sensitivity, etc.).
|
|
8145
|
+
* Strips credentials and hash to prevent bypass via @ symbol.
|
|
8146
|
+
*/
|
|
8147
|
+
urlsMatch(url1, url2) {
|
|
8148
|
+
const normalize = (u) => {
|
|
8149
|
+
try {
|
|
8150
|
+
const parsed = new URL2(u);
|
|
8151
|
+
const normalized = `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`;
|
|
8152
|
+
return normalized.replace(/\/$/, "").toLowerCase();
|
|
8153
|
+
} catch {
|
|
8154
|
+
return u.replace(/\/$/, "").replace(/\\/g, "/").toLowerCase();
|
|
8155
|
+
}
|
|
8156
|
+
};
|
|
8157
|
+
return normalize(url1) === normalize(url2);
|
|
8158
|
+
}
|
|
8159
|
+
/**
|
|
8160
|
+
* Format error message for spec not on allowlist
|
|
8161
|
+
*/
|
|
8162
|
+
formatAllowlistError(specUrl) {
|
|
8163
|
+
const lines = [`Spec '${specUrl}' is not on the approved list.`];
|
|
8164
|
+
if (this.allowlist.length === 0) {
|
|
8165
|
+
lines.push("");
|
|
8166
|
+
lines.push("No approved specs are configured.");
|
|
8167
|
+
lines.push("Add approved specs to stackwright.yml under prebuild.security.allowlist");
|
|
8168
|
+
} else {
|
|
8169
|
+
lines.push("");
|
|
8170
|
+
lines.push("Allowed specs:");
|
|
8171
|
+
for (const spec of this.allowlist) {
|
|
8172
|
+
lines.push(` \u2022 ${spec.name}`);
|
|
8173
|
+
lines.push(` ${spec.url}`);
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8176
|
+
return lines.join("\n");
|
|
8177
|
+
}
|
|
8178
|
+
/**
|
|
8179
|
+
* Format error message for hash mismatch
|
|
8180
|
+
*/
|
|
8181
|
+
formatHashMismatchError(approved, actualHash) {
|
|
8182
|
+
const lines = [
|
|
8183
|
+
`Spec '${approved.url}' has been modified since approval.`,
|
|
8184
|
+
"",
|
|
8185
|
+
"Expected hash (approved): " + approved.sha256,
|
|
8186
|
+
"Actual hash (current): " + actualHash,
|
|
8187
|
+
"",
|
|
8188
|
+
"This may indicate:",
|
|
8189
|
+
" \u2022 The spec has been updated on the server",
|
|
8190
|
+
" \u2022 Network corruption during download",
|
|
8191
|
+
" \u2022 A man-in-the-middle attack",
|
|
8192
|
+
"",
|
|
8193
|
+
"If this is expected, update the sha256 in stackwright.yml."
|
|
8194
|
+
];
|
|
8195
|
+
return lines.join("\n");
|
|
8196
|
+
}
|
|
8197
|
+
/**
|
|
8198
|
+
* Clear the download cache.
|
|
8199
|
+
* Useful for long-running processes.
|
|
8200
|
+
*/
|
|
8201
|
+
clearCache() {
|
|
8202
|
+
this.cache.clear();
|
|
8203
|
+
}
|
|
8204
|
+
};
|
|
8205
|
+
|
|
8206
|
+
// src/prebuild/OpenAPIPlugin.ts
|
|
7638
8207
|
var OpenAPIPlugin = class {
|
|
7639
8208
|
name = "@stackwright-pro/openapi";
|
|
7640
8209
|
async beforeBuild(context) {
|
|
7641
8210
|
const { siteConfig, projectRoot } = context;
|
|
8211
|
+
const prebuildConfig = siteConfig.prebuild || {};
|
|
8212
|
+
const securityConfig = prebuildConfig.security;
|
|
8213
|
+
let validator = null;
|
|
8214
|
+
if (securityConfig?.enabled) {
|
|
8215
|
+
const skipApprovedSpecs = process.argv.includes("--skip-approved-specs");
|
|
8216
|
+
if (skipApprovedSpecs) {
|
|
8217
|
+
const isProductionBuild = process.env.NODE_ENV === "production" || process.env.CI === "true";
|
|
8218
|
+
if (isProductionBuild) {
|
|
8219
|
+
console.error("\n\u274C FATAL: --skip-approved-specs not allowed in production/CI builds\n");
|
|
8220
|
+
throw new Error(
|
|
8221
|
+
"SECURITY_CONFIG_VIOLATION: Cannot use --skip-approved-specs in production or CI environments"
|
|
8222
|
+
);
|
|
8223
|
+
}
|
|
8224
|
+
console.log(
|
|
8225
|
+
"\n[@stackwright-pro/openapi] \u26A0\uFE0F Approved-specs enforcement DISABLED (--skip-approved-specs)\n"
|
|
8226
|
+
);
|
|
8227
|
+
console.log(" This is ONLY allowed in development mode.\n");
|
|
8228
|
+
} else {
|
|
8229
|
+
validator = new ApprovedSpecsValidator(securityConfig);
|
|
8230
|
+
console.log("\n[@stackwright-pro/openapi] \u{1F512} Approved-specs enforcement ENABLED");
|
|
8231
|
+
console.log(` Approved specs: ${validator.getAllowlistCount()}`);
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
7642
8234
|
const integrations = siteConfig.integrations;
|
|
7643
8235
|
if (!integrations || !Array.isArray(integrations)) {
|
|
7644
8236
|
return;
|
|
@@ -7649,6 +8241,17 @@ var OpenAPIPlugin = class {
|
|
|
7649
8241
|
if (openAPIIntegrations.length === 0) {
|
|
7650
8242
|
return;
|
|
7651
8243
|
}
|
|
8244
|
+
if (validator) {
|
|
8245
|
+
const specUrls = openAPIIntegrations.map((i) => i.spec);
|
|
8246
|
+
const results = await validator.validateAll(specUrls);
|
|
8247
|
+
for (const [url, result] of results) {
|
|
8248
|
+
if (!result.valid) {
|
|
8249
|
+
this.printSecurityRejection(result);
|
|
8250
|
+
throw new Error(`SPEC_NOT_APPROVED: ${result.error}`);
|
|
8251
|
+
}
|
|
8252
|
+
console.log(` \u2713 Approved: ${url}`);
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
7652
8255
|
console.log(
|
|
7653
8256
|
`
|
|
7654
8257
|
[@stackwright-pro/openapi] Processing ${openAPIIntegrations.length} integration(s)...`
|
|
@@ -7658,23 +8261,74 @@ var OpenAPIPlugin = class {
|
|
|
7658
8261
|
}
|
|
7659
8262
|
console.log("[@stackwright-pro/openapi] Generation complete\n");
|
|
7660
8263
|
}
|
|
8264
|
+
/**
|
|
8265
|
+
* Print a security rejection error in a formatted box
|
|
8266
|
+
*/
|
|
8267
|
+
printSecurityRejection(result) {
|
|
8268
|
+
const lines = [];
|
|
8269
|
+
lines.push(
|
|
8270
|
+
"\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"
|
|
8271
|
+
);
|
|
8272
|
+
lines.push(
|
|
8273
|
+
"\u2551 \u{1F512} SECURITY REJECTED \u2551"
|
|
8274
|
+
);
|
|
8275
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
8276
|
+
lines.push(`\u2551 Error: ${(result.errorCode || "UNKNOWN").padEnd(73)} \u2551`);
|
|
8277
|
+
lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
8278
|
+
const errorLines = (result.error || "Unknown error").split("\n");
|
|
8279
|
+
const maxLen = 76;
|
|
8280
|
+
for (const line of errorLines) {
|
|
8281
|
+
if (line.length <= maxLen) {
|
|
8282
|
+
lines.push(`\u2551 ${line.padEnd(maxLen)} \u2551`);
|
|
8283
|
+
} else {
|
|
8284
|
+
let remaining = line;
|
|
8285
|
+
while (remaining.length > 0) {
|
|
8286
|
+
lines.push(`\u2551 ${remaining.substring(0, maxLen).padEnd(maxLen)} \u2551`);
|
|
8287
|
+
remaining = remaining.substring(maxLen);
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
}
|
|
8291
|
+
lines.push(
|
|
8292
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n"
|
|
8293
|
+
);
|
|
8294
|
+
for (const line of lines) {
|
|
8295
|
+
console.error(line);
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
7661
8298
|
async processIntegration(config, projectRoot) {
|
|
7662
|
-
const { name, spec, auth, collections } = config;
|
|
8299
|
+
const { name, spec, auth, mockUrl, collections, endpoints } = config;
|
|
7663
8300
|
console.log(` - Processing integration: ${name}`);
|
|
7664
|
-
const specPath = spec.startsWith("http") ? spec :
|
|
8301
|
+
const specPath = spec.startsWith("http") ? spec : path2.resolve(projectRoot, spec);
|
|
7665
8302
|
const parser = new OpenAPIParser();
|
|
7666
8303
|
const { document } = await parser.parse(specPath);
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
8304
|
+
if (mockUrl) {
|
|
8305
|
+
const originalServer = document.servers?.[0]?.url || "unknown";
|
|
8306
|
+
document.servers = [{ url: mockUrl }];
|
|
8307
|
+
console.log(` > Using mock URL: ${mockUrl} (was: ${originalServer})`);
|
|
8308
|
+
}
|
|
8309
|
+
const endpointFilter = new EndpointFilter(endpoints);
|
|
8310
|
+
if (endpoints && (endpoints.include?.length || endpoints.exclude?.length)) {
|
|
8311
|
+
const includeStr = endpoints.include?.join(", ") || "/**";
|
|
8312
|
+
const excludeStr = endpoints.exclude?.length ? ` (exclude: ${endpoints.exclude.join(", ")})` : "";
|
|
8313
|
+
console.log(` > Filter: include [${includeStr}]${excludeStr}`);
|
|
8314
|
+
}
|
|
8315
|
+
const outputDir = path2.join(projectRoot, "src", "generated", name);
|
|
8316
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
8317
|
+
const schemaMapping = await this.generateSchemas(
|
|
8318
|
+
document,
|
|
8319
|
+
collections || [],
|
|
8320
|
+
outputDir,
|
|
8321
|
+
name,
|
|
8322
|
+
endpointFilter
|
|
8323
|
+
);
|
|
7670
8324
|
await this.generateTypes(document, collections || [], outputDir, name);
|
|
7671
8325
|
if (collections && collections.length > 0) {
|
|
7672
8326
|
await this.generateProvider(document, config, outputDir, name);
|
|
7673
8327
|
}
|
|
7674
|
-
await this.generateClient(document, outputDir, name, schemaMapping);
|
|
7675
|
-
console.log(`
|
|
8328
|
+
await this.generateClient(document, outputDir, name, schemaMapping, endpointFilter);
|
|
8329
|
+
console.log(` > Generated code in src/generated/${name}/`);
|
|
7676
8330
|
}
|
|
7677
|
-
async generateSchemas(document, collections, outputDir, integrationName) {
|
|
8331
|
+
async generateSchemas(document, collections, outputDir, integrationName, endpointFilter) {
|
|
7678
8332
|
const resolver = new SchemaResolver(document);
|
|
7679
8333
|
const zodGenerator = new ZodSchemaGenerator();
|
|
7680
8334
|
let schemasCode = `/**
|
|
@@ -7694,14 +8348,14 @@ import { z } from 'zod';
|
|
|
7694
8348
|
const method = "get";
|
|
7695
8349
|
const schema = resolver.getResponseSchema(endpoint, method);
|
|
7696
8350
|
if (!schema) {
|
|
7697
|
-
console.warn(`
|
|
8351
|
+
console.warn(` > No schema found for ${method.toUpperCase()} ${endpoint}`);
|
|
7698
8352
|
continue;
|
|
7699
8353
|
}
|
|
7700
8354
|
const collectionName = this.sanitizeName(endpoint);
|
|
7701
8355
|
const schemaName = `${this.capitalize(collectionName)}Schema`;
|
|
7702
8356
|
const isArray = schema.type === "array";
|
|
7703
8357
|
const itemSchema = isArray ? schema.items : schema;
|
|
7704
|
-
const zodCode = zodGenerator.generate(itemSchema, { schemaName });
|
|
8358
|
+
const zodCode = zodGenerator.generate(itemSchema, { schemaName, bare: true });
|
|
7705
8359
|
schemasCode += zodCode + "\n";
|
|
7706
8360
|
if (isArray) {
|
|
7707
8361
|
schemasCode += `export const ${this.capitalize(collectionName)}ArraySchema = z.array(${schemaName});
|
|
@@ -7710,22 +8364,27 @@ import { z } from 'zod';
|
|
|
7710
8364
|
}
|
|
7711
8365
|
}
|
|
7712
8366
|
}
|
|
7713
|
-
schemasCode += "\n// Response schemas for
|
|
8367
|
+
schemasCode += "\n// Response schemas for filtered endpoints\n";
|
|
7714
8368
|
const generatedSchemas = /* @__PURE__ */ new Set();
|
|
8369
|
+
let filteredCount = 0;
|
|
7715
8370
|
const paths = document.paths || {};
|
|
7716
|
-
for (const [
|
|
8371
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
8372
|
+
if (!endpointFilter.matches(pathStr)) {
|
|
8373
|
+
filteredCount++;
|
|
8374
|
+
continue;
|
|
8375
|
+
}
|
|
7717
8376
|
const methods = ["get", "post", "put", "patch", "delete"];
|
|
7718
8377
|
for (const method of methods) {
|
|
7719
8378
|
const operation = pathItem[method];
|
|
7720
8379
|
if (!operation) continue;
|
|
7721
|
-
const opId = operation.operationId || this.generateOperationId(
|
|
8380
|
+
const opId = operation.operationId || this.generateOperationId(pathStr, method);
|
|
7722
8381
|
const responseSchemaName = `${this.getOperationTypeName(opId)}ResponseSchema`;
|
|
7723
8382
|
if (generatedSchemas.has(responseSchemaName)) {
|
|
7724
8383
|
continue;
|
|
7725
8384
|
}
|
|
7726
|
-
const responseSchema = resolver.getResponseSchema(
|
|
8385
|
+
const responseSchema = resolver.getResponseSchema(pathStr, method);
|
|
7727
8386
|
if (!responseSchema) {
|
|
7728
|
-
console.warn(`
|
|
8387
|
+
console.warn(` > No response schema for ${method.toUpperCase()} ${pathStr}`);
|
|
7729
8388
|
continue;
|
|
7730
8389
|
}
|
|
7731
8390
|
const isArray = responseSchema.type === "array";
|
|
@@ -7740,32 +8399,44 @@ import { z } from 'zod';
|
|
|
7740
8399
|
`;
|
|
7741
8400
|
generatedSchemas.add(responseSchemaName);
|
|
7742
8401
|
} else if (isArray) {
|
|
7743
|
-
const zodCode = zodGenerator.generate(responseSchema, {
|
|
8402
|
+
const zodCode = zodGenerator.generate(responseSchema, {
|
|
8403
|
+
schemaName: responseSchemaName,
|
|
8404
|
+
bare: true
|
|
8405
|
+
});
|
|
7744
8406
|
schemasCode += zodCode + "\n";
|
|
7745
8407
|
generatedSchemas.add(responseSchemaName);
|
|
7746
8408
|
} else {
|
|
7747
|
-
const zodCode = zodGenerator.generate(responseSchema, {
|
|
8409
|
+
const zodCode = zodGenerator.generate(responseSchema, {
|
|
8410
|
+
schemaName: responseSchemaName,
|
|
8411
|
+
bare: true
|
|
8412
|
+
});
|
|
7748
8413
|
schemasCode += zodCode + "\n";
|
|
7749
8414
|
generatedSchemas.add(responseSchemaName);
|
|
7750
8415
|
}
|
|
7751
8416
|
}
|
|
7752
8417
|
}
|
|
8418
|
+
if (filteredCount > 0) {
|
|
8419
|
+
console.log(` > Skipped ${filteredCount} paths (filtered)`);
|
|
8420
|
+
}
|
|
7753
8421
|
const schemaMapping = {};
|
|
7754
|
-
for (const [
|
|
8422
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
8423
|
+
if (!endpointFilter.matches(pathStr)) {
|
|
8424
|
+
continue;
|
|
8425
|
+
}
|
|
7755
8426
|
const methods = ["get", "post", "put", "patch", "delete"];
|
|
7756
8427
|
for (const method of methods) {
|
|
7757
8428
|
const operation = pathItem[method];
|
|
7758
8429
|
if (!operation) continue;
|
|
7759
|
-
const opId = operation.operationId || this.generateOperationId(
|
|
8430
|
+
const opId = operation.operationId || this.generateOperationId(pathStr, method);
|
|
7760
8431
|
const responseSchemaName = `${this.getOperationTypeName(opId)}ResponseSchema`;
|
|
7761
8432
|
schemaMapping[opId] = {
|
|
7762
8433
|
requestSchema: null,
|
|
7763
|
-
//
|
|
8434
|
+
// ClientGenerator handles request schema generation + type inference
|
|
7764
8435
|
responseSchema: responseSchemaName
|
|
7765
8436
|
};
|
|
7766
8437
|
}
|
|
7767
8438
|
}
|
|
7768
|
-
|
|
8439
|
+
fs2.writeFileSync(path2.join(outputDir, "schemas.ts"), schemasCode);
|
|
7769
8440
|
return schemaMapping;
|
|
7770
8441
|
}
|
|
7771
8442
|
async generateTypes(document, collections, outputDir, integrationName) {
|
|
@@ -7790,7 +8461,7 @@ import type * as schemas from './schemas';
|
|
|
7790
8461
|
`;
|
|
7791
8462
|
}
|
|
7792
8463
|
}
|
|
7793
|
-
|
|
8464
|
+
fs2.writeFileSync(path2.join(outputDir, "types.ts"), typesCode);
|
|
7794
8465
|
}
|
|
7795
8466
|
async generateProvider(document, config, outputDir, integrationName) {
|
|
7796
8467
|
const generator = new CollectionProviderGenerator(document);
|
|
@@ -7798,22 +8469,15 @@ import type * as schemas from './schemas';
|
|
|
7798
8469
|
if (!collections || collections.length === 0) {
|
|
7799
8470
|
return;
|
|
7800
8471
|
}
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
* Integration: ${integrationName}
|
|
7804
|
-
*
|
|
7805
|
-
* DO NOT EDIT - This file is auto-generated by @stackwright-pro/openapi
|
|
7806
|
-
* Regenerate by running: pnpm prebuild
|
|
7807
|
-
*/
|
|
7808
|
-
|
|
7809
|
-
`;
|
|
8472
|
+
const schemaImports = /* @__PURE__ */ new Set();
|
|
8473
|
+
const classBlocks = [];
|
|
7810
8474
|
for (const collection of collections) {
|
|
7811
8475
|
const collectionConfig = {
|
|
7812
8476
|
endpoint: collection.endpoint,
|
|
7813
8477
|
slugField: collection.slug_field,
|
|
7814
8478
|
filters: collection.filters
|
|
7815
8479
|
};
|
|
7816
|
-
let providerOptions = {};
|
|
8480
|
+
let providerOptions = { bare: true };
|
|
7817
8481
|
if (auth) {
|
|
7818
8482
|
if (auth.type === "bearer" || auth.type === "apiKey") {
|
|
7819
8483
|
providerOptions.auth = {
|
|
@@ -7822,16 +8486,50 @@ import type * as schemas from './schemas';
|
|
|
7822
8486
|
};
|
|
7823
8487
|
} else if (auth.type === "oauth2") {
|
|
7824
8488
|
console.warn(
|
|
7825
|
-
`
|
|
8489
|
+
` > OAuth2 auth not yet supported for ${collection.endpoint} (coming soon)`
|
|
7826
8490
|
);
|
|
7827
8491
|
}
|
|
7828
8492
|
}
|
|
7829
8493
|
const code = generator.generate(collectionConfig, providerOptions);
|
|
7830
|
-
|
|
8494
|
+
classBlocks.push(code);
|
|
8495
|
+
const collectionName = this.sanitizeName(collection.endpoint);
|
|
8496
|
+
const resolver = new SchemaResolver(document);
|
|
8497
|
+
const schema = resolver.getResponseSchema(collection.endpoint, "get");
|
|
8498
|
+
const isArray = schema?.type === "array";
|
|
8499
|
+
if (isArray) {
|
|
8500
|
+
schemaImports.add(`${this.capitalize(collectionName)}ArraySchema`);
|
|
8501
|
+
} else {
|
|
8502
|
+
schemaImports.add(`${this.capitalize(collectionName)}Schema`);
|
|
8503
|
+
}
|
|
7831
8504
|
}
|
|
7832
|
-
|
|
8505
|
+
let providerCode = `/**
|
|
8506
|
+
* Generated CollectionProvider from OpenAPI spec
|
|
8507
|
+
* Integration: ${integrationName}
|
|
8508
|
+
*
|
|
8509
|
+
* DO NOT EDIT - This file is auto-generated by @stackwright-pro/openapi
|
|
8510
|
+
* Regenerate by running: pnpm prebuild
|
|
8511
|
+
*/
|
|
8512
|
+
|
|
8513
|
+
import type { CollectionProvider, CollectionItem } from '@stackwright/collections';
|
|
8514
|
+
import { ${Array.from(schemaImports).join(", ")} } from './schemas';
|
|
8515
|
+
|
|
8516
|
+
`;
|
|
8517
|
+
providerCode += classBlocks.join("\n");
|
|
8518
|
+
fs2.writeFileSync(path2.join(outputDir, "provider.ts"), providerCode);
|
|
7833
8519
|
}
|
|
7834
|
-
async generateClient(document, outputDir, integrationName, schemaMapping) {
|
|
8520
|
+
async generateClient(document, outputDir, integrationName, schemaMapping, endpointFilter) {
|
|
8521
|
+
const paths = document.paths || {};
|
|
8522
|
+
let filteredEndpoints = 0;
|
|
8523
|
+
for (const pathStr of Object.keys(paths)) {
|
|
8524
|
+
if (!endpointFilter.matches(pathStr)) {
|
|
8525
|
+
filteredEndpoints++;
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
if (filteredEndpoints > 0) {
|
|
8529
|
+
console.log(
|
|
8530
|
+
` > Generating client for ${Object.keys(paths).length - filteredEndpoints} endpoints (${filteredEndpoints} filtered)`
|
|
8531
|
+
);
|
|
8532
|
+
}
|
|
7835
8533
|
const generator = new ClientGenerator(document);
|
|
7836
8534
|
const className = `${this.capitalize(integrationName)}Client`;
|
|
7837
8535
|
const clientCode = generator.generate({
|
|
@@ -7841,7 +8539,7 @@ import type * as schemas from './schemas';
|
|
|
7841
8539
|
validateResponses: true,
|
|
7842
8540
|
strictValidation: false
|
|
7843
8541
|
});
|
|
7844
|
-
|
|
8542
|
+
fs2.writeFileSync(path2.join(outputDir, "client.ts"), clientCode);
|
|
7845
8543
|
}
|
|
7846
8544
|
extractComponentName(ref) {
|
|
7847
8545
|
const parts = ref.split("/");
|
|
@@ -7850,20 +8548,20 @@ import type * as schemas from './schemas';
|
|
|
7850
8548
|
getOperationTypeName(operationId) {
|
|
7851
8549
|
return operationId.charAt(0).toUpperCase() + operationId.slice(1);
|
|
7852
8550
|
}
|
|
7853
|
-
generateOperationId(
|
|
7854
|
-
const sanitized = this.sanitizeName(
|
|
8551
|
+
generateOperationId(pathStr, method) {
|
|
8552
|
+
const sanitized = this.sanitizeName(pathStr);
|
|
7855
8553
|
return `${method}${this.capitalize(sanitized)}`;
|
|
7856
8554
|
}
|
|
7857
|
-
getResponseSchemaName(
|
|
7858
|
-
const sanitized = this.sanitizeName(
|
|
8555
|
+
getResponseSchemaName(pathStr, method) {
|
|
8556
|
+
const sanitized = this.sanitizeName(pathStr);
|
|
7859
8557
|
const baseName = this.capitalize(sanitized);
|
|
7860
|
-
if (method === "get" && !
|
|
8558
|
+
if (method === "get" && !pathStr.includes("{")) {
|
|
7861
8559
|
return `${baseName}ArraySchema`;
|
|
7862
8560
|
}
|
|
7863
8561
|
return `${baseName}Schema`;
|
|
7864
8562
|
}
|
|
7865
|
-
sanitizeName(
|
|
7866
|
-
return
|
|
8563
|
+
sanitizeName(pathStr) {
|
|
8564
|
+
return pathStr.replace(/^\//, "").replace(/\//g, "-").replace(/[{}:]/g, "").replace(/-+/g, "-");
|
|
7867
8565
|
}
|
|
7868
8566
|
capitalize(str) {
|
|
7869
8567
|
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
@@ -7872,14 +8570,137 @@ import type * as schemas from './schemas';
|
|
|
7872
8570
|
function createOpenAPIPlugin() {
|
|
7873
8571
|
return new OpenAPIPlugin();
|
|
7874
8572
|
}
|
|
8573
|
+
|
|
8574
|
+
// src/sources/openapi.ts
|
|
8575
|
+
var BLOCKED_HOST_PATTERNS = [
|
|
8576
|
+
/^localhost$/i,
|
|
8577
|
+
/^127\./,
|
|
8578
|
+
// Loopback (127.0.0.0/8)
|
|
8579
|
+
/^10\./,
|
|
8580
|
+
// Class A private (10.0.0.0/8)
|
|
8581
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
8582
|
+
// Class B private (172.16.0.0/12)
|
|
8583
|
+
/^192\.168\./,
|
|
8584
|
+
// Class C private (192.168.0.0/16)
|
|
8585
|
+
/^169\.254\./,
|
|
8586
|
+
// Link-local (169.254.0.0/16) - AWS/GCP metadata
|
|
8587
|
+
/^0\./,
|
|
8588
|
+
// Current network (0.0.0.0/8)
|
|
8589
|
+
/^::1$/,
|
|
8590
|
+
// IPv6 loopback (no brackets)
|
|
8591
|
+
/^\[::1\]$/i,
|
|
8592
|
+
// IPv6 loopback (with brackets)
|
|
8593
|
+
/^[fF][cCdD][0-9a-fA-F]{2}:/,
|
|
8594
|
+
// IPv6 private (fc00::/7)
|
|
8595
|
+
/^169\.254\.169\.254$/i,
|
|
8596
|
+
// AWS metadata endpoint
|
|
8597
|
+
/^metadata\.googleapis\.com$/i,
|
|
8598
|
+
// GCP metadata endpoint
|
|
8599
|
+
/^100\.100\.100\.200$/
|
|
8600
|
+
// Alibaba Cloud metadata
|
|
8601
|
+
];
|
|
8602
|
+
var ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
8603
|
+
function validateFetchUrl(baseUrl) {
|
|
8604
|
+
let url;
|
|
8605
|
+
try {
|
|
8606
|
+
url = new URL(baseUrl);
|
|
8607
|
+
} catch {
|
|
8608
|
+
throw new Error("SSRF Prevention: baseUrl must be a valid absolute URL");
|
|
8609
|
+
}
|
|
8610
|
+
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
|
|
8611
|
+
throw new Error(
|
|
8612
|
+
"SSRF Prevention: Only HTTP and HTTPS protocols are allowed, got " + url.protocol
|
|
8613
|
+
);
|
|
8614
|
+
}
|
|
8615
|
+
const hostname = url.hostname.toLowerCase();
|
|
8616
|
+
for (const pattern of BLOCKED_HOST_PATTERNS) {
|
|
8617
|
+
if (pattern.test(hostname)) {
|
|
8618
|
+
throw new Error("SSRF Prevention: Blocked internal network address: " + hostname);
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
const ip = url.hostname;
|
|
8622
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) {
|
|
8623
|
+
const octets = ip.split(".").map(Number);
|
|
8624
|
+
if (octets[0] === 127) {
|
|
8625
|
+
throw new Error("SSRF Prevention: Blocked loopback address: " + ip);
|
|
8626
|
+
}
|
|
8627
|
+
if (octets[0] === 10) {
|
|
8628
|
+
throw new Error("SSRF Prevention: Blocked private address: " + ip);
|
|
8629
|
+
}
|
|
8630
|
+
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
|
|
8631
|
+
throw new Error("SSRF Prevention: Blocked private address: " + ip);
|
|
8632
|
+
}
|
|
8633
|
+
if (octets[0] === 192 && octets[1] === 168) {
|
|
8634
|
+
throw new Error("SSRF Prevention: Blocked private address: " + ip);
|
|
8635
|
+
}
|
|
8636
|
+
if (octets[0] === 169 && octets[1] === 254) {
|
|
8637
|
+
throw new Error("SSRF Prevention: Blocked link-local address: " + ip);
|
|
8638
|
+
}
|
|
8639
|
+
if (octets[0] === 0) {
|
|
8640
|
+
throw new Error("SSRF Prevention: Blocked current network address: " + ip);
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
return url;
|
|
8644
|
+
}
|
|
8645
|
+
function validateEndpoint(endpoint) {
|
|
8646
|
+
if (endpoint.includes("..")) {
|
|
8647
|
+
throw new Error("SSRF Prevention: Path traversal detected in endpoint");
|
|
8648
|
+
}
|
|
8649
|
+
if (endpoint.includes("\\")) {
|
|
8650
|
+
throw new Error("SSRF Prevention: Backslash detected in endpoint");
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
function createOpenAPIFetcher(config) {
|
|
8654
|
+
const { baseUrl, endpoint, method = "get", params, body, headers = {}, auth, schema } = config;
|
|
8655
|
+
const validatedBaseUrl = validateFetchUrl(baseUrl);
|
|
8656
|
+
validateEndpoint(endpoint);
|
|
8657
|
+
return async () => {
|
|
8658
|
+
const url = new URL(endpoint, validatedBaseUrl.toString());
|
|
8659
|
+
if (params) {
|
|
8660
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
8661
|
+
url.searchParams.append(key, String(value));
|
|
8662
|
+
});
|
|
8663
|
+
}
|
|
8664
|
+
const requestHeaders = {
|
|
8665
|
+
"Content-Type": "application/json",
|
|
8666
|
+
Accept: "application/json",
|
|
8667
|
+
...headers
|
|
8668
|
+
};
|
|
8669
|
+
if (auth) {
|
|
8670
|
+
if (auth.type === "bearer" && auth.token) {
|
|
8671
|
+
requestHeaders["Authorization"] = `Bearer ${auth.token}`;
|
|
8672
|
+
} else if (auth.type === "apiKey" && auth.apiKey) {
|
|
8673
|
+
requestHeaders[auth.headerName ?? "X-API-Key"] = auth.apiKey;
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
const supportsBody = ["post", "put", "patch", "delete"].includes(method);
|
|
8677
|
+
const response = await fetch(url.toString(), {
|
|
8678
|
+
method: method.toUpperCase(),
|
|
8679
|
+
headers: requestHeaders,
|
|
8680
|
+
body: supportsBody && body ? JSON.stringify(body) : void 0
|
|
8681
|
+
});
|
|
8682
|
+
if (!response.ok) {
|
|
8683
|
+
const safeStatus = response.status;
|
|
8684
|
+
throw new Error("API error: " + safeStatus);
|
|
8685
|
+
}
|
|
8686
|
+
const result = await response.json();
|
|
8687
|
+
if (schema) {
|
|
8688
|
+
return schema.parse(result);
|
|
8689
|
+
}
|
|
8690
|
+
return result;
|
|
8691
|
+
};
|
|
8692
|
+
}
|
|
7875
8693
|
export {
|
|
8694
|
+
ApprovedSpecsValidator,
|
|
7876
8695
|
ClientGenerator,
|
|
7877
8696
|
CollectionProviderGenerator,
|
|
8697
|
+
EndpointFilter,
|
|
7878
8698
|
OpenAPICollectionProvider,
|
|
7879
8699
|
OpenAPIParser,
|
|
7880
8700
|
OpenAPIPlugin,
|
|
7881
8701
|
SchemaResolver,
|
|
7882
8702
|
TypeGenerator,
|
|
7883
8703
|
ZodSchemaGenerator,
|
|
8704
|
+
createOpenAPIFetcher,
|
|
7884
8705
|
createOpenAPIPlugin
|
|
7885
8706
|
};
|