@scalar/mock-server 0.12.13 → 0.14.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 (64) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/create-asyncapi-mock-server.d.ts +8 -2
  3. package/dist/create-asyncapi-mock-server.d.ts.map +1 -1
  4. package/dist/create-asyncapi-mock-server.js +5 -2
  5. package/dist/create-mock-server.d.ts.map +1 -1
  6. package/dist/create-mock-server.js +112 -7
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/routes/mock-any-response.d.ts +1 -4
  10. package/dist/routes/mock-any-response.d.ts.map +1 -1
  11. package/dist/routes/mock-any-response.js +43 -22
  12. package/dist/routes/mock-handler-response.d.ts.map +1 -1
  13. package/dist/routes/mock-handler-response.js +2 -1
  14. package/dist/types.d.ts +15 -0
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/utils/build-handler-context.d.ts +0 -2
  17. package/dist/utils/build-handler-context.d.ts.map +1 -1
  18. package/dist/utils/build-handler-context.js +3 -4
  19. package/dist/utils/build-seed-context.d.ts +5 -23
  20. package/dist/utils/build-seed-context.d.ts.map +1 -1
  21. package/dist/utils/build-seed-context.js +1 -49
  22. package/dist/utils/collect-sse-events.d.ts +30 -0
  23. package/dist/utils/collect-sse-events.d.ts.map +1 -0
  24. package/dist/utils/collect-sse-events.js +137 -0
  25. package/dist/utils/execute-handler.d.ts +5 -2
  26. package/dist/utils/execute-handler.d.ts.map +1 -1
  27. package/dist/utils/execute-handler.js +11 -13
  28. package/dist/utils/execute-seed.d.ts +5 -2
  29. package/dist/utils/execute-seed.d.ts.map +1 -1
  30. package/dist/utils/execute-seed.js +13 -21
  31. package/dist/utils/hono-route-from-path.d.ts +16 -3
  32. package/dist/utils/hono-route-from-path.d.ts.map +1 -1
  33. package/dist/utils/hono-route-from-path.js +98 -5
  34. package/dist/utils/log-authentication-instructions.d.ts +7 -2
  35. package/dist/utils/log-authentication-instructions.d.ts.map +1 -1
  36. package/dist/utils/log-authentication-instructions.js +64 -60
  37. package/dist/utils/path-parameters.d.ts +13 -0
  38. package/dist/utils/path-parameters.d.ts.map +1 -0
  39. package/dist/utils/path-parameters.js +17 -0
  40. package/dist/utils/process-openapi-document.d.ts.map +1 -1
  41. package/dist/utils/process-openapi-document.js +8 -1
  42. package/dist/utils/replace-circular-markers.d.ts +22 -0
  43. package/dist/utils/replace-circular-markers.d.ts.map +1 -0
  44. package/dist/utils/replace-circular-markers.js +226 -0
  45. package/dist/utils/request-matches-pinned-query.d.ts +10 -0
  46. package/dist/utils/request-matches-pinned-query.d.ts.map +1 -0
  47. package/dist/utils/request-matches-pinned-query.js +13 -0
  48. package/dist/utils/resolve-logger.d.ts +12 -0
  49. package/dist/utils/resolve-logger.d.ts.map +1 -0
  50. package/dist/utils/resolve-logger.js +16 -0
  51. package/dist/utils/sandbox.d.ts +25 -0
  52. package/dist/utils/sandbox.d.ts.map +1 -0
  53. package/dist/utils/sandbox.js +252 -0
  54. package/dist/utils/serialize-response-body.d.ts +11 -0
  55. package/dist/utils/serialize-response-body.d.ts.map +1 -0
  56. package/dist/utils/serialize-response-body.js +88 -0
  57. package/dist/utils/split-path-key.d.ts +30 -0
  58. package/dist/utils/split-path-key.d.ts.map +1 -0
  59. package/dist/utils/split-path-key.js +77 -0
  60. package/dist/utils/store-wrapper.d.ts +1 -1
  61. package/dist/utils/store-wrapper.d.ts.map +1 -1
  62. package/dist/utils/validate-request.d.ts.map +1 -1
  63. package/dist/utils/validate-request.js +13 -2
  64. package/package.json +7 -6
@@ -1,60 +1,12 @@
1
- import { faker } from '@faker-js/faker';
2
1
  import { store } from '../libs/store.js';
