@scalar/mock-server 0.14.4 → 0.16.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/create-asyncapi-mock-server.d.ts +8 -10
  3. package/dist/create-asyncapi-mock-server.d.ts.map +1 -1
  4. package/dist/create-asyncapi-mock-server.js +6 -10
  5. package/dist/create-mock-server.d.ts.map +1 -1
  6. package/dist/create-mock-server.js +36 -17
  7. package/dist/routes/mock-any-response.d.ts.map +1 -1
  8. package/dist/routes/mock-any-response.js +28 -13
  9. package/dist/routes/mock-handler-response.d.ts +1 -2
  10. package/dist/routes/mock-handler-response.d.ts.map +1 -1
  11. package/dist/routes/mock-handler-response.js +15 -10
  12. package/dist/types.d.ts +2 -2
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/build-handler-context.d.ts +3 -2
  15. package/dist/utils/build-handler-context.d.ts.map +1 -1
  16. package/dist/utils/build-handler-context.js +23 -2
  17. package/dist/utils/get-oauth2-metadata.d.ts +1 -0
  18. package/dist/utils/get-oauth2-metadata.d.ts.map +1 -1
  19. package/dist/utils/get-oauth2-metadata.js +5 -1
  20. package/dist/utils/get-open-auth-token-urls.d.ts +2 -2
  21. package/dist/utils/get-open-auth-token-urls.d.ts.map +1 -1
  22. package/dist/utils/get-open-auth-token-urls.js +5 -3
  23. package/dist/utils/get-operation.d.ts +4 -2
  24. package/dist/utils/get-operation.d.ts.map +1 -1
  25. package/dist/utils/get-operation.js +9 -5
  26. package/dist/utils/handle-authentication.d.ts +2 -2
  27. package/dist/utils/handle-authentication.d.ts.map +1 -1
  28. package/dist/utils/log-authentication-instructions.d.ts +2 -2
  29. package/dist/utils/log-authentication-instructions.d.ts.map +1 -1
  30. package/dist/utils/log-authentication-instructions.js +7 -0
  31. package/dist/utils/negotiate-content-type.d.ts +4 -0
  32. package/dist/utils/negotiate-content-type.d.ts.map +1 -0
  33. package/dist/utils/negotiate-content-type.js +12 -0
  34. package/dist/utils/process-openapi-document.d.ts +7 -4
  35. package/dist/utils/process-openapi-document.d.ts.map +1 -1
  36. package/dist/utils/process-openapi-document.js +26 -11
  37. package/dist/utils/querystring-parameter.d.ts +11 -0
  38. package/dist/utils/querystring-parameter.d.ts.map +1 -0
  39. package/dist/utils/querystring-parameter.js +135 -0
  40. package/dist/utils/select-response-example.d.ts +3 -1
  41. package/dist/utils/select-response-example.d.ts.map +1 -1
  42. package/dist/utils/set-up-authentication-routes.d.ts.map +1 -1
  43. package/dist/utils/set-up-authentication-routes.js +2 -0
  44. package/dist/utils/set-up-device-authorization.d.ts +5 -0
  45. package/dist/utils/set-up-device-authorization.d.ts.map +1 -0
  46. package/dist/utils/set-up-device-authorization.js +192 -0
  47. package/dist/utils/streaming-response.d.ts +24 -0
  48. package/dist/utils/streaming-response.d.ts.map +1 -0
  49. package/dist/utils/streaming-response.js +65 -0
  50. package/dist/utils/validate-request.d.ts.map +1 -1
  51. package/dist/utils/validate-request.js +29 -1
  52. package/package.json +12 -9
