@scalar/mock-server 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/create-asyncapi-mock-server.d.ts +51 -0
- package/dist/create-asyncapi-mock-server.d.ts.map +1 -0
- package/dist/create-asyncapi-mock-server.js +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/transports/index.d.ts +9 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +9 -0
- package/dist/transports/sse.d.ts +11 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +37 -0
- package/dist/transports/types.d.ts +94 -0
- package/dist/transports/types.d.ts.map +1 -0
- package/dist/transports/types.js +1 -0
- package/dist/transports/websocket.d.ts +10 -0
- package/dist/transports/websocket.d.ts.map +1 -0
- package/dist/transports/websocket.js +45 -0
- package/dist/utils/deserialize-parameter.d.ts +76 -0
- package/dist/utils/deserialize-parameter.d.ts.map +1 -0
- package/dist/utils/deserialize-parameter.js +297 -0
- package/dist/utils/generate-message.d.ts +12 -0
- package/dist/utils/generate-message.d.ts.map +1 -0
- package/dist/utils/generate-message.js +41 -0
- package/dist/utils/process-asyncapi-document.d.ts +23 -0
- package/dist/utils/process-asyncapi-document.d.ts.map +1 -0
- package/dist/utils/process-asyncapi-document.js +56 -0
- package/dist/utils/resolve-channels.d.ts +11 -0
- package/dist/utils/resolve-channels.d.ts.map +1 -0
- package/dist/utils/resolve-channels.js +100 -0
- package/dist/utils/validate-request.d.ts +2 -2
- package/dist/utils/validate-request.d.ts.map +1 -1
- package/dist/utils/validate-request.js +173 -22
- package/package.json +6 -4
|
@@ -2,22 +2,66 @@ import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref
|
|
|
2
2
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
3
3
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
4
4
|
import addFormats from 'ajv-formats';
|
|
5
|
+
import { getCookie } from 'hono/cookie';
|
|
6
|
+
import { deserializeArrayParameter, deserializeObjectParameter, getObjectPropertyNames, isArraySchema, isObjectSchema, resolveSerialization, } from './deserialize-parameter.js';
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
8
|
+
* Header parameters named `Accept`, `Content-Type`, or `Authorization` are defined elsewhere in
|
|
9
|
+
* OpenAPI (through `content` and `security`), so the spec says such parameter definitions SHALL be
|
|
10
|
+
* ignored. We compare case-insensitively because header names are case-insensitive.
|
|
11
|
+
*/
|
|
12
|
+
const IGNORED_HEADER_PARAMETERS = new Set(['accept', 'content-type', 'authorization']);
|
|
13
|
+
/**
|
|
14
|
+
* Collapse a `key -> string[]` map (as returned by Hono's `c.req.queries()`) into a `key -> string |
|
|
15
|
+
* string[]` map: a single value becomes a plain string, while repeated keys keep their array so
|
|
16
|
+
* array-valued object properties are not silently truncated to the first value.
|
|
17
|
+
*/
|
|
18
|
+
const mapRepeatedValues = (values) => {
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const [key, list] of Object.entries(values)) {
|
|
21
|
+
map[key] = list.length > 1 ? list : (list[0] ?? '');
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Build a JSON Schema object for the parameters declared `in` the given location, alongside a
|
|
27
|
+
* descriptor for each declared parameter.
|
|
7
28
|
*
|
|
8
29
|
* Parameters arrive as strings, so the caller compiles these with `coerceTypes: true` to let
|
|
9
|
-
* `type: integer`/`boolean` validate correctly.
|
|
30
|
+
* `type: integer`/`boolean` validate correctly. The descriptors let the middleware gather only
|
|
31
|
+
* declared values and deserialize array parameters by their `style`/`explode`. Returns `null` when
|
|
32
|
+
* there is nothing to validate.
|
|
10
33
|
*/
|
|
11
34
|
const buildParameterSchema = (parameters, location) => {
|
|
12
35
|
const properties = {};
|
|
13
36
|
const required = [];
|
|
37
|
+
const descriptors = [];
|
|
14
38
|
for (const parameterOrRef of parameters ?? []) {
|
|
15
39
|
const parameter = getResolvedRef(parameterOrRef);
|
|
16
40
|
if (parameter?.in !== location) {
|
|
17
41
|
continue;
|
|
18
42
|
}
|
|
19
|
-
|
|
20
|
-
|
|
43
|
+
// Per the OpenAPI spec, these header parameters are defined elsewhere and ignored as parameters.
|
|
44
|
+
if (location === 'header' && IGNORED_HEADER_PARAMETERS.has(parameter.name.toLowerCase())) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const resolvedSchema = parameter.schema
|
|
48
|
+
? getResolvedRefDeep(parameter.schema)
|
|
49
|
+
: undefined;
|
|
50
|
+
const { style, explode } = resolveSerialization(location, parameter.style, parameter.explode);
|
|
51
|
+
// Property names let exploded `form` objects be gathered from matching top-level query keys. They
|
|
52
|
+
// are collected through any `anyOf`/`oneOf`/`allOf` composition so a composed object schema does not
|
|
53
|
+
// fall back to free-form gathering and claim unrelated keys.
|
|
54
|
+
const propertyNames = getObjectPropertyNames(resolvedSchema);
|
|
55
|
+
descriptors.push({
|
|
56
|
+
name: parameter.name,
|
|
57
|
+
style,
|
|
58
|
+
explode,
|
|
59
|
+
isArray: isArraySchema(resolvedSchema),
|
|
60
|
+
isObject: isObjectSchema(resolvedSchema),
|
|
61
|
+
propertyNames,
|
|
62
|
+
});
|
|
63
|
+
if (resolvedSchema) {
|
|
64
|
+
properties[parameter.name] = resolvedSchema;
|
|
21
65
|
}
|
|
22
66
|
if (parameter.required) {
|
|
23
67
|
required.push(parameter.name);
|
|
@@ -27,7 +71,7 @@ const buildParameterSchema = (parameters, location) => {
|
|
|
27
71
|
return null;
|
|
28
72
|
}
|
|
29
73
|
// Allow undeclared parameters to pass through; we only enforce what the operation declares.
|
|
30
|
-
return { type: 'object', properties, required, additionalProperties: true };
|
|
74
|
+
return { schema: { type: 'object', properties, required, additionalProperties: true }, parameters: descriptors };
|
|
31
75
|
};
|
|
32
76
|
/**
|
|
33
77
|
* Merge path-item-level parameters with operation-level parameters.
|
|
@@ -81,6 +125,10 @@ const compileValidators = (operation, pathItemParameters) => {
|
|
|
81
125
|
addFormats(bodyAjv);
|
|
82
126
|
// Path-item parameters apply to every operation, so fold them in before building the schemas.
|
|
83
127
|
const parameters = mergeParameters(pathItemParameters, operation.parameters);
|
|
128
|
+
const pathParameters = buildParameterSchema(parameters, 'path');
|
|
129
|
+
const queryParameters = buildParameterSchema(parameters, 'query');
|
|
130
|
+
const headerParameters = buildParameterSchema(parameters, 'header');
|
|
131
|
+
const cookieParameters = buildParameterSchema(parameters, 'cookie');
|
|
84
132
|
const requestBody = getResolvedRef(operation.requestBody);
|
|
85
133
|
// Build the body schema defensively; resolving a malformed `$ref` should not crash setup.
|
|
86
134
|
let bodySchema = null;
|
|
@@ -92,22 +140,35 @@ const compileValidators = (operation, pathItemParameters) => {
|
|
|
92
140
|
console.error('Error resolving request body schema, skipping body validation:', error);
|
|
93
141
|
}
|
|
94
142
|
return {
|
|
95
|
-
path: compileSchema(parameterAjv,
|
|
96
|
-
query: compileSchema(parameterAjv,
|
|
143
|
+
path: compileSchema(parameterAjv, pathParameters?.schema ?? null, 'path parameter'),
|
|
144
|
+
query: compileSchema(parameterAjv, queryParameters?.schema ?? null, 'query parameter'),
|
|
145
|
+
header: compileSchema(parameterAjv, headerParameters?.schema ?? null, 'header parameter'),
|
|
146
|
+
cookie: compileSchema(parameterAjv, cookieParameters?.schema ?? null, 'cookie parameter'),
|
|
147
|
+
pathParameters: pathParameters?.parameters ?? [],
|
|
148
|
+
queryParameters: queryParameters?.parameters ?? [],
|
|
149
|
+
headerParameters: headerParameters?.parameters ?? [],
|
|
150
|
+
cookieParameters: cookieParameters?.parameters ?? [],
|
|
97
151
|
body: compileSchema(bodyAjv, bodySchema, 'request body'),
|
|
98
152
|
// Required-body enforcement is independent of whether the body schema compiles.
|
|
99
153
|
bodyRequired: requestBody?.required === true,
|
|
100
154
|
};
|
|
101
155
|
};
|
|
102
156
|
/**
|
|
103
|
-
* Map Ajv errors into our violation shape. For
|
|
104
|
-
* is prepended to the message so the response is readable without
|
|
157
|
+
* Map Ajv errors into our violation shape. For non-body parameters (path, query, header, cookie) the
|
|
158
|
+
* offending parameter name is prepended to the message so the response is readable without
|
|
159
|
+
* cross-referencing the pointer.
|
|
105
160
|
*/
|
|
106
161
|
const mapErrors = (errors, location) => (errors ?? []).map((error) => {
|
|
107
162
|
const missingProperty = typeof error.params === 'object' && error.params && 'missingProperty' in error.params
|
|
108
163
|
? String(error.params.missingProperty)
|
|
109
164
|
: '';
|
|
110
165
|
const path = error.instancePath || (missingProperty ? `/${missingProperty}` : '');
|
|
166
|
+
// A missing parameter surfaces as a top-level `required` error whose `missingProperty` is the
|
|
167
|
+
// parameter name. Phrase it as "<name> is required" instead of the confusing, self-referential
|
|
168
|
+
// "<name> must have required property '<name>'" that Ajv produces against the synthetic wrapper.
|
|
169
|
+
if (location !== 'body' && missingProperty && !error.instancePath) {
|
|
170
|
+
return { location, path, message: `${missingProperty} is required` };
|
|
171
|
+
}
|
|
111
172
|
const name = error.instancePath.replace(/^\//, '') || missingProperty;
|
|
112
173
|
const message = location !== 'body' && name ? `${name} ${error.message ?? ''}`.trim() : (error.message ?? '');
|
|
113
174
|
return { location, path, message };
|
|
@@ -129,25 +190,115 @@ const isJsonRequest = (c) => {
|
|
|
129
190
|
* violation the middleware short-circuits with a `422` and a `application/problem+json` body;
|
|
130
191
|
* otherwise it calls `next()` and the normal mock handler runs.
|
|
131
192
|
*
|
|
132
|
-
* TODO: Parity follow-ups, intentionally deferred
|
|
133
|
-
*
|
|
193
|
+
* TODO: Parity follow-ups, intentionally deferred — response validation, non-JSON body validation,
|
|
194
|
+
* and validation proxy mode.
|
|
134
195
|
*/
|
|
135
196
|
export const validateRequest = (operation, pathItemParameters) => {
|
|
136
197
|
const validators = compileValidators(operation, pathItemParameters);
|
|
137
198
|
return async (c, next) => {
|
|
138
199
|
const violations = [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Gather declared parameter values for one location into a plain object for validation. Array and
|
|
202
|
+
* object parameters are deserialized by their `style`/`explode`; everything else is read as a
|
|
203
|
+
* single string. Values are looked up by declared name (rather than dumping every request value)
|
|
204
|
+
* so header names match case-insensitively and only declared parameters are enforced.
|
|
205
|
+
*
|
|
206
|
+
* `getValues` (repeated query keys) is only supplied for the query location, where exploded arrays
|
|
207
|
+
* need it. `getMap` returns the full key/value map for the location and is supplied for query and
|
|
208
|
+
* cookie, where exploded-`form` objects (and query `deepObject`) spread properties across keys —
|
|
209
|
+
* for cookies each object property is its own cookie (`Cookie: r=100; g=200`).
|
|
210
|
+
*/
|
|
211
|
+
const gather = (descriptors, getValue, getValues, getMap) => {
|
|
212
|
+
// Names of every declared parameter in this location, so a free-form exploded object does not
|
|
213
|
+
// swallow a sibling parameter's key (e.g. a required free-form object satisfied by `?limit=5`).
|
|
214
|
+
const declaredNames = new Set(descriptors.map((descriptor) => descriptor.name));
|
|
215
|
+
const readAsObject = (descriptor) => deserializeObjectParameter({
|
|
216
|
+
style: descriptor.style,
|
|
217
|
+
explode: descriptor.explode,
|
|
218
|
+
single: getValue(descriptor.name),
|
|
219
|
+
map: getMap?.(),
|
|
220
|
+
name: descriptor.name,
|
|
221
|
+
propertyNames: descriptor.propertyNames,
|
|
222
|
+
reservedKeys: declaredNames,
|
|
223
|
+
});
|
|
224
|
+
const readAsArray = (descriptor) => deserializeArrayParameter({
|
|
225
|
+
style: descriptor.style,
|
|
226
|
+
explode: descriptor.explode,
|
|
227
|
+
single: getValue(descriptor.name),
|
|
228
|
+
multi: getValues?.(descriptor.name),
|
|
229
|
+
});
|
|
230
|
+
const data = {};
|
|
231
|
+
for (const descriptor of descriptors) {
|
|
232
|
+
let value;
|
|
233
|
+
if (descriptor.isArray && descriptor.isObject) {
|
|
234
|
+
// A schema composed with `anyOf`/`oneOf`/`allOf` can look like both an array and an object, and
|
|
235
|
+
// form encoding cannot say which the client meant. Prefer a non-empty array — that only happens
|
|
236
|
+
// when the request carries the parameter's own (possibly repeated) key — and fall back to the
|
|
237
|
+
// object reading otherwise. This parses array-style input (`?filter=1&filter=2`) as an array while
|
|
238
|
+
// still letting object-style input (properties spread across other keys) be gathered as an object.
|
|
239
|
+
// A lone empty value (`?filter=`) deserializes to `[]`, which is not array-shaped input, so it must
|
|
240
|
+
// not short-circuit the object fallback; the empty array is only kept when no object value is found.
|
|
241
|
+
const asArray = readAsArray(descriptor);
|
|
242
|
+
value = Array.isArray(asArray) && asArray.length > 0 ? asArray : (readAsObject(descriptor) ?? asArray);
|
|
243
|
+
}
|
|
244
|
+
else if (descriptor.isObject) {
|
|
245
|
+
value = readAsObject(descriptor);
|
|
246
|
+
}
|
|
247
|
+
else if (descriptor.isArray) {
|
|
248
|
+
value = readAsArray(descriptor);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
value = getValue(descriptor.name);
|
|
252
|
+
}
|
|
253
|
+
if (value !== undefined) {
|
|
254
|
+
data[descriptor.name] = value;
|
|
255
|
+
}
|
|
144
256
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
257
|
+
return data;
|
|
258
|
+
};
|
|
259
|
+
// Validate each parameter location with its own request accessors. Values are coerced from strings.
|
|
260
|
+
// The differences between locations are only the accessors:
|
|
261
|
+
// - query supplies `getValues` (repeated keys feed exploded arrays) and a `getMap` that keeps
|
|
262
|
+
// repeated keys as arrays, so an array-valued object property (`filter[tags]=a&filter[tags]=b`)
|
|
263
|
+
// survives deserialization;
|
|
264
|
+
// - cookie supplies a `getMap` (each exploded `form` object property is its own cookie);
|
|
265
|
+
// - header names are matched case-insensitively by Hono's `c.req.header`.
|
|
266
|
+
const parameterLocations = [
|
|
267
|
+
{
|
|
268
|
+
validator: validators.path,
|
|
269
|
+
descriptors: validators.pathParameters,
|
|
270
|
+
location: 'path',
|
|
271
|
+
getValue: (name) => c.req.param(name),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
validator: validators.query,
|
|
275
|
+
descriptors: validators.queryParameters,
|
|
276
|
+
location: 'query',
|
|
277
|
+
getValue: (name) => c.req.query(name),
|
|
278
|
+
getValues: (name) => c.req.queries(name),
|
|
279
|
+
getMap: () => mapRepeatedValues(c.req.queries()),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
validator: validators.header,
|
|
283
|
+
descriptors: validators.headerParameters,
|
|
284
|
+
location: 'header',
|
|
285
|
+
getValue: (name) => c.req.header(name),
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
validator: validators.cookie,
|
|
289
|
+
descriptors: validators.cookieParameters,
|
|
290
|
+
location: 'cookie',
|
|
291
|
+
getValue: (name) => getCookie(c, name),
|
|
292
|
+
getMap: () => getCookie(c),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
for (const { validator, descriptors, location, getValue, getValues, getMap } of parameterLocations) {
|
|
296
|
+
if (!validator) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const data = gather(descriptors, getValue, getValues, getMap);
|
|
300
|
+
if (!validator(data)) {
|
|
301
|
+
violations.push(...mapErrors(validator.errors, location));
|
|
151
302
|
}
|
|
152
303
|
}
|
|
153
304
|
// Request body — only `application/json` in this slice
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"swagger",
|
|
17
17
|
"cli"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.12.0",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -52,15 +52,17 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@faker-js/faker": "10.4.0",
|
|
55
|
+
"@hono/node-ws": "^1.2.0",
|
|
55
56
|
"ajv": "^8.17.1",
|
|
56
57
|
"ajv-formats": "^3.0.1",
|
|
57
58
|
"hono": "^4.12.7",
|
|
58
59
|
"yaml": "^2.8.3",
|
|
59
60
|
"@scalar/helpers": "0.8.2",
|
|
60
|
-
"@scalar/openapi-types": "0.9.1",
|
|
61
|
-
"@scalar/json-magic": "0.12.16",
|
|
62
61
|
"@scalar/openapi-upgrader": "0.2.9",
|
|
63
|
-
"@scalar/
|
|
62
|
+
"@scalar/json-magic": "0.12.16",
|
|
63
|
+
"@scalar/openapi-types": "0.9.1",
|
|
64
|
+
"@scalar/workspace-store": "0.54.5",
|
|
65
|
+
"@scalar/types": "0.15.0"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@types/node": "^24.1.0",
|