3
2
  import { createStoreWrapper } from './store-wrapper.js';
4
3
  /**
5
- * Build the seed context with a seed helper function.
6
- * The seed helper automatically uses the schema key as the collection name.
4
+ * Build the seed context for a schema.
7
5
  */
8
6
  export function buildSeedContext(schemaKey) {
9
7
  const { wrappedStore } = createStoreWrapper(store);
10
- /**
11
- * Seed helper function that provides a Laravel-inspired API.
12
- */
13
- const seedHelper = ((arg1, arg2) => {
14
- // Case 1: seed.count(n, factory)
15
- if (typeof arg1 === 'number' && typeof arg2 === 'function') {
16
- const count = arg1;
17
- const factory = arg2;
18
- const items = [];
19
- for (let i = 0; i < count; i++) {
20
- const item = factory();
21
- const created = wrappedStore.create(schemaKey, item);
22
- items.push(created);
23
- }
24
- return items;
25
- }
26
- // Case 2: seed(array)
27
- if (Array.isArray(arg1)) {
28
- const items = [];
29
- for (const item of arg1) {
30
- const created = wrappedStore.create(schemaKey, item);
31
- items.push(created);
32
- }
33
- return items;
34
- }
35
- // Case 3: seed(factory) - single item
36
- if (typeof arg1 === 'function') {
37
- const factory = arg1;
38
- const item = factory();
39
- const created = wrappedStore.create(schemaKey, item);
40
- return created;
41
- }
42
- throw new Error('Invalid seed() usage. Use seed.count(n, factory), seed(array), or seed(factory)');
43
- });
44
- // Add count method to the function
45
- seedHelper.count = (n, factory) => {
46
- const items = [];
47
- for (let i = 0; i < n; i++) {
48
- const item = factory();
49
- const created = wrappedStore.create(schemaKey, item);
50
- items.push(created);
51
- }
52
- return items;
53
- };
54
8
  return {
55
9
  store: wrappedStore,
56
- faker,
57
- seed: seedHelper,
58
10
  schema: schemaKey,
59
11
  };
60
12
  }