@@ -0,0 +1,192 @@
1
+ import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
+ import { html } from 'hono/html';
3
+ import { getPathFromUrl } from './get-open-auth-token-urls.js';
4
+ /** Both endpoints compare the decoded client ID, regardless of credential location. */
5
+ const getClientId = (clientId, authorization) => {
6
+ if (typeof clientId === 'string') {
7
+ return clientId;
8
+ }
9
+ if (!authorization?.startsWith('Basic ')) {
10
+ return '';
11
+ }
12
+ const credentials = Buffer.from(authorization.slice(6), 'base64').toString();
13
+ const separator = credentials.indexOf(':');
14
+ if (separator === -1) {
15
+ return '';
16
+ }
17
+ try {
18
+ return decodeURIComponent(credentials.slice(0, separator).replace(/\+/g, ' '));
19
+ }
20
+ catch {
21
+ return '';
22
+ }
23
+ };
24
+ /** Keeps the device form and its outcomes consistent with the mock OAuth authorization page. */
25
+ const renderDevicePage = (title, heading, content) => html `
26
+ <!doctype html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="utf-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1">
31
+ <title>${heading}</title>
32
+ <style>
33
+ * { box-sizing: border-box; }
34
+ body { margin: 0; min-height: 100svh; display: grid; place-items: center; padding: 32px 16px; background: #f3f4f6; color: #4b5563; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; line-height: 1.5; }
35
+ main { width: 100%; max-width: 448px; }
36
+ .brand { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 20px; color: #111827; font-size: 18px; font-weight: 500; }
37
+ .brand img { width: 24px; }
38
+ .brand span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
39
+ .card { padding: 4px; border-radius: 8px; background: #f9fafb; box-shadow: 0 1px 3px #0000001a, 0 1px 2px #0000001a; }
40
+ h1 { margin: 0; padding: 8px 24px 12px; color: #1f2937; font-size: 16px; font-weight: 500; }
41
+ .content { padding: 24px; border-radius: 4px; background: white; }
42
+ p { margin: 0 0 20px; }
43
+ .content > p:last-child { margin-bottom: 0; }
44
+ label { display: block; margin-bottom: 8px; color: #374151; font-weight: 500; }
45
+ input { width: 100%; min-width: 0; padding: 12px; border: 1px solid #d1d5db; border-radius: 4px; background: #f9fafb; color: #111827; font: 18px ui-monospace, monospace; letter-spacing: .15em; }
46
+ .actions { display: flex; flex-direction: row-reverse; justify-content: space-between; gap: 12px; margin-top: 24px; }
47
+ button { padding: 8px 24px; border: 1px solid #e5e7eb; border-radius: 4px; background: white; color: #4b5563; font: inherit; cursor: pointer; }
48
+ button:hover { background: #f3f4f6; }
49
+ button[value="approve"] { border-color: black; background: black; color: white; }
50
+ button[value="approve"]:hover { background: #1f2937; }
51
+ :focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; }
52
+ footer { margin-top: 20px; color: #6b7280; text-align: center; font-size: 12px; }
53
+ a { color: #4b5563; text-underline-offset: 2px; }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <main>
58
+ <div class="brand">
59
+ <img src="https://cdn.scalar.com/images/logo-dark.svg" alt="Scalar">
60
+ <span>${title}</span>
61
+ </div>
62
+ <section class="card" aria-labelledby="page-title">
63
+ <h1 id="page-title">${heading}</h1>
64
+ <div class="content">${content}</div>
65
+ </section>
66
+ <footer>
67
+ This authorization page is provided by the
68
+ <a href="https://scalar.com/tools/mock-server/getting-started" target="_blank" rel="noopener noreferrer">Scalar Mock Server</a>.
69
+ </footer>
70
+ </main>
71
+ </body>
72
+ </html>
73
+ `;
74
+ /** Registers a local RFC8628 approval flow before the permissive mock token handlers. */
75
+ export const setUpDeviceAuthorization = (app, document) => {
76
+ const title = document?.info?.title ?? '';
77
+ const grants = new Map();
78
+ const devicePaths = new Map();
79
+ const securitySchemes = document?.components?.securitySchemes ?? {};
80
+ for (const rawScheme of Object.values(securitySchemes)) {
81
+ const scheme = getResolvedRef(rawScheme);
82
+ if (scheme?.type === 'oauth2' && scheme.flows?.deviceAuthorization) {
83
+ const flow = scheme.flows.deviceAuthorization;
84
+ devicePaths.set(getPathFromUrl(flow.deviceAuthorizationUrl || '/oauth/device'), getPathFromUrl(flow.tokenUrl || '/oauth/token'));
85
+ }
86
+ }
87
+ const tokenPaths = new Set(devicePaths.values());
88
+ const removeExpired = () => {
89
+ for (const [code, grant] of grants) {
90
+ if (grant.expiresAt <= Date.now()) {
91
+ grants.delete(code);
92
+ }
93
+ }
94
+ };
95
+ for (const [devicePath, tokenPath] of devicePaths) {
96
+ const verificationPath = `${devicePath.replace(/\/$/, '')}/verify`;
97
+ app.post(devicePath, async (c) => {
98
+ const body = await c.req.parseBody();
99
+ const clientId = getClientId(body.client_id, c.req.header('Authorization'));
100
+ c.header('Cache-Control', 'no-store');
101
+ c.header('Pragma', 'no-cache');
102
+ if (!clientId) {
103
+ return c.json({ error: 'invalid_request', error_description: 'Missing client_id' }, 400);
104
+ }
105
+ removeExpired();
106
+ const deviceCode = crypto.randomUUID();
107
+ const userCode = crypto.randomUUID().slice(0, 8).toUpperCase();
108
+ grants.set(deviceCode, {
109
+ clientId,
110
+ userCode,
111
+ tokenPath,
112
+ expiresAt: Date.now() + 600000,
113
+ nextPoll: 0,
114
+ interval: 5000,
115
+ status: 'pending',
116
+ });
117
+ const verificationUri = new URL(verificationPath, c.req.url).href;
118
+ return c.json({
119
+ device_code: deviceCode,
120
+ user_code: userCode,
121
+ verification_uri: verificationUri,
122
+ verification_uri_complete: `${verificationUri}?user_code=${userCode}`,
123
+ expires_in: 600,
124
+ interval: 5,
125
+ });
126
+ });
127
+ app.get(verificationPath, (c) => c.html(renderDevicePage(title, 'Authorize device', html `
128
+ <p>Enter the code shown in your API client to approve this mock authorization request.</p>
129
+ <form method="post">
130
+ <label for="user-code">Device code</label>
131
+ <input id="user-code" name="user_code" required value="${c.req.query('user_code') ?? ''}" autocomplete="one-time-code" spellcheck="false" autocapitalize="characters">
132
+ <div class="actions">
133
+ <button name="decision" value="approve">Approve</button>
134
+ <button name="decision" value="deny">Deny</button>
135
+ </div>
136
+ </form>
137
+ `)));
138
+ app.post(verificationPath, async (c) => {
139
+ const body = await c.req.parseBody();
140
+ const grant = [...grants.values()].find((entry) => entry.userCode === String(body.user_code).trim().toUpperCase() && entry.tokenPath === tokenPath);
141
+ if (!grant || grant.expiresAt <= Date.now() || grant.status !== 'pending') {
142
+ return c.html(renderDevicePage(title, 'Invalid or expired device code', html `<p>Start a new authorization request in your API client and enter the new device code.</p>`), 400);
143
+ }
144
+ if (body.decision !== 'approve' && body.decision !== 'deny') {
145
+ return c.html(renderDevicePage(title, 'Choose Approve or Deny', html `<p>Return to the device authorization form and choose whether to approve or deny the request.</p>`), 400);
146
+ }
147
+ grant.status = body.decision === 'approve' ? 'approved' : 'denied';
148
+ return c.html(renderDevicePage(title, grant.status === 'approved' ? 'Device authorized' : 'Device authorization denied', grant.status === 'approved'
149
+ ? html `<p>Device authorized. You can return to your API client.</p>`
150
+ : html `<p>Device authorization denied. You can return to your API client to start a new request.</p>`));
151
+ });
152
+ }
153
+ for (const tokenPath of tokenPaths) {
154
+ app.post(tokenPath, async (c, next) => {
155
+ const body = await c.req.parseBody();
156
+ if (body.grant_type !== 'urn:ietf:params:oauth:grant-type:device_code') {
157
+ return next();
158
+ }
159
+ c.header('Cache-Control', 'no-store');
160
+ c.header('Pragma', 'no-cache');
161
+ const clientId = getClientId(body.client_id, c.req.header('Authorization'));
162
+ const code = String(body.device_code ?? '');
163
+ const grant = grants.get(code);
164
+ if (!grant || grant.tokenPath !== tokenPath || grant.clientId !== clientId) {
165
+ return c.json({ error: 'invalid_grant' }, 400);
166
+ }
167
+ if (grant.expiresAt <= Date.now()) {
168
+ grants.delete(code);
169
+ return c.json({ error: 'expired_token' }, 400);
170
+ }
171
+ if (Date.now() < grant.nextPoll) {
172
+ grant.interval += 5000;
173
+ grant.nextPoll = Date.now() + grant.interval;
174
+ return c.json({ error: 'slow_down' }, 400);
175
+ }
176
+ grant.nextPoll = Date.now() + grant.interval;
177
+ if (grant.status === 'pending') {
178
+ return c.json({ error: 'authorization_pending' }, 400);
179
+ }
180
+ grants.delete(code);
181
+ if (grant.status === 'denied') {
182
+ return c.json({ error: 'access_denied' }, 400);
183
+ }
184
+ return c.json({
185
+ access_token: 'super-secret-access-token',
186
+ token_type: 'Bearer',
187
+ expires_in: 3600,
188
+ refresh_token: 'example-refresh-token',
189
+ });
190
+ });
191
+ }
192
+ };
@@ -0,0 +1,24 @@
1
+ import type { OpenAPIV3_2 } from '@scalar/openapi-types';
2
+ import type { Context } from 'hono';
3
+ /** A finite mock stream, with its data-model value available to custom handlers. */
4
+ type StreamingResponse = {
5
+ body: unknown;
6
+ chunks: string[];
7
+ contentType: string;
8
+ };
9
+ /**
10
+ * Build a finite response for media types with an OpenAPI 3.2 itemSchema.
11
+ * Explicit examples describe the whole response. Generated item-only streams contain three items,
12
+ * keeping array-valued items intact. A complete schema controls the sequence when also present.
13
+ */
14
+ export declare const getStreamingResponse: (mediaType: (Omit<OpenAPIV3_2.MediaTypeObject, "itemSchema"> & {
15
+ itemSchema?: OpenAPIV3_2.MediaTypeObject["itemSchema"] | boolean;
16
+ }) | undefined, contentType: string, options?: {
17
+ exampleName?: string;
18
+ variables?: Record<string, unknown>;
19
+ body?: unknown;
20
+ }) => StreamingResponse | undefined;
21
+ /** Write each mocked item separately and close the stream when the finite sequence is exhausted. */
22
+ export declare const sendStreamingResponse: (c: Context, response: StreamingResponse) => Response;
23
+ export {};
24
+ //# sourceMappingURL=streaming-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-response.d.ts","sourceRoot":"","sources":["../../src/utils/streaming-response.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAKxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAMnC,oFAAoF;AACpF,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,WACI,CAAC,IAAI,CAAC,WAAW,CAAC,eAAe,EAAE,YAAY,CAAC,GAAG;IACjD,UAAU,CAAC,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,GAAG,OAAO,CAAA;CACjE,CAAC,GACF,SAAS,EACb,aAAa,MAAM,EACnB,UAAS;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,KAC1F,iBAAiB,GAAG,SA8CtB,CAAA;AAED,oGAAoG;AACpG,eAAO,MAAM,qBAAqB,GAAI,GAAG,OAAO,EAAE,UAAU,iBAAiB,KAAG,QAY/E,CAAA"}
@@ -0,0 +1,65 @@
1
+ import { getStreamFormat } from '@scalar/helpers/http/is-streaming-content-type';
2
+ import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
3
+ import { serializeStreamExample } from '@scalar/workspace-store/helpers/serialize-stream-example';
4
+ import { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
5
+ import { SchemaObjectSchema } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
6
+ import { stream } from 'hono/streaming';
7
+ import { generateResponseExample } from './generate-response-example.js';
8
+ import { selectResponseExample } from './select-response-example.js';
9
+ /**
10
+ * Build a finite response for media types with an OpenAPI 3.2 itemSchema.
11
+ * Explicit examples describe the whole response. Generated item-only streams contain three items,
12
+ * keeping array-valued items intact. A complete schema controls the sequence when also present.
13
+ */
14
+ export const getStreamingResponse = (mediaType, contentType, options = {}) => {
15
+ if (mediaType?.itemSchema === undefined) {
16
+ return undefined;
17
+ }
18
+ const format = getStreamFormat(contentType);
19
+ if (format === undefined || format === 'multipart') {
20
+ return undefined;
21
+ }
22
+ const example = options.body !== undefined ? { value: options.body } : selectResponseExample(mediaType, options.exampleName);
23
+ if (example && typeof example.value === 'string') {
24
+ return { body: example.value, chunks: [example.value], contentType };
25
+ }
26
+ const itemSchema = typeof mediaType.itemSchema === 'boolean' ? mediaType.itemSchema : getResolvedRefDeep(mediaType.itemSchema);
27
+ const completeSchema = typeof mediaType.schema === 'boolean' ? mediaType.schema : getResolvedRefDeep(mediaType.schema);
28
+ const generateBody = () => {
29
+ if (itemSchema === false || completeSchema === false) {
30
+ return [];
31
+ }
32
+ if (completeSchema === undefined) {
33
+ return Array.from({ length: 3 }, () => itemSchema === true
34
+ ? null
35
+ : generateResponseExample(coerceValue(SchemaObjectSchema, itemSchema), options.variables));
36
+ }
37
+ if (typeof completeSchema === 'object' && 'type' in completeSchema && completeSchema.type === 'array') {
38
+ return generateResponseExample(coerceValue(SchemaObjectSchema, {
39
+ ...completeSchema,
40
+ items: ('items' in completeSchema ? completeSchema.items : undefined) ?? itemSchema,
41
+ }), options.variables);
42
+ }
43
+ return generateResponseExample(coerceValue(SchemaObjectSchema, completeSchema), options.variables);
44
+ };
45
+ const body = example ? example.value : generateBody();
46
+ const items = body === undefined ? [] : Array.isArray(body) ? body : [body];
47
+ const chunks = items
48
+ .filter((item) => item !== undefined)
49
+ .map((item) => serializeStreamExample(item, contentType, true) ?? '');
50
+ return { body, chunks, contentType };
51
+ };
52
+ /** Write each mocked item separately and close the stream when the finite sequence is exhausted. */
53
+ export const sendStreamingResponse = (c, response) => {
54
+ c.header('Content-Type', response.contentType);
55
+ c.header('Cache-Control', 'no-cache');
56
+ c.header('X-Accel-Buffering', 'no');
57
+ return stream(c, async (writer) => {
58
+ for (const chunk of response.chunks) {
59
+ if (writer.aborted) {
60
+ break;
61
+ }
62
+ await writer.write(chunk);
63
+ }
64
+ });
65
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"validate-request.d.ts","sourceRoot":"","sources":["../../src/utils/validate-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAMxD,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAoStD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,WAAW,CAAC,eAAe,EACtC,qBAAqB,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,KAC5D,iBAkKF,CAAA"}
1
+ {"version":3,"file":"validate-request.d.ts","sourceRoot":"","sources":["../../src/utils/validate-request.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,uBAAuB,CAAA;AAMrE,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAuTtD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,WAAW,CAAC,eAAe,EACtC,qBAAqB,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,KAC5D,iBAmLF,CAAA"}
@@ -1,9 +1,12 @@
1
+ import { getFirstMediaType } from '@scalar/helpers/http/get-first-media-type';
2
+ import { parseMimeType } from '@scalar/helpers/http/mime-type';
1
3
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
4
  import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
3
5
  import Ajv2020 from 'ajv/dist/2020.js';
4
6
  import addFormats from 'ajv-formats';
5
7
  import { getCookie } from 'hono/cookie';
6
8
  import { deserializeArrayParameter, deserializeObjectParameter, getObjectPropertyNames, isArraySchema, isObjectSchema, resolveSerialization, } from './deserialize-parameter.js';
9
+ import { findQuerystringParameter, getQuerystringJsonSchema, parseQuerystringParameter } from './querystring-parameter.js';
7
10
  import { replaceCircularMarkers } from './replace-circular-markers.js';
8
11
  /**
9
12
  * Prepare a resolved schema for Ajv by replacing the `'[circular]'` markers a recursive schema leaves
@@ -113,7 +116,7 @@ const mergeParameters = (pathItemParameters, operationParameters) => {
113
116
  * in isolation so one broken schema never disables the others.
114
117
  */
115
118
  const compileSchema = (ajv, schema, label) => {
116
- if (!schema) {
119
+ if (schema === null || schema === undefined) {
117
120
  return null;
118
121
  }
119
122
  try {
@@ -140,6 +143,9 @@ const compileValidators = (operation, pathItemParameters) => {
140
143
  const queryParameters = buildParameterSchema(parameters, 'query');
141
144
  const headerParameters = buildParameterSchema(parameters, 'header');
142
145
  const cookieParameters = buildParameterSchema(parameters, 'cookie');
146
+ const querystringParameter = findQuerystringParameter(operation, pathItemParameters);
147
+ const [querystringContentType, querystringMedia] = getFirstMediaType(querystringParameter?.content) ?? [];
148
+ const querystringSchema = querystringMedia?.schema;
143
149
  const requestBody = getResolvedRef(operation.requestBody);
144
150
  // Build the body schema defensively; resolving a malformed `$ref` should not crash setup.
145
151
  let bodySchema = null;
@@ -151,6 +157,9 @@ const compileValidators = (operation, pathItemParameters) => {
151
157
  console.error('Error resolving request body schema, skipping body validation:', error);
152
158
  }
153
159
  return {
160
+ querystringParameter,
161
+ querystringJson: compileSchema(bodyAjv, getQuerystringJsonSchema(querystringParameter), 'querystring JSON properties'),
162
+ querystring: compileSchema(parseMimeType(querystringContentType).essence === 'application/x-www-form-urlencoded' ? parameterAjv : bodyAjv, querystringSchema === undefined ? null : asCompilableSchema(getResolvedRefDeep(querystringSchema)), 'querystring parameter'),
154
163
  path: compileSchema(parameterAjv, pathParameters?.schema ?? null, 'path parameter'),
155
164
  query: compileSchema(parameterAjv, queryParameters?.schema ?? null, 'query parameter'),
156
165
  header: compileSchema(parameterAjv, headerParameters?.schema ?? null, 'header parameter'),
@@ -312,6 +321,25 @@ export const validateRequest = (operation, pathItemParameters) => {
312
321
  violations.push(...mapErrors(validator.errors, location));
313
322
  }
314
323
  }
324
+ if (validators.querystringParameter) {
325
+ try {
326
+ const value = parseQuerystringParameter(c.req.url, validators.querystringParameter);
327
+ if (value === undefined) {
328
+ if (validators.querystringParameter.required) {
329
+ violations.push({ location: 'query', path: '', message: 'Query string is required' });
330
+ }
331
+ }
332
+ else if (validators.querystringJson && !validators.querystringJson(value)) {
333
+ violations.push(...mapErrors(validators.querystringJson.errors, 'query'));
334
+ }
335
+ else if (validators.querystring && !validators.querystring(value)) {
336
+ violations.push(...mapErrors(validators.querystring.errors, 'query'));
337
+ }
338
+ }
339
+ catch {
340
+ violations.push({ location: 'query', path: '', message: 'Query string could not be decoded' });
341
+ }
342
+ }
315
343
  // Request body — only `application/json` in this slice
316
344
  if (validators.body || validators.bodyRequired) {
317
345
  // Read from a clone so the original request stream stays intact for the mock or `x-handler`
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "swagger",
17
17
  "cli"
18
18
  ],
19
- "version": "0.14.4",
19
+ "version": "0.16.0",
20
20
  "engines": {
21
21
  "node": ">=22"
22
22
  },
@@ -52,21 +52,24 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@faker-js/faker": "10.6.0",
55
- "@hono/node-ws": "^1.2.0",
56
- "@scalar/helpers": "0.12.0",
57
- "@scalar/json-magic": "0.14.0",
58
- "@scalar/openapi-types": "0.9.6",
59
- "@scalar/openapi-upgrader": "0.2.17",
60
- "@scalar/types": "0.20.1",
61
- "@scalar/workspace-store": "0.64.0",
55
+ "@hono/node-server": "^2.1.1",
56
+ "@scalar/helpers": "0.14.0",
57
+ "@scalar/json-magic": "0.15.1",
58
+ "@scalar/openapi-types": "0.9.7",
59
+ "@scalar/openapi-upgrader": "0.3.1",
60
+ "@scalar/types": "0.22.0",
61
+ "@scalar/workspace-store": "0.66.0",
62
62
  "ajv": "^8.20.0",
63
63
  "ajv-formats": "^3.0.1",
64
- "hono": "^4.13.7",
64
+ "hono": "^4.13.8",
65
65
  "quickjs-emscripten": "0.32.0",
66
+ "ws": "8.21.3",
66
67
  "yaml": "^2.9.0"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@types/node": "^24.1.0",
71
+ "@types/ws": "8.18.1",
72
+ "undici": "7.24.4",
70
73
  "vite": "8.1.5"
71
74
  },
72
75
  "scripts": {