@scalar/mock-server 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/create-mock-server.d.ts.map +1 -1
- package/dist/routes/mock-any-response.d.ts.map +1 -1
- package/dist/routes/mock-any-response.js +9 -9
- package/dist/routes/mock-handler-response.d.ts.map +1 -1
- package/dist/routes/mock-handler-response.js +2 -6
- package/dist/utils/build-handler-context.d.ts +0 -2
- package/dist/utils/build-handler-context.d.ts.map +1 -1
- package/dist/utils/build-handler-context.js +2 -8
- package/dist/utils/build-seed-context.d.ts +5 -23
- package/dist/utils/build-seed-context.d.ts.map +1 -1
- package/dist/utils/build-seed-context.js +1 -49
- package/dist/utils/deserialize-parameter.d.ts.map +1 -1
- package/dist/utils/deserialize-parameter.js +3 -8
- package/dist/utils/execute-handler.d.ts +5 -2
- package/dist/utils/execute-handler.d.ts.map +1 -1
- package/dist/utils/execute-handler.js +11 -13
- package/dist/utils/execute-seed.d.ts +5 -2
- package/dist/utils/execute-seed.d.ts.map +1 -1
- package/dist/utils/execute-seed.js +13 -21
- package/dist/utils/generate-message.d.ts +1 -1
- package/dist/utils/generate-message.d.ts.map +1 -1
- package/dist/utils/generate-message.js +5 -6
- package/dist/utils/generate-response-example.d.ts +25 -0
- package/dist/utils/generate-response-example.d.ts.map +1 -0
- package/dist/utils/generate-response-example.js +27 -0
- package/dist/utils/process-openapi-document.d.ts.map +1 -1
- package/dist/utils/process-openapi-document.js +8 -1
- package/dist/utils/sandbox.d.ts +25 -0
- package/dist/utils/sandbox.d.ts.map +1 -0
- package/dist/utils/sandbox.js +252 -0
- package/dist/utils/store-wrapper.d.ts +1 -1
- package/dist/utils/store-wrapper.d.ts.map +1 -1
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @scalar/mock-server
|
|
2
2
|
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#10190](https://github.com/scalar/scalar/pull/10190): Update Hono to allow HTTP QUERY requests in default CORS preflight responses.
|
|
8
|
+
- [#10164](https://github.com/scalar/scalar/pull/10164): Serve deprecated response schemas from the mock instead of answering a declared JSON response with an empty body, and generate a deprecated AsyncAPI message payload instead of sending `null`. `getExampleFromSchema` takes a new `includeDeprecated` option for callers that must produce a value satisfying the schema. A declared response header that generates no value is now skipped rather than clearing a header of the same name the mock already set, such as the CORS headers.
|
|
9
|
+
- [#10140](https://github.com/scalar/scalar/pull/10140): Replace redundant type assertions with compiler-checked annotations, typed accumulators, and existing guards across helpers, API conversion, request handling, and schema rendering.
|
|
10
|
+
|
|
11
|
+
Narrow DOM elements and caught errors before accessing their properties. Correct header lookup to include missing values and handle them during PowerShell snippet generation.
|
|
12
|
+
|
|
13
|
+
Validate release-note provider responses, represent unresolved references and absent groups in helper return types, and require narrowing merged object values. Preserve AsyncAPI broker credentials separately from HTTP authentication schemes.
|
|
14
|
+
|
|
15
|
+
## 0.14.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#10082](https://github.com/scalar/scalar/pull/10082): Run `x-handler` and `x-seed` code in a real sandbox
|
|
20
|
+
|
|
21
|
+
Handler and seed code used to run with the `Function` constructor, which gave it full access to the Node.js host (`process`, `require`, and more). It now runs inside a QuickJS WebAssembly sandbox with memory and time limits, so even untrusted code from a remote or `$ref`-loaded document cannot reach the host.
|
|
22
|
+
|
|
23
|
+
The `store`, `faker`, `req`, `res`, `schema`, and `seed` APIs work as before. The one exception is faker methods that take a callback (for example `faker.helpers.multiple(fn)`), which are no longer supported because functions cannot cross the sandbox boundary.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#10079](https://github.com/scalar/scalar/pull/10079): Harden the mock server against SSRF and local file disclosure through OpenAPI `$ref`s. External `$ref` resolution now refuses to fetch private, loopback, link-local, and metadata addresses, and confines local file reads to the document's directory. The `fetchUrls` and `readFiles` bundling plugins gain opt-in `blockPrivateNetworks` and `basePath` options, so other callers keep their current behavior unless they opt in.
|
|
28
|
+
|
|
3
29
|
## 0.13.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
|
@@ -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,
|
|
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 +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;
|
|
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"}
|
|
@@ -5,6 +5,7 @@ import { accepts } from 'hono/accepts';
|
|
|
5
5
|
import { streamSSE } from 'hono/streaming';
|
|
6
6
|
import { collectSseEvents, isEventStreamContentType } from '../utils/collect-sse-events.js';
|
|
7
7
|
import { findPreferredResponseKey } from '../utils/find-preferred-response-key.js';
|
|
8
|
+
import { generateResponseExample } from '../utils/generate-response-example.js';
|
|
8
9
|
import { normalizeResponseBody } from '../utils/normalize-response-body.js';
|
|
9
10
|
import { parsePreferHeader } from '../utils/parse-prefer-header.js';
|
|
10
11
|
import { pathParameters } from '../utils/path-parameters.js';
|
|
@@ -36,10 +37,15 @@ export function mockAnyResponse(c, operation) {
|
|
|
36
37
|
const headers = selectedResponse?.headers ?? {};
|
|
37
38
|
Object.keys(headers).forEach((header) => {
|
|
38
39
|
const headerObject = getResolvedRef(headers[header]);
|
|
40
|
+
// Headers need `includeDeprecated` but none of `generateResponseExample`'s other options — see
|
|
41
|
+
// the note on that helper for why passing them would change what declared headers emit.
|
|
39
42
|
const value = headerObject?.schema
|
|
40
|
-
? getExampleFromSchema(getResolvedRefDeep(headerObject.schema))
|
|
43
|
+
? getExampleFromSchema(getResolvedRefDeep(headerObject.schema), { includeDeprecated: true })
|
|
41
44
|
: null;
|
|
42
|
-
|
|
45
|
+
// Loose check on purpose: Hono *deletes* a header when handed `undefined`. This loop is the first
|
|
46
|
+
// thing to set the declared headers, so in practice a delete removes a header set earlier by
|
|
47
|
+
// middleware — `cors()` sets `Access-Control-Allow-Origin` before the handler runs.
|
|
48
|
+
if (value != null) {
|
|
43
49
|
c.header(header, value);
|
|
44
50
|
}
|
|
45
51
|
});
|
|
@@ -66,13 +72,7 @@ export function mockAnyResponse(c, operation) {
|
|
|
66
72
|
const acceptedResponse = selectedResponse?.content?.[acceptedContentType];
|
|
67
73
|
const responseSchema = acceptedResponse?.schema ? getResolvedRefDeep(acceptedResponse.schema) : undefined;
|
|
68
74
|
/** Generates the response body from the schema, or returns `undefined` when there is no schema. */
|
|
69
|
-
const generateFromSchema = () => responseSchema
|
|
70
|
-
? getExampleFromSchema(responseSchema, {
|
|
71
|
-
emptyString: 'string',
|
|
72
|
-
variables: pathParameters(c),
|
|
73
|
-
mode: 'read',
|
|
74
|
-
})
|
|
75
|
-
: undefined;
|
|
75
|
+
const generateFromSchema = () => responseSchema ? generateResponseExample(responseSchema, pathParameters(c)) : undefined;
|
|
76
76
|
// Server-Sent Events are a framed, multi-event wire format, so they cannot go out as one buffered
|
|
77
77
|
// body: a client reading the stream expects `data:` lines terminated by a blank line. Everything
|
|
78
78
|
// else (JSON, XML, text) keeps taking the single-body path below.
|
|
@@ -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;
|
|
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,8 +1,8 @@
|
|
|
1
1
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
2
|
-
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
3
2
|
import { accepts } from 'hono/accepts';
|
|
4
3
|
import { buildHandlerContext } from '../utils/build-handler-context.js';
|
|
5
4
|
import { executeHandler } from '../utils/execute-handler.js';
|
|
5
|
+
import { generateResponseExample } from '../utils/generate-response-example.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';
|
|
@@ -47,11 +47,7 @@ function getExampleFromResponse(c, statusCode, responses, exampleName) {
|
|
|
47
47
|
return selectedExample
|
|
48
48
|
? normalizeResponseBody(selectedExample.value, responseSchema)
|
|
49
49
|
: responseSchema
|
|
50
|
-
? normalizeResponseBody(
|
|
51
|
-
emptyString: 'string',
|
|
52
|
-
variables: pathParameters(c),
|
|
53
|
-
mode: 'read',
|
|
54
|
-
}), responseSchema)
|
|
50
|
+
? normalizeResponseBody(generateResponseExample(responseSchema, pathParameters(c)), responseSchema)
|
|
55
51
|
: null;
|
|
56
52
|
}
|
|
57
53
|
/**
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
1
|
import type { OpenAPIV3_1 } from '@scalar/openapi-types';
|
|
3
2
|
import type { Context } from 'hono';
|
|
4
3
|
import { type StoreOperationTracking, createStoreWrapper } from './store-wrapper.js';
|
|
@@ -7,7 +6,6 @@ import { type StoreOperationTracking, createStoreWrapper } from './store-wrapper
|
|
|
7
6
|
*/
|
|
8
7
|
export type HandlerContext = {
|
|
9
8
|
store: ReturnType<typeof createStoreWrapper>['wrappedStore'];
|
|
10
|
-
faker: typeof faker;
|
|
11
9
|
req: {
|
|
12
10
|
body: any;
|
|
13
11
|
params: Record<string, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-handler-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-handler-context.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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,9 +1,8 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
1
|
import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
2
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
4
|
-
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
5
3
|
import { accepts } from 'hono/accepts';
|
|
6
4
|
import { store } from '../libs/store.js';
|
|
5
|
+
import { generateResponseExample } from './generate-response-example.js';
|
|
7
6
|
import { normalizeResponseBody } from './normalize-response-body.js';
|
|
8
7
|
import { pathParameters } from './path-parameters.js';
|
|
9
8
|
import { createStoreWrapper } from './store-wrapper.js';
|
|
@@ -41,11 +40,7 @@ function getExampleFromResponse(c, statusCode, responses) {
|
|
|
41
40
|
return acceptedResponse.example !== undefined
|
|
42
41
|
? normalizeResponseBody(acceptedResponse.example, responseSchema)
|
|
43
42
|
: responseSchema
|
|
44
|
-
? normalizeResponseBody(
|
|
45
|
-
emptyString: 'string',
|
|
46
|
-
variables: pathParameters(c),
|
|
47
|
-
mode: 'read',
|
|
48
|
-
}), responseSchema)
|
|
43
|
+
? normalizeResponseBody(generateResponseExample(responseSchema, pathParameters(c)), responseSchema)
|
|
49
44
|
: null;
|
|
50
45
|
}
|
|
51
46
|
/**
|
|
@@ -84,7 +79,6 @@ export async function buildHandlerContext(c, operation) {
|
|
|
84
79
|
return {
|
|
85
80
|
context: {
|
|
86
81
|
store: wrappedStore,
|
|
87
|
-
faker,
|
|
88
82
|
req: {
|
|
89
83
|
body,
|
|
90
84
|
params: pathParameters(c),
|
|
@@ -1,35 +1,17 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
1
|
import { createStoreWrapper } from './store-wrapper.js';
|
|
3
|
-
/**
|
|
4
|
-
* Seed helper function type.
|
|
5
|
-
*/
|
|
6
|
-
type SeedHelper = {
|
|
7
|
-
/**
|
|
8
|
-
* Create n items using a factory function.
|
|
9
|
-
*/
|
|
10
|
-
count: (n: number, factory: () => any) => any[];
|
|
11
|
-
/**
|
|
12
|
-
* Create items from an array of objects.
|
|
13
|
-
*/
|
|
14
|
-
(items: any[]): any[];
|
|
15
|
-
/**
|
|
16
|
-
* Create a single item using a factory function (shorthand for count(1, factory)).
|
|
17
|
-
*/
|
|
18
|
-
(factory: () => any): any;
|
|
19
|
-
};
|
|
20
2
|
/**
|
|
21
3
|
* Context object provided to x-seed code.
|
|
4
|
+
*
|
|
5
|
+
* The `seed` helper itself lives inside the sandbox (see `sandbox.ts`); it
|
|
6
|
+
* persists generated items through this store using the schema key as the
|
|
7
|
+
* collection name.
|
|
22
8
|
*/
|
|
23
9
|
export type SeedContext = {
|
|
24
10
|
store: ReturnType<typeof createStoreWrapper>['wrappedStore'];
|
|
25
|
-
faker: typeof faker;
|
|
26
|
-
seed: SeedHelper;
|
|
27
11
|
schema: string;
|
|
28
12
|
};
|
|
29
13
|
/**
|
|
30
|
-
* Build the seed context
|
|
31
|
-
* The seed helper automatically uses the schema key as the collection name.
|
|
14
|
+
* Build the seed context for a schema.
|
|
32
15
|
*/
|
|
33
16
|
export declare function buildSeedContext(schemaKey: string): SeedContext;
|
|
34
|
-
export {};
|
|
35
17
|
//# sourceMappingURL=build-seed-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-seed-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-seed-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build-seed-context.d.ts","sourceRoot":"","sources":["../../src/utils/build-seed-context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,cAAc,CAAC,CAAA;IAC5D,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAO/D"}
|
|
@@ -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
|
|
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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deserialize-parameter.d.ts","sourceRoot":"","sources":["../../src/utils/deserialize-parameter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deserialize-parameter.d.ts","sourceRoot":"","sources":["../../src/utils/deserialize-parameter.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,oFAAoF;AACpF,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAUtE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,UAAU,iBAAiB,EAC3B,QAAQ,MAAM,EACd,UAAU,OAAO,KAChB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAGnC,CAAA;AA2BD,8GAA8G;AAC9G,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,OAU3E,CAAA;AAED,gHAAgH;AAChH,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,OAU5E,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAAG,MAAM,EA4B1F,CAAA;AAgGD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,oCAKvC;IACD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,iGAAiG;IACjG,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CAC7B,KAAG,MAAM,EAAE,GAAG,SAoBd,CAAA;AAgCD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GAAI,qEAQxC;IACD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,+FAA+F;IAC/F,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,SAAS,CAAA;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,mGAAmG;IACnG,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAA;CACvC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,SAoEvC,CAAA"}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Helpers for turning string-encoded request parameters back into the structured values that JSON
|
|
3
|
-
* Schema validation expects, following the OpenAPI `style`/`explode` serialization rules.
|
|
4
|
-
*
|
|
5
|
-
* @see https://spec.openapis.org/oas/v3.1.1.html#style-values
|
|
6
|
-
*/
|
|
1
|
+
import { isObjectLike } from '@scalar/helpers/object/is-object';
|
|
7
2
|
/** Default serialization style per parameter location. */
|
|
8
3
|
const DEFAULT_STYLE = {
|
|
9
4
|
query: 'form',
|
|
@@ -34,7 +29,7 @@ const matchesComposedSchema = (schema, predicate) => {
|
|
|
34
29
|
const subSchemas = schema[keyword];
|
|
35
30
|
if (Array.isArray(subSchemas)) {
|
|
36
31
|
for (const subSchema of subSchemas) {
|
|
37
|
-
if (subSchema &&
|
|
32
|
+
if (isObjectLike(subSchema) && predicate(subSchema)) {
|
|
38
33
|
return true;
|
|
39
34
|
}
|
|
40
35
|
}
|
|
@@ -87,7 +82,7 @@ export const getObjectPropertyNames = (schema) => {
|
|
|
87
82
|
const subSchemas = schema[keyword];
|
|
88
83
|
if (Array.isArray(subSchemas)) {
|
|
89
84
|
for (const subSchema of subSchemas) {
|
|
90
|
-
if (subSchema
|
|
85
|
+
if (isObjectLike(subSchema)) {
|
|
91
86
|
for (const name of getObjectPropertyNames(subSchema)) {
|
|
92
87
|
names.add(name);
|
|
93
88
|
}
|
|
@@ -6,8 +6,11 @@ type HandlerExecutionResult = {
|
|
|
6
6
|
result: any;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* Execute handler code
|
|
10
|
-
*
|
|
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;
|
|
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
|
|
3
|
-
*
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
10
|
-
*
|
|
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;
|
|
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
|
|
3
|
-
*
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
}
|
|
@@ -2,7 +2,7 @@ import type { MockMessage, ResolvedChannel } from '../transports/types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Generate an encoded mock frame for a channel message — the AsyncAPI analogue of the REST
|
|
4
4
|
* mocker's response generation. Prefers a defined example, otherwise generates a value from the
|
|
5
|
-
* message payload schema
|
|
5
|
+
* message payload schema through the same `generateResponseExample` the HTTP mocker uses.
|
|
6
6
|
*
|
|
7
7
|
* @param channel - The resolved channel to mock a message for.
|
|
8
8
|
* @param messageId - Which message to emit; defaults to the channel's first message.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-message.d.ts","sourceRoot":"","sources":["../../src/utils/generate-message.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate-message.d.ts","sourceRoot":"","sources":["../../src/utils/generate-message.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAmB,MAAM,oBAAoB,CAAA;AAYvF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAuBhG"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getResolvedRefDeep } from '@scalar/workspace-store/helpers/get-resolved-ref-deep';
|
|
2
|
-
import {
|
|
2
|
+
import { generateResponseExample } from '../utils/generate-response-example.js';
|
|
3
3
|
/** Encode a generated value to a wire string. Strings pass through; everything else is JSON. */
|
|
4
4
|
function encode(value) {
|
|
5
5
|
if (typeof value === 'string') {
|
|
@@ -10,7 +10,7 @@ function encode(value) {
|
|
|
10
10
|
/**
|
|
11
11
|
* Generate an encoded mock frame for a channel message — the AsyncAPI analogue of the REST
|
|
12
12
|
* mocker's response generation. Prefers a defined example, otherwise generates a value from the
|
|
13
|
-
* message payload schema
|
|
13
|
+
* message payload schema through the same `generateResponseExample` the HTTP mocker uses.
|
|
14
14
|
*
|
|
15
15
|
* @param channel - The resolved channel to mock a message for.
|
|
16
16
|
* @param messageId - Which message to emit; defaults to the channel's first message.
|
|
@@ -29,10 +29,9 @@ export function generateMessage(channel, messageId) {
|
|
|
29
29
|
value = message.examples[0];
|
|
30
30
|
}
|
|
31
31
|
else if (message.payload) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
32
|
+
// No `variables`: `generateMessage` is never handed the Hono context, so a channel route's path
|
|
33
|
+
// parameters are not in scope for `x-variable` substitution. It does run per request.
|
|
34
|
+
value = generateResponseExample(getResolvedRefDeep(message.payload));
|
|
36
35
|
}
|
|
37
36
|
return {
|
|
38
37
|
data: encode(value),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
2
|
+
/** The schema shape `getExampleFromSchema` accepts, so callers do not have to name it themselves. */
|
|
3
|
+
export type ExampleSchema = Parameters<typeof getExampleFromSchema>[0];
|
|
4
|
+
/**
|
|
5
|
+
* Generate a mocked response value from a schema the mock has declared.
|
|
6
|
+
*
|
|
7
|
+
* Every value the mock puts on the wire — an HTTP body, an SSE frame, a channel message — has to
|
|
8
|
+
* satisfy the schema it was declared with, which is why this passes `includeDeprecated: true`:
|
|
9
|
+
* `deprecated` marks a field as discouraged, not absent, so omitting it answered a declared JSON
|
|
10
|
+
* response with zero bytes and dropped required properties. Owning the option set in one place keeps
|
|
11
|
+
* a newly added response path from quietly missing that.
|
|
12
|
+
*
|
|
13
|
+
* Not for response headers. Two of the options here would change what an already-declared header
|
|
14
|
+
* emits: `emptyString` yields the literal `string` for a bare `type: 'string'` and switches on
|
|
15
|
+
* format-based generation (a `format: 'date-time'` header would emit a fabricated timestamp), and
|
|
16
|
+
* `mode: 'read'` drops a `writeOnly` header entirely. Headers set `includeDeprecated` on their own
|
|
17
|
+
* call instead.
|
|
18
|
+
*
|
|
19
|
+
* @param schema - The resolved schema to generate a value for.
|
|
20
|
+
* @param variables - Values for `x-variable` substitution, usually a request's path parameters.
|
|
21
|
+
* Omitted by callers that have no request in scope, such as channel messages.
|
|
22
|
+
* @returns The generated value, or `undefined` when the schema yields nothing.
|
|
23
|
+
*/
|
|
24
|
+
export declare const generateResponseExample: (schema: ExampleSchema, variables?: Record<string, unknown>) => unknown;
|
|
25
|
+
//# sourceMappingURL=generate-response-example.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-response-example.d.ts","sourceRoot":"","sources":["../../src/utils/generate-response-example.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAA;AAE9E,qGAAqG;AACrG,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,aAAa,EAAE,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,OAMjG,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a mocked response value from a schema the mock has declared.
|
|
4
|
+
*
|
|
5
|
+
* Every value the mock puts on the wire — an HTTP body, an SSE frame, a channel message — has to
|
|
6
|
+
* satisfy the schema it was declared with, which is why this passes `includeDeprecated: true`:
|
|
7
|
+
* `deprecated` marks a field as discouraged, not absent, so omitting it answered a declared JSON
|
|
8
|
+
* response with zero bytes and dropped required properties. Owning the option set in one place keeps
|
|
9
|
+
* a newly added response path from quietly missing that.
|
|
10
|
+
*
|
|
11
|
+
* Not for response headers. Two of the options here would change what an already-declared header
|
|
12
|
+
* emits: `emptyString` yields the literal `string` for a bare `type: 'string'` and switches on
|
|
13
|
+
* format-based generation (a `format: 'date-time'` header would emit a fabricated timestamp), and
|
|
14
|
+
* `mode: 'read'` drops a `writeOnly` header entirely. Headers set `includeDeprecated` on their own
|
|
15
|
+
* call instead.
|
|
16
|
+
*
|
|
17
|
+
* @param schema - The resolved schema to generate a value for.
|
|
18
|
+
* @param variables - Values for `x-variable` substitution, usually a request's path parameters.
|
|
19
|
+
* Omitted by callers that have no request in scope, such as channel messages.
|
|
20
|
+
* @returns The generated value, or `undefined` when the schema yields nothing.
|
|
21
|
+
*/
|
|
22
|
+
export const generateResponseExample = (schema, variables) => getExampleFromSchema(schema, {
|
|
23
|
+
emptyString: 'string',
|
|
24
|
+
variables,
|
|
25
|
+
mode: 'read',
|
|
26
|
+
includeDeprecated: true,
|
|
27
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-openapi-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-openapi-document.ts"],"names":[],"mappings":"
|
|
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,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"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { getQuickJS } from 'quickjs-emscripten';
|
|
3
|
+
/**
|
|
4
|
+
* Maximum memory the sandboxed guest may allocate. Handler and seed code only
|
|
5
|
+
* shuffles small amounts of mock data around, so this is generous while still
|
|
6
|
+
* bounding a runaway allocation.
|
|
7
|
+
*/
|
|
8
|
+
const MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum wall-clock time a single handler or seed run may take. This is the
|
|
11
|
+
* only defence against an infinite loop, since the guest cannot reach anything
|
|
12
|
+
* else on the host.
|
|
13
|
+
*/
|
|
14
|
+
const EXECUTION_TIMEOUT_MS = 1_000;
|
|
15
|
+
/**
|
|
16
|
+
* Property names that let code climb from an object onto its prototype chain and
|
|
17
|
+
* ultimately reach the host `Function` constructor. They are never legitimate
|
|
18
|
+
* faker module or method names, so the bridge refuses to walk through them.
|
|
19
|
+
*/
|
|
20
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
21
|
+
/** Store methods the guest bridge is allowed to call. */
|
|
22
|
+
const STORE_METHODS = new Set(['list', 'get', 'create', 'update', 'delete', 'clear']);
|
|
23
|
+
/**
|
|
24
|
+
* The QuickJS WebAssembly module is expensive to instantiate, so it is loaded
|
|
25
|
+
* once and shared. Each run still gets its own isolated context and runtime.
|
|
26
|
+
*/
|
|
27
|
+
let quickJSModule;
|
|
28
|
+
const loadQuickJS = () => (quickJSModule ??= getQuickJS());
|
|
29
|
+
/**
|
|
30
|
+
* Walk faker with a guest-provided property path and invoke the resolved method.
|
|
31
|
+
* The faker instance lives on the host; only the path and JSON arguments cross
|
|
32
|
+
* the boundary, so guest code can never obtain a reference to faker itself.
|
|
33
|
+
*/
|
|
34
|
+
function callFaker(path, args) {
|
|
35
|
+
const method = path.at(-1);
|
|
36
|
+
if (method === undefined || FORBIDDEN_KEYS.has(method)) {
|
|
37
|
+
throw new Error(`faker: "${method}" is not accessible`);
|
|
38
|
+
}
|
|
39
|
+
// Resolve everything except the last segment to the faker module owning the method.
|
|
40
|
+
let receiver = faker;
|
|
41
|
+
for (const key of path.slice(0, -1)) {
|
|
42
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
43
|
+
throw new Error(`faker: "${key}" is not accessible`);
|
|
44
|
+
}
|
|
45
|
+
receiver = receiver?.[key];
|
|
46
|
+
if (receiver === undefined || receiver === null) {
|
|
47
|
+
throw new Error(`faker: "${key}" does not exist`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fn = receiver?.[method];
|
|
51
|
+
if (typeof fn !== 'function') {
|
|
52
|
+
throw new Error(`faker: "${path.join('.')}" is not a function`);
|
|
53
|
+
}
|
|
54
|
+
// Bind to the resolved module so faker methods keep their expected `this`.
|
|
55
|
+
return fn.apply(receiver, args);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a store method requested by the guest and return a JSON envelope.
|
|
59
|
+
*/
|
|
60
|
+
function runStoreBridge(store, method, argsJson) {
|
|
61
|
+
try {
|
|
62
|
+
if (!STORE_METHODS.has(method)) {
|
|
63
|
+
throw new Error(`store: "${method}" is not a method`);
|
|
64
|
+
}
|
|
65
|
+
const args = JSON.parse(argsJson);
|
|
66
|
+
const result = store[method](...args);
|
|
67
|
+
return serializeEnvelope({ ok: true, value: result ?? null });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return serializeEnvelope({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run a faker call requested by the guest and return a JSON envelope.
|
|
75
|
+
*/
|
|
76
|
+
function runFakerBridge(pathJson, argsJson) {
|
|
77
|
+
try {
|
|
78
|
+
const path = JSON.parse(pathJson);
|
|
79
|
+
const args = JSON.parse(argsJson);
|
|
80
|
+
return serializeEnvelope({ ok: true, value: callFaker(path, args) ?? null });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return serializeEnvelope({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Serialize a bridge envelope. Faker occasionally returns values such as `Date`
|
|
88
|
+
* that JSON cannot represent as-is; those are converted to their JSON form,
|
|
89
|
+
* which matches what handler code would send over the wire anyway.
|
|
90
|
+
*/
|
|
91
|
+
function serializeEnvelope(envelope) {
|
|
92
|
+
return JSON.stringify(envelope);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Guest-side bootstrap. It rebuilds `store` and `faker` on top of the host
|
|
96
|
+
* bridges so guest code sees the same API as before, but every call is just a
|
|
97
|
+
* JSON message to the host.
|
|
98
|
+
*/
|
|
99
|
+
const GUEST_PRELUDE = `
|
|
100
|
+
const __unwrap = (raw) => {
|
|
101
|
+
const parsed = JSON.parse(raw)
|
|
102
|
+
if (!parsed.ok) {
|
|
103
|
+
throw new Error(parsed.error)
|
|
104
|
+
}
|
|
105
|
+
return parsed.value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const store = {
|
|
109
|
+
list: (...args) => __unwrap(__store('list', JSON.stringify(args))),
|
|
110
|
+
get: (...args) => __unwrap(__store('get', JSON.stringify(args))),
|
|
111
|
+
create: (...args) => __unwrap(__store('create', JSON.stringify(args))),
|
|
112
|
+
update: (...args) => __unwrap(__store('update', JSON.stringify(args))),
|
|
113
|
+
delete: (...args) => __unwrap(__store('delete', JSON.stringify(args))),
|
|
114
|
+
clear: (...args) => __unwrap(__store('clear', JSON.stringify(args))),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const __makeFaker = (path) =>
|
|
118
|
+
new Proxy(function () {}, {
|
|
119
|
+
get: (_target, prop) => (typeof prop === 'string' ? __makeFaker(path.concat(prop)) : undefined),
|
|
120
|
+
apply: (_target, _thisArg, args) => __unwrap(__faker(JSON.stringify(path), JSON.stringify(args))),
|
|
121
|
+
})
|
|
122
|
+
const faker = __makeFaker([])
|
|
123
|
+
`;
|
|
124
|
+
/**
|
|
125
|
+
* Guest-side `seed` helper. It mirrors the Laravel-inspired API and runs the
|
|
126
|
+
* factory callbacks inside the sandbox, persisting through the `store` bridge.
|
|
127
|
+
*/
|
|
128
|
+
const SEED_PRELUDE = `
|
|
129
|
+
const seed = (() => {
|
|
130
|
+
const create = (item) => store.create(schema, item)
|
|
131
|
+
const helper = (arg1, arg2) => {
|
|
132
|
+
if (typeof arg1 === 'number' && typeof arg2 === 'function') {
|
|
133
|
+
const items = []
|
|
134
|
+
for (let index = 0; index < arg1; index++) {
|
|
135
|
+
items.push(create(arg2()))
|
|
136
|
+
}
|
|
137
|
+
return items
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(arg1)) {
|
|
140
|
+
return arg1.map(create)
|
|
141
|
+
}
|
|
142
|
+
if (typeof arg1 === 'function') {
|
|
143
|
+
return create(arg1())
|
|
144
|
+
}
|
|
145
|
+
throw new Error('Invalid seed() usage. Use seed.count(n, factory), seed(array), or seed(factory)')
|
|
146
|
+
}
|
|
147
|
+
helper.count = (n, factory) => {
|
|
148
|
+
const items = []
|
|
149
|
+
for (let index = 0; index < n; index++) {
|
|
150
|
+
items.push(create(factory()))
|
|
151
|
+
}
|
|
152
|
+
return items
|
|
153
|
+
}
|
|
154
|
+
return helper
|
|
155
|
+
})()
|
|
156
|
+
`;
|
|
157
|
+
/**
|
|
158
|
+
* Assemble the full guest program: bridges, injected JSON globals, the optional
|
|
159
|
+
* seed helper, and the user code wrapped in an async IIFE whose result becomes
|
|
160
|
+
* the completion value QuickJS hands back.
|
|
161
|
+
*/
|
|
162
|
+
function buildGuestSource(code, jsonGlobalNames, includeSeed) {
|
|
163
|
+
const jsonGlobals = jsonGlobalNames.map((name) => `const ${name} = JSON.parse(__json_${name})`).join('\n');
|
|
164
|
+
return `
|
|
165
|
+
${GUEST_PRELUDE}
|
|
166
|
+
${jsonGlobals}
|
|
167
|
+
${includeSeed ? SEED_PRELUDE : ''}
|
|
168
|
+
;(async () => {
|
|
169
|
+
${code}
|
|
170
|
+
})()
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Rebuild a host error from a dumped QuickJS error so callers see a normal
|
|
175
|
+
* `Error` instead of an opaque value.
|
|
176
|
+
*/
|
|
177
|
+
function toError(dumped) {
|
|
178
|
+
if (dumped instanceof Error) {
|
|
179
|
+
return dumped;
|
|
180
|
+
}
|
|
181
|
+
if (dumped && typeof dumped === 'object' && 'message' in dumped) {
|
|
182
|
+
const error = new Error(String(dumped.message));
|
|
183
|
+
const name = dumped.name;
|
|
184
|
+
if (typeof name === 'string') {
|
|
185
|
+
error.name = name;
|
|
186
|
+
}
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
return new Error(typeof dumped === 'string' ? dumped : 'Sandbox execution failed');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Execute untrusted handler or seed code inside a QuickJS WebAssembly sandbox.
|
|
193
|
+
*
|
|
194
|
+
* The guest has no access to the host runtime (`process`, `require`, `fetch`,
|
|
195
|
+
* the `Function` constructor, and so on). It can only talk to the `store` and
|
|
196
|
+
* `faker` bridges and read the injected JSON globals. Memory and time limits
|
|
197
|
+
* bound the remaining denial-of-service risk.
|
|
198
|
+
*/
|
|
199
|
+
export async function runInSandbox(options) {
|
|
200
|
+
const { code, store, jsonGlobals = {}, includeSeed = false } = options;
|
|
201
|
+
const quickJS = await loadQuickJS();
|
|
202
|
+
const context = quickJS.newContext();
|
|
203
|
+
try {
|
|
204
|
+
const { runtime } = context;
|
|
205
|
+
runtime.setMemoryLimit(MEMORY_LIMIT_BYTES);
|
|
206
|
+
const deadline = Date.now() + EXECUTION_TIMEOUT_MS;
|
|
207
|
+
runtime.setInterruptHandler(() => Date.now() > deadline);
|
|
208
|
+
// Host bridge: store operations run the real (tracked) store and return JSON.
|
|
209
|
+
const storeBridge = context.newFunction('__store', (methodHandle, argsHandle) => context.newString(runStoreBridge(store, context.getString(methodHandle), context.getString(argsHandle))));
|
|
210
|
+
context.setProp(context.global, '__store', storeBridge);
|
|
211
|
+
storeBridge.dispose();
|
|
212
|
+
// Host bridge: faker. Only a property path and JSON arguments cross the boundary.
|
|
213
|
+
const fakerBridge = context.newFunction('__faker', (pathHandle, argsHandle) => context.newString(runFakerBridge(context.getString(pathHandle), context.getString(argsHandle))));
|
|
214
|
+
context.setProp(context.global, '__faker', fakerBridge);
|
|
215
|
+
fakerBridge.dispose();
|
|
216
|
+
// Inject read-only inputs (req/res/schema) as JSON strings the guest parses.
|
|
217
|
+
for (const [name, value] of Object.entries(jsonGlobals)) {
|
|
218
|
+
const handle = context.newString(JSON.stringify(value ?? null));
|
|
219
|
+
context.setProp(context.global, `__json_${name}`, handle);
|
|
220
|
+
handle.dispose();
|
|
221
|
+
}
|
|
222
|
+
const evalResult = context.evalCode(buildGuestSource(code, Object.keys(jsonGlobals), includeSeed));
|
|
223
|
+
if (evalResult.error) {
|
|
224
|
+
const error = context.dump(evalResult.error);
|
|
225
|
+
evalResult.error.dispose();
|
|
226
|
+
throw toError(error);
|
|
227
|
+
}
|
|
228
|
+
// The completion value is the async IIFE's promise. Resolve it, then drain the
|
|
229
|
+
// job queue; every host bridge is synchronous, so one pass settles the promise.
|
|
230
|
+
const promiseHandle = evalResult.value;
|
|
231
|
+
const resolved = context.resolvePromise(promiseHandle);
|
|
232
|
+
promiseHandle.dispose();
|
|
233
|
+
const jobs = runtime.executePendingJobs();
|
|
234
|
+
if (jobs.error) {
|
|
235
|
+
const error = context.dump(jobs.error);
|
|
236
|
+
jobs.error.dispose();
|
|
237
|
+
throw toError(error);
|
|
238
|
+
}
|
|
239
|
+
const settled = await resolved;
|
|
240
|
+
if (settled.error) {
|
|
241
|
+
const error = context.dump(settled.error);
|
|
242
|
+
settled.error.dispose();
|
|
243
|
+
throw toError(error);
|
|
244
|
+
}
|
|
245
|
+
const value = context.dump(settled.value);
|
|
246
|
+
settled.value.dispose();
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
context.dispose();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { Store } from '../libs/store.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Public interface of the Store class (methods only, no private properties).
|
|
4
4
|
*/
|
|
5
|
-
type StoreInterface = Pick<Store, 'list' | 'get' | 'create' | 'update' | 'delete' | 'clear'>;
|
|
5
|
+
export type StoreInterface = Pick<Store, 'list' | 'get' | 'create' | 'update' | 'delete' | 'clear'>;
|
|
6
6
|
/**
|
|
7
7
|
* Represents a single store operation with its result.
|
|
8
8
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-wrapper.d.ts","sourceRoot":"","sources":["../../src/utils/store-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAE1C;;GAEG;AACH,
|
|
1
|
+
{"version":3,"file":"store-wrapper.d.ts","sourceRoot":"","sources":["../../src/utils/store-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAE1C;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,CAAA;AAEnG;;GAEG;AACH,KAAK,cAAc,GAAG;IACpB,iDAAiD;IACjD,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC1D,mCAAmC;IACnC,MAAM,EAAE,GAAG,CAAA;CACZ,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,0CAA0C;IAC1C,UAAU,EAAE,cAAc,EAAE,CAAA;CAC7B,CAAA;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG;IAChD,YAAY,EAAE,cAAc,CAAA;IAC5B,QAAQ,EAAE,sBAAsB,CAAA;CACjC,CA2CA"}
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"swagger",
|
|
17
17
|
"cli"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.14.1",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -55,14 +55,15 @@
|
|
|
55
55
|
"@hono/node-ws": "^1.2.0",
|
|
56
56
|
"ajv": "^8.20.0",
|
|
57
57
|
"ajv-formats": "^3.0.1",
|
|
58
|
-
"hono": "^4.
|
|
58
|
+
"hono": "^4.13.7",
|
|
59
|
+
"quickjs-emscripten": "0.32.0",
|
|
59
60
|
"yaml": "^2.9.0",
|
|
60
|
-
"@scalar/helpers": "0.
|
|
61
|
-
"@scalar/json-magic": "0.13.
|
|
62
|
-
"@scalar/openapi-types": "0.9.
|
|
63
|
-
"@scalar/openapi-upgrader": "0.2.
|
|
64
|
-
"@scalar/workspace-store": "0.
|
|
65
|
-
"@scalar/types": "0.
|
|
61
|
+
"@scalar/helpers": "0.12.0",
|
|
62
|
+
"@scalar/json-magic": "0.13.5",
|
|
63
|
+
"@scalar/openapi-types": "0.9.6",
|
|
64
|
+
"@scalar/openapi-upgrader": "0.2.16",
|
|
65
|
+
"@scalar/workspace-store": "0.61.0",
|
|
66
|
+
"@scalar/types": "0.20.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@types/node": "^24.1.0",
|