@@ -0,0 +1,30 @@
1
+ import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ /** One event of a mocked Server-Sent Events response. */
3
+ type SseEvent = {
4
+ /** Already-framed event text when `framed` is true, otherwise the `data` payload of one event. */
5
+ text: string;
6
+ /** Whether `text` is SSE framing of its own, to be written as is rather than wrapped in a `data:` line. */
7
+ framed: boolean;
8
+ };
9
+ /**
10
+ * The events a `text/event-stream` response emits, in order.
11
+ *
12
+ * Examples win over the schema, mirroring `selectResponseExample`, but a stream reads them as a
13
+ * sequence rather than a single body:
14
+ * 1. A named example requested via `Prefer: example=<name>`.
15
+ * 2. The singular `example` keyword.
16
+ * 3. Every entry of the `examples` map, in declaration order — an event stream that documents a
17
+ * `summary` and a `row` example is documenting the two events it sends. A map of nothing but
18
+ * complete framing is the exception: those are alternative streams, so only the first is served.
19
+ * 4. Nothing declared: a schema-generated payload, repeated so the stream has more than one event.
20
+ *
21
+ * `generate` is a callback so the schema is only turned into an example when no example is declared.
22
+ */
23
+ export declare const collectSseEvents: (mediaType: OpenAPIV3_1.MediaTypeObject | undefined, { exampleName, generate }: {
24
+ exampleName?: string;
25
+ generate: () => unknown;
26
+ }) => SseEvent[];
27
+ /** Whether a media type is Server-Sent Events, ignoring parameters such as `; charset=utf-8`. */
28
+ export declare const isEventStreamContentType: (contentType: string | undefined) => boolean;
29
+ export {};
30
+ //# sourceMappingURL=collect-sse-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-sse-events.d.ts","sourceRoot":"","sources":["../../src/utils/collect-sse-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAcxD,yDAAyD;AACzD,KAAK,QAAQ,GAAG;IACd,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAA;IACZ,2GAA2G;IAC3G,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAgFD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,WAAW,CAAC,eAAe,GAAG,SAAS,EAClD,2BAA2B;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,OAAO,CAAA;CAAE,KAC3E,QAAQ,EAoDV,CAAA;AAED,iGAAiG;AACjG,eAAO,MAAM,wBAAwB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,OACf,CAAA"}
@@ -0,0 +1,137 @@
1
+ import { parseMimeType } from '@scalar/helpers/http/mime-type';
2
+ import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
3
+ /**
4
+ * How many events a schema-generated `text/event-stream` response emits.
5
+ *
6
+ * A single event barely exercises a client's read loop, and an endless stream would never let a
7
+ * request finish, so the mock sends a short, finite burst and closes.
8
+ */
9
+ const GENERATED_EVENT_COUNT = 3;
10
+ /** The field prefixes an SSE line may start with (`data`, `event`, `id`, `retry`). */
11
+ const SSE_FIELD = /^(?:data|event|id|retry):/;
12
+ /**
13
+ * Whether text is already Server-Sent Events framing, so it goes to the wire as its own framing.
14
+ *
15
+ * Some documents spell the wire format out in their example (`data: {"type":"edit"}`) instead of
16
+ * describing a single event payload. Wrapping that in another `data:` line would hand the client the
17
+ * framing as its payload, so it is written as is, with only its terminating blank line normalized.
18
+ *
19
+ * The test is deliberately narrow: real framing opens with an SSE field or a `:` comment on its first
20
+ * line and carries at least one `data:` line. Prose that merely happens to contain a colon
21
+ * (`user created\nid: 42`) is not framing — passing it through would make a compliant client dispatch
22
+ * nothing at all.
23
+ */
24
+ const isFramed = (text) => {
25
+ const lines = text.split(/\r\n|\r|\n/).filter((line) => line.trim() !== '');
26
+ const [firstLine] = lines;
27
+ if (firstLine === undefined) {
28
+ return false;
29
+ }
30
+ // Text made only of `:` comments is the keep-alive form nothing but a stream writes, so it is
31
+ // framing even though it carries no event of its own.
32
+ if (lines.every((line) => line.startsWith(':'))) {
33
+ return true;
34
+ }
35
+ return (firstLine.startsWith(':') || SSE_FIELD.test(firstLine)) && lines.some((line) => line.startsWith('data:'));
36
+ };
37
+ /**
38
+ * Closes framed text with the blank line that ends an SSE event.
39
+ *
40
+ * Documents terminate their framing inconsistently — a YAML block scalar drops all but one newline,
41
+ * and a Windows-authored document uses CRLF — so the tail is normalized to exactly one blank line in
42
+ * the document's own line ending instead of being appended to blindly.
43
+ */
44
+ const terminate = (text) => {
45
+ const lineEnding = text.includes('\r\n') ? '\r\n' : '\n';
46
+ // Walk back over the trailing blank lines, taking the indentation a block scalar left on them.
47
+ // Spaces are only dropped once a line break is found past them, so spaces inside the last field's
48
+ // value survive. A scan rather than a regex: the pattern for this needs nested quantifiers, which
49
+ // backtrack quadratically on a long run of spaces.
50
+ let cursor = text.length;
51
+ let end = text.length;
52
+ while (cursor > 0) {
53
+ const character = text[cursor - 1];
54
+ if (character === '\n' || character === '\r') {
55
+ cursor -= 1;
56
+ end = cursor;
57
+ }
58
+ else if (character === ' ' || character === '\t') {
59
+ cursor -= 1;
60
+ }
61
+ else {
62
+ break;
63
+ }
64
+ }
65
+ return `${text.slice(0, end)}${lineEnding}${lineEnding}`;
66
+ };
67
+ /** Turns one payload into an event, serializing anything that is not already text. */
68
+ const toEvent = (payload) => {
69
+ if (typeof payload !== 'string') {
70
+ // `undefined` has no JSON representation, so fall back to `null` rather than an empty event.
71
+ return { text: JSON.stringify(payload) ?? 'null', framed: false };
72
+ }
73
+ return isFramed(payload) ? { text: terminate(payload), framed: true } : { text: payload, framed: false };
74
+ };
75
+ /**
76
+ * Expands one payload into the events it stands for. An array is read as the sequence of events the
77
+ * endpoint emits, not as a single event carrying a JSON array — that is the shape a stream describes.
78
+ */
79
+ const expand = (payload) => (Array.isArray(payload) ? payload.map(toEvent) : [toEvent(payload)]);
80
+ /**
81
+ * The events a `text/event-stream` response emits, in order.
82
+ *
83
+ * Examples win over the schema, mirroring `selectResponseExample`, but a stream reads them as a
84
+ * sequence rather than a single body:
85
+ * 1. A named example requested via `Prefer: example=<name>`.
86
+ * 2. The singular `example` keyword.
87
+ * 3. Every entry of the `examples` map, in declaration order — an event stream that documents a
88
+ * `summary` and a `row` example is documenting the two events it sends. A map of nothing but
89
+ * complete framing is the exception: those are alternative streams, so only the first is served.
90
+ * 4. Nothing declared: a schema-generated payload, repeated so the stream has more than one event.
91
+ *
92
+ * `generate` is a callback so the schema is only turned into an example when no example is declared.
93
+ */
94
+ export const collectSseEvents = (mediaType, { exampleName, generate }) => {
95
+ const { example, examples } = mediaType ?? {};
96
+ if (exampleName && examples && exampleName in examples) {
97
+ const value = getResolvedRef(examples[exampleName])?.value;
98
+ if (value !== undefined) {
99
+ return expand(value);
100
+ }
101
+ }
102
+ if (example !== undefined) {
103
+ return expand(example);
104
+ }
105
+ if (examples) {
106
+ // An Example Object that only carries an `externalValue` has no value to send, so it is skipped
107
+ // rather than turned into a `data: null` event.
108
+ const values = Object.values(examples)
109
+ .map((entry) => getResolvedRef(entry)?.value)
110
+ .filter((value) => value !== undefined);
111
+ const [firstValue] = values;
112
+ if (values.length > 0) {
113
+ const events = values.flatMap(expand);
114
+ // An example that is framing already describes a whole stream, so a map of nothing but those
115
+ // lists alternative streams rather than consecutive events. Sending them back to back would
116
+ // replay a terminal event such as `[DONE]`, so only the first is served —
117
+ // `Prefer: example=<name>` picks another one. A map that mixes framing with payloads is still
118
+ // one sequence: there the framing is a single chunk of it, such as a documented `[DONE]`.
119
+ return events.every((event) => event.framed) ? expand(firstValue) : events;
120
+ }
121
+ }
122
+ const generated = generate();
123
+ // Without a schema there is nothing to send, so the stream opens and closes without an event.
124
+ if (generated === undefined) {
125
+ return [];
126
+ }
127
+ const events = expand(generated);
128
+ const [firstEvent] = events;
129
+ // Only a lone generated payload is repeated. Framing generated from the schema already spells the
130
+ // whole stream out (repeating it would replay a terminal event such as `[DONE]`), and a generated
131
+ // sequence of several events is a sequence already.
132
+ return events.length === 1 && firstEvent && !firstEvent.framed
133
+ ? Array.from({ length: GENERATED_EVENT_COUNT }, () => firstEvent)
134
+ : events;
135
+ };
136
+ /** Whether a media type is Server-Sent Events, ignoring parameters such as `; charset=utf-8`. */
137
+ export const isEventStreamContentType = (contentType) => parseMimeType(contentType).essence === 'text/event-stream';
@@ -6,8 +6,11 @@ type HandlerExecutionResult = {
6
6
  result: any;
7
7
  };
