@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,14 +1,18 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
2
  import { getPathFromUrl } from './get-open-auth-token-urls.js';
3
3
  /**
4
- * 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.
5
9
  */
6
- export function logAuthenticationInstructions(securitySchemes) {
10
+ export function logAuthenticationInstructions(securitySchemes, log = (line) => console.log(line)) {
7
11
  if (!securitySchemes || Object.keys(securitySchemes).length === 0) {
8
12
  return;
9
13
  }
10
- console.log('Authentication:');
11
- console.log();
14
+ log('Authentication:');
15
+ log('');
12
16
  Object.entries(securitySchemes).forEach(([_, rawScheme]) => {
13
17
  const scheme = getResolvedRef(rawScheme);
14
18
  // Skip schemes that could not be resolved (e.g. a `$ref` to a missing component)
@@ -18,25 +22,25 @@ export function logAuthenticationInstructions(securitySchemes) {
18
22
  switch (scheme.type) {
19
23
  case 'apiKey':
20
24
  if (scheme.in === 'header') {
21
- console.log('✅ API Key Authentication');
22
- console.log(` Use any API key in the ${scheme.name} header`);
23
- console.log();
24
- console.log(` ${scheme.name}: YOUR_API_KEY_HERE`);
25
- console.log();
25
+ log('✅ API Key Authentication');
26
+ log(` Use any API key in the ${scheme.name} header`);
27
+ log('');
28
+ log(` ${scheme.name}: YOUR_API_KEY_HERE`);
29
+ log('');
26
30
  }
27
31
  else if (scheme.in === 'query') {
28
- console.log('✅ API Key Authentication');
29
- console.log(` Use any API key in the ${scheme.name} query parameter:`);
30
- console.log();
31
- console.log(` ?${scheme.name}=YOUR_API_KEY_HERE`);
32
- console.log();
32
+ log('✅ API Key Authentication');
33
+ log(` Use any API key in the ${scheme.name} query parameter:`);
34
+ log('');
35
+ log(` ?${scheme.name}=YOUR_API_KEY_HERE`);
36
+ log('');
33
37
  }
34
38
  else if (scheme.in === 'cookie') {
35
- console.log('✅ API Key Authentication');
36
- console.log(` Use any API key in the ${scheme.name} cookie:`);
37
- console.log();
38
- console.log(` Cookie: ${scheme.name}=YOUR_API_KEY_HERE`);
39
- console.log();
39
+ log('✅ API Key Authentication');
40
+ log(` Use any API key in the ${scheme.name} cookie:`);
41
+ log('');
42
+ log(` Cookie: ${scheme.name}=YOUR_API_KEY_HERE`);
43
+ log('');
40
44
  }
41
45
  else {
42
46
  console.error(`❌ Unsupported API Key Location: ${scheme.in}`);
@@ -44,18 +48,18 @@ export function logAuthenticationInstructions(securitySchemes) {
44
48
  break;
45
49
  case 'http':
46
50
  if (scheme.scheme === 'basic') {
47
- console.log('✅ HTTP Basic Authentication');
48
- console.log(' Use an Authorization header with any credentials ("username:password" in base64):');
49
- console.log();
50
- console.log(' Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
51
- console.log();
51
+ log('✅ HTTP Basic Authentication');
52
+ log(' Use an Authorization header with any credentials ("username:password" in base64):');
53
+ log('');
54
+ log(' Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
55
+ log('');
52
56
  }
53
57
  else if (scheme.scheme === 'bearer') {
54
- console.log('✅ Bearer Token Authentication');
55
- console.log(' Use an Authorization header with any bearer token');
56
- console.log();
57
- console.log(' Authorization: Bearer YOUR_TOKEN_HERE');
58
- console.log();
58
+ log('✅ Bearer Token Authentication');
59
+ log(' Use an Authorization header with any bearer token');
60
+ log('');
61
+ log(' Authorization: Bearer YOUR_TOKEN_HERE');
62
+ log('');
59
63
  }
60
64
  else {
61
65
  console.error('❌ Unknown Security Scheme:', scheme);
@@ -66,38 +70,38 @@ export function logAuthenticationInstructions(securitySchemes) {
66
70
  Object.keys(scheme.flows).forEach((flow) => {
67
71
  switch (flow) {
68
72
  case 'implicit':
69
- console.log('✅ OAuth 2.0 Implicit Flow');
70
- console.log(' Use the following URL to initiate the OAuth 2.0 Implicit Flow:');
71
- console.log();
72
- console.log(` GET ${scheme?.flows?.implicit?.authorizationUrl || '/oauth/authorize'}?response_type=token&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=YOUR_SCOPES`);
73
- console.log();
73
+ log('✅ OAuth 2.0 Implicit Flow');
74
+ log(' Use the following URL to initiate the OAuth 2.0 Implicit Flow:');
75
+ log('');
76
+ log(` GET ${scheme?.flows?.implicit?.authorizationUrl || '/oauth/authorize'}?response_type=token&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=YOUR_SCOPES`);
77
+ log('');
74
78
  break;
75
79
  case 'password':
76
- console.log('✅ OAuth 2.0 Password Flow');
77
- console.log(' Use the following URL to obtain an access token:');
78
- console.log();
79
- console.log(` POST ${getPathFromUrl(scheme?.flows?.password?.tokenUrl || '/oauth/token')}`);
80
- console.log(' Content-Type: application/x-www-form-urlencoded');
81
- console.log();
82
- console.log(' grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET');
83
- console.log();
80
+ log('✅ OAuth 2.0 Password Flow');
81
+ log(' Use the following URL to obtain an access token:');
82
+ log('');
83
+ log(` POST ${getPathFromUrl(scheme?.flows?.password?.tokenUrl || '/oauth/token')}`);
84
+ log(' Content-Type: application/x-www-form-urlencoded');
85
+ log('');
86
+ log(' grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET');
87
+ log('');
84
88
  break;
85
89
  case 'clientCredentials':
86
- console.log('✅ OAuth 2.0 Client Credentials Flow');
87
- console.log(' Use the following URL to obtain an access token:');
88
- console.log();
89
- console.log(` POST ${getPathFromUrl(scheme?.flows?.clientCredentials?.tokenUrl || '/oauth/token')}`);
90
- console.log(' Content-Type: application/x-www-form-urlencoded');
91
- console.log();
92
- console.log(' grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET');
93
- console.log();
90
+ log('✅ OAuth 2.0 Client Credentials Flow');
91
+ log(' Use the following URL to obtain an access token:');
92
+ log('');
93
+ log(` POST ${getPathFromUrl(scheme?.flows?.clientCredentials?.tokenUrl || '/oauth/token')}`);
94
+ log(' Content-Type: application/x-www-form-urlencoded');
95
+ log('');
96
+ log(' grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET');
97
+ log('');
94
98
  break;
95
99
  case 'authorizationCode':
96
- console.log('✅ OAuth 2.0 Authorization Code Flow');
97
- console.log(' Use the following URL to initiate the OAuth 2.0 Authorization Code Flow:');
98
- console.log();
99
- console.log(' GET', `${getPathFromUrl(scheme?.flows?.authorizationCode?.authorizationUrl || '/oauth/authorize')}?redirect_uri=https://YOUR_REDIRECT_URI_HERE`);
100
- console.log();
100
+ log('✅ OAuth 2.0 Authorization Code Flow');
101
+ log(' Use the following URL to initiate the OAuth 2.0 Authorization Code Flow:');
102
+ log('');
103
+ log(` GET ${getPathFromUrl(scheme?.flows?.authorizationCode?.authorizationUrl || '/oauth/authorize')}?redirect_uri=https://YOUR_REDIRECT_URI_HERE`);
104
+ log('');
101
105
  break;
102
106
  default:
103
107
  console.warn(`Unsupported OAuth 2.0 flow: ${flow}`);
@@ -106,11 +110,11 @@ export function logAuthenticationInstructions(securitySchemes) {
106
110
  }
107
111
  break;
108
112
  case 'openIdConnect':
109
- console.log('✅ OpenID Connect Authentication');
110
- console.log(' Use the following OpenID Connect discovery URL:');
111
- console.log();
112
- console.log(` ${getPathFromUrl(scheme.openIdConnectUrl || '/.well-known/openid-configuration')}`);
113
- console.log();
113
+ log('✅ OpenID Connect Authentication');
114
+ log(' Use the following OpenID Connect discovery URL:');
115
+ log('');
116
+ log(` ${getPathFromUrl(scheme.openIdConnectUrl || '/.well-known/openid-configuration')}`);
117
+ log('');
114
118
  break;
115
119
  default:
116
120
  console.warn(`Unsupported security scheme type: ${scheme.type}`);
@@ -0,0 +1,13 @@
1
+ import type { Context } from 'hono';
2
+ /**
3
+ * Read the path parameters of a request, without the ones synthesized for routing.
4
+ *
5
+ * A path segment Hono cannot match as written is registered as a parameter with a generated name
6
+ * (see `honoRouteFromPath`). That name is an implementation detail, so it has no business showing up
7
+ * in an `x-handler` context or among the variables a mocked response body is generated from.
8
+ *
9
+ * The names are read back off the matched route rather than recognized by their prefix, so a
10
+ * document that happens to declare a parameter of the same name keeps it.
11
+ */
12
+ export declare const pathParameters: (context: Context) => Record<string, string>;
13
+ //# sourceMappingURL=path-parameters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-parameters.d.ts","sourceRoot":"","sources":["../../src/utils/path-parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAOnC;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAItE,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { LITERAL_PARAMETER_PREFIX } from '../utils/hono-route-from-path.js';
2
+ /** Matches the parameters `honoRouteFromPath` synthesizes, which always carry an explicit pattern. */
3
+ const synthesizedParameter = new RegExp(`:(${LITERAL_PARAMETER_PREFIX}\\d+)\\{`, 'g');
4
+ /**
5
+ * Read the path parameters of a request, without the ones synthesized for routing.
6
+ *
7
+ * A path segment Hono cannot match as written is registered as a parameter with a generated name
8
+ * (see `honoRouteFromPath`). That name is an implementation detail, so it has no business showing up
9
+ * in an `x-handler` context or among the variables a mocked response body is generated from.
10
+ *
11
+ * The names are read back off the matched route rather than recognized by their prefix, so a
12
+ * document that happens to declare a parameter of the same name keeps it.
13
+ */
14
+ export const pathParameters = (context) => {
15
+ const synthesized = new Set([...context.req.routePath.matchAll(synthesizedParameter)].map(([, name]) => name));
16
+ return Object.fromEntries(Object.entries(context.req.param()).filter(([name]) => !synthesized.has(name)));
17
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"process-openapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-openapi-document.ts"],"names":[],"mappings":"AAGA,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,CAiD/B"}
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,5 +1,8 @@
1
+ import path from 'node:path';
2
+ import { cwd } from 'node:process';
1
3
  import { bundle } from '@scalar/json-magic/bundle';
2
4
  import { fetchUrls, parseJson, parseYaml, readFiles } from '@scalar/json-magic/bundle/plugins/node';
5
+ import { isFilePath } from '@scalar/json-magic/helpers/is-file-path';
3
6
  import { createMagicProxy } from '@scalar/json-magic/magic-proxy';
4
7
  import { upgrade } from '@scalar/openapi-upgrader';
5
8
  /**
@@ -29,11 +32,15 @@ export async function processOpenApiDocument(document) {
29
32
  };
30
33
  }
31
34
  let bundled;
35
+ // Confine local file `$ref`s to the document's own directory (or the working directory when the
36
+ // document is an object or inline string), and refuse to fetch private or internal addresses.
37
+ // 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();
32
39
  try {
33
40
  // Bundle external references with Node.js plugins
34
41
  // Include parseJson and parseYaml to handle string inputs
35
42
  bundled = await bundle(document, {
36
- plugins: [parseJson(), parseYaml(), readFiles(), fetchUrls()],
43
+ plugins: [parseJson(), parseYaml(), readFiles({ basePath }), fetchUrls({ blockPrivateNetworks: true })],
37
44
  treeShake: false,
38
45
  });
39
46
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Replace the `'[circular]'` markers `getResolvedRefDeep` leaves in schema positions with an empty
3
+ * (always-valid) schema.
4
+ *
5
+ * A recursive schema — a `Node` whose `child` is another `Node` — resolves to a document where the
6
+ * recursion point is the *string* `'[circular]'`. Ajv rejects the whole schema for it
7
+ * (`data/properties/child must be object,boolean`), so a single recursive type silently disabled
8
+ * validation of the request body, or of every parameter in the same location. Accepting anything at
9
+ * the point where the cycle was cut keeps the rest of the schema enforceable.
10
+ *
11
+ * The rewrite only ever loosens what is enforced, so it can lose a violation but never invent one.
12
+ * That is why relaxing is tracked as it goes: under `not`, `if`, `oneOf` or `contains` a looser
13
+ * subschema would make the schema *stricter*, so those keywords are dropped rather than rewritten,
14
+ * and a sibling `unevaluatedProperties`/`unevaluatedItems` goes with a relaxed in-place applicator
15
+ * for the same reason.
16
+ *
17
+ * Only known schema positions are rewritten. A marker anywhere else — `enum`, `const`, `default`,
18
+ * `example`, or a vendor extension — is data rather than a schema Ajv compiles, so it is copied
19
+ * through untouched.
20
+ */
21
+ export declare const replaceCircularMarkers: (schema: unknown) => unknown;
22
+ //# sourceMappingURL=replace-circular-markers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replace-circular-markers.d.ts","sourceRoot":"","sources":["../../src/utils/replace-circular-markers.ts"],"names":[],"mappings":"AAgGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ,OAAO,KAAG,OAoJxD,CAAA"}
@@ -0,0 +1,226 @@
1
+ import { isObject } from '@scalar/helpers/object/is-object';
2
+ /**
3
+ * The placeholder `getResolvedRefDeep` leaves behind wherever it had to cut a `$ref` cycle.
4
+ *
5
+ * The marker is owned by `@scalar/workspace-store`; the Ajv test in this file's suite resolves a real
6
+ * recursive schema, so it fails here if that sentinel ever changes.
7
+ */
8
+ const CIRCULAR_MARKER = '[circular]';
9
+ /**
10
+ * Keywords whose value is a schema, or an array of schemas.
11
+ *
12
+ * `items` covers the JSON Schema 2020-12 form as well as the older tuple form, which the walker reads
13
+ * structurally — a tuple `items` does not compile under 2020-12 either way, with or without a cut.
14
+ * `additionalItems` is listed for the same reason: Ajv 2020 ignores the keyword, but an older document
15
+ * carrying one still walks cleanly.
16
+ */
17
+ const SCHEMA_KEYWORDS = new Set([
18
+ 'additionalItems',
19
+ 'additionalProperties',
20
+ 'allOf',
21
+ 'anyOf',
22
+ 'contains',
23
+ 'contentSchema',
24
+ 'else',
25
+ 'if',
26
+ 'items',
27
+ 'not',
28
+ 'oneOf',
29
+ 'prefixItems',
30
+ 'propertyNames',
31
+ 'then',
32
+ 'unevaluatedItems',
33
+ 'unevaluatedProperties',
34
+ ]);
35
+ /**
36
+ * Keywords whose value maps names to schemas. Their keys are author-chosen names rather than
37
+ * keywords, so the walker must not read them as keywords of their own.
38
+ */
39
+ const SCHEMA_MAP_KEYWORDS = new Set([
40
+ '$defs',
41
+ 'definitions',
42
+ // Ajv 2020 still implements the draft-07 `dependencies`, so a marker under it has to be rewritten
43
+ // too. Its array form (`{ name: ['other'] }`) walks through untouched.
44
+ 'dependencies',
45
+ 'dependentSchemas',
46
+ 'patternProperties',
47
+ 'properties',
48
+ ]);
49
+ /** Map keywords `additionalProperties` reads to decide which properties it still applies to */
50
+ const PROPERTY_MAP_KEYWORDS = new Set(['patternProperties', 'properties']);
51
+ /**
52
+ * Keywords that invert what a relaxed subschema means: matching more values there makes the schema as
53
+ * a whole reject more requests. Verified against Ajv — `not: {}` rejects every request, `if: {}`
54
+ * forces `then` onto every request, `oneOf: [{}, …]` rejects anything that also matches a sibling
55
+ * branch (a recursive union or a nullable recursive reference being the realistic cases), and a
56
+ * `contains` that matches more items can overshoot `maxContains`. Whenever the cycle was cut anywhere
57
+ * below one of these, the keyword is dropped instead, so a valid request is never turned away.
58
+ */
59
+ const INVERTING_SCHEMA_KEYWORDS = new Set(['contains', 'if', 'not', 'oneOf']);
60
+ /**
61
+ * In-place applicators, whose subschemas decide which properties and items count as evaluated. A
62
+ * relaxed branch stops contributing those annotations, so a sibling `unevaluatedProperties` or
63
+ * `unevaluatedItems` would start rejecting values it used to accept. `if` and `oneOf` are listed for
64
+ * completeness; a relaxed one of those is dropped by the inverting rule before it gets here, which
65
+ * clears the sibling anyway.
66
+ */
67
+ const IN_PLACE_SCHEMA_KEYWORDS = new Set([
68
+ 'allOf',
69
+ 'anyOf',
70
+ 'dependencies',
71
+ 'dependentSchemas',
72
+ 'else',
73
+ 'if',
74
+ 'oneOf',
75
+ 'then',
76
+ ]);
77
+ /**
78
+ * Keywords whose value has to be an array of schemas. A cut at the array itself cannot be answered
79
+ * with a schema, so the keyword is dropped rather than left as something Ajv refuses to compile.
80
+ * `oneOf` belongs here too but never reaches it, because the inverting rule already drops it.
81
+ */
82
+ const SCHEMA_ARRAY_KEYWORDS = new Set(['allOf', 'anyOf', 'prefixItems']);
83
+ /**
84
+ * Replace the `'[circular]'` markers `getResolvedRefDeep` leaves in schema positions with an empty
85
+ * (always-valid) schema.
86
+ *
87
+ * A recursive schema — a `Node` whose `child` is another `Node` — resolves to a document where the
88
+ * recursion point is the *string* `'[circular]'`. Ajv rejects the whole schema for it
89
+ * (`data/properties/child must be object,boolean`), so a single recursive type silently disabled
90
+ * validation of the request body, or of every parameter in the same location. Accepting anything at
91
+ * the point where the cycle was cut keeps the rest of the schema enforceable.
92
+ *
93
+ * The rewrite only ever loosens what is enforced, so it can lose a violation but never invent one.
94
+ * That is why relaxing is tracked as it goes: under `not`, `if`, `oneOf` or `contains` a looser
95
+ * subschema would make the schema *stricter*, so those keywords are dropped rather than rewritten,
96
+ * and a sibling `unevaluatedProperties`/`unevaluatedItems` goes with a relaxed in-place applicator
97
+ * for the same reason.
98
+ *
99
+ * Only known schema positions are rewritten. A marker anywhere else — `enum`, `const`, `default`,
100
+ * `example`, or a vendor extension — is data rather than a schema Ajv compiles, so it is copied
101
+ * through untouched.
102
+ */
103
+ export const replaceCircularMarkers = (schema) => {
104
+ // `getResolvedRefDeep` returns a graph, not a tree: one resolved schema object is shared by every
105
+ // place that referenced it. Reusing the rewritten copy keeps a widely shared schema from being
106
+ // walked once per occurrence, and is what makes the walk terminate rather than recur forever should
107
+ // it ever be handed a genuinely cyclic object — which the resolver, having cut every cycle, is not
108
+ // able to produce.
109
+ //
110
+ // Schemas and schema maps are cached apart, because the same object read as one or the other
111
+ // rewrites differently: `{ not: … }` is a keyword in a schema and a schema named `not` in a map.
112
+ const rewritten = new WeakMap();
113
+ const rewrittenMaps = new WeakMap();
114
+ const asSchema = (value) => {
115
+ // The cycle was cut here, so nothing is known about the value any more: accept anything.
116
+ if (value === CIRCULAR_MARKER) {
117
+ return { value: {}, relaxed: true };
118
+ }
119
+ if (!isObject(value) && !Array.isArray(value)) {
120
+ return { value, relaxed: false };
121
+ }
122
+ const cached = rewritten.get(value);
123
+ if (cached) {
124
+ return cached;
125
+ }
126
+ // Register each copy before filling it, so a self-referencing value resolves to the copy itself.
127
+ if (Array.isArray(value)) {
128
+ const items = [];
129
+ const rewrite = { value: items, relaxed: false };
130
+ rewritten.set(value, rewrite);
131
+ for (const item of value) {
132
+ const child = asSchema(item);
133
+ items.push(child.value);
134
+ rewrite.relaxed ||= child.relaxed;
135
+ }
136
+ return rewrite;
137
+ }
138
+ const result = {};
139
+ const rewrite = { value: result, relaxed: false };
140
+ rewritten.set(value, rewrite);
141
+ let droppedIf = false;
142
+ let droppedContains = false;
143
+ let droppedPrefixItems = false;
144
+ let droppedPropertyMap = false;
145
+ // Whether this schema stopped saying which properties and items it accounted for — either a
146
+ // keyword was dropped outright, or a relaxed in-place branch no longer contributes what it did.
147
+ let annotationsLost = false;
148
+ for (const [keyword, child] of Object.entries(value)) {
149
+ if (!SCHEMA_KEYWORDS.has(keyword) && !SCHEMA_MAP_KEYWORDS.has(keyword)) {
150
+ // Data rather than a schema, so it is carried over as it stands. Nothing mutates the result,
151
+ // so sharing the value with the resolved document it came from is safe.
152
+ result[keyword] = child;
153
+ continue;
154
+ }
155
+ // A cut at an array-valued keyword itself cannot become a schema, so the keyword goes. A dropped
156
+ // keyword also stops saying which properties and items it accounted for, so any `unevaluated*`
157
+ // sibling has to go with it.
158
+ if (SCHEMA_ARRAY_KEYWORDS.has(keyword) && child === CIRCULAR_MARKER) {
159
+ droppedPrefixItems ||= keyword === 'prefixItems';
160
+ rewrite.relaxed = true;
161
+ annotationsLost = true;
162
+ continue;
163
+ }
164
+ // A cut at a schema map itself leaves an empty map, which accounts for nothing any more, so an
165
+ // `unevaluated*` sibling has to go the same way a dropped keyword takes it.
166
+ if (SCHEMA_MAP_KEYWORDS.has(keyword) && child === CIRCULAR_MARKER) {
167
+ result[keyword] = {};
168
+ droppedPropertyMap ||= PROPERTY_MAP_KEYWORDS.has(keyword);
169
+ rewrite.relaxed = true;
170
+ annotationsLost = true;
171
+ continue;
172
+ }
173
+ const rewrittenChild = SCHEMA_MAP_KEYWORDS.has(keyword) && isObject(child) ? asSchemaMap(child) : asSchema(child);
174
+ if (INVERTING_SCHEMA_KEYWORDS.has(keyword) && rewrittenChild.relaxed) {
175
+ droppedIf ||= keyword === 'if';
176
+ droppedContains ||= keyword === 'contains';
177
+ rewrite.relaxed = true;
178
+ annotationsLost = true;
179
+ continue;
180
+ }
181
+ result[keyword] = rewrittenChild.value;
182
+ rewrite.relaxed ||= rewrittenChild.relaxed;
183
+ annotationsLost ||= rewrittenChild.relaxed && IN_PLACE_SCHEMA_KEYWORDS.has(keyword);
184
+ }
185
+ // `then` and `else` only apply alongside an `if`, so they leave with the dropped condition, and
186
+ // `minContains`/`maxContains` only qualify a `contains`.
187
+ if (droppedIf) {
188
+ delete result.then;
189
+ delete result.else;
190
+ }
191
+ if (droppedContains) {
192
+ delete result.minContains;
193
+ delete result.maxContains;
194
+ }
195
+ // `additionalProperties` and `items` only apply to what their siblings did not cover, so once that
196
+ // sibling is gone they would start policing values it used to account for.
197
+ if (droppedPropertyMap) {
198
+ delete result.additionalProperties;
199
+ }
200
+ if (droppedPrefixItems) {
201
+ delete result.items;
202
+ }
203
+ if (annotationsLost) {
204
+ delete result.unevaluatedProperties;
205
+ delete result.unevaluatedItems;
206
+ }
207
+ return rewrite;
208
+ };
209
+ /** Rewrite every schema under a map keyword, keeping its author-chosen names as they are */
210
+ const asSchemaMap = (map) => {
211
+ const cached = rewrittenMaps.get(map);
212
+ if (cached) {
213
+ return cached;
214
+ }
215
+ const result = {};
216
+ const rewrite = { value: result, relaxed: false };
217
+ rewrittenMaps.set(map, rewrite);
218
+ for (const [name, sub] of Object.entries(map)) {
219
+ const child = asSchema(sub);
220
+ result[name] = child.value;
221
+ rewrite.relaxed ||= child.relaxed;
222
+ }
223
+ return rewrite;
224
+ };
225
+ return asSchema(schema).value;
226
+ };
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'hono';
2
+ import type { PinnedQueryParameter } from '../utils/split-path-key.js';
3
+ /**
4
+ * Check whether a request carries every query parameter a path key pins.
5
+ *
6
+ * `/v1/messages?beta=true` describes a variant of `/v1/messages`, so it may only answer requests
7
+ * that actually send `beta=true`. A parameter pinned without a value (`?beta`) matches any value.
8
+ */
9
+ export declare const requestMatchesPinnedQuery: (context: Context, query: PinnedQueryParameter[]) => boolean;
10
+ //# sourceMappingURL=request-matches-pinned-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-matches-pinned-query.d.ts","sourceRoot":"","sources":["../../src/utils/request-matches-pinned-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAElE;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,OAAO,EAAE,OAAO,oBAAoB,EAAE,KAAG,OASxF,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check whether a request carries every query parameter a path key pins.
3
+ *
4
+ * `/v1/messages?beta=true` describes a variant of `/v1/messages`, so it may only answer requests
5
+ * that actually send `beta=true`. A parameter pinned without a value (`?beta`) matches any value.
6
+ */
7
+ export const requestMatchesPinnedQuery = (context, query) => query.every(({ name, value }) => {
8
+ const values = context.req.queries(name);
9
+ if (!values?.length) {
10
+ return false;
11
+ }
12
+ return value === undefined || values.includes(value);
13
+ });
@@ -0,0 +1,12 @@
1
+ import type { MockServerLogger } from '../types.js';
2
+ /**
3
+ * Resolve the `logger` option accepted by the mock server factories into a concrete sink.
4
+ *
5
+ * The option is a superset of a plain sink:
6
+ * - a function is used as-is,
7
+ * - `true` logs each line to the console,
8
+ * - `false` drops every line,
9
+ * - `undefined` falls back to the factory's default (`enabledByDefault`).
10
+ */
11
+ export declare const resolveLogger: (logger: boolean | MockServerLogger | undefined, enabledByDefault: boolean) => MockServerLogger;
12
+ //# sourceMappingURL=resolve-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-logger.d.ts","sourceRoot":"","sources":["../../src/utils/resolve-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,OAAO,GAAG,gBAAgB,GAAG,SAAS,EAC9C,kBAAkB,OAAO,KACxB,gBAQF,CAAA"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolve the `logger` option accepted by the mock server factories into a concrete sink.
3
+ *
4
+ * The option is a superset of a plain sink:
5
+ * - a function is used as-is,
6
+ * - `true` logs each line to the console,
7
+ * - `false` drops every line,
8
+ * - `undefined` falls back to the factory's default (`enabledByDefault`).
9
+ */
10
+ export const resolveLogger = (logger, enabledByDefault) => {
11
+ const value = logger ?? enabledByDefault;
12
+ if (typeof value === 'function') {
13
+ return value;
14
+ }
15
+ return value ? (line) => console.log(line) : () => undefined;
16
+ };
@@ -0,0 +1,25 @@
1
+ import type { StoreInterface } from './store-wrapper.js';
2
+ /**
3
+ * Options for {@link runInSandbox}.
4
+ */
5
+ type RunInSandboxOptions = {
6
+ /** User-supplied handler or seed code. It runs inside an async wrapper, so top-level `return` and `await` work. */
7
+ code: string;
8
+ /** Host store bridge (already wrapped with operation tracking). */
9
+ store: StoreInterface;
10
+ /** Plain-JSON values exposed to the guest as globals (for example `req`, `res`, `schema`). */
11
+ jsonGlobals?: Record<string, unknown>;
12
+ /** When true, exposes the guest-side `seed` helper. Requires a `schema` entry in {@link jsonGlobals}. */
13
+ includeSeed?: boolean;
14
+ };
15
+ /**
16
+ * Execute untrusted handler or seed code inside a QuickJS WebAssembly sandbox.
17
+ *
18
+ * The guest has no access to the host runtime (`process`, `require`, `fetch`,
19
+ * the `Function` constructor, and so on). It can only talk to the `store` and
20
+ * `faker` bridges and read the injected JSON globals. Memory and time limits
21
+ * bound the remaining denial-of-service risk.
22
+ */
23
+ export declare function runInSandbox(options: RunInSandboxOptions): Promise<unknown>;
24
+ export {};
25
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/utils/sandbox.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAiCrD;;GAEG;AACH,KAAK,mBAAmB,GAAG;IACzB,mHAAmH;IACnH,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,KAAK,EAAE,cAAc,CAAA;IACrB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,yGAAyG;IACzG,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AA8LD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CAqEjF"}