@mintlify/common 1.0.1131 → 1.0.1133
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/openapi/fetchYamlDocument.d.ts +2 -0
- package/dist/openapi/fetchYamlDocument.js +23 -0
- package/dist/openapi/getOpenApiDocumentFromUrl.d.ts +9 -1
- package/dist/openapi/getOpenApiDocumentFromUrl.js +12 -12
- package/dist/openapi/index.d.ts +4 -0
- package/dist/openapi/index.js +4 -0
- package/dist/openapi/overlay/applyOverlay.d.ts +27 -0
- package/dist/openapi/overlay/applyOverlay.js +360 -0
- package/dist/openapi/overlay/errors.d.ts +18 -0
- package/dist/openapi/overlay/errors.js +14 -0
- package/dist/openapi/overlay/jsonValue.d.ts +19 -0
- package/dist/openapi/overlay/jsonValue.js +64 -0
- package/dist/openapi/overlay/mergeValue.d.ts +9 -0
- package/dist/openapi/overlay/mergeValue.js +43 -0
- package/dist/openapi/overlay/overlayRegistry.d.ts +47 -0
- package/dist/openapi/overlay/overlayRegistry.js +107 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
import { exponentialBackoff } from '../exponentialBackoff.js';
|
|
12
|
+
/** Fetches and parses a JSON or YAML document from a URL with exponential backoff. */
|
|
13
|
+
export const fetchYamlDocument = (url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
14
|
+
const response = yield exponentialBackoff(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
15
|
+
const res = yield fetch(url);
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
return res;
|
|
20
|
+
}));
|
|
21
|
+
const data = yield response.text();
|
|
22
|
+
return yaml.load(data);
|
|
23
|
+
});
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { OpenAPI } from 'openapi-types';
|
|
2
|
-
|
|
2
|
+
import { type LoadedOverlay } from './overlay/applyOverlay.js';
|
|
3
|
+
export declare const getOpenApiDocumentFromUrl: (url: string, options?: {
|
|
4
|
+
overlays?: readonly LoadedOverlay[];
|
|
5
|
+
/**
|
|
6
|
+
* Overrides fetching and parsing of the spec URL. Hosted environments substitute an
|
|
7
|
+
* SSRF-safe fetch that blocks private, loopback, and metadata targets across redirects.
|
|
8
|
+
*/
|
|
9
|
+
fetchRemoteDocument?: (url: string) => Promise<unknown>;
|
|
10
|
+
}) => Promise<OpenAPI.Document>;
|
|
@@ -7,19 +7,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
10
|
+
import { fetchYamlDocument } from './fetchYamlDocument.js';
|
|
11
|
+
import { applyOverlays } from './overlay/applyOverlay.js';
|
|
12
12
|
import { validate } from './validate.js';
|
|
13
|
-
export const getOpenApiDocumentFromUrl = (
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
export const getOpenApiDocumentFromUrl = (url_1, ...args_1) => __awaiter(void 0, [url_1, ...args_1], void 0, function* (url, options = {}) {
|
|
14
|
+
const fetched = options.fetchRemoteDocument
|
|
15
|
+
? yield options.fetchRemoteDocument(url)
|
|
16
|
+
: yield fetchYamlDocument(url);
|
|
17
|
+
if (fetched == undefined) {
|
|
18
|
+
throw Error('Could not parse OpenAPI document.');
|
|
19
|
+
}
|
|
20
|
+
const openApiDocument = options.overlays && options.overlays.length > 0
|
|
21
|
+
? applyOverlays(fetched, options.overlays)
|
|
22
|
+
: fetched;
|
|
23
23
|
if (openApiDocument == undefined) {
|
|
24
24
|
throw Error('Could not parse OpenAPI document.');
|
|
25
25
|
}
|
package/dist/openapi/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { getOpenApiOperationMethodAndEndpoint } from './getOpenApiOperationMethodAndEndpoint.js';
|
|
2
|
+
export { applyOverlay, applyOverlays, type LoadedOverlay } from './overlay/applyOverlay.js';
|
|
3
|
+
export { OverlayApplicationError } from './overlay/errors.js';
|
|
4
|
+
export { createOverlayRegistry, normalizeOverlayKey, parseLoadedOverlayDocument, resolveExtends, resolveOverlaysForSpec, type CreateOverlayRegistryOptions, type DiscoveredOverlay, type OverlayRegistry, } from './overlay/overlayRegistry.js';
|
|
5
|
+
export { fetchYamlDocument } from './fetchYamlDocument.js';
|
|
2
6
|
export { truncateCircularReferences } from './truncateCircularReferences.js';
|
|
3
7
|
export { parseOpenApiString, potentiallyParseOpenApiString, parseOpenApiSchemaString, } from './parseOpenApiString.js';
|
|
4
8
|
export { getOpenApiTitleAndDescription } from './getOpenApiTitleAndDescription.js';
|
package/dist/openapi/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { getOpenApiOperationMethodAndEndpoint } from './getOpenApiOperationMethodAndEndpoint.js';
|
|
2
|
+
export { applyOverlay, applyOverlays } from './overlay/applyOverlay.js';
|
|
3
|
+
export { OverlayApplicationError } from './overlay/errors.js';
|
|
4
|
+
export { createOverlayRegistry, normalizeOverlayKey, parseLoadedOverlayDocument, resolveExtends, resolveOverlaysForSpec, } from './overlay/overlayRegistry.js';
|
|
5
|
+
export { fetchYamlDocument } from './fetchYamlDocument.js';
|
|
2
6
|
export { truncateCircularReferences } from './truncateCircularReferences.js';
|
|
3
7
|
export { parseOpenApiString, potentiallyParseOpenApiString, parseOpenApiSchemaString, } from './parseOpenApiString.js';
|
|
4
8
|
export { getOpenApiTitleAndDescription } from './getOpenApiTitleAndDescription.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OverlayDocument } from '@mintlify/validation';
|
|
2
|
+
import { type JsonValue } from './jsonValue.js';
|
|
3
|
+
export type LoadedOverlay = {
|
|
4
|
+
document: OverlayDocument;
|
|
5
|
+
/** Path or URL of the overlay file, used in error messages. */
|
|
6
|
+
location?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Guardrails against pathological overlay documents tying up shared workers: a cap on
|
|
10
|
+
* actions per overlay and a wall-clock budget per application run, checked before each
|
|
11
|
+
* action (a single JSONPath evaluation cannot be preempted, but repeated expensive
|
|
12
|
+
* actions are bounded).
|
|
13
|
+
*/
|
|
14
|
+
export declare const MAX_OVERLAY_ACTIONS = 1000;
|
|
15
|
+
export declare const OVERLAY_TIME_BUDGET_MS = 30000;
|
|
16
|
+
/**
|
|
17
|
+
* Applies one Overlay document to a parsed OpenAPI description, returning a
|
|
18
|
+
* transformed copy. Throws {@link OverlayApplicationError} on failure.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyOverlay(document: unknown, overlay: OverlayDocument, options?: {
|
|
21
|
+
source?: string;
|
|
22
|
+
timeBudgetMs?: number;
|
|
23
|
+
}): JsonValue;
|
|
24
|
+
/** Applies Overlay documents in order, returning a transformed copy. */
|
|
25
|
+
export declare function applyOverlays(document: unknown, overlays: readonly LoadedOverlay[], options?: {
|
|
26
|
+
timeBudgetMs?: number;
|
|
27
|
+
}): unknown;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// json-p3 is pinned to exactly 2.2.2: the 2.3.0 release was published without its
|
|
2
|
+
// TypeScript declaration files. Unpin once a typed release (2.3.1+) ships.
|
|
3
|
+
import { check as isIRegexpPattern } from 'iregexp-check';
|
|
4
|
+
import { FunctionExpressionType, JSONPathEnvironment } from 'json-p3';
|
|
5
|
+
import { RE2JS } from 're2js';
|
|
6
|
+
import { OverlayApplicationError } from './errors.js';
|
|
7
|
+
import { deepCloneJson, isJsonArray, isJsonObject, isJsonPrimitive, isJsonValue, setOwnProperty, toJsonValue, } from './jsonValue.js';
|
|
8
|
+
import { IncompatibleMergeError, mergeObjectInto } from './mergeValue.js';
|
|
9
|
+
/** Caps on regex filter functions, enforced mid-query by the guarded environment. */
|
|
10
|
+
const MAX_FILTER_REGEX_PATTERN_LENGTH = 512;
|
|
11
|
+
const MAX_FILTER_REGEX_INPUT_LENGTH = 100000;
|
|
12
|
+
class QueryBudgetError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Deadline consulted by the guarded match()/search() implementations, so a single
|
|
16
|
+
* long-running filter evaluation is interrupted mid-query instead of only between
|
|
17
|
+
* actions. Module-level is safe: overlay application is fully synchronous.
|
|
18
|
+
*/
|
|
19
|
+
let activeQueryDeadline;
|
|
20
|
+
function guardFilterRegexCall(input, pattern) {
|
|
21
|
+
if (activeQueryDeadline !== undefined && Date.now() >= activeQueryDeadline) {
|
|
22
|
+
throw new QueryBudgetError('JSONPath evaluation exceeded the overlay processing budget');
|
|
23
|
+
}
|
|
24
|
+
if (typeof pattern === 'string' && pattern.length > MAX_FILTER_REGEX_PATTERN_LENGTH) {
|
|
25
|
+
throw new QueryBudgetError(`JSONPath regex patterns are limited to ${MAX_FILTER_REGEX_PATTERN_LENGTH} characters`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof input === 'string' && input.length > MAX_FILTER_REGEX_INPUT_LENGTH) {
|
|
28
|
+
throw new QueryBudgetError(`JSONPath regex input strings are limited to ${MAX_FILTER_REGEX_INPUT_LENGTH} characters`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Maps an I-Regexp pattern for RE2 evaluation (RFC 9485 semantics): `.` outside
|
|
33
|
+
* character classes matches any character except CR/LF, and `^`/`$` outside character
|
|
34
|
+
* classes are literal characters, not anchors. RE2 handles surrogate pairs natively so
|
|
35
|
+
* no astral-pair alternation is needed.
|
|
36
|
+
*/
|
|
37
|
+
function mapIRegexpForRE2(pattern) {
|
|
38
|
+
let escaped = false;
|
|
39
|
+
let charClass = false;
|
|
40
|
+
const parts = [];
|
|
41
|
+
for (const ch of pattern) {
|
|
42
|
+
if (escaped) {
|
|
43
|
+
parts.push(ch);
|
|
44
|
+
escaped = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === '\\') {
|
|
48
|
+
escaped = true;
|
|
49
|
+
parts.push(ch);
|
|
50
|
+
}
|
|
51
|
+
else if (ch === '.' && !charClass) {
|
|
52
|
+
parts.push('[^\\r\\n]');
|
|
53
|
+
}
|
|
54
|
+
else if ((ch === '^' || ch === '$') && !charClass) {
|
|
55
|
+
parts.push(`\\${ch}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (ch === '[')
|
|
59
|
+
charClass = true;
|
|
60
|
+
else if (ch === ']')
|
|
61
|
+
charClass = false;
|
|
62
|
+
parts.push(ch);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.join('');
|
|
66
|
+
}
|
|
67
|
+
const RE2_CACHE_LIMIT = 64;
|
|
68
|
+
const re2Cache = new Map();
|
|
69
|
+
/** Compiles an I-Regexp pattern to a linear-time RE2 matcher, or null when invalid. */
|
|
70
|
+
function compileIRegexp(pattern) {
|
|
71
|
+
const cached = re2Cache.get(pattern);
|
|
72
|
+
if (cached !== undefined)
|
|
73
|
+
return cached;
|
|
74
|
+
let compiled = null;
|
|
75
|
+
if (isIRegexpPattern(pattern)) {
|
|
76
|
+
try {
|
|
77
|
+
compiled = RE2JS.compile(mapIRegexpForRE2(pattern));
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
compiled = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (re2Cache.size >= RE2_CACHE_LIMIT)
|
|
84
|
+
re2Cache.clear();
|
|
85
|
+
re2Cache.set(pattern, compiled);
|
|
86
|
+
return compiled;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* RFC 9535 match()/search() evaluated with RE2 (via re2js), which guarantees linear-time
|
|
90
|
+
* matching with no backtracking, so a customer-supplied pattern cannot produce
|
|
91
|
+
* catastrophic-backtracking CPU blowups (CWE-1333). Non-I-Regexp or invalid patterns
|
|
92
|
+
* evaluate to false, matching json-p3's standard behavior.
|
|
93
|
+
*/
|
|
94
|
+
class LinearTimeRegexFilter {
|
|
95
|
+
constructor(fullMatch) {
|
|
96
|
+
this.fullMatch = fullMatch;
|
|
97
|
+
this.argTypes = [FunctionExpressionType.ValueType, FunctionExpressionType.ValueType];
|
|
98
|
+
this.returnType = FunctionExpressionType.LogicalType;
|
|
99
|
+
}
|
|
100
|
+
call(...args) {
|
|
101
|
+
const [s, pattern] = args;
|
|
102
|
+
guardFilterRegexCall(s, pattern);
|
|
103
|
+
if (typeof s !== 'string' || typeof pattern !== 'string')
|
|
104
|
+
return false;
|
|
105
|
+
const re = compileIRegexp(pattern);
|
|
106
|
+
if (re == null)
|
|
107
|
+
return false;
|
|
108
|
+
const matcher = re.matcher(s);
|
|
109
|
+
// RFC 9535: match() full-matches, search() finds a substring match. I-Regexp has no
|
|
110
|
+
// anchor metacharacters; ^ and $ are escaped to literals in the RE2 mapping.
|
|
111
|
+
return this.fullMatch ? matcher.matches() : matcher.find();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Recursive descent depth is capped by the environment default (50); match()/search()
|
|
115
|
+
// run on a linear-time engine and check the deadline, so a single query's cost stays
|
|
116
|
+
// bounded even mid-evaluation.
|
|
117
|
+
const overlayQueryEnvironment = new JSONPathEnvironment();
|
|
118
|
+
overlayQueryEnvironment.functionRegister.set('match', new LinearTimeRegexFilter(true));
|
|
119
|
+
overlayQueryEnvironment.functionRegister.set('search', new LinearTimeRegexFilter(false));
|
|
120
|
+
function queryNodes(document, expression, expressionLabel, fail) {
|
|
121
|
+
const nodes = [];
|
|
122
|
+
try {
|
|
123
|
+
for (const node of overlayQueryEnvironment.query(expression, document).nodes) {
|
|
124
|
+
if (node.value === undefined || !isJsonValue(node.value))
|
|
125
|
+
continue;
|
|
126
|
+
nodes.push({ value: node.value, path: [...node.location] });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof QueryBudgetError) {
|
|
131
|
+
fail(error.message);
|
|
132
|
+
}
|
|
133
|
+
fail(`invalid JSONPath expression ${expressionLabel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
return nodes.filter((node) => {
|
|
137
|
+
const key = JSON.stringify(node.path);
|
|
138
|
+
if (seen.has(key))
|
|
139
|
+
return false;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
return true;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function resolvePath(document, path, fail) {
|
|
145
|
+
let current = document;
|
|
146
|
+
for (const segment of path) {
|
|
147
|
+
if (typeof segment === 'number') {
|
|
148
|
+
if (!isJsonArray(current)) {
|
|
149
|
+
fail(`expected an array while resolving the selected node's location`);
|
|
150
|
+
}
|
|
151
|
+
const next = current[segment];
|
|
152
|
+
if (next === undefined) {
|
|
153
|
+
fail(`the selected node's location no longer exists in the document`);
|
|
154
|
+
}
|
|
155
|
+
current = next;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (!isJsonObject(current)) {
|
|
159
|
+
fail(`expected an object while resolving the selected node's location`);
|
|
160
|
+
}
|
|
161
|
+
const next = current[segment];
|
|
162
|
+
if (next === undefined) {
|
|
163
|
+
fail(`the selected node's location no longer exists in the document`);
|
|
164
|
+
}
|
|
165
|
+
current = next;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return current;
|
|
169
|
+
}
|
|
170
|
+
// Deeper nodes first, then descending array indexes, so paths stay valid mid-removal.
|
|
171
|
+
function removalOrder(a, b) {
|
|
172
|
+
if (a.path.length !== b.path.length)
|
|
173
|
+
return b.path.length - a.path.length;
|
|
174
|
+
for (let i = 0; i < a.path.length; i++) {
|
|
175
|
+
const segmentA = a.path[i];
|
|
176
|
+
const segmentB = b.path[i];
|
|
177
|
+
if (segmentA === segmentB || segmentA === undefined || segmentB === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
if (typeof segmentA === 'number' && typeof segmentB === 'number') {
|
|
180
|
+
return segmentB - segmentA;
|
|
181
|
+
}
|
|
182
|
+
return String(segmentA) < String(segmentB) ? -1 : 1;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
function removeNodes(document, nodes, fail) {
|
|
187
|
+
const sorted = [...nodes].sort(removalOrder);
|
|
188
|
+
for (const node of sorted) {
|
|
189
|
+
if (node.path.length === 0) {
|
|
190
|
+
fail('the document root cannot be removed');
|
|
191
|
+
}
|
|
192
|
+
const parent = resolvePath(document, node.path.slice(0, -1), fail);
|
|
193
|
+
const key = node.path[node.path.length - 1];
|
|
194
|
+
if (isJsonArray(parent) && typeof key === 'number') {
|
|
195
|
+
parent.splice(key, 1);
|
|
196
|
+
}
|
|
197
|
+
else if (isJsonObject(parent) && typeof key === 'string') {
|
|
198
|
+
delete parent[key];
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
fail('the selected node cannot be removed from its parent');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function nodeKind(value) {
|
|
206
|
+
if (isJsonObject(value))
|
|
207
|
+
return 'object';
|
|
208
|
+
if (isJsonArray(value))
|
|
209
|
+
return 'array';
|
|
210
|
+
return 'primitive';
|
|
211
|
+
}
|
|
212
|
+
function applyValueToNodes(document, nodes, value, fail) {
|
|
213
|
+
const kinds = new Set(nodes.map((node) => nodeKind(node.value)));
|
|
214
|
+
if (kinds.size > 1) {
|
|
215
|
+
fail('the target expression selects nodes of mixed types. Selected nodes must be all objects, all arrays, or all primitives');
|
|
216
|
+
}
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
const target = node.value;
|
|
219
|
+
if (isJsonObject(target)) {
|
|
220
|
+
if (!isJsonObject(value)) {
|
|
221
|
+
fail('the value must be an object to merge with the selected object nodes');
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
mergeObjectInto(target, value);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (error instanceof IncompatibleMergeError)
|
|
228
|
+
fail(error.message);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (isJsonArray(target)) {
|
|
233
|
+
if (isJsonArray(value)) {
|
|
234
|
+
for (const entry of value) {
|
|
235
|
+
target.push(deepCloneJson(entry));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
target.push(deepCloneJson(value));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (!isJsonPrimitive(value)) {
|
|
244
|
+
fail('the value must be a primitive to replace the selected primitive nodes');
|
|
245
|
+
}
|
|
246
|
+
if (node.path.length === 0) {
|
|
247
|
+
fail('the document root cannot be replaced with a primitive value');
|
|
248
|
+
}
|
|
249
|
+
const parent = resolvePath(document, node.path.slice(0, -1), fail);
|
|
250
|
+
const key = node.path[node.path.length - 1];
|
|
251
|
+
if (isJsonArray(parent) && typeof key === 'number') {
|
|
252
|
+
parent[key] = value;
|
|
253
|
+
}
|
|
254
|
+
else if (isJsonObject(parent) && typeof key === 'string') {
|
|
255
|
+
setOwnProperty(parent, key, value);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
fail('the selected node cannot be replaced in its parent');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function applyAction(document, action, actionIndex, overlaySource) {
|
|
264
|
+
const fail = (message) => {
|
|
265
|
+
throw new OverlayApplicationError({
|
|
266
|
+
message,
|
|
267
|
+
overlaySource,
|
|
268
|
+
actionIndex,
|
|
269
|
+
target: action.target,
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const targetNodes = queryNodes(document, action.target, `"${action.target}"`, fail);
|
|
273
|
+
if (action.remove === true) {
|
|
274
|
+
removeNodes(document, targetNodes, fail);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Spec: a zero-match target succeeds without changing the document.
|
|
278
|
+
if (targetNodes.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
let value;
|
|
281
|
+
if (action.copy !== undefined) {
|
|
282
|
+
const copyNodes = queryNodes(document, action.copy, `"${action.copy}" (copy)`, fail);
|
|
283
|
+
const single = copyNodes[0];
|
|
284
|
+
if (copyNodes.length !== 1 || single === undefined) {
|
|
285
|
+
fail(`the "copy" expression "${action.copy}" must select exactly one node, but selected ${copyNodes.length}`);
|
|
286
|
+
}
|
|
287
|
+
value = deepCloneJson(single.value);
|
|
288
|
+
}
|
|
289
|
+
else if (action.update !== undefined) {
|
|
290
|
+
if (!isJsonValue(action.update)) {
|
|
291
|
+
fail('the "update" value is not JSON-representable');
|
|
292
|
+
}
|
|
293
|
+
value = action.update;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
applyValueToNodes(document, targetNodes, value, fail);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Guardrails against pathological overlay documents tying up shared workers: a cap on
|
|
302
|
+
* actions per overlay and a wall-clock budget per application run, checked before each
|
|
303
|
+
* action (a single JSONPath evaluation cannot be preempted, but repeated expensive
|
|
304
|
+
* actions are bounded).
|
|
305
|
+
*/
|
|
306
|
+
export const MAX_OVERLAY_ACTIONS = 1000;
|
|
307
|
+
export const OVERLAY_TIME_BUDGET_MS = 30000;
|
|
308
|
+
function applyOverlayActions(document, overlay, source, deadline, budgetMs) {
|
|
309
|
+
var _a;
|
|
310
|
+
if (overlay.actions.length > MAX_OVERLAY_ACTIONS) {
|
|
311
|
+
const firstExcess = overlay.actions[MAX_OVERLAY_ACTIONS];
|
|
312
|
+
throw new OverlayApplicationError({
|
|
313
|
+
message: `the overlay declares ${overlay.actions.length} actions, above the supported maximum of ${MAX_OVERLAY_ACTIONS}`,
|
|
314
|
+
overlaySource: source,
|
|
315
|
+
actionIndex: MAX_OVERLAY_ACTIONS,
|
|
316
|
+
target: (_a = firstExcess === null || firstExcess === void 0 ? void 0 : firstExcess.target) !== null && _a !== void 0 ? _a : '',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
activeQueryDeadline = deadline;
|
|
320
|
+
try {
|
|
321
|
+
overlay.actions.forEach((action, index) => {
|
|
322
|
+
if (Date.now() >= deadline) {
|
|
323
|
+
throw new OverlayApplicationError({
|
|
324
|
+
message: `overlay application exceeded the ${budgetMs}ms processing budget`,
|
|
325
|
+
overlaySource: source,
|
|
326
|
+
actionIndex: index,
|
|
327
|
+
target: action.target,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
applyAction(document, action, index, source);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
activeQueryDeadline = undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Applies one Overlay document to a parsed OpenAPI description, returning a
|
|
339
|
+
* transformed copy. Throws {@link OverlayApplicationError} on failure.
|
|
340
|
+
*/
|
|
341
|
+
export function applyOverlay(document, overlay, options = {}) {
|
|
342
|
+
var _a;
|
|
343
|
+
const cloned = toJsonValue(document, 'OpenAPI document');
|
|
344
|
+
const budgetMs = (_a = options.timeBudgetMs) !== null && _a !== void 0 ? _a : OVERLAY_TIME_BUDGET_MS;
|
|
345
|
+
applyOverlayActions(cloned, overlay, options.source, Date.now() + budgetMs, budgetMs);
|
|
346
|
+
return cloned;
|
|
347
|
+
}
|
|
348
|
+
/** Applies Overlay documents in order, returning a transformed copy. */
|
|
349
|
+
export function applyOverlays(document, overlays, options = {}) {
|
|
350
|
+
var _a;
|
|
351
|
+
if (overlays.length === 0)
|
|
352
|
+
return document;
|
|
353
|
+
const current = toJsonValue(document, 'OpenAPI document');
|
|
354
|
+
const budgetMs = (_a = options.timeBudgetMs) !== null && _a !== void 0 ? _a : OVERLAY_TIME_BUDGET_MS;
|
|
355
|
+
const deadline = Date.now() + budgetMs;
|
|
356
|
+
for (const { document: overlayDocument, location } of overlays) {
|
|
357
|
+
applyOverlayActions(current, overlayDocument, location, deadline, budgetMs);
|
|
358
|
+
}
|
|
359
|
+
return current;
|
|
360
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an OpenAPI Overlay document cannot be applied to a target document.
|
|
3
|
+
* Carries enough context to point the customer at the exact failing action.
|
|
4
|
+
*/
|
|
5
|
+
export declare class OverlayApplicationError extends Error {
|
|
6
|
+
/** Path or URL of the overlay document, when known. */
|
|
7
|
+
overlaySource?: string;
|
|
8
|
+
/** Zero-based index of the failing action in the overlay's `actions` array. */
|
|
9
|
+
actionIndex: number;
|
|
10
|
+
/** The action's `target` JSONPath expression. */
|
|
11
|
+
target: string;
|
|
12
|
+
constructor({ message, overlaySource, actionIndex, target, }: {
|
|
13
|
+
message: string;
|
|
14
|
+
overlaySource?: string;
|
|
15
|
+
actionIndex: number;
|
|
16
|
+
target: string;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an OpenAPI Overlay document cannot be applied to a target document.
|
|
3
|
+
* Carries enough context to point the customer at the exact failing action.
|
|
4
|
+
*/
|
|
5
|
+
export class OverlayApplicationError extends Error {
|
|
6
|
+
constructor({ message, overlaySource, actionIndex, target, }) {
|
|
7
|
+
const sourceLabel = overlaySource ? ` in overlay ${overlaySource}` : '';
|
|
8
|
+
super(`Overlay action ${actionIndex + 1}${sourceLabel} (target: ${target}): ${message}`);
|
|
9
|
+
this.name = 'OverlayApplicationError';
|
|
10
|
+
this.overlaySource = overlaySource;
|
|
11
|
+
this.actionIndex = actionIndex;
|
|
12
|
+
this.target = target;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A JSON-representable value. Unlike json-p3's JSONValue, `undefined` is excluded. */
|
|
2
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export type JsonObject = {
|
|
6
|
+
[key: string]: JsonValue;
|
|
7
|
+
};
|
|
8
|
+
export declare function isJsonPrimitive(value: JsonValue): value is string | number | boolean | null;
|
|
9
|
+
export declare function isJsonObject(value: JsonValue): value is JsonObject;
|
|
10
|
+
export declare function isJsonArray(value: JsonValue): value is JsonValue[];
|
|
11
|
+
export declare function isJsonValue(value: unknown): value is JsonValue;
|
|
12
|
+
/** Clones a parsed document into a JSON-representable value using JSON semantics. */
|
|
13
|
+
export declare function toJsonValue(value: unknown, label: string): JsonValue;
|
|
14
|
+
/**
|
|
15
|
+
* Writes an own data property without triggering prototype setters, so hostile keys
|
|
16
|
+
* like `__proto__` cannot pollute the prototype chain.
|
|
17
|
+
*/
|
|
18
|
+
export declare function setOwnProperty(target: JsonObject, key: string, value: JsonValue): void;
|
|
19
|
+
export declare function deepCloneJson(value: JsonValue): JsonValue;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function isJsonPrimitive(value) {
|
|
2
|
+
return value === null || typeof value !== 'object';
|
|
3
|
+
}
|
|
4
|
+
export function isJsonObject(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function isJsonArray(value) {
|
|
8
|
+
return Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
export function isJsonValue(value) {
|
|
11
|
+
if (value === null)
|
|
12
|
+
return true;
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case 'string':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return true;
|
|
17
|
+
case 'number':
|
|
18
|
+
return Number.isFinite(value);
|
|
19
|
+
case 'object':
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.every((entry) => isJsonValue(entry));
|
|
22
|
+
}
|
|
23
|
+
return Object.values(value).every((entry) => isJsonValue(entry));
|
|
24
|
+
default:
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Clones a parsed document into a JSON-representable value using JSON semantics. */
|
|
29
|
+
export function toJsonValue(value, label) {
|
|
30
|
+
let cloned;
|
|
31
|
+
try {
|
|
32
|
+
cloned = JSON.parse(JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`${label} is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
|
+
}
|
|
37
|
+
if (cloned === undefined || !isJsonValue(cloned)) {
|
|
38
|
+
throw new Error(`${label} is not a valid JSON document`);
|
|
39
|
+
}
|
|
40
|
+
return cloned;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Writes an own data property without triggering prototype setters, so hostile keys
|
|
44
|
+
* like `__proto__` cannot pollute the prototype chain.
|
|
45
|
+
*/
|
|
46
|
+
export function setOwnProperty(target, key, value) {
|
|
47
|
+
Object.defineProperty(target, key, {
|
|
48
|
+
value,
|
|
49
|
+
writable: true,
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function deepCloneJson(value) {
|
|
55
|
+
if (isJsonPrimitive(value))
|
|
56
|
+
return value;
|
|
57
|
+
if (isJsonArray(value))
|
|
58
|
+
return value.map((entry) => deepCloneJson(entry));
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
61
|
+
setOwnProperty(result, key, deepCloneJson(entry));
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type JsonObject } from './jsonValue.js';
|
|
2
|
+
export declare class IncompatibleMergeError extends Error {
|
|
3
|
+
constructor(propertyPath: string[]);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Recursively merges an overlay `update`/`copy` object into a target object per the
|
|
7
|
+
* Overlay Specification merge rules, mutating the target in place.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mergeObjectInto(target: JsonObject, update: JsonObject, propertyPath?: string[]): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { deepCloneJson, isJsonArray, isJsonObject, isJsonPrimitive, setOwnProperty, } from './jsonValue.js';
|
|
2
|
+
export class IncompatibleMergeError extends Error {
|
|
3
|
+
constructor(propertyPath) {
|
|
4
|
+
super(`Cannot merge incompatible values at "${propertyPath.join('.')}". A primitive can only replace a primitive, an array can only be concatenated with an array, and an object can only be merged with an object.`);
|
|
5
|
+
this.name = 'IncompatibleMergeError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Recursively merges an overlay `update`/`copy` object into a target object per the
|
|
10
|
+
* Overlay Specification merge rules, mutating the target in place.
|
|
11
|
+
*/
|
|
12
|
+
export function mergeObjectInto(target, update, propertyPath = []) {
|
|
13
|
+
for (const [key, updateValue] of Object.entries(update)) {
|
|
14
|
+
// Own-property checks and setOwnProperty writes block prototype pollution.
|
|
15
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
16
|
+
setOwnProperty(target, key, deepCloneJson(updateValue));
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const targetValue = target[key];
|
|
20
|
+
if (targetValue === undefined) {
|
|
21
|
+
setOwnProperty(target, key, deepCloneJson(updateValue));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
mergeValueInto(target, key, targetValue, updateValue, [...propertyPath, key]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function mergeValueInto(parent, key, targetValue, updateValue, propertyPath) {
|
|
28
|
+
if (isJsonPrimitive(updateValue) && isJsonPrimitive(targetValue)) {
|
|
29
|
+
setOwnProperty(parent, key, updateValue);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isJsonArray(updateValue) && isJsonArray(targetValue)) {
|
|
33
|
+
for (const entry of updateValue) {
|
|
34
|
+
targetValue.push(deepCloneJson(entry));
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (isJsonObject(updateValue) && isJsonObject(targetValue)) {
|
|
39
|
+
mergeObjectInto(targetValue, updateValue, propertyPath);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
throw new IncompatibleMergeError(propertyPath);
|
|
43
|
+
}
|