8
8
  /**
9
- * Execute handler code in a sandboxed environment.
10
- * The code has access only to the provided context (store, faker, req, res).
9
+ * Execute handler code inside the QuickJS sandbox.
10
+ *
11
+ * The handler can only reach the `store` and `faker` bridges and the injected
12
+ * `req`/`res` inputs. It cannot touch the host runtime, so even untrusted code
13
+ * from a remote or `$ref`-loaded document is safe to run.
11
14
  */
12
15
  export declare function executeHandler(code: string, context: HandlerContext): Promise<HandlerExecutionResult>;
13
16
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"execute-handler.d.ts","sourceRoot":"","sources":["../../src/utils/execute-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAE7D;;GAEG;AACH,KAAK,sBAAsB,GAAG;IAC5B,MAAM,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAqB3G"}
1
+ {"version":3,"file":"execute-handler.d.ts","sourceRoot":"","sources":["../../src/utils/execute-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAG7D;;GAEG;AACH,KAAK,sBAAsB,GAAG;IAC5B,MAAM,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAQ3G"}
@@ -1,18 +1,16 @@
1
+ import { runInSandbox } from './sandbox.js';
1
2
  /**
2
- * Execute handler code in a sandboxed environment.
3
- * The code has access only to the provided context (store, faker, req, res).
3
+ * Execute handler code inside the QuickJS sandbox.
4
+ *
5
+ * The handler can only reach the `store` and `faker` bridges and the injected
6
+ * `req`/`res` inputs. It cannot touch the host runtime, so even untrusted code
7
+ * from a remote or `$ref`-loaded document is safe to run.
4
8
  */
5
9
  export async function executeHandler(code, context) {
6
- // Create a function that executes the handler code with the context
7
- // Using Function constructor to create a sandboxed environment
8
- // The code is wrapped in a function body that returns the result
9
- const handlerFunction = new Function('store', 'faker', 'req', 'res', `
10
- ${code}
11
- `);
12
- const result = handlerFunction(context.store, context.faker, context.req, context.res);
13
- // If the result is a Promise, await it
14
- if (result instanceof Promise) {
15
- return { result: await result };
16
- }
10
+ const result = await runInSandbox({
11
+ code,
12
+ store: context.store,
13
+ jsonGlobals: { req: context.req, res: context.res },
14
+ });
17
15
  return { result };
18
16
  }
@@ -6,8 +6,11 @@ type SeedExecutionResult = {
6
6
  result: any;
7
7
  };
8
8
  /**
9
- * Execute seed code in a sandboxed environment.
10
- * The code has access only to the provided context (store, faker, seed, schema).
9
+ * Execute seed code inside the QuickJS sandbox.
10
+ *
11
+ * The seed code can only reach the `store` and `faker` bridges and the `seed`
12
+ * helper, which persists generated items through the store. It cannot touch the
13
+ * host runtime.
11
14
  */
12
15
  export declare function executeSeed(code: string, context: SeedContext): Promise<SeedExecutionResult>;
13
16
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"execute-seed.d.ts","sourceRoot":"","sources":["../../src/utils/execute-seed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD;;GAEG;AACH,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA4BlG"}
1
+ {"version":3,"file":"execute-seed.d.ts","sourceRoot":"","sources":["../../src/utils/execute-seed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAGvD;;GAEG;AACH,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CASlG"}
@@ -1,25 +1,17 @@
1
+ import { runInSandbox } from './sandbox.js';
1
2
  /**
2
- * Execute seed code in a sandboxed environment.
3
- * The code has access only to the provided context (store, faker, seed, schema).
3
+ * Execute seed code inside the QuickJS sandbox.
4
+ *
5
+ * The seed code can only reach the `store` and `faker` bridges and the `seed`
6
+ * helper, which persists generated items through the store. It cannot touch the
7
+ * host runtime.
4
8
  */
5
9
  export async function executeSeed(code, context) {
6
- // Create a function that executes the seed code with the context
7
- // Using Function constructor to create a sandboxed environment
8
- // The code is wrapped in a function body that returns the result
9
- const seedFunction = new Function('store', 'faker', 'seed', 'schema', `
10
- ${code}
11
- `);
12
- // Execute the seed function with the context
13
- try {
14
- const result = seedFunction(context.store, context.faker, context.seed, context.schema);
15
- // If the result is a Promise, await it
16
- if (result instanceof Promise) {
17
- return { result: await result };
18
- }
19
- return { result };
20
- }
21
- catch (error) {
22
- // Re-throw to be caught by the caller
23
- throw error;
24
- }
10
+ const result = await runInSandbox({
11
+ code,
12
+ store: context.store,
13
+ jsonGlobals: { schema: context.schema },
14
+ includeSeed: true,
15
+ });
16
+ return { result };
25
17
  }
@@ -1,6 +1,19 @@
1
+ /** Prefix for the parameters we synthesize to match a literal path segment verbatim. */
2
+ export declare const LITERAL_PARAMETER_PREFIX = "__scalar_literal_";
1
3
  /**
2
- * Convert path to route
3
- * Example: /posts/{id} -> /posts/:id
4
+ * Convert an OpenAPI path key into a Hono route.
5
+ *
6
+ * Example: `/posts/{id}` → `/posts/:id`
7
+ *
8
+ * A segment whose literal text would be read as routing syntax is registered as a single parameter
9
+ * with an explicit pattern instead, because Hono allows only one parameter per segment and that is
10
+ * the only way to make it match such a segment verbatim. The request then routes to the right
11
+ * operation, but the path parameters of that one segment are no longer bound by name: they surface
12
+ * as the synthesized parameter, and a required path parameter in it reads as missing to request
13
+ * validation. Hono cannot express both at once, and matching the wrong operation is worse.
14
+ *
15
+ * A query string in the key (`/v1/messages?beta=true`) is dropped here — it is matched against the
16
+ * incoming request separately, see `splitPathKey`.
4
17
  */
5
- export declare function honoRouteFromPath(path: string): string;
18
+ export declare const honoRouteFromPath: (path: string) => string;
6
19
  //# sourceMappingURL=hono-route-from-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hono-route-from-path.d.ts","sourceRoot":"","sources":["../../src/utils/hono-route-from-path.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,UAE7C"}
1
+ {"version":3,"file":"hono-route-from-path.d.ts","sourceRoot":"","sources":["../../src/utils/hono-route-from-path.ts"],"names":[],"mappings":"AAaA,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,sBAAsB,CAAA;AA2D3D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MAiChD,CAAA"}
@@ -1,7 +1,100 @@
1
+ import { PATH_KEY_TEMPLATE, splitPathKey } from '../utils/split-path-key.js';
1
2
  /**
2
- * Convert path to route
3
- * Example: /posts/{id} -> /posts/:id
3
+ * Characters Hono reads as routing syntax instead of as literal path text.
4
+ *
5
+ * `:` starts a path parameter and `*` is a wildcard. The remaining ones end up verbatim in the
6
+ * regular expression that Hono's `RegExpRouter` compiles, where they act as quantifier (`?`),
7
+ * alternation (`|`) and pattern delimiters (`{`, `}`) — a path key containing them either routes
8
+ * the wrong requests or makes the router throw. Hono escapes the other regular expression
9
+ * metacharacters (`.`, `+`, `(`, `[`, …) itself.
4
10
  */
5
- export function honoRouteFromPath(path) {
6
- return path.replace(/{/g, ':').replace(/}/g, '');
7
- }
11
+ const HONO_ROUTING_CHARACTERS = /[:*?|{}]/;
12
+ /** Prefix for the parameters we synthesize to match a literal path segment verbatim. */
13
+ export const LITERAL_PARAMETER_PREFIX = '__scalar_literal_';
14
+ /**
15
+ * Escape a literal for use inside the regular expression of a Hono `:name{pattern}` parameter.
16
+ *
17
+ * Every non-alphanumeric ASCII character becomes a `\xHH` escape. That keeps regular expression
18
+ * metacharacters inert and — just as importantly — keeps `{` and `}` out of the pattern, which Hono
19
+ * uses to delimit it. Non-ASCII characters carry no regular expression meaning and are left alone,
20
+ * because `\xHH` cannot express them.
21
+ */
22
+ const escapeRegExpLiteral = (value) => value.replace(/[^A-Za-z0-9]/g, (character) => {
23
+ const code = character.charCodeAt(0);
24
+ return code < 128 ? `\\x${code.toString(16).padStart(2, '0')}` : character;
25
+ });
26
+ /**
27
+ * Build a regular expression that matches a path segment verbatim.
28
+ *
29
+ * Literal text is escaped and a template matches anything but a slash. Every template except the
30
+ * last one also excludes the character the literal behind it starts with, so its match has exactly
31
+ * one possible end and the engine never has to search: an OpenAPI document is untrusted input, and a
32
+ * segment made of several plain `[^/]+` groups backtracks exponentially on a crafted request, which
33
+ * would block the event loop of the whole server. The last template stays greedy, so the common
34
+ * `{name}:cancel` shape still accepts a value that contains the delimiter.
35
+ */
36
+ const patternFromSegment = (segment) => {
37
+ // Splitting on a regular expression with a capturing group interleaves literals and parameter
38
+ // names, so every odd entry is a template and the list always begins and ends with a literal.
39
+ const parts = segment.split(PATH_KEY_TEMPLATE);
40
+ // The split always yields `2n + 1` entries for `n` templates, so the last template sits two
41
+ // entries from the end — and at `-1` when the segment carries no template at all.
42
+ const lastTemplate = parts.length - 2;
43
+ let pattern = '';
44
+ for (let index = 0; index < parts.length; index++) {
45
+ if (index % 2 === 0) {
46
+ pattern += escapeRegExpLiteral(parts[index] ?? '');
47
+ continue;
48
+ }
49
+ const followingLiteral = parts[index + 1] ?? '';
50
+ // Templates with nothing between them cannot be told apart, so they match as a single group.
51
+ if (followingLiteral === '' && index !== lastTemplate) {
52
+ continue;
53
+ }
54
+ const delimiter = index === lastTemplate ? '' : escapeRegExpLiteral(followingLiteral.slice(0, 1));
55
+ pattern += `[^/${delimiter}]+`;
56
+ }
57
+ return pattern;
58
+ };
59
+ /**
60
+ * Convert an OpenAPI path key into a Hono route.
61
+ *
62
+ * Example: `/posts/{id}` → `/posts/:id`
63
+ *
64
+ * A segment whose literal text would be read as routing syntax is registered as a single parameter
65
+ * with an explicit pattern instead, because Hono allows only one parameter per segment and that is
66
+ * the only way to make it match such a segment verbatim. The request then routes to the right
67
+ * operation, but the path parameters of that one segment are no longer bound by name: they surface
68
+ * as the synthesized parameter, and a required path parameter in it reads as missing to request
69
+ * validation. Hono cannot express both at once, and matching the wrong operation is worse.
70
+ *
71
+ * A query string in the key (`/v1/messages?beta=true`) is dropped here — it is matched against the
72
+ * incoming request separately, see `splitPathKey`.
73
+ */
74
+ export const honoRouteFromPath = (path) => {
75
+ const { path: pathname } = splitPathKey(path);
76
+ const route = [];
77
+ let literalIndex = 0;
78
+ let previousIsPattern = false;
79
+ for (const segment of pathname.split('/')) {
80
+ // Check the literal text and the parameter names, but not the braces around them — the `:` we
81
+ // generate for a template is meant to be routing syntax, a `?` in a parameter name is not.
82
+ const routeText = segment.replace(PATH_KEY_TEMPLATE, '$1');
83
+ const plainSegment = segment.replace(PATH_KEY_TEMPLATE, ':$1');
84
+ // Hono splices the segment that follows a pattern into a lookahead without escaping it, unless
85
+ // that segment is a parameter itself. So once one segment is a pattern, every segment behind it
86
+ // has to be one too — otherwise a regular expression metacharacter further down the path makes
87
+ // the router throw on every request. An empty segment (a trailing slash, or `//`) is exempt:
88
+ // Hono skips the lookahead for it, and it has no literal text to turn into a pattern.
89
+ const needsPattern = HONO_ROUTING_CHARACTERS.test(routeText) ||
90
+ (previousIsPattern && plainSegment !== '' && !plainSegment.startsWith(':'));
91
+ if (needsPattern) {
92
+ route.push(`:${LITERAL_PARAMETER_PREFIX}${literalIndex++}{${patternFromSegment(segment)}}`);
93
+ }
94
+ else {
95
+ route.push(plainSegment);
96
+ }
97
+ previousIsPattern = needsPattern;
98
+ }
99
+ return route.join('/');
100
+ };
@@ -1,6 +1,11 @@
1
1
  import type { OpenAPIV3_1 } from '@scalar/openapi-types';
2
+ import type { MockServerLogger } from '../types.js';
2
3
  /**
3
- * Log authentication instructions for different security schemes
4
+ * Log authentication instructions for different security schemes.
5
+ *
6
+ * Only the informational lines go through the provided `log` sink. Warnings and errors about
7
+ * security schemes the mock server cannot handle are printed unconditionally, so a silenced startup
8
+ * still surfaces schemes that will not work.
4
9
  */
5
- export declare function logAuthenticationInstructions(securitySchemes: Record<string, OpenAPIV3_1.SecuritySchemeObject>): void;
10
+ export declare function logAuthenticationInstructions(securitySchemes: Record<string, OpenAPIV3_1.SecuritySchemeObject>, log?: MockServerLogger): void;
6
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;AAKxD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,oBAAoB,CAAC,QA0H9G"}
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,QAwHpD"}