@gcoredev/fastedge-test 0.1.7 → 0.2.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.
- package/dist/frontend/assets/{index-BCXfEMSq.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +130 -62
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +130 -62
- package/dist/lib/runner/HeaderManager.d.ts +6 -4
- package/dist/lib/runner/HostFunctions.d.ts +5 -5
- package/dist/lib/runner/HttpWasmRunner.d.ts +13 -4
- package/dist/lib/runner/IStateManager.d.ts +7 -7
- package/dist/lib/runner/IWasmRunner.d.ts +17 -9
- package/dist/lib/runner/PropertyResolver.d.ts +3 -3
- package/dist/lib/runner/ProxyWasmRunner.d.ts +5 -2
- package/dist/lib/runner/standalone.d.ts +1 -1
- package/dist/lib/runner/types.d.ts +17 -8
- package/dist/lib/schemas/api.d.ts +0 -8
- package/dist/lib/schemas/config.d.ts +0 -13
- package/dist/lib/schemas/index.d.ts +2 -2
- package/dist/lib/test-framework/assertions.d.ts +18 -4
- package/dist/lib/test-framework/index.cjs +18593 -111
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18610 -100
- package/dist/lib/test-framework/mock-origins.d.ts +56 -0
- package/dist/lib/test-framework/types.d.ts +1 -5
- package/dist/server.js +33 -33
- package/docs/API.md +19 -49
- package/docs/DEBUGGER.md +6 -7
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +96 -81
- package/docs/TEST_CONFIG.md +9 -22
- package/docs/TEST_FRAMEWORK.md +206 -31
- package/docs/WEBSOCKET.md +25 -21
- package/docs/quickstart.md +1 -13
- package/package.json +4 -1
- package/schemas/api-config.schema.json +0 -24
- package/schemas/api-send.schema.json +0 -20
- package/schemas/fastedge-config.test.schema.json +0 -24
- package/schemas/full-flow-result.schema.json +17 -7
- package/schemas/hook-call.schema.json +16 -6
- package/schemas/hook-result.schema.json +16 -6
- package/schemas/http-response.schema.json +227 -5
package/docs/API.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The `@gcoredev/fastedge-test` debugger server exposes a REST API for loading WASM modules, executing requests, and managing test configuration.
|
|
4
4
|
|
|
5
|
+
> **Note on header values.** Response-side and hook-result headers use `Record<string, string | string[]>` — single-valued headers are a `string`, multi-valued headers (notably `Set-Cookie` per RFC 6265) are a `string[]`. Request-side header inputs are single-valued `Record<string, string>`.
|
|
6
|
+
|
|
5
7
|
## Base URL
|
|
6
8
|
|
|
7
9
|
```
|
|
@@ -257,22 +259,16 @@ For **HTTP-WASM**, provide either `path` (preferred) or `url` (legacy). When `pa
|
|
|
257
259
|
}
|
|
258
260
|
```
|
|
259
261
|
|
|
260
|
-
For **Proxy-WASM**, the top-level `url` field is required. The full CDN flow is controlled via nested `request
|
|
262
|
+
For **Proxy-WASM**, the top-level `url` field is required. The full CDN flow is controlled via nested `request` and `properties` fields. The upstream response is generated at runtime — either by a real fetch against `url`, or by the built-in responder when `url === "built-in"`:
|
|
261
263
|
|
|
262
264
|
```typescript
|
|
263
265
|
{
|
|
264
|
-
url: string; // Request URL (required)
|
|
266
|
+
url: string; // Request URL, or "built-in" (required)
|
|
265
267
|
request?: {
|
|
266
268
|
method?: string; // HTTP method (default: "GET")
|
|
267
269
|
headers?: Record<string, string>; // Request headers (default: {})
|
|
268
270
|
body?: string; // Request body (default: "")
|
|
269
271
|
};
|
|
270
|
-
response?: {
|
|
271
|
-
headers?: Record<string, string>; // Simulated upstream response headers (default: {})
|
|
272
|
-
body?: string; // Simulated upstream response body (default: "")
|
|
273
|
-
status?: number; // Simulated upstream response status (default: 200)
|
|
274
|
-
statusText?: string; // Simulated upstream response status text (default: "OK")
|
|
275
|
-
};
|
|
276
272
|
properties?: Record<string, unknown>; // CDN properties (default: {})
|
|
277
273
|
}
|
|
278
274
|
```
|
|
@@ -285,7 +281,7 @@ For **Proxy-WASM**, the top-level `url` field is required. The full CDN flow is
|
|
|
285
281
|
result: {
|
|
286
282
|
status: number;
|
|
287
283
|
statusText: string;
|
|
288
|
-
headers: Record<string, string>;
|
|
284
|
+
headers: Record<string, string | string[]>;
|
|
289
285
|
body: string;
|
|
290
286
|
contentType: string | null;
|
|
291
287
|
isBase64?: boolean;
|
|
@@ -303,7 +299,7 @@ For **Proxy-WASM**, the top-level `url` field is required. The full CDN flow is
|
|
|
303
299
|
finalResponse: {
|
|
304
300
|
status: number;
|
|
305
301
|
statusText: string;
|
|
306
|
-
headers: Record<string, string>;
|
|
302
|
+
headers: Record<string, string | string[]>;
|
|
307
303
|
body: string;
|
|
308
304
|
contentType: string;
|
|
309
305
|
isBase64?: boolean;
|
|
@@ -319,13 +315,13 @@ type HookResult = {
|
|
|
319
315
|
returnCode: number | null;
|
|
320
316
|
logs: Array<{ level: number; message: string }>;
|
|
321
317
|
input: {
|
|
322
|
-
request: { headers: Record<string, string>; body: string };
|
|
323
|
-
response: { headers: Record<string, string>; body: string };
|
|
318
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
319
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
324
320
|
properties?: Record<string, unknown>;
|
|
325
321
|
};
|
|
326
322
|
output: {
|
|
327
|
-
request: { headers: Record<string, string>; body: string };
|
|
328
|
-
response: { headers: Record<string, string>; body: string };
|
|
323
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
324
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
329
325
|
properties?: Record<string, unknown>;
|
|
330
326
|
};
|
|
331
327
|
properties: Record<string, unknown>;
|
|
@@ -377,12 +373,6 @@ curl -X POST http://localhost:5179/api/execute \
|
|
|
377
373
|
"headers": { "host": "example.com" },
|
|
378
374
|
"body": ""
|
|
379
375
|
},
|
|
380
|
-
"response": {
|
|
381
|
-
"headers": { "content-type": "text/html" },
|
|
382
|
-
"body": "<html/>",
|
|
383
|
-
"status": 200,
|
|
384
|
-
"statusText": "OK"
|
|
385
|
-
},
|
|
386
376
|
"properties": {}
|
|
387
377
|
}'
|
|
388
378
|
```
|
|
@@ -475,13 +465,13 @@ type HookResult = {
|
|
|
475
465
|
returnCode: number | null;
|
|
476
466
|
logs: Array<{ level: number; message: string }>;
|
|
477
467
|
input: {
|
|
478
|
-
request: { headers: Record<string, string>; body: string };
|
|
479
|
-
response: { headers: Record<string, string>; body: string };
|
|
468
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
469
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
480
470
|
properties?: Record<string, unknown>;
|
|
481
471
|
};
|
|
482
472
|
output: {
|
|
483
|
-
request: { headers: Record<string, string>; body: string };
|
|
484
|
-
response: { headers: Record<string, string>; body: string };
|
|
473
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
474
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
485
475
|
properties?: Record<string, unknown>;
|
|
486
476
|
};
|
|
487
477
|
properties: Record<string, unknown>;
|
|
@@ -556,22 +546,18 @@ Requires a WASM module to be loaded via `POST /api/load`. Accepts an optional [`
|
|
|
556
546
|
|
|
557
547
|
```typescript
|
|
558
548
|
{
|
|
559
|
-
url: string | "built-in"; // Full request URL, or "built-in" to use the
|
|
549
|
+
url: string | "built-in"; // Full request URL, or "built-in" to use the built-in responder
|
|
560
550
|
request?: {
|
|
561
551
|
method?: string; // HTTP method (default: "GET")
|
|
562
552
|
url?: string;
|
|
563
553
|
headers?: Record<string, string>; // Request headers (default: {})
|
|
564
554
|
body?: string; // Request body (default: "")
|
|
565
555
|
};
|
|
566
|
-
response?: {
|
|
567
|
-
headers?: Record<string, string>; // Simulated upstream response headers (default: {})
|
|
568
|
-
body?: string; // Simulated upstream response body (default: "")
|
|
569
|
-
};
|
|
570
556
|
properties: Record<string, unknown>; // CDN properties (required; use {} if none)
|
|
571
557
|
}
|
|
572
558
|
```
|
|
573
559
|
|
|
574
|
-
The
|
|
560
|
+
The upstream response is generated at runtime — either by a real fetch against `url`, or by the built-in responder when `url === "built-in"`.
|
|
575
561
|
|
|
576
562
|
**Response**
|
|
577
563
|
|
|
@@ -582,7 +568,7 @@ The `response` object for this endpoint does not accept `status` or `statusText`
|
|
|
582
568
|
finalResponse: {
|
|
583
569
|
status: number;
|
|
584
570
|
statusText: string;
|
|
585
|
-
headers: Record<string, string>;
|
|
571
|
+
headers: Record<string, string | string[]>;
|
|
586
572
|
body: string;
|
|
587
573
|
contentType: string;
|
|
588
574
|
isBase64?: boolean;
|
|
@@ -606,10 +592,6 @@ curl -X POST http://localhost:5179/api/send \
|
|
|
606
592
|
"headers": { "content-type": "application/json" },
|
|
607
593
|
"body": "{\"key\":\"value\"}"
|
|
608
594
|
},
|
|
609
|
-
"response": {
|
|
610
|
-
"headers": { "content-type": "application/json" },
|
|
611
|
-
"body": "{\"result\":\"ok\"}"
|
|
612
|
-
},
|
|
613
595
|
"properties": {
|
|
614
596
|
"client.geo.country": "DE"
|
|
615
597
|
}
|
|
@@ -670,8 +652,8 @@ curl -X POST http://localhost:5179/api/send \
|
|
|
670
652
|
|
|
671
653
|
**Error Responses**
|
|
672
654
|
|
|
673
|
-
| Status | Condition
|
|
674
|
-
| ------ |
|
|
655
|
+
| Status | Condition |
|
|
656
|
+
| ------ | ---------------------------------------------------------------------------- |
|
|
675
657
|
| `400` | Validation failed (missing `url` or `properties`), or no WASM module loaded |
|
|
676
658
|
| `500` | Execution failed |
|
|
677
659
|
|
|
@@ -712,10 +694,6 @@ type ProxyWasmConfig = {
|
|
|
712
694
|
headers: Record<string, string>;
|
|
713
695
|
body: string;
|
|
714
696
|
};
|
|
715
|
-
response?: {
|
|
716
|
-
headers: Record<string, string>;
|
|
717
|
-
body: string;
|
|
718
|
-
};
|
|
719
697
|
properties: Record<string, unknown>;
|
|
720
698
|
dotenv?: { enabled?: boolean; path?: string };
|
|
721
699
|
};
|
|
@@ -758,10 +736,6 @@ curl http://localhost:5179/api/config
|
|
|
758
736
|
"headers": {},
|
|
759
737
|
"body": ""
|
|
760
738
|
},
|
|
761
|
-
"response": {
|
|
762
|
-
"headers": {},
|
|
763
|
-
"body": ""
|
|
764
|
-
},
|
|
765
739
|
"properties": {}
|
|
766
740
|
},
|
|
767
741
|
"valid": true
|
|
@@ -816,10 +790,6 @@ curl -X POST http://localhost:5179/api/config \
|
|
|
816
790
|
"headers": { "accept": "text/html" },
|
|
817
791
|
"body": ""
|
|
818
792
|
},
|
|
819
|
-
"response": {
|
|
820
|
-
"headers": {},
|
|
821
|
-
"body": ""
|
|
822
|
-
},
|
|
823
793
|
"properties": {
|
|
824
794
|
"client.geo.country": "US"
|
|
825
795
|
}
|
package/docs/DEBUGGER.md
CHANGED
|
@@ -98,13 +98,12 @@ curl http://localhost:5179/health
|
|
|
98
98
|
|
|
99
99
|
## Environment Variables
|
|
100
100
|
|
|
101
|
-
| Variable | Type | Default | Description
|
|
102
|
-
| -------------------- | -------- | ------- |
|
|
103
|
-
| `PORT` | `number` | unset | Port the HTTP server listens on. Defaults to `5179` when not set.
|
|
104
|
-
| `PROXY_RUNNER_DEBUG` | `"1"` | unset | Enable verbose debug logging for WebSocket and runner activity.
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `FASTEDGE_RUN_PATH` | `string` | unset | Override the path to the `fastedge-run` CLI binary used to execute WASM modules. |
|
|
101
|
+
| Variable | Type | Default | Description |
|
|
102
|
+
| -------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `PORT` | `number` | unset | Port the HTTP server listens on. Defaults to `5179` when not set. |
|
|
104
|
+
| `PROXY_RUNNER_DEBUG` | `"1"` | unset | Enable verbose debug logging for WebSocket and runner activity. |
|
|
105
|
+
| `WORKSPACE_PATH` | `string` | unset | Absolute path to the workspace root; used as the `.env` file base and for port file placement. |
|
|
106
|
+
| `FASTEDGE_RUN_PATH` | `string` | unset | Override the path to the `fastedge-run` CLI binary used to execute WASM modules. |
|
|
108
107
|
|
|
109
108
|
### Usage examples
|
|
110
109
|
|
package/docs/INDEX.md
CHANGED
|
@@ -79,12 +79,13 @@ import { defineTestSuite, runAndExit } from "@gcoredev/fastedge-test/test";
|
|
|
79
79
|
| `runFlow` | function | Executes a single request flow directly |
|
|
80
80
|
| `runHttpRequest` | function | Executes a single HTTP request directly |
|
|
81
81
|
| `loadConfigFile` | function | Loads and validates a `fastedge-config.test.json` file |
|
|
82
|
+
| `mockOrigins` | function | Creates mock HTTP origins that respond to outgoing HTTP calls |
|
|
82
83
|
| `assertRequestHeader` | function | Asserts a header is present on the outgoing request |
|
|
83
84
|
| `assertNoRequestHeader` | function | Asserts a header is absent from the outgoing request |
|
|
84
85
|
| `assertResponseHeader` | function | Asserts a header is present on the final response |
|
|
85
86
|
| `assertNoResponseHeader` | function | Asserts a header is absent from the final response |
|
|
86
87
|
| `assertFinalStatus` | function | Asserts the final HTTP status code |
|
|
87
|
-
| `assertFinalHeader` | function | Asserts a header on the final response
|
|
88
|
+
| `assertFinalHeader` | function | Asserts a header on the final response |
|
|
88
89
|
| `assertReturnCode` | function | Asserts the proxy-wasm return code |
|
|
89
90
|
| `assertLog` | function | Asserts a log entry was emitted |
|
|
90
91
|
| `assertNoLog` | function | Asserts a log entry was not emitted |
|
|
@@ -101,6 +102,8 @@ import { defineTestSuite, runAndExit } from "@gcoredev/fastedge-test/test";
|
|
|
101
102
|
| `assertHttpContentType` | function | Asserts the HTTP response Content-Type header |
|
|
102
103
|
| `assertHttpLog` | function | Asserts a log entry was emitted during HTTP request handling |
|
|
103
104
|
| `assertHttpNoLog` | function | Asserts a log entry was not emitted during HTTP request handling |
|
|
105
|
+
| `MockOriginsHandle` | type | Handle returned by `mockOrigins` — use to stop the mock servers |
|
|
106
|
+
| `MockOriginsOptions` | type | Options accepted by `mockOrigins` |
|
|
104
107
|
| `TestSuite` | type | Suite definition — one of `wasmPath` or `wasmBuffer` plus test cases |
|
|
105
108
|
| `TestCase` | type | A single test scenario with config and assertions |
|
|
106
109
|
| `TestResult` | type | Result of a single test case execution |
|
package/docs/RUNNER.md
CHANGED
|
@@ -102,10 +102,6 @@ interface IWasmRunner {
|
|
|
102
102
|
method: string,
|
|
103
103
|
headers: Record<string, string>,
|
|
104
104
|
body: string,
|
|
105
|
-
responseHeaders: Record<string, string>,
|
|
106
|
-
responseBody: string,
|
|
107
|
-
responseStatus: number,
|
|
108
|
-
responseStatusText: string,
|
|
109
105
|
properties: Record<string, unknown>,
|
|
110
106
|
enforceProductionPropertyRules: boolean
|
|
111
107
|
): Promise<FullFlowResult>;
|
|
@@ -161,7 +157,7 @@ const runner = await createRunner('./my-http-app.wasm');
|
|
|
161
157
|
let response = await runner.execute({ path: '/moved', method: 'GET', headers: {} });
|
|
162
158
|
if (response.status >= 300 && response.status < 400 && response.headers['location']) {
|
|
163
159
|
response = await runner.execute({
|
|
164
|
-
path: response.headers['location'], // e.g. "/new-location"
|
|
160
|
+
path: response.headers['location'] as string, // e.g. "/new-location"
|
|
165
161
|
method: 'GET',
|
|
166
162
|
headers: {},
|
|
167
163
|
});
|
|
@@ -192,10 +188,6 @@ callFullFlow(
|
|
|
192
188
|
method: string,
|
|
193
189
|
headers: Record<string, string>,
|
|
194
190
|
body: string,
|
|
195
|
-
responseHeaders: Record<string, string>,
|
|
196
|
-
responseBody: string,
|
|
197
|
-
responseStatus: number,
|
|
198
|
-
responseStatusText: string,
|
|
199
191
|
properties: Record<string, unknown>,
|
|
200
192
|
enforceProductionPropertyRules: boolean
|
|
201
193
|
): Promise<FullFlowResult>
|
|
@@ -203,22 +195,20 @@ callFullFlow(
|
|
|
203
195
|
|
|
204
196
|
**Parameters**
|
|
205
197
|
|
|
206
|
-
| Parameter | Type | Description
|
|
207
|
-
| -------------------------------- | ------------------------- |
|
|
208
|
-
| `url` | `string` | Full request URL, or `BUILTIN_SHORTHAND` (`"built-in"`) to use the built-in responder instead of a real origin fetch
|
|
209
|
-
| `method` | `string` | HTTP method
|
|
210
|
-
| `headers` | `Record<string, string>` | Request headers
|
|
211
|
-
| `body` | `string` | Request body
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
| `properties` | `Record<string, unknown>` | Shared properties passed to all hooks |
|
|
217
|
-
| `enforceProductionPropertyRules` | `boolean` | When `true`, restricts property access to match CDN production behavior |
|
|
198
|
+
| Parameter | Type | Description |
|
|
199
|
+
| -------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
200
|
+
| `url` | `string` | Full request URL, or `BUILTIN_SHORTHAND` (`"built-in"`) to use the built-in responder instead of a real origin fetch |
|
|
201
|
+
| `method` | `string` | HTTP method |
|
|
202
|
+
| `headers` | `Record<string, string>` | Request headers |
|
|
203
|
+
| `body` | `string` | Request body |
|
|
204
|
+
| `properties` | `Record<string, unknown>` | Shared properties passed to all hooks |
|
|
205
|
+
| `enforceProductionPropertyRules` | `boolean` | When `true`, restricts property access to match CDN production behavior |
|
|
206
|
+
|
|
207
|
+
The upstream response is generated at runtime — either by a real HTTP fetch against `url`, or by the built-in responder when `url === "built-in"`. There is no caller-provided mock response.
|
|
218
208
|
|
|
219
209
|
Hook execution order: `onRequestHeaders` → `onRequestBody` → *(real HTTP fetch or built-in responder)* → `onResponseHeaders` → `onResponseBody`.
|
|
220
210
|
|
|
221
|
-
**Local response short-circuit
|
|
211
|
+
**Local response short-circuit.** If a WASM module calls `proxy_send_local_response` during `onRequestHeaders` or `onRequestBody` and returns `StopIteration` (return code `1`), the remaining hooks and origin fetch are skipped. The `finalResponse` in the result is built from the locally-sent status, headers, and body — matching CDN production behavior. This is how redirect modules (e.g., geo-redirect) and early error responses work.
|
|
222
212
|
|
|
223
213
|
Only available on `ProxyWasmRunner`. Calling on `HttpWasmRunner` throws.
|
|
224
214
|
|
|
@@ -314,16 +304,17 @@ const result: FullFlowResult = await runner.callFullFlow(
|
|
|
314
304
|
'GET',
|
|
315
305
|
{ 'accept': 'application/json' },
|
|
316
306
|
'',
|
|
317
|
-
{},
|
|
307
|
+
{}, // properties
|
|
308
|
+
true, // enforceProductionPropertyRules
|
|
318
309
|
);
|
|
319
310
|
```
|
|
320
311
|
|
|
321
312
|
**Built-in responder behavior** — controlled by request headers set before the origin phase:
|
|
322
313
|
|
|
323
|
-
| Header | Effect
|
|
324
|
-
| -------------------- |
|
|
325
|
-
| `x-debugger-status` | HTTP status code for the generated response (default: `200`)
|
|
326
|
-
| `x-debugger-content` | Response body mode: `"body-only"`, `"status-only"`, or full JSON echo (default)
|
|
314
|
+
| Header | Effect |
|
|
315
|
+
| -------------------- | -------------------------------------------------------------------------------- |
|
|
316
|
+
| `x-debugger-status` | HTTP status code for the generated response (default: `200`) |
|
|
317
|
+
| `x-debugger-content` | Response body mode: `"body-only"`, `"status-only"`, or full JSON echo (default) |
|
|
327
318
|
|
|
328
319
|
When `x-debugger-content` is omitted, the built-in responder returns a JSON echo of the request method, headers, body, and URL. Both control headers are stripped before response hooks execute so they do not appear in hook input state.
|
|
329
320
|
|
|
@@ -345,13 +336,13 @@ interface RunnerConfig {
|
|
|
345
336
|
}
|
|
346
337
|
```
|
|
347
338
|
|
|
348
|
-
| Field | Type | Default
|
|
349
|
-
| -------------------------------- | ---------- |
|
|
350
|
-
| `dotenv.enabled` | `boolean` | `false`
|
|
351
|
-
| `dotenv.path` | `string` | `undefined`
|
|
352
|
-
| `enforceProductionPropertyRules` | `boolean` | `true`
|
|
353
|
-
| `runnerType` | `WasmType` | auto-detected | Override WASM type detection
|
|
354
|
-
| `httpPort` | `number` | `undefined`
|
|
339
|
+
| Field | Type | Default | Description |
|
|
340
|
+
| -------------------------------- | ---------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
341
|
+
| `dotenv.enabled` | `boolean` | `false` | Whether to load `.env` files |
|
|
342
|
+
| `dotenv.path` | `string` | `undefined` | Directory to load dotenv files from. When omitted, `fastedge-run` uses the process CWD — correct for most npm package users whose `.env` files live at the project root. Only set this when your dotenv files are in a non-standard location (e.g. a test fixture directory). |
|
|
343
|
+
| `enforceProductionPropertyRules` | `boolean` | `true` | Restrict property access to match CDN production behavior |
|
|
344
|
+
| `runnerType` | `WasmType` | *auto-detected* | Override WASM type detection |
|
|
345
|
+
| `httpPort` | `number` | `undefined` | HTTP-WASM only. Pin the spawned `fastedge-run` subprocess to a specific port instead of allocating from the dynamic pool (8100–8199). `load()` throws if the port is busy — there is no fallback to dynamic allocation. Intended for Codespaces/Docker port-forwarding or external tooling requiring a fixed address. Ignored for proxy-wasm. |
|
|
355
346
|
|
|
356
347
|
### HttpRequest & HttpResponse
|
|
357
348
|
|
|
@@ -366,7 +357,11 @@ interface HttpRequest {
|
|
|
366
357
|
interface HttpResponse {
|
|
367
358
|
status: number;
|
|
368
359
|
statusText: string;
|
|
369
|
-
|
|
360
|
+
// Node's IncomingHttpHeaders (from "node:http"):
|
|
361
|
+
// known single-valued headers (content-type, location, etag, …) are typed as string
|
|
362
|
+
// set-cookie is always string[] when present
|
|
363
|
+
// unknown headers are string | string[] | undefined
|
|
364
|
+
headers: IncomingHttpHeaders;
|
|
370
365
|
body: string;
|
|
371
366
|
contentType: string | null;
|
|
372
367
|
isBase64?: boolean;
|
|
@@ -380,20 +375,37 @@ interface HttpResponse {
|
|
|
380
375
|
|
|
381
376
|
`HttpResponse.logs` contains log entries captured from the `fastedge-run` process stdout/stderr during the request.
|
|
382
377
|
|
|
378
|
+
**Multi-valued headers.** `Set-Cookie` is preserved as a `string[]` — each `Set-Cookie` header emitted by the WASM app (or an upstream origin) becomes a separate array entry. This matches RFC 6265 §3 and Node's fetch behavior. Example:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const response = await runner.execute({ path: '/login', method: 'POST', headers: {} });
|
|
382
|
+
const cookies = response.headers['set-cookie']; // string[] | undefined
|
|
383
|
+
for (const cookie of cookies ?? []) {
|
|
384
|
+
console.log(cookie);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Single-valued headers read as plain strings with no narrowing needed:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const location = response.headers['location']; // string | undefined
|
|
392
|
+
const contentType = response.headers['content-type']; // string | undefined
|
|
393
|
+
```
|
|
394
|
+
|
|
383
395
|
### HookCall
|
|
384
396
|
|
|
385
397
|
```typescript
|
|
386
398
|
type HookCall = {
|
|
387
399
|
hook: string;
|
|
388
400
|
request: {
|
|
389
|
-
headers:
|
|
401
|
+
headers: HeaderRecord;
|
|
390
402
|
body: string;
|
|
391
403
|
method?: string;
|
|
392
404
|
path?: string;
|
|
393
405
|
scheme?: string;
|
|
394
406
|
};
|
|
395
|
-
response
|
|
396
|
-
headers:
|
|
407
|
+
response?: {
|
|
408
|
+
headers: HeaderRecord;
|
|
397
409
|
body: string;
|
|
398
410
|
status?: number;
|
|
399
411
|
statusText?: string;
|
|
@@ -404,14 +416,16 @@ type HookCall = {
|
|
|
404
416
|
};
|
|
405
417
|
```
|
|
406
418
|
|
|
407
|
-
| Field | Description
|
|
408
|
-
| -------------------------------- |
|
|
409
|
-
| `hook` | Hook name: `"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`
|
|
410
|
-
| `request` | Request state passed to the hook
|
|
411
|
-
| `response` |
|
|
412
|
-
| `properties` | Shared properties (e.g. `request.path`, `vm_config`, `plugin_config`)
|
|
413
|
-
| `dotenvEnabled` | Optional per-call dotenv override. Use `applyDotenv()` for persistent changes.
|
|
414
|
-
| `enforceProductionPropertyRules` | Defaults to `true`. Set to `false` to allow property reads that would be blocked on production CDN.
|
|
419
|
+
| Field | Description |
|
|
420
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| `hook` | Hook name: `"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"` |
|
|
422
|
+
| `request` | Request state passed to the hook |
|
|
423
|
+
| `response` | Seed state for response hooks called via `callHook()`. Ignored by `callFullFlow()` and by request hooks — the full-flow path generates the upstream response at runtime. |
|
|
424
|
+
| `properties` | Shared properties (e.g. `request.path`, `vm_config`, `plugin_config`) |
|
|
425
|
+
| `dotenvEnabled` | Optional per-call dotenv override. Use `applyDotenv()` for persistent changes. |
|
|
426
|
+
| `enforceProductionPropertyRules` | Defaults to `true`. Set to `false` to allow property reads that would be blocked on production CDN. |
|
|
427
|
+
|
|
428
|
+
`HeaderRecord` is `Record<string, string | string[]>` — multi-valued headers (e.g. multiple `Set-Cookie`) are represented as `string[]`.
|
|
415
429
|
|
|
416
430
|
### HookResult
|
|
417
431
|
|
|
@@ -420,26 +434,26 @@ type HookResult = {
|
|
|
420
434
|
returnCode: number | null;
|
|
421
435
|
logs: { level: number; message: string }[];
|
|
422
436
|
input: {
|
|
423
|
-
request: { headers:
|
|
424
|
-
response: { headers:
|
|
437
|
+
request: { headers: HeaderRecord; body: string };
|
|
438
|
+
response: { headers: HeaderRecord; body: string };
|
|
425
439
|
properties?: Record<string, unknown>;
|
|
426
440
|
};
|
|
427
441
|
output: {
|
|
428
|
-
request: { headers:
|
|
429
|
-
response: { headers:
|
|
442
|
+
request: { headers: HeaderRecord; body: string };
|
|
443
|
+
response: { headers: HeaderRecord; body: string };
|
|
430
444
|
properties?: Record<string, unknown>;
|
|
431
445
|
};
|
|
432
446
|
properties: Record<string, unknown>;
|
|
433
447
|
};
|
|
434
448
|
```
|
|
435
449
|
|
|
436
|
-
| Field | Description
|
|
437
|
-
| ------------ |
|
|
438
|
-
| `returnCode` | The numeric value returned by the WASM hook export, or `null` if the export was not found
|
|
439
|
-
| `logs` | Log entries emitted via `proxy_log` during hook execution
|
|
440
|
-
| `input` | Request/response state as seen by the hook before execution
|
|
441
|
-
| `output` | Request/response state after hook execution (reflects WASM mutations)
|
|
442
|
-
| `properties` | All shared properties after hook execution
|
|
450
|
+
| Field | Description |
|
|
451
|
+
| ------------ | ------------------------------------------------------------------------------------------- |
|
|
452
|
+
| `returnCode` | The numeric value returned by the WASM hook export, or `null` if the export was not found |
|
|
453
|
+
| `logs` | Log entries emitted via `proxy_log` during hook execution |
|
|
454
|
+
| `input` | Request/response state as seen by the hook before execution |
|
|
455
|
+
| `output` | Request/response state after hook execution (reflects WASM mutations) |
|
|
456
|
+
| `properties` | All shared properties after hook execution |
|
|
443
457
|
|
|
444
458
|
### FullFlowResult
|
|
445
459
|
|
|
@@ -449,7 +463,7 @@ type FullFlowResult = {
|
|
|
449
463
|
finalResponse: {
|
|
450
464
|
status: number;
|
|
451
465
|
statusText: string;
|
|
452
|
-
headers:
|
|
466
|
+
headers: HeaderRecord;
|
|
453
467
|
body: string;
|
|
454
468
|
contentType: string;
|
|
455
469
|
isBase64?: boolean;
|
|
@@ -458,19 +472,23 @@ type FullFlowResult = {
|
|
|
458
472
|
};
|
|
459
473
|
```
|
|
460
474
|
|
|
461
|
-
| Field | Description
|
|
462
|
-
| ---------------------- |
|
|
463
|
-
| `hookResults` | A `Record` keyed by hook name (`"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`), each containing a `HookResult`
|
|
464
|
-
| `finalResponse` | The final response after all hooks have executed, or the local response if a hook short-circuited (see `callFullFlow`). `body` is base64-encoded when `isBase64` is `true`.
|
|
465
|
-
| `calculatedProperties` | Runtime properties computed from the request URL (e.g. `request.path`, `request.host`)
|
|
475
|
+
| Field | Description |
|
|
476
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
477
|
+
| `hookResults` | A `Record` keyed by hook name (`"onRequestHeaders"`, `"onRequestBody"`, `"onResponseHeaders"`, `"onResponseBody"`), each containing a `HookResult` |
|
|
478
|
+
| `finalResponse` | The final response after all hooks have executed, or the local response if a hook short-circuited (see `callFullFlow`). `body` is base64-encoded when `isBase64` is `true`. |
|
|
479
|
+
| `calculatedProperties` | Runtime properties computed from the request URL (e.g. `request.path`, `request.host`) |
|
|
466
480
|
|
|
467
481
|
### Supporting Types
|
|
468
482
|
|
|
469
483
|
```typescript
|
|
470
484
|
type WasmType = 'http-wasm' | 'proxy-wasm';
|
|
471
485
|
|
|
486
|
+
// Single-valued headers only — used as callFullFlow input parameters
|
|
472
487
|
type HeaderMap = Record<string, string>;
|
|
473
488
|
|
|
489
|
+
// Single- or multi-valued headers — used in HookCall, HookResult, and FullFlowResult
|
|
490
|
+
type HeaderRecord = Record<string, string | string[]>;
|
|
491
|
+
|
|
474
492
|
type LogEntry = {
|
|
475
493
|
level: number;
|
|
476
494
|
message: string;
|
|
@@ -498,7 +516,7 @@ interface IStateManager {
|
|
|
498
516
|
emitRequestStarted(
|
|
499
517
|
url: string,
|
|
500
518
|
method: string,
|
|
501
|
-
headers: Record<string, string>,
|
|
519
|
+
headers: Record<string, string | string[]>,
|
|
502
520
|
source?: EventSource,
|
|
503
521
|
): void;
|
|
504
522
|
|
|
@@ -507,12 +525,12 @@ interface IStateManager {
|
|
|
507
525
|
returnCode: number | null,
|
|
508
526
|
logCount: number,
|
|
509
527
|
input: {
|
|
510
|
-
request: { headers: Record<string, string>; body: string };
|
|
511
|
-
response: { headers: Record<string, string>; body: string };
|
|
528
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
529
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
512
530
|
},
|
|
513
531
|
output: {
|
|
514
|
-
request: { headers: Record<string, string>; body: string };
|
|
515
|
-
response: { headers: Record<string, string>; body: string };
|
|
532
|
+
request: { headers: Record<string, string | string[]>; body: string };
|
|
533
|
+
response: { headers: Record<string, string | string[]>; body: string };
|
|
516
534
|
},
|
|
517
535
|
source?: EventSource,
|
|
518
536
|
): void;
|
|
@@ -522,7 +540,7 @@ interface IStateManager {
|
|
|
522
540
|
finalResponse: {
|
|
523
541
|
status: number;
|
|
524
542
|
statusText: string;
|
|
525
|
-
headers: Record<string, string>;
|
|
543
|
+
headers: Record<string, string | string[]>;
|
|
526
544
|
body: string;
|
|
527
545
|
contentType: string;
|
|
528
546
|
isBase64?: boolean;
|
|
@@ -544,7 +562,7 @@ interface IStateManager {
|
|
|
544
562
|
response: {
|
|
545
563
|
status: number;
|
|
546
564
|
statusText: string;
|
|
547
|
-
headers: Record<string, string>;
|
|
565
|
+
headers: Record<string, string | string[] | undefined>;
|
|
548
566
|
body: string;
|
|
549
567
|
contentType: string | null;
|
|
550
568
|
isBase64?: boolean;
|
|
@@ -575,19 +593,15 @@ async function testCdnApp() {
|
|
|
575
593
|
try {
|
|
576
594
|
// Execute the full CDN request/response lifecycle
|
|
577
595
|
const result: FullFlowResult = await runner.callFullFlow(
|
|
578
|
-
'https://example.com/api/data',
|
|
579
|
-
'GET',
|
|
580
|
-
{ 'accept': 'application/json' },
|
|
581
|
-
'',
|
|
582
|
-
{ 'content-type': 'application/json' }, // upstream response headers
|
|
583
|
-
'{"key":"value"}', // upstream response body
|
|
584
|
-
200, // upstream response status
|
|
585
|
-
'OK', // upstream response status text
|
|
596
|
+
'https://example.com/api/data', // request URL
|
|
597
|
+
'GET', // method
|
|
598
|
+
{ 'accept': 'application/json' }, // request headers
|
|
599
|
+
'', // request body
|
|
586
600
|
{
|
|
587
601
|
'request.path': '/api/data',
|
|
588
602
|
'request.host': 'example.com',
|
|
589
|
-
},
|
|
590
|
-
true,
|
|
603
|
+
}, // shared properties
|
|
604
|
+
true, // enforce production property rules
|
|
591
605
|
);
|
|
592
606
|
|
|
593
607
|
// Inspect hook results
|
|
@@ -647,7 +661,8 @@ async function testCdnAppOffline() {
|
|
|
647
661
|
'GET',
|
|
648
662
|
{ 'accept': 'application/json' },
|
|
649
663
|
'',
|
|
650
|
-
{},
|
|
664
|
+
{}, // properties
|
|
665
|
+
true, // enforceProductionPropertyRules
|
|
651
666
|
);
|
|
652
667
|
|
|
653
668
|
console.log('Final status:', result.finalResponse.status);
|