@scalar/mock-server 0.14.4 → 0.15.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 (43) hide show
  1. package/CHANGELOG.md +54 -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 +12 -4
  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 +13 -8
  12. package/dist/types.d.ts +2 -0
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/build-handler-context.d.ts.map +1 -1
  15. package/dist/utils/build-handler-context.js +9 -0
  16. package/dist/utils/get-oauth2-metadata.d.ts +1 -0
  17. package/dist/utils/get-oauth2-metadata.d.ts.map +1 -1
  18. package/dist/utils/get-oauth2-metadata.js +5 -1
  19. package/dist/utils/get-open-auth-token-urls.d.ts +2 -2
  20. package/dist/utils/get-open-auth-token-urls.d.ts.map +1 -1
  21. package/dist/utils/get-open-auth-token-urls.js +5 -3
  22. package/dist/utils/handle-authentication.d.ts +2 -2
  23. package/dist/utils/handle-authentication.d.ts.map +1 -1
  24. package/dist/utils/log-authentication-instructions.d.ts +2 -2
  25. package/dist/utils/log-authentication-instructions.d.ts.map +1 -1
  26. package/dist/utils/log-authentication-instructions.js +7 -0
  27. package/dist/utils/negotiate-content-type.d.ts +4 -0
  28. package/dist/utils/negotiate-content-type.d.ts.map +1 -0
  29. package/dist/utils/negotiate-content-type.js +12 -0
  30. package/dist/utils/process-openapi-document.d.ts +7 -4
  31. package/dist/utils/process-openapi-document.d.ts.map +1 -1
  32. package/dist/utils/process-openapi-document.js +26 -11
  33. package/dist/utils/select-response-example.d.ts +3 -1
  34. package/dist/utils/select-response-example.d.ts.map +1 -1
  35. package/dist/utils/set-up-authentication-routes.d.ts.map +1 -1
  36. package/dist/utils/set-up-authentication-routes.js +2 -0
  37. package/dist/utils/set-up-device-authorization.d.ts +5 -0
  38. package/dist/utils/set-up-device-authorization.d.ts.map +1 -0
  39. package/dist/utils/set-up-device-authorization.js +192 -0
  40. package/dist/utils/streaming-response.d.ts +24 -0
  41. package/dist/utils/streaming-response.d.ts.map +1 -0
  42. package/dist/utils/streaming-response.js +65 -0
  43. package/package.json +12 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @scalar/mock-server
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#10290](https://github.com/scalar/scalar/pull/10290): Update Hono and its Node.js server, WebSocket, and OpenAPI integration dependencies.
8
+
9
+ Replace the deprecated `@hono/node-ws` adapter with Node server v2 WebSocket support. `createAsyncApiMockServer()` now returns `websocket` instead of `injectWebSocket`. Start the server with `serve({ fetch: app.fetch, websocket })` instead of calling `injectWebSocket(server)`.
10
+
11
+ AsyncAPI callers must upgrade to `@hono/node-server` v2. Node server v1 ignores the `websocket` option, so WebSocket channels will silently stop accepting connections if the server dependency is not upgraded.
12
+
13
+ - [#10286](https://github.com/scalar/scalar/pull/10286): Fix JSON and YAML exports for file and URL inputs. Add an origin option to resolve relative references in already loaded documents without fetching the root document again.
14
+ - [#10288](https://github.com/scalar/scalar/pull/10288): Upgrade documents to OpenAPI 3.2 when preparing mock responses, and use OpenAPI 3.2 for the empty-document default.
15
+ - [#10176](https://github.com/scalar/scalar/pull/10176): Generate finite SSE, JSON Lines, NDJSON, and JSON Sequence mock responses from OpenAPI 3.2 itemSchema definitions, including custom handler responses.
16
+
17
+ Honor named examples in custom stream handlers and keep media-type recognition consistent with stream serialization.
18
+
19
+ Use the same stream serializer as documentation examples. SSE objects without valid fields are omitted with a console warning per serialization call; other records still stream normally. The mock serializes each item separately to retain individual chunk writes.
20
+
21
+ - [#10191](https://github.com/scalar/scalar/pull/10191): Support OpenAPI 3.2 OAuth device authorization with verification codes, cancellable token polling, stored credentials, and OAuth metadata discovery. Add mock device authorization and approval endpoints with pending, denial, expiry, and polling backoff responses.
22
+
23
+ Use consistent form-encoded Basic credentials and environment substitution across OAuth token and refresh flows. Allow HTTP metadata and verification links on local development hosts and reserved test domains, coerce discovery fields consistently, and report device-code expiry clearly.
24
+
25
+ ### Patch Changes
26
+
27
+ - [#10211](https://github.com/scalar/scalar/pull/10211): Preserve literal data and tag groups when upgrading to OpenAPI 3.2, migrate XML metadata only in schemas, and remove incompatible legacy XML flags. Make 3.2 upgrades leave the input unchanged, match the complete source version, prevent previously inactive parameter settings from changing serialization, and report path-specific errors for detected compatibility issues that require an author's decision.
28
+
29
+ Tag `kind` values may change: navigation groups are classified from actual operation-tag usage instead of name substrings. Malformed 3.1 versions now report explicit errors, and successful 3.2 upgrades clone the input only once.
30
+
31
+ Expose `UpgradeIncompatibilityError` so Markdown generation can retain OpenAPI 3.1 for descriptions requiring author decisions instead of failing or silently changing semantics. Clone safety and malformed-version errors still propagate.
32
+
33
+ The mock server also retains OpenAPI 3.1 when the strict 3.2 migration reports compatibility diagnostics. Existing inline XML descriptions continue loading without inventing element names.
34
+
35
+ Read only own data properties during migration so inherited parameter lists, XML metadata, and reference targets cannot modify prototype-owned objects.
36
+
37
+ Add `upgrade(input, '3.2', { onIncompatible: 'collect' })` to return a complete document and compatibility diagnostics. Compatible descriptions upgrade to 3.2; incompatible descriptions retain 3.1 without partial transformations. Strict mode remains the default, and malformed-version and clone-safety errors still propagate. The Markdown converter and mock server now use the shared collect mode.
38
+
39
+ - [#10203](https://github.com/scalar/scalar/pull/10203): Add a picker for generated response examples with anyOf or oneOf schema variants.
40
+
41
+ Apply union selections to primitive and array examples in the shared generator without reusing the selection for nested unions.
42
+
43
+ The shared generator change also affects request examples, snippets, mock responses, and AsyncAPI payloads: root primitive/array unions now generate their chosen branch before type inference from sibling properties or items. For example, a string schema with `oneOf: [{ const: "first" }, { const: "second" }]` now generates `"first"` by default, and selecting the second branch generates `"second"`. Keywords for unrelated types do not force object/array generation. Root selections are consumed once; nested unions retain their own default or path-specific choice.
44
+
45
+ Do not show a response variant picker for an empty enum, which permits no valid alternatives.
46
+
47
+ Preserve the generated branch shape in mock HTTP responses instead of re-wrapping selected primitive values as arrays based on root sibling `items`. Explicit authored examples retain the existing array normalization.
48
+
49
+ - [#10206](https://github.com/scalar/scalar/pull/10206): Add generic document identity hooks for bundling and an explicit root URI option for reference proxies. Honor OpenAPI 3.2 `$self` through an OpenAPI plugin in workspace-store, including external documents and partial bundles, and enable it in OpenAPI bundling callers.
50
+
51
+ URI resolution now honors root-relative and protocol-relative URLs, query/fragment references, and trailing-slash directory bases for all bundler consumers. Absolute non-HTTP identifiers remain unchanged instead of becoming filesystem paths; loader support is unchanged. Relative HTTP references retain query strings and fragments and are emitted only when they round-trip to the original URL.
52
+
53
+ Preserve authored reference spellings through serialized partial bundles and editable exports, while keeping older OpenAPI resolution and configured loader restrictions unchanged.
54
+
55
+ Keep references matching authored root schema identifiers intact so schema labels and anchors retain their existing behavior.
56
+
3
57
  ## 0.14.4
4
58
 
5
59
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import { createNodeWebSocket } from '@hono/node-ws';
1
+ import { type WebSocketServerLike } from '@hono/node-server';
2
2
  import { Hono } from 'hono';
3
3
  import type { MessageDirection, MockTransport } from './transports/types.js';
4
4
  import type { MockServerLogger } from './types.js';
@@ -33,11 +33,10 @@ export type AsyncApiMockServerOptions = {
33
33
  export type AsyncApiMockServer = {
34
34
  /** The Hono app serving SSE channels and WebSocket upgrade routes. */
35
35
  app: Hono;
36
- /**
37
- * Attaches WebSocket handling to the running Node HTTP server returned by `@hono/node-server`'s
38
- * `serve()`. Must be called for WebSocket channels to accept connections.
39
- */
40
- injectWebSocket: ReturnType<typeof createNodeWebSocket>['injectWebSocket'];
36
+ /** Pass this option to `@hono/node-server`'s `serve()` to enable WebSocket channels. */
37
+ websocket: {
38
+ server: WebSocketServerLike;
39
+ };
41
40
  };
42
41
  /**
43
42
  * Create a mock server for an AsyncAPI 3.1 document — the event-driven counterpart of
@@ -45,12 +44,11 @@ export type AsyncApiMockServer = {
45
44
  * default) that emits realistic mock messages generated from the channel's message payload
46
45
  * schemas, the same way the REST mocker generates HTTP response bodies.
47
46
  *
48
- * WebSocket support requires attaching to the HTTP server after `serve()`:
47
+ * Pass the returned WebSocket option to `serve()`:
49
48
  *
50
49
  * ```ts
51
- * const { app, injectWebSocket } = await createAsyncApiMockServer({ document })
52
- * const server = serve({ fetch: app.fetch, port: 3000 })
53
- * injectWebSocket(server)
50
+ * const { app, websocket } = await createAsyncApiMockServer({ document })
51
+ * serve({ fetch: app.fetch, port: 3000, websocket })
54
52
  * ```
55
53
  */
56
54
  export declare function createAsyncApiMockServer(options: AsyncApiMockServerOptions): Promise<AsyncApiMockServer>;
@@ -1 +1 @@
1
- {"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,iBAAiB,CAAC,CAAA;CAC3E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0C9G"}
1
+ {"version":3,"file":"create-asyncapi-mock-server.d.ts","sourceRoot":"","sources":["../src/create-asyncapi-mock-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAoB,MAAM,mBAAmB,CAAA;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAK3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,EAAE,CAAA;IAE5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sEAAsE;IACtE,GAAG,EAAE,IAAI,CAAA;IACT,wFAAwF;IACxF,SAAS,EAAE;QAAE,MAAM,EAAE,mBAAmB,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqC9G"}
@@ -1,6 +1,7 @@
1
- import { createNodeWebSocket } from '@hono/node-ws';
1
+ import { upgradeWebSocket } from '@hono/node-server';
2
2
  import { Hono } from 'hono';
3
3
  import { cors } from 'hono/cors';
4
+ import { WebSocketServer } from 'ws';
4
5
  import { defaultTransports } from './transports/index.js';
5
6
  import { generateMessage } from './utils/generate-message.js';
6
7
  import { processAsyncApiDocument } from './utils/process-asyncapi-document.js';
@@ -12,20 +13,15 @@ import { resolveLogger } from './utils/resolve-logger.js';
12
13
  * default) that emits realistic mock messages generated from the channel's message payload
13
14
  * schemas, the same way the REST mocker generates HTTP response bodies.
14
15
  *
15
- * WebSocket support requires attaching to the HTTP server after `serve()`:
16
+ * Pass the returned WebSocket option to `serve()`:
16
17
  *
17
18
  * ```ts
18
- * const { app, injectWebSocket } = await createAsyncApiMockServer({ document })
19
- * const server = serve({ fetch: app.fetch, port: 3000 })
20
- * injectWebSocket(server)
19
+ * const { app, websocket } = await createAsyncApiMockServer({ document })
20
+ * serve({ fetch: app.fetch, port: 3000, websocket })
21
21
  * ```
22
22
  */
23
23
  export async function createAsyncApiMockServer(options) {
24
24
  const app = new Hono();
25
- // The Node WebSocket adapter must be created against the app before routes are registered so the
26
- // `upgradeWebSocket` helper shares this app's lifecycle. `injectWebSocket` is wired to the
27
- // HTTP server by the caller after `serve()`.
28
- const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
29
25
  const document = await processAsyncApiDocument(options.document);
30
26
  const channels = resolveChannels(document);
31
27
  const transports = [...defaultTransports, ...(options.transports ?? [])];
@@ -50,5 +46,5 @@ export async function createAsyncApiMockServer(options) {
50
46
  transport.register(channel, context);
51
47
  log(`[asyncapi] ${transport.name} -> ${channel.route} (channel "${channel.id}")`);
52
48
  }
53
- return { app, injectWebSocket };
49
+ return { app, websocket: { server: new WebSocketServer({ noServer: true }) } };
54
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,IAAI,EAA0B,MAAM,MAAM,CAAA;AAIjE,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwD5D;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2NtF"}
1
+ {"version":3,"file":"create-mock-server.d.ts","sourceRoot":"","sources":["../src/create-mock-server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,IAAI,EAA0B,MAAM,MAAM,CAAA;AAIjE,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwD5D;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,aAAa,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8NtF"}
@@ -1,3 +1,5 @@
1
+ import { normalize } from '@scalar/json-magic/helpers/normalize';
2
+ import { getRaw } from '@scalar/json-magic/magic-proxy';
1
3
  import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
4
  import { Hono } from 'hono';
3
5
  import { every } from 'hono/combine';
@@ -66,8 +68,14 @@ export async function createMockServer(configuration) {
66
68
  ...(operation ? { operation } : {}),
67
69
  }, 500);
68
70
  });
69
- /** Dereferenced OpenAPI document */
70
- const schema = await processOpenApiDocument(configuration?.document ?? configuration?.specification);
71
+ const input = configuration?.document ?? configuration?.specification;
72
+ const schema = await processOpenApiDocument(input, configuration?.origin);
73
+ const sourceDocument = typeof input === 'string' ? normalize(input) : input;
74
+ // Source locations need a bundled export so relative references remain usable outside the server.
75
+ const exportDocument = configuration?.origin ||
76
+ (typeof input === 'string' && (sourceDocument === null || typeof sourceDocument !== 'object'))
77
+ ? getRaw(schema)
78
+ : input;
71
79
  // Seed data from schemas with x-seed extension
72
80
  // This happens before routes are set up so data is available immediately
73
81
  const schemas = schema?.components?.schemas;
@@ -203,8 +211,8 @@ export async function createMockServer(configuration) {
203
211
  });
204
212
  });
205
213
  // OpenAPI JSON file
206
- app.get('/openapi.json', (c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification, 'json'));
214
+ app.get('/openapi.json', (c) => respondWithOpenApiDocument(c, exportDocument, 'json'));
207
215
  // OpenAPI YAML file
208
- app.get('/openapi.yaml', (c) => respondWithOpenApiDocument(c, configuration?.document ?? configuration?.specification, 'yaml'));
216
+ app.get('/openapi.yaml', (c) => respondWithOpenApiDocument(c, exportDocument, 'yaml'));
209
217
  return app;
210
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAcnC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,YA4HjF"}
1
+ {"version":3,"file":"mock-any-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-any-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAenC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,YA6IjF"}
@@ -1,16 +1,17 @@
1
1
  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 { getExampleFromSchema } from '@scalar/workspace-store/request-example';
4
- import { accepts } from 'hono/accepts';
5
4
  import { streamSSE } from 'hono/streaming';
6
5
  import { collectSseEvents, isEventStreamContentType } from '../utils/collect-sse-events.js';
7
6
  import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
8
7
  import { generateResponseExample } from '../utils/generate-response-example.js';
8
+ import { negotiateContentType } from '../utils/negotiate-content-type.js';
9
9
  import { normalizeResponseBody } from '../utils/normalize-response-body.js';
10
10
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
11
11
  import { pathParameters } from '../utils/path-parameters.js';
12
12
  import { selectResponseExample } from '../utils/select-response-example.js';
13
13
  import { serializeResponseBody } from '../utils/serialize-response-body.js';
14
+ import { getStreamingResponse, sendStreamingResponse } from '../utils/streaming-response.js';
14
15
  /**
15
16
  * Mock any response
16
17
  */
@@ -61,15 +62,17 @@ export function mockAnyResponse(c, operation) {
61
62
  return c.body(null);
62
63
  }
63
64
  // Content-Type
64
- const acceptedContentType = accepts(c, {
65
- header: 'Accept',
66
- supports: supportedContentTypes,
67
- default: supportedContentTypes.includes('application/json')
68
- ? 'application/json'
69
- : (supportedContentTypes[0] ?? 'text/plain;charset=UTF-8'),
70
- });
65
+ const acceptedContentType = negotiateContentType(c, selectedResponse.content);
71
66
  c.header('Content-Type', acceptedContentType);
72
67
  const acceptedResponse = selectedResponse?.content?.[acceptedContentType];
68
+ const streamingResponse = getStreamingResponse(acceptedResponse, acceptedContentType, {
69
+ exampleName: prefer.example,
70
+ variables: pathParameters(c),
71
+ });
72
+ if (streamingResponse) {
73
+ c.status(statusCode);
74
+ return sendStreamingResponse(c, streamingResponse);
75
+ }
73
76
  const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
74
77
  /** Generates the response body from the schema, or returns `undefined` when there is no schema. */
75
78
  const generateFromSchema = () => responseSchema ? generateResponseExample(responseSchema, pathParameters(c)) : undefined;
@@ -99,11 +102,23 @@ export function mockAnyResponse(c, operation) {
99
102
  // Body: a named/singular/first example if one is defined, otherwise generate
100
103
  // a value from the schema. `Prefer: example=<name>` picks a named example.
101
104
  const selectedExample = selectResponseExample(acceptedResponse, prefer.example);
102
- const body = selectedExample
103
- ? normalizeResponseBody(selectedExample.value, responseSchema)
104
- : responseSchema
105
- ? normalizeResponseBody(generateFromSchema(), responseSchema)
106
- : null;
105
+ const body = (() => {
106
+ if (selectedExample) {
107
+ return normalizeResponseBody(selectedExample.value, responseSchema);
108
+ }
109
+ if (!responseSchema) {
110
+ return null;
111
+ }
112
+ const generated = generateFromSchema();
113
+ // Schema-level examples are authored values too, so retain their array normalization.
114
+ if (responseSchema.example !== undefined ||
115
+ (Array.isArray(responseSchema.examples) && responseSchema.examples.length)) {
116
+ return normalizeResponseBody(generated, responseSchema);
117
+ }
118
+ // The generator already chooses the value shape, including root union branches.
119
+ // Re-inferring it from sibling items would wrap a selected primitive in an array.
120
+ return generated;
121
+ })();
107
122
  c.status(statusCode);
108
123
  const serializedBody = serializeResponseBody(body, acceptedContentType, responseSchema);
109
124
  // `JSON.stringify` returns `undefined` for an `undefined` body, which is an empty response.
@@ -1,9 +1,8 @@
1
1
  import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
2
  import type { Context } from 'hono';
3
- import type { StatusCode } from 'hono/utils/http-status';
4
3
  /**
5
4
  * Mock response using x-handler code.
6
5
  * Executes the handler and returns its result as the response.
7
6
  */
8
- export declare function mockHandlerResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Promise<(Response & import("hono").TypedResponse<null, StatusCode, "body">) | (Response & import("hono").TypedResponse<any, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
7
+ export declare function mockHandlerResponse(c: Context, operation: OpenAPIV3_1.OperationObject): Promise<Response>;
9
8
  //# sourceMappingURL=mock-handler-response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AA+HxD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,gMAyD3F"}
1
+ {"version":3,"file":"mock-handler-response.d.ts","sourceRoot":"","sources":["../../src/routes/mock-handler-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AA4HnC;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,eAAe,qBAoE3F"}
@@ -1,12 +1,13 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
- import { accepts } from 'hono/accepts';
3
2
  import { buildHandlerContext } from '../utils/build-handler-context.js';
4
3
  import { executeHandler } from '../utils/execute-handler.js';
5
4
  import { generateResponseExample } from '../utils/generate-response-example.js';
5
+ import { negotiateContentType } from '../utils/negotiate-content-type.js';
6
6
  import { normalizeResponseBody } from '../utils/normalize-response-body.js';
7
7
  import { parsePreferHeader } from '../utils/parse-prefer-header.js';
8
8
  import { pathParameters } from '../utils/path-parameters.js';
9
9
  import { selectResponseExample } from '../utils/select-response-example.js';
10
+ import { getStreamingResponse, sendStreamingResponse } from '../utils/streaming-response.js';
10
11
  /**
11
12
  * Get example response from OpenAPI spec for a given status code.
12
13
  * Returns the example value if found, or null if not available.
@@ -30,13 +31,7 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
30
31
  return null;
31
32
  }
32
33
  // Content-Type negotiation
33
- const acceptedContentType = accepts(c, {
34
- header: 'Accept',
35
- supports: supportedContentTypes,
36
- default: supportedContentTypes.includes('application/json')
37
- ? 'application/json'
38
- : (supportedContentTypes[0] ?? 'text/plain;charset=UTF-8'),
39
- });
34
+ const acceptedContentType = negotiateContentType(c, response.content);
40
35
  const acceptedResponse = response.content?.[acceptedContentType];
41
36
  if (!acceptedResponse) {
42
37
  return null;
@@ -125,6 +120,16 @@ export async function mockHandlerResponse(c, operation) {
125
120
  if (statusCode === 204) {
126
121
  return c.body(null);
127
122
  }
123
+ const response = getResolvedRef(operation.responses?.[String(statusCode)] ?? operation.responses?.default);
124
+ const contentType = negotiateContentType(c, response?.content);
125
+ const streamingResponse = getStreamingResponse(response?.content?.[contentType], contentType, {
126
+ body: result ?? undefined,
127
+ exampleName: parsePreferHeader(c.req.header('Prefer')).example,
128
+ variables: pathParameters(c),
129
+ });
130
+ if (streamingResponse) {
131
+ return sendStreamingResponse(c, streamingResponse);
132
+ }
128
133
  // Set Content-Type header for other responses
129
134
  c.header('Content-Type', 'application/json');
130
135
  // Return the handler result as JSON
package/dist/types.d.ts CHANGED
@@ -13,6 +13,8 @@ type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyo
13
13
  /** A sink for the informational log lines the mock servers print while starting up. */
14
14
  export type MockServerLogger = (line: string) => void;
15
15
  type BaseMockServerOptions = {
16
+ /** Source file path or URL used to resolve relative references in an already loaded document. */
17
+ origin?: string;
16
18
  /**
17
19
  * The OpenAPI document to use for mocking.
18
20
  * Can be a string (URL or file path) or an object.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAErD,KAAK,qBAAqB,GAAG;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,eAAe,GAAG,UAAU,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,6CAA6C;AAC7C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AAExF,wBAAwB;AACxB,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAErD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,GACzF;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAA;AAET,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAErD,KAAK,qBAAqB,GAAG;IAC3B,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEvC;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC,eAAe,CAAA;KAAE,KAAK,IAAI,CAAA;IAExF;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,eAAe,GAAG,UAAU,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAOnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AAqDD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CA4C/B"}
1
+ {"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAQnC,OAAO,EAAE,KAAK,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGjF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,GAAG,EAAE;QACH,IAAI,EAAE,GAAG,CAAA;QACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,CAAA;IACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CAAA;AA6DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,EACV,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,GACtC,OAAO,CAAC,oBAAoB,CAAC,CA4C/B"}
@@ -4,8 +4,10 @@ import { accepts } from 'hono/accepts';
4
4
  import { store } from '../libs/store.js';
5
5
  import { generateResponseExample } from './generate-response-example.js';
6
6
  import { normalizeResponseBody } from './normalize-response-body.js';
7
+ import { parsePreferHeader } from './parse-prefer-header.js';
7
8
  import { pathParameters } from './path-parameters.js';
8
9
  import { createStoreWrapper } from './store-wrapper.js';
10
+ import { getStreamingResponse } from './streaming-response.js';
9
11
  /**
10
12
  * Get example response from OpenAPI spec for a given status code.
11
13
  * Returns the example value if found, or null if not available.
@@ -35,6 +37,13 @@ function getExampleFromResponse(c, statusCode, responses) {
35
37
  if (!acceptedResponse) {
36
38
  return null;
37
39
  }
40
+ const streamingResponse = getStreamingResponse(acceptedResponse, acceptedContentType, {
41
+ exampleName: parsePreferHeader(c.req.header('Prefer')).example,
42
+ variables: pathParameters(c),
43
+ });
44
+ if (streamingResponse) {
45
+ return streamingResponse.body;
46
+ }
38
47
  const responseSchema = acceptedResponse.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
39
48
  // Extract example from example property or generate from schema
40
49
  return acceptedResponse.example !== undefined
@@ -3,6 +3,7 @@ import type { OpenAPIV3_2 } from '@scalar/openapi-types';
3
3
  type OAuth2Metadata = {
4
4
  issuer: string;
5
5
  authorization_endpoint?: string;
6
+ device_authorization_endpoint?: string;
6
7
  token_endpoint?: string;
7
8
  response_types_supported: string[];
8
9
  grant_types_supported: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"get-oauth2-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/get-oauth2-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,mEAAmE;AACnE,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wBAAwB,EAAE,MAAM,EAAE,CAAA;IAClC,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAA;AAED,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,GAAI,OAAO,WAAW,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,QAAQ,MAAM,KAAG,cAqBpG,CAAA"}
1
+ {"version":3,"file":"get-oauth2-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/get-oauth2-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIxD,mEAAmE;AACnE,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,6BAA6B,CAAC,EAAE,MAAM,CAAA;IACtC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,wBAAwB,EAAE,MAAM,EAAE,CAAA;IAClC,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAA;AAED,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,GAAI,OAAO,WAAW,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,QAAQ,MAAM,KAAG,cA0BpG,CAAA"}
@@ -2,16 +2,20 @@ import { getPathFromUrl } from './get-open-auth-token-urls.js';
2
2
  /** Advertises local mock endpoints instead of sending clients to the real authorization server. */
3
3
  export const getOAuth2Metadata = (flows, origin) => {
4
4
  const authorizationFlow = flows?.authorizationCode ?? flows?.implicit;
5
- const tokenFlow = flows?.authorizationCode ?? flows?.clientCredentials ?? flows?.password;
5
+ const tokenFlow = flows?.authorizationCode ?? flows?.clientCredentials ?? flows?.password ?? flows?.deviceAuthorization;
6
6
  const localUrl = (url) => new URL(getPathFromUrl(url), origin).href;
7
7
  const supportedFlows = [
8
8
  { flow: flows?.authorizationCode, grant: 'authorization_code', response: 'code' },
9
9
  { flow: flows?.implicit, grant: 'implicit', response: 'token' },
10
10
  { flow: flows?.clientCredentials, grant: 'client_credentials' },
11
11
  { flow: flows?.password, grant: 'password' },
12
+ { flow: flows?.deviceAuthorization, grant: 'urn:ietf:params:oauth:grant-type:device_code' },
12
13
  ].filter(({ flow }) => flow);
13
14
  return {
14
15
  issuer: origin,
16
+ ...(flows?.deviceAuthorization
17
+ ? { device_authorization_endpoint: localUrl(flows.deviceAuthorization.deviceAuthorizationUrl || '/oauth/device') }
18
+ : {}),
15
19
  ...(authorizationFlow
16
20
  ? { authorization_endpoint: localUrl(authorizationFlow.authorizationUrl ?? '/oauth/authorize') }
17
21
  : {}),
@@ -1,7 +1,7 @@
1
1
  import type { OpenAPI } from '@scalar/openapi-types';
2
2
  /**
3
- * Extract path from URL. Metadata is fetched at its exact declared URL, so its routes
4
- * preserve trailing slashes instead of using the token-route normalization.
3
+ * Extract path from URL. Metadata routes preserve trailing slashes because discovery
4
+ * fetches the exact declared URL, unlike normalized token routes.
5
5
  */
6
6
  export declare function getPathFromUrl(url: string, { preserveTrailingSlash }?: {
7
7
  preserveTrailingSlash?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"get-open-auth-token-urls.d.ts","sourceRoot":"","sources":["../../src/utils/get-open-auth-token-urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAG5E;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,EAAE,qBAA6B,EAAE,GAAE;IAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1E,MAAM,CAYR;AAiBD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,EAAE,CAuCxE"}
1
+ {"version":3,"file":"get-open-auth-token-urls.d.ts","sourceRoot":"","sources":["../../src/utils/get-open-auth-token-urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAuC,MAAM,uBAAuB,CAAA;AAGzF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,EAAE,qBAA6B,EAAE,GAAE;IAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1E,MAAM,CAYR;AAiBD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,EAAE,CAyCxE"}
@@ -1,7 +1,7 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
2
  /**
3
- * Extract path from URL. Metadata is fetched at its exact declared URL, so its routes
4
- * preserve trailing slashes instead of using the token-route normalization.
3
+ * Extract path from URL. Metadata routes preserve trailing slashes because discovery
4
+ * fetches the exact declared URL, unlike normalized token routes.
5
5
  */
6
6
  export function getPathFromUrl(url, { preserveTrailingSlash = false } = {}) {
7
7
  try {
@@ -41,13 +41,15 @@ export function getOpenAuthTokenUrls(schema) {
41
41
  if (!scheme || !isOAuth2Scheme(scheme)) {
42
42
  continue;
43
43
  }
44
- const flows = scheme.flows; // Type assertion no longer needed
44
+ const flows = scheme.flows;
45
45
  // Helper to safely add valid OAuth URLs
46
46
  const addOAuthUrl = (url) => {
47
47
  if (url && isValidOAuthUrl(url)) {
48
48
  oauthUrls.add(getPathFromUrl(url));
49
49
  }
50
50
  };
51
+ addOAuthUrl(flows?.deviceAuthorization?.tokenUrl);
52
+ addOAuthUrl(flows?.deviceAuthorization?.refreshUrl);
51
53
  addOAuthUrl(flows?.password?.tokenUrl);
52
54
  addOAuthUrl(flows?.password?.refreshUrl);
53
55
  addOAuthUrl(flows?.clientCredentials?.tokenUrl);
@@ -1,4 +1,4 @@
1
- import type { OpenAPIV3_1 } from '@scalar/openapi-types';
1
+ import type { OpenAPIV3_1, OpenAPIV3_2 } from '@scalar/openapi-types';
2
2
  import type { Context } from 'hono';
3
3
  /**
4
4
  * Handles authentication for incoming requests based on the OpenAPI document.
@@ -8,5 +8,5 @@ import type { Context } from 'hono';
8
8
  * only when *every* scheme it lists is satisfied. An empty requirement object (`{}`)
9
9
  * means authentication is optional and always passes.
10
10
  */
11
- export declare function handleAuthentication(schema?: OpenAPIV3_1.Document, operation?: OpenAPIV3_1.OperationObject): (c: Context, next: () => Promise<void>) => Promise<Response | void>;
11
+ export declare function handleAuthentication(schema?: OpenAPIV3_1.Document | OpenAPIV3_2.Document, operation?: OpenAPIV3_2.OperationObject): (c: Context, next: () => Promise<void>) => Promise<Response | void>;
12
12
  //# sourceMappingURL=handle-authentication.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handle-authentication.d.ts","sourceRoot":"","sources":["../../src/utils/handle-authentication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAiInC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,IAC3F,GAAG,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA4D/E"}
1
+ {"version":3,"file":"handle-authentication.d.ts","sourceRoot":"","sources":["../../src/utils/handle-authentication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEhF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAiInC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,GAAG,WAAW,CAAC,QAAQ,EACpD,SAAS,CAAC,EAAE,WAAW,CAAC,eAAe,IAEzB,GAAG,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA4D/E"}
@@ -1,4 +1,4 @@
1
- import type { OpenAPIV3_1 } from '@scalar/openapi-types';
1
+ import type { OpenAPIV3_2 } from '@scalar/openapi-types';
2
2
  import type { MockServerLogger } from '../types.js';
3
3
  /**
4
4
  * Log authentication instructions for different security schemes.
@@ -7,5 +7,5 @@ import type { MockServerLogger } from '../types.js';
7
7
  * security schemes the mock server cannot handle are printed unconditionally, so a silenced startup
8
8
  * still surfaces schemes that will not work.
9
9
  */
10
- export declare function logAuthenticationInstructions(securitySchemes: Record<string, OpenAPIV3_1.SecuritySchemeObject>, log?: MockServerLogger): void;
10
+ export declare function logAuthenticationInstructions(securitySchemes: Record<string, OpenAPIV3_2.SecuritySchemeObject>, log?: MockServerLogger): void;
11
11
  //# sourceMappingURL=log-authentication-instructions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"log-authentication-instructions.d.ts","sourceRoot":"","sources":["../../src/utils/log-authentication-instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACjE,GAAG,GAAE,gBAA8C,QAiIpD"}
1
+ {"version":3,"file":"log-authentication-instructions.d.ts","sourceRoot":"","sources":["../../src/utils/log-authentication-instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,oBAAoB,CAAC,EACjE,GAAG,GAAE,gBAA8C,QA4IpD"}
@@ -76,6 +76,13 @@ export function logAuthenticationInstructions(securitySchemes, log = (line) => c
76
76
  if (scheme.flows) {
77
77
  Object.keys(scheme.flows).forEach((flow) => {
78
78
  switch (flow) {
79
+ case 'deviceAuthorization':
80
+ log('✅ OAuth 2.0 Device Authorization Flow');
81
+ log(` POST ${getPathFromUrl(scheme.flows?.deviceAuthorization?.deviceAuthorizationUrl || '/oauth/device')}`);
82
+ log(' Send client_id and scope as form data, open verification_uri, and enter user_code.');
83
+ log(` Poll ${getPathFromUrl(scheme.flows?.deviceAuthorization?.tokenUrl || '/oauth/token')} with grant_type=urn:ietf:params:oauth:grant-type:device_code and device_code.`);
84
+ log('');
85
+ break;
79
86
  case 'implicit':
80
87
  log('✅ OAuth 2.0 Implicit Flow');
81
88
  log(' Use the following URL to initiate the OAuth 2.0 Implicit Flow:');
@@ -0,0 +1,4 @@
1
+ import type { Context } from 'hono';
2
+ /** Use the same response media-type preference for generated and custom-handler responses. */
3
+ export declare const negotiateContentType: (c: Context, content: Record<string, unknown> | undefined) => string;
4
+ //# sourceMappingURL=negotiate-content-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate-content-type.d.ts","sourceRoot":"","sources":["../../src/utils/negotiate-content-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAGnC,8FAA8F;AAC9F,eAAO,MAAM,oBAAoB,GAAI,GAAG,OAAO,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,MAS/F,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { accepts } from 'hono/accepts';
2
+ /** Use the same response media-type preference for generated and custom-handler responses. */
3
+ export const negotiateContentType = (c, content) => {
4
+ const supportedContentTypes = Object.keys(content ?? {});
5
+ return accepts(c, {
6
+ header: 'Accept',
7
+ supports: supportedContentTypes,
8
+ default: supportedContentTypes.includes('application/json')
9
+ ? 'application/json'
10
+ : (supportedContentTypes[0] ?? 'text/plain;charset=UTF-8'),
11
+ });
12
+ };
@@ -1,6 +1,6 @@
1
- import type { OpenAPIV3_1 } from '@scalar/openapi-types';
1
+ import type { OpenAPIV3_1, OpenAPIV3_2 } from '@scalar/openapi-types';
2
2
  /**
3
- * Processes an OpenAPI document by bundling external references, upgrading to OpenAPI 3.1,
3
+ * Processes an OpenAPI document by bundling external references, upgrading compatible input to OpenAPI 3.2,
4
4
  * and wrapping it so internal references stay intact but resolve lazily.
5
5
  *
6
6
  * Unlike a full dereference, the returned document keeps `$ref` nodes in place. Consumers
@@ -8,9 +8,12 @@ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
8
8
  * `$ref-value` exposed by the magic proxy. This avoids eagerly flattening (and duplicating)
9
9
  * the whole document up front.
10
10
  *
11
+ * Compatibility failures retain the OpenAPI 3.1 document and its version, without applying partial migrations.
12
+ *
11
13
  * @param document - The OpenAPI document to process. Can be a string (URL/path) or an object.
12
- * @returns A promise that resolves to the OpenAPI 3.1 document with lazily resolvable references.
14
+ * @param origin - Source file path or URL for resolving references in an already loaded document.
15
+ * @returns A promise that resolves to the document with lazily resolvable references.
13
16
  * @throws Error if the document cannot be processed or is invalid.
14
17
  */
15
- export declare function processOpenApiDocument(document: string | Record<string, any> | undefined): Promise<OpenAPIV3_1.Document>;
18
+ export declare function processOpenApiDocument(document: string | Record<string, any> | undefined, origin?: string): Promise<OpenAPIV3_1.Document | OpenAPIV3_2.Document>;
16
19
  //# sourceMappingURL=process-openapi-document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-openapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-openapi-document.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GACjD,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAsD/B"}
1
+ {"version":3,"file":"process-openapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-openapi-document.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAIrE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAClD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAkEtD"}
@@ -3,10 +3,12 @@ import { cwd } from 'node:process';
3
3
  import { bundle } from '@scalar/json-magic/bundle';
4
4
  import { fetchUrls, parseJson, parseYaml, readFiles } from '@scalar/json-magic/bundle/plugins/node';
5
5
  import { isFilePath } from '@scalar/json-magic/helpers/is-file-path';
6
+ import { isHttpUrl } from '@scalar/json-magic/helpers/is-http-url';
6
7
  import { createMagicProxy } from '@scalar/json-magic/magic-proxy';
7
8
  import { upgrade } from '@scalar/openapi-upgrader';
9
+ import { openApiDocument, resolveOpenApiDocument } from '@scalar/workspace-store/plugins/bundler';
8
10
  /**
9
- * Processes an OpenAPI document by bundling external references, upgrading to OpenAPI 3.1,
11
+ * Processes an OpenAPI document by bundling external references, upgrading compatible input to OpenAPI 3.2,
10
12
  * and wrapping it so internal references stay intact but resolve lazily.
11
13
  *
12
14
  * Unlike a full dereference, the returned document keeps `$ref` nodes in place. Consumers
@@ -14,16 +16,19 @@ import { upgrade } from '@scalar/openapi-upgrader';
14
16
  * `$ref-value` exposed by the magic proxy. This avoids eagerly flattening (and duplicating)
15
17
  * the whole document up front.
16
18
  *
19
+ * Compatibility failures retain the OpenAPI 3.1 document and its version, without applying partial migrations.
20
+ *
17
21
  * @param document - The OpenAPI document to process. Can be a string (URL/path) or an object.
18
- * @returns A promise that resolves to the OpenAPI 3.1 document with lazily resolvable references.
22
+ * @param origin - Source file path or URL for resolving references in an already loaded document.
23
+ * @returns A promise that resolves to the document with lazily resolvable references.
19
24
  * @throws Error if the document cannot be processed or is invalid.
20
25
  */
21
- export async function processOpenApiDocument(document) {
26
+ export async function processOpenApiDocument(document, origin) {
22
27
  // Handle empty/undefined input gracefully
23
28
  if (!document || (typeof document === 'object' && Object.keys(document).length === 0)) {
24
- // Return a minimal valid OpenAPI 3.1 document
29
+ // Return a minimal valid OpenAPI 3.2 document
25
30
  return {
26
- openapi: '3.1.0',
31
+ openapi: '3.2.0',
27
32
  info: {
28
33
  title: 'Mock API',
29
34
  version: '1.0.0',
@@ -35,12 +40,20 @@ export async function processOpenApiDocument(document) {
35
40
  // Confine local file `$ref`s to the document's own directory (or the working directory when the
36
41
  // document is an object or inline string), and refuse to fetch private or internal addresses.
37
42
  // Without these guards a `$ref` could read arbitrary local files or reach internal services.
38
- const basePath = typeof document === 'string' && isFilePath(document) ? path.dirname(path.resolve(document)) : cwd();
43
+ const source = origin ?? document;
44
+ const basePath = typeof source === 'string' && isFilePath(source) ? path.dirname(path.resolve(source)) : cwd();
39
45
  try {
40
46
  // Bundle external references with Node.js plugins
41
47
  // Include parseJson and parseYaml to handle string inputs
42
48
  bundled = await bundle(document, {
43
- plugins: [parseJson(), parseYaml(), readFiles({ basePath }), fetchUrls({ blockPrivateNetworks: true })],
49
+ origin,
50
+ plugins: [
51
+ openApiDocument(),
52
+ parseJson(),
53
+ parseYaml(),
54
+ readFiles({ basePath }),
55
+ fetchUrls({ blockPrivateNetworks: true }),
56
+ ],
44
57
  treeShake: false,
45
58
  });
46
59
  }
@@ -50,18 +63,20 @@ export async function processOpenApiDocument(document) {
50
63
  if (!bundled || typeof bundled !== 'object') {
51
64
  throw new Error('Bundled document is invalid: expected an object');
52
65
  }
66
+ // Upgrading must not activate a $self field authored in an older OpenAPI version.
67
+ const retrievalUri = origin ?? (typeof document === 'string' && (isFilePath(document) || isHttpUrl(document)) ? document : '/');
68
+ const documentUri = resolveOpenApiDocument(bundled, retrievalUri)?.baseUri;
53
69
  let upgraded;
54
70
  try {
55
- // Upgrade to OpenAPI 3.1
56
- upgraded = upgrade(bundled, '3.1');
71
+ upgraded = upgrade(bundled, '3.2', { onIncompatible: 'collect' }).document;
57
72
  }
58
73
  catch (error) {
59
- throw new Error(`Failed to upgrade OpenAPI document to 3.1: ${error instanceof Error ? error.message : String(error)}`);
74
+ throw new Error(`Failed to upgrade OpenAPI document to 3.2: ${error instanceof Error ? error.message : String(error)}`);
60
75
  }
61
76
  if (!upgraded) {
62
77
  throw new Error('Upgraded document is invalid: upgrade returned null or undefined');
63
78
  }
64
79
  // Wrap the document in a magic proxy so internal references resolve lazily via `$ref-value`.
65
80
  // External references were already pulled inline by `bundle` above, so only local `$ref`s remain.
66
- return createMagicProxy(upgraded);
81
+ return createMagicProxy(upgraded, { documentUri });
67
82
  }
@@ -16,7 +16,9 @@ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
16
16
  * caller still gets a schema-generated body. An unknown `exampleName` simply
17
17
  * falls through to the later steps.
18
18
  */
19
- export declare const selectResponseExample: (mediaType: OpenAPIV3_1.MediaTypeObject | undefined, exampleName?: string) => {
19
+ export declare const selectResponseExample: <T extends Pick<OpenAPIV3_1.MediaTypeObject, "example" | "examples"> & {
20
+ schema?: unknown;
21
+ }>(mediaType: T | undefined, exampleName?: string) => {
20
22
  value: unknown;
21
23
  } | undefined;
22
24
  //# sourceMappingURL=select-response-example.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-response-example.d.ts","sourceRoot":"","sources":["../../src/utils/select-response-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GAChC,WAAW,WAAW,CAAC,eAAe,GAAG,SAAS,EAClD,cAAc,MAAM,KACnB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAoCvB,CAAA"}
1
+ {"version":3,"file":"select-response-example.d.ts","sourceRoot":"","sources":["../../src/utils/select-response-example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGxD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GAChC,CAAC,SAAS,IAAI,CAAC,WAAW,CAAC,eAAe,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAE1F,WAAW,CAAC,GAAG,SAAS,EACxB,cAAc,MAAM,KACnB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAoCvB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"set-up-authentication-routes.d.ts","sourceRoot":"","sources":["../../src/utils/set-up-authentication-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAE5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAQhC;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,QA+G7E"}
1
+ {"version":3,"file":"set-up-authentication-routes.d.ts","sourceRoot":"","sources":["../../src/utils/set-up-authentication-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,uBAAuB,CAAA;AAE5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAShC;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,QAiH7E"}
@@ -3,11 +3,13 @@ import { respondWithAuthorizePage } from '../routes/respond-with-authorize-page.
3
3
  import { respondWithToken } from '../routes/respond-with-token.js';
4
4
  import { getOAuth2Metadata } from './get-oauth2-metadata.js';
5
5
  import { getOpenAuthTokenUrls, getPathFromUrl } from './get-open-auth-token-urls.js';
6
+ import { setUpDeviceAuthorization } from './set-up-device-authorization.js';
6
7
  /**
7
8
  * Helper function to set up authentication routes for OAuth 2.0 flows
8
9
  */
9
10
  export function setUpAuthenticationRoutes(app, schema) {
10
11
  const securitySchemes = schema?.components?.securitySchemes || {};
12
+ setUpDeviceAuthorization(app, schema);
11
13
  // Set up authentication routes for OAuth 2.0 flows
12
14
  getOpenAuthTokenUrls(schema).forEach((tokenUrl) => {
13
15
  app.post(tokenUrl, (c) => {
@@ -0,0 +1,5 @@
1
+ import type { OpenAPI } from '@scalar/openapi-types';
2
+ import type { Hono } from 'hono';
3
+ /** Registers a local RFC8628 approval flow before the permissive mock token handlers. */
4
+ export declare const setUpDeviceAuthorization: (app: Hono, document?: OpenAPI.Document) => void;
5
+ //# sourceMappingURL=set-up-device-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-up-device-authorization.d.ts","sourceRoot":"","sources":["../../src/utils/set-up-device-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,uBAAuB,CAAA;AAEjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AA0FhC,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,GAAI,KAAK,IAAI,EAAE,WAAW,OAAO,CAAC,QAAQ,KAAG,IAsJjF,CAAA"}
@@ -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
+ };
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "swagger",
17
17
  "cli"
18
18
  ],
19
- "version": "0.14.4",
19
+ "version": "0.15.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.13.0",
57
+ "@scalar/json-magic": "0.15.0",
58
+ "@scalar/openapi-types": "0.9.7",
59
+ "@scalar/openapi-upgrader": "0.3.0",
60
+ "@scalar/types": "0.21.0",
61
+ "@scalar/workspace-store": "0.65.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": {