@gcoredev/fastedge-test 0.1.7 → 0.2.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/dist/fastedge-cli/METADATA.json +1 -1
- package/dist/fastedge-cli/fastedge-run-darwin-arm64 +0 -0
- package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
- package/dist/fastedge-cli/fastedge-run.exe +0 -0
- package/dist/frontend/assets/{index-BCXfEMSq.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +292 -140
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +292 -140
- package/dist/lib/runner/HeaderManager.d.ts +7 -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 +6 -3
- 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 +18754 -189
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18771 -178
- 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 +23 -53
- package/docs/DEBUGGER.md +7 -7
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +79 -64
- package/docs/TEST_CONFIG.md +28 -41
- package/docs/TEST_FRAMEWORK.md +205 -32
- 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/dist/lib/index.cjs
CHANGED
|
@@ -49,13 +49,11 @@ var import_node_wasi = require("node:wasi");
|
|
|
49
49
|
var textEncoder = new TextEncoder();
|
|
50
50
|
var textDecoder = new TextDecoder();
|
|
51
51
|
var MemoryManager = class {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.isInitializing = false;
|
|
58
|
-
}
|
|
52
|
+
memory = null;
|
|
53
|
+
instance = null;
|
|
54
|
+
hostAllocOffset = 0;
|
|
55
|
+
logCallback = null;
|
|
56
|
+
isInitializing = false;
|
|
59
57
|
setMemory(memory) {
|
|
60
58
|
this.memory = memory;
|
|
61
59
|
}
|
|
@@ -218,14 +216,64 @@ var MemoryManager = class {
|
|
|
218
216
|
|
|
219
217
|
// server/runner/HeaderManager.ts
|
|
220
218
|
var textEncoder2 = new TextEncoder();
|
|
221
|
-
var HeaderManager = class {
|
|
219
|
+
var HeaderManager = class _HeaderManager {
|
|
220
|
+
// Read a header value as a single string. For multi-valued headers (string[]) returns the first.
|
|
221
|
+
// Use when callers know the header is conventionally single-valued (content-type, host, location, etc.)
|
|
222
|
+
// and need to satisfy APIs that take a string.
|
|
223
|
+
static firstValue(v) {
|
|
224
|
+
return Array.isArray(v) ? v[0] : v;
|
|
225
|
+
}
|
|
226
|
+
// Flatten a HeaderRecord to a HeaderMap (single string per key) for consumers
|
|
227
|
+
// that can't handle multi-valued headers (e.g. fetch's HeadersInit).
|
|
228
|
+
// Multi-valued entries are joined with ", " — caller must be sure this is acceptable
|
|
229
|
+
// (NOT valid for Set-Cookie; route those through a separate channel).
|
|
230
|
+
static flattenToMap(headers) {
|
|
231
|
+
const flat = {};
|
|
232
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
233
|
+
if (Array.isArray(v)) {
|
|
234
|
+
flat[k] = v.join(", ");
|
|
235
|
+
} else if (v !== void 0) {
|
|
236
|
+
flat[k] = String(v);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return flat;
|
|
240
|
+
}
|
|
222
241
|
static normalize(headers) {
|
|
223
|
-
const normalized =
|
|
242
|
+
const normalized = /* @__PURE__ */ Object.create(null);
|
|
224
243
|
for (const [key, value] of Object.entries(headers)) {
|
|
225
|
-
|
|
244
|
+
const k = key.toLowerCase();
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
normalized[k] = value.map(String);
|
|
247
|
+
} else {
|
|
248
|
+
normalized[k] = String(value);
|
|
249
|
+
}
|
|
226
250
|
}
|
|
227
251
|
return normalized;
|
|
228
252
|
}
|
|
253
|
+
// Append-merge two header records: for keys present in both, values are
|
|
254
|
+
// concatenated into a string[] rather than the right-hand side overwriting
|
|
255
|
+
// the left. Keys are lowercased on the way in (consistent with `normalize`).
|
|
256
|
+
//
|
|
257
|
+
// Used by the runner to combine request-phase response-header state with
|
|
258
|
+
// the origin's response headers, mirroring how Envoy serves
|
|
259
|
+
// `add_http_response_header` calls issued during onRequestHeaders /
|
|
260
|
+
// onRequestBody against the actual upstream response — both values survive
|
|
261
|
+
// as a multi-value list (preserving the proxy-wasm cross-phase pattern).
|
|
262
|
+
static appendMerge(left, right) {
|
|
263
|
+
const result = _HeaderManager.normalize(left);
|
|
264
|
+
for (const [key, value] of Object.entries(right)) {
|
|
265
|
+
const k = key.toLowerCase();
|
|
266
|
+
const incoming = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
267
|
+
if (Object.hasOwn(result, k)) {
|
|
268
|
+
const existing = result[k];
|
|
269
|
+
const existingArr = Array.isArray(existing) ? existing : [existing];
|
|
270
|
+
result[k] = [...existingArr, ...incoming];
|
|
271
|
+
} else {
|
|
272
|
+
result[k] = incoming.length === 1 ? incoming[0] : incoming;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
229
277
|
static serialize(headers) {
|
|
230
278
|
const pairs = Object.entries(headers);
|
|
231
279
|
const numPairs = pairs.length;
|
|
@@ -301,13 +349,31 @@ var HeaderManager = class {
|
|
|
301
349
|
}
|
|
302
350
|
// --- Tuple-based methods for multi-valued header support ---
|
|
303
351
|
static recordToTuples(headers) {
|
|
304
|
-
|
|
352
|
+
const tuples = [];
|
|
353
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
354
|
+
const key = k.toLowerCase();
|
|
355
|
+
if (Array.isArray(v)) {
|
|
356
|
+
for (const val of v) tuples.push([key, String(val)]);
|
|
357
|
+
} else if (v !== void 0) {
|
|
358
|
+
tuples.push([key, String(v)]);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return tuples;
|
|
305
362
|
}
|
|
363
|
+
// Lossless projection of tuples to a Record: single-valued keys are string,
|
|
364
|
+
// multi-valued keys are string[] — matching Node's IncomingHttpHeaders shape.
|
|
365
|
+
// Set-Cookie and other legitimately-repeatable headers are preserved across duplicates.
|
|
306
366
|
static tuplesToRecord(tuples) {
|
|
307
367
|
const record = {};
|
|
308
368
|
for (const [key, value] of tuples) {
|
|
309
369
|
const existing = record[key];
|
|
310
|
-
|
|
370
|
+
if (existing === void 0) {
|
|
371
|
+
record[key] = value;
|
|
372
|
+
} else if (Array.isArray(existing)) {
|
|
373
|
+
existing.push(value);
|
|
374
|
+
} else {
|
|
375
|
+
record[key] = [existing, value];
|
|
376
|
+
}
|
|
311
377
|
}
|
|
312
378
|
return record;
|
|
313
379
|
}
|
|
@@ -386,20 +452,18 @@ var HeaderManager = class {
|
|
|
386
452
|
|
|
387
453
|
// server/runner/PropertyResolver.ts
|
|
388
454
|
var PropertyResolver = class {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.responseStatusText = "OK";
|
|
402
|
-
}
|
|
455
|
+
properties = {};
|
|
456
|
+
requestHeaders = {};
|
|
457
|
+
requestMethod = "GET";
|
|
458
|
+
requestPath = "/";
|
|
459
|
+
requestScheme = "https";
|
|
460
|
+
requestUrl = "";
|
|
461
|
+
requestHost = "";
|
|
462
|
+
requestQuery = "";
|
|
463
|
+
requestExtension = "";
|
|
464
|
+
responseHeaders = {};
|
|
465
|
+
responseStatus = 200;
|
|
466
|
+
responseStatusText = "OK";
|
|
403
467
|
setProperties(properties) {
|
|
404
468
|
this.properties = properties;
|
|
405
469
|
}
|
|
@@ -453,10 +517,11 @@ var PropertyResolver = class {
|
|
|
453
517
|
const url = new URL(targetUrl);
|
|
454
518
|
this.requestUrl = targetUrl;
|
|
455
519
|
this.requestHost = url.hostname + (url.port ? `:${url.port}` : "");
|
|
456
|
-
this.requestPath = url.pathname || "/";
|
|
520
|
+
this.requestPath = (url.pathname || "/") + url.search;
|
|
457
521
|
this.requestQuery = url.search.startsWith("?") ? url.search.substring(1) : url.search;
|
|
458
522
|
this.requestScheme = url.protocol.replace(":", "");
|
|
459
|
-
const
|
|
523
|
+
const pathOnly = url.pathname || "/";
|
|
524
|
+
const pathParts = pathOnly.split("/");
|
|
460
525
|
const lastPart = pathParts[pathParts.length - 1];
|
|
461
526
|
const dotIndex = lastPart.lastIndexOf(".");
|
|
462
527
|
if (dotIndex > 0 && dotIndex < lastPart.length - 1) {
|
|
@@ -526,28 +591,28 @@ var PropertyResolver = class {
|
|
|
526
591
|
if (path2 === "request.url")
|
|
527
592
|
return this.requestUrl || `${this.requestScheme}://${this.requestHost}${this.requestPath}`;
|
|
528
593
|
if (path2 === "request.host")
|
|
529
|
-
return this.requestHost || this.requestHeaders["host"] || "localhost";
|
|
594
|
+
return this.requestHost || HeaderManager.firstValue(this.requestHeaders["host"]) || "localhost";
|
|
530
595
|
if (path2 === "request.scheme") return this.requestScheme;
|
|
531
596
|
if (path2 === "request.protocol") return this.requestScheme;
|
|
532
597
|
if (path2 === "request.query") return this.requestQuery;
|
|
533
598
|
if (path2 === "request.extension") return this.requestExtension;
|
|
534
599
|
if (path2 === "request.content_type") {
|
|
535
|
-
return this.requestHeaders["content-type"] || "";
|
|
600
|
+
return HeaderManager.firstValue(this.requestHeaders["content-type"]) || "";
|
|
536
601
|
}
|
|
537
602
|
if (path2.startsWith("request.headers.")) {
|
|
538
603
|
const headerName = path2.substring("request.headers.".length).toLowerCase();
|
|
539
|
-
return this.requestHeaders[headerName] || "";
|
|
604
|
+
return HeaderManager.firstValue(this.requestHeaders[headerName]) || "";
|
|
540
605
|
}
|
|
541
606
|
if (path2 === "response.code") return this.responseStatus;
|
|
542
607
|
if (path2 === "response.status") return this.responseStatus;
|
|
543
608
|
if (path2 === "response.status_code") return this.responseStatus;
|
|
544
609
|
if (path2 === "response.code_details") return this.responseStatusText;
|
|
545
610
|
if (path2 === "response.content_type") {
|
|
546
|
-
return this.responseHeaders["content-type"] || "";
|
|
611
|
+
return HeaderManager.firstValue(this.responseHeaders["content-type"]) || "";
|
|
547
612
|
}
|
|
548
613
|
if (path2.startsWith("response.headers.")) {
|
|
549
614
|
const headerName = path2.substring("response.headers.".length).toLowerCase();
|
|
550
|
-
return this.responseHeaders[headerName] || "";
|
|
615
|
+
return HeaderManager.firstValue(this.responseHeaders[headerName]) || "";
|
|
551
616
|
}
|
|
552
617
|
return void 0;
|
|
553
618
|
}
|
|
@@ -590,6 +655,7 @@ var PropertyResolver = class {
|
|
|
590
655
|
|
|
591
656
|
// server/fastedge-host/SecretStore.ts
|
|
592
657
|
var SecretStore = class {
|
|
658
|
+
secrets;
|
|
593
659
|
constructor(initialSecrets) {
|
|
594
660
|
this.secrets = /* @__PURE__ */ new Map();
|
|
595
661
|
if (initialSecrets) {
|
|
@@ -672,6 +738,7 @@ var SecretStore = class {
|
|
|
672
738
|
|
|
673
739
|
// server/fastedge-host/Dictionary.ts
|
|
674
740
|
var Dictionary = class {
|
|
741
|
+
data;
|
|
675
742
|
constructor(initialData) {
|
|
676
743
|
this.data = /* @__PURE__ */ new Map();
|
|
677
744
|
if (initialData) {
|
|
@@ -839,26 +906,33 @@ function createFastEdgeHostFunctions(memory, secretStore, dictionary, logDebug)
|
|
|
839
906
|
// server/runner/HostFunctions.ts
|
|
840
907
|
var textEncoder3 = new TextEncoder();
|
|
841
908
|
var HostFunctions = class {
|
|
909
|
+
memory;
|
|
910
|
+
propertyResolver;
|
|
911
|
+
propertyAccessControl;
|
|
912
|
+
getCurrentHook;
|
|
913
|
+
logs = [];
|
|
914
|
+
requestHeaders = [];
|
|
915
|
+
responseHeaders = [];
|
|
916
|
+
requestBody = "";
|
|
917
|
+
responseBody = "";
|
|
918
|
+
vmConfig = "";
|
|
919
|
+
pluginConfig = "";
|
|
920
|
+
currentContextId = 1;
|
|
921
|
+
lastHostCall = null;
|
|
922
|
+
debug = false;
|
|
923
|
+
currentLogLevel = 0 /* Trace */;
|
|
924
|
+
// Default to show all logs
|
|
925
|
+
// http_call state
|
|
926
|
+
nextTokenId = 0;
|
|
927
|
+
pendingHttpCall = null;
|
|
928
|
+
httpCallResponse = null;
|
|
929
|
+
streamClosed = false;
|
|
930
|
+
// Local response state (from proxy_send_local_response / send_http_response)
|
|
931
|
+
localResponse = null;
|
|
932
|
+
// FastEdge extensions
|
|
933
|
+
secretStore;
|
|
934
|
+
dictionary;
|
|
842
935
|
constructor(memory, propertyResolver, propertyAccessControl, getCurrentHook, debug = false, secretStore, dictionary) {
|
|
843
|
-
this.logs = [];
|
|
844
|
-
this.requestHeaders = [];
|
|
845
|
-
this.responseHeaders = [];
|
|
846
|
-
this.requestBody = "";
|
|
847
|
-
this.responseBody = "";
|
|
848
|
-
this.vmConfig = "";
|
|
849
|
-
this.pluginConfig = "";
|
|
850
|
-
this.currentContextId = 1;
|
|
851
|
-
this.lastHostCall = null;
|
|
852
|
-
this.debug = false;
|
|
853
|
-
this.currentLogLevel = 0 /* Trace */;
|
|
854
|
-
// Default to show all logs
|
|
855
|
-
// http_call state
|
|
856
|
-
this.nextTokenId = 0;
|
|
857
|
-
this.pendingHttpCall = null;
|
|
858
|
-
this.httpCallResponse = null;
|
|
859
|
-
this.streamClosed = false;
|
|
860
|
-
// Local response state (from proxy_send_local_response / send_http_response)
|
|
861
|
-
this.localResponse = null;
|
|
862
936
|
this.memory = memory;
|
|
863
937
|
this.propertyResolver = propertyResolver;
|
|
864
938
|
this.propertyAccessControl = propertyAccessControl;
|
|
@@ -902,7 +976,8 @@ var HostFunctions = class {
|
|
|
902
976
|
return call;
|
|
903
977
|
}
|
|
904
978
|
setHttpCallResponse(tokenId, headers, body) {
|
|
905
|
-
|
|
979
|
+
const tuples = Array.isArray(headers) ? headers : HeaderManager.recordToTuples(headers);
|
|
980
|
+
this.httpCallResponse = { tokenId, headers: tuples, body };
|
|
906
981
|
}
|
|
907
982
|
clearHttpCallResponse() {
|
|
908
983
|
this.httpCallResponse = null;
|
|
@@ -1332,7 +1407,7 @@ var HostFunctions = class {
|
|
|
1332
1407
|
return this.responseHeaders;
|
|
1333
1408
|
}
|
|
1334
1409
|
if (mapType === 6 /* HttpCallResponseHeaders */ || mapType === 7 /* HttpCallResponseTrailers */) {
|
|
1335
|
-
return
|
|
1410
|
+
return this.httpCallResponse?.headers ?? [];
|
|
1336
1411
|
}
|
|
1337
1412
|
return this.requestHeaders;
|
|
1338
1413
|
}
|
|
@@ -1588,6 +1663,8 @@ var BUILT_IN_PROPERTIES = [
|
|
|
1588
1663
|
}
|
|
1589
1664
|
];
|
|
1590
1665
|
var PropertyAccessControl = class {
|
|
1666
|
+
builtInProperties;
|
|
1667
|
+
customProperties;
|
|
1591
1668
|
constructor() {
|
|
1592
1669
|
this.builtInProperties = /* @__PURE__ */ new Map();
|
|
1593
1670
|
this.customProperties = /* @__PURE__ */ new Map();
|
|
@@ -1805,21 +1882,28 @@ var textEncoder4 = new TextEncoder();
|
|
|
1805
1882
|
var BUILTIN_URL = "http://fastedge-builtin.debug";
|
|
1806
1883
|
var BUILTIN_SHORTHAND = "built-in";
|
|
1807
1884
|
var ProxyWasmRunner = class {
|
|
1885
|
+
module = null;
|
|
1886
|
+
// Compiled module (reused)
|
|
1887
|
+
instance = null;
|
|
1888
|
+
// Current instance (transient per hook)
|
|
1889
|
+
memory;
|
|
1890
|
+
propertyResolver;
|
|
1891
|
+
propertyAccessControl;
|
|
1892
|
+
currentHook = null;
|
|
1893
|
+
hostFunctions;
|
|
1894
|
+
logs = [];
|
|
1895
|
+
rootContextId = 1;
|
|
1896
|
+
nextContextId = 2;
|
|
1897
|
+
currentContextId = 1;
|
|
1898
|
+
isInitializing = false;
|
|
1899
|
+
debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1900
|
+
stateManager = null;
|
|
1901
|
+
secretStore;
|
|
1902
|
+
dictionary;
|
|
1903
|
+
dotenvEnabled = true;
|
|
1904
|
+
// Default to enabled
|
|
1905
|
+
dotenvPath = ".";
|
|
1808
1906
|
constructor(fastEdgeConfig, dotenvEnabled = true) {
|
|
1809
|
-
this.module = null;
|
|
1810
|
-
// Compiled module (reused)
|
|
1811
|
-
this.instance = null;
|
|
1812
|
-
this.currentHook = null;
|
|
1813
|
-
this.logs = [];
|
|
1814
|
-
this.rootContextId = 1;
|
|
1815
|
-
this.nextContextId = 2;
|
|
1816
|
-
this.currentContextId = 1;
|
|
1817
|
-
this.isInitializing = false;
|
|
1818
|
-
this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
|
|
1819
|
-
this.stateManager = null;
|
|
1820
|
-
this.dotenvEnabled = true;
|
|
1821
|
-
// Default to enabled
|
|
1822
|
-
this.dotenvPath = ".";
|
|
1823
1907
|
this.memory = new MemoryManager();
|
|
1824
1908
|
this.propertyResolver = new PropertyResolver();
|
|
1825
1909
|
this.propertyAccessControl = new PropertyAccessControl();
|
|
@@ -1992,7 +2076,7 @@ var ProxyWasmRunner = class {
|
|
|
1992
2076
|
const local = this.hostFunctions.getLocalResponse();
|
|
1993
2077
|
const responseHeaders2 = results.onRequestHeaders.output.response.headers;
|
|
1994
2078
|
this.hostFunctions.resetLocalResponse();
|
|
1995
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
2079
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
1996
2080
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
1997
2081
|
return {
|
|
1998
2082
|
hookResults: results,
|
|
@@ -2035,7 +2119,7 @@ var ProxyWasmRunner = class {
|
|
|
2035
2119
|
const local = this.hostFunctions.getLocalResponse();
|
|
2036
2120
|
const responseHeaders2 = results.onRequestBody.output.response.headers;
|
|
2037
2121
|
this.hostFunctions.resetLocalResponse();
|
|
2038
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
2122
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
2039
2123
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
2040
2124
|
return {
|
|
2041
2125
|
hookResults: results,
|
|
@@ -2066,7 +2150,7 @@ var ProxyWasmRunner = class {
|
|
|
2066
2150
|
try {
|
|
2067
2151
|
if (isBuiltIn) {
|
|
2068
2152
|
this.logDebug("Using built-in responder");
|
|
2069
|
-
const rawStatus = (modifiedRequestHeaders["x-debugger-status"] || "").trim();
|
|
2153
|
+
const rawStatus = (HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-status"]) || "").trim();
|
|
2070
2154
|
if (rawStatus === "") {
|
|
2071
2155
|
responseStatus = 200;
|
|
2072
2156
|
} else {
|
|
@@ -2077,7 +2161,7 @@ var ProxyWasmRunner = class {
|
|
|
2077
2161
|
}
|
|
2078
2162
|
}
|
|
2079
2163
|
responseStatusText = responseStatus === 200 ? "OK" : String(responseStatus);
|
|
2080
|
-
const responseContentMode = modifiedRequestHeaders["x-debugger-content"] || "";
|
|
2164
|
+
const responseContentMode = HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-content"]) || "";
|
|
2081
2165
|
delete modifiedRequestHeaders["x-debugger-status"];
|
|
2082
2166
|
delete modifiedRequestHeaders["x-debugger-content"];
|
|
2083
2167
|
if (responseContentMode === "status-only") {
|
|
@@ -2085,14 +2169,14 @@ var ProxyWasmRunner = class {
|
|
|
2085
2169
|
contentType = "text/plain";
|
|
2086
2170
|
} else if (responseContentMode === "body-only") {
|
|
2087
2171
|
responseBody = modifiedRequestBody || "";
|
|
2088
|
-
contentType = modifiedRequestHeaders["content-type"] || "text/plain";
|
|
2172
|
+
contentType = HeaderManager.firstValue(modifiedRequestHeaders["content-type"]) || "text/plain";
|
|
2089
2173
|
} else {
|
|
2090
2174
|
contentType = "application/json";
|
|
2091
2175
|
responseBody = JSON.stringify({
|
|
2092
2176
|
method: requestMethod,
|
|
2093
2177
|
reqHeaders: modifiedRequestHeaders,
|
|
2094
2178
|
reqBody: modifiedRequestBody || "",
|
|
2095
|
-
requestUrl: BUILTIN_URL
|
|
2179
|
+
requestUrl: propertiesAfterRequestBody["request.url"] || BUILTIN_URL
|
|
2096
2180
|
});
|
|
2097
2181
|
}
|
|
2098
2182
|
responseHeaders = {
|
|
@@ -2103,27 +2187,30 @@ var ProxyWasmRunner = class {
|
|
|
2103
2187
|
`Built-in responder: ${responseStatus} ${responseStatusText}, mode=${responseContentMode || "full"}`
|
|
2104
2188
|
);
|
|
2105
2189
|
} else {
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2108
|
-
const modifiedPath = propertiesAfterRequestBody["request.path"] || "/";
|
|
2109
|
-
const modifiedQuery = propertiesAfterRequestBody["request.query"] || "";
|
|
2110
|
-
const actualTargetUrl = `${modifiedScheme}://${modifiedHost}${modifiedPath}${modifiedQuery ? "?" + modifiedQuery : ""}`;
|
|
2190
|
+
const actualTargetUrl = propertiesAfterRequestBody["request.url"] || targetUrl;
|
|
2191
|
+
const actualScheme = new URL(actualTargetUrl).protocol.replace(":", "");
|
|
2111
2192
|
this.logDebug(`Original URL: ${targetUrl}`);
|
|
2112
|
-
this.logDebug(`
|
|
2193
|
+
this.logDebug(`Effective URL: ${actualTargetUrl}`);
|
|
2113
2194
|
this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
|
|
2114
|
-
const fetchHeaders =
|
|
2115
|
-
|
|
2116
|
-
|
|
2195
|
+
const fetchHeaders = HeaderManager.flattenToMap(
|
|
2196
|
+
modifiedRequestHeaders
|
|
2197
|
+
);
|
|
2198
|
+
for (const key of Object.keys(fetchHeaders)) {
|
|
2199
|
+
if (key.startsWith(":")) {
|
|
2200
|
+
delete fetchHeaders[key];
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2117
2203
|
const hostHeader = Object.entries(modifiedRequestHeaders).find(
|
|
2118
2204
|
([key]) => key.toLowerCase() === "host"
|
|
2119
2205
|
);
|
|
2120
2206
|
if (hostHeader) {
|
|
2121
|
-
|
|
2122
|
-
|
|
2207
|
+
const hostValue = HeaderManager.firstValue(hostHeader[1]) ?? "";
|
|
2208
|
+
fetchHeaders["x-forwarded-host"] = hostValue;
|
|
2209
|
+
this.logDebug(`Adding x-forwarded-host: ${hostValue}`);
|
|
2123
2210
|
}
|
|
2124
|
-
fetchHeaders["x-forwarded-proto"] =
|
|
2125
|
-
this.logDebug(`Adding x-forwarded-proto: ${
|
|
2126
|
-
fetchHeaders["x-forwarded-port"] =
|
|
2211
|
+
fetchHeaders["x-forwarded-proto"] = actualScheme;
|
|
2212
|
+
this.logDebug(`Adding x-forwarded-proto: ${actualScheme}`);
|
|
2213
|
+
fetchHeaders["x-forwarded-port"] = actualScheme === "https" ? "443" : "80";
|
|
2127
2214
|
this.logDebug(
|
|
2128
2215
|
`Adding x-forwarded-port: ${fetchHeaders["x-forwarded-port"]}`
|
|
2129
2216
|
);
|
|
@@ -2143,8 +2230,14 @@ var ProxyWasmRunner = class {
|
|
|
2143
2230
|
const response = await fetch(actualTargetUrl, fetchOptions);
|
|
2144
2231
|
responseHeaders = {};
|
|
2145
2232
|
response.headers.forEach((value, key) => {
|
|
2146
|
-
|
|
2233
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
2234
|
+
responseHeaders[key] = value;
|
|
2235
|
+
}
|
|
2147
2236
|
});
|
|
2237
|
+
const setCookies = response.headers.getSetCookie();
|
|
2238
|
+
if (setCookies.length > 0) {
|
|
2239
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
2240
|
+
}
|
|
2148
2241
|
contentType = response.headers.get("content-type") || "text/plain";
|
|
2149
2242
|
responseStatus = response.status;
|
|
2150
2243
|
responseStatusText = response.statusText;
|
|
@@ -2163,6 +2256,14 @@ var ProxyWasmRunner = class {
|
|
|
2163
2256
|
`Fetch completed: ${responseStatus} ${responseStatusText}`
|
|
2164
2257
|
);
|
|
2165
2258
|
}
|
|
2259
|
+
const requestPhaseResponseHeaders = {
|
|
2260
|
+
...results.onRequestHeaders.output.response.headers ?? {},
|
|
2261
|
+
...results.onRequestBody.output.response.headers ?? {}
|
|
2262
|
+
};
|
|
2263
|
+
const mergedResponseHeaders = HeaderManager.appendMerge(
|
|
2264
|
+
requestPhaseResponseHeaders,
|
|
2265
|
+
responseHeaders
|
|
2266
|
+
);
|
|
2166
2267
|
const responseCall = {
|
|
2167
2268
|
...call,
|
|
2168
2269
|
request: {
|
|
@@ -2171,7 +2272,7 @@ var ProxyWasmRunner = class {
|
|
|
2171
2272
|
body: modifiedRequestBody
|
|
2172
2273
|
},
|
|
2173
2274
|
response: {
|
|
2174
|
-
headers:
|
|
2275
|
+
headers: mergedResponseHeaders,
|
|
2175
2276
|
body: responseBody,
|
|
2176
2277
|
status: responseStatus,
|
|
2177
2278
|
statusText: responseStatusText
|
|
@@ -2193,6 +2294,25 @@ var ProxyWasmRunner = class {
|
|
|
2193
2294
|
"system"
|
|
2194
2295
|
);
|
|
2195
2296
|
}
|
|
2297
|
+
if (results.onResponseHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
2298
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
2299
|
+
const headers = results.onResponseHeaders.output.response.headers;
|
|
2300
|
+
this.hostFunctions.resetLocalResponse();
|
|
2301
|
+
const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
|
|
2302
|
+
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
2303
|
+
return {
|
|
2304
|
+
hookResults: results,
|
|
2305
|
+
finalResponse: {
|
|
2306
|
+
status: local.statusCode,
|
|
2307
|
+
statusText: local.statusText,
|
|
2308
|
+
headers,
|
|
2309
|
+
body,
|
|
2310
|
+
contentType: contentType2,
|
|
2311
|
+
isBase64: isBase642
|
|
2312
|
+
},
|
|
2313
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2196
2316
|
const headersAfterResponseHeaders = results.onResponseHeaders.output.response.headers;
|
|
2197
2317
|
const propertiesAfterResponseHeaders = results.onResponseHeaders.properties;
|
|
2198
2318
|
this.logDebug(
|
|
@@ -2217,11 +2337,30 @@ var ProxyWasmRunner = class {
|
|
|
2217
2337
|
"system"
|
|
2218
2338
|
);
|
|
2219
2339
|
}
|
|
2340
|
+
if (results.onResponseBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
|
|
2341
|
+
const local = this.hostFunctions.getLocalResponse();
|
|
2342
|
+
const headers = results.onResponseBody.output.response.headers;
|
|
2343
|
+
this.hostFunctions.resetLocalResponse();
|
|
2344
|
+
const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
|
|
2345
|
+
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
2346
|
+
return {
|
|
2347
|
+
hookResults: results,
|
|
2348
|
+
finalResponse: {
|
|
2349
|
+
status: local.statusCode,
|
|
2350
|
+
statusText: local.statusText,
|
|
2351
|
+
headers,
|
|
2352
|
+
body,
|
|
2353
|
+
contentType: contentType2,
|
|
2354
|
+
isBase64: isBase642
|
|
2355
|
+
},
|
|
2356
|
+
calculatedProperties: this.propertyResolver.getCalculatedProperties()
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2220
2359
|
const finalHeaders = results.onResponseBody.output.response.headers;
|
|
2221
2360
|
const finalBody = results.onResponseBody.output.response.body;
|
|
2222
2361
|
this.logDebug(`Final response body length: ${finalBody.length}`);
|
|
2223
2362
|
const calculatedProperties = this.propertyResolver.getCalculatedProperties();
|
|
2224
|
-
const finalContentType = finalHeaders["content-type"] || contentType;
|
|
2363
|
+
const finalContentType = HeaderManager.firstValue(finalHeaders["content-type"]) || contentType;
|
|
2225
2364
|
return {
|
|
2226
2365
|
hookResults: results,
|
|
2227
2366
|
finalResponse: {
|
|
@@ -2314,13 +2453,13 @@ var ProxyWasmRunner = class {
|
|
|
2314
2453
|
this.hostFunctions.setLogLevel(0);
|
|
2315
2454
|
const requestHeaders = HeaderManager.normalize(call.request.headers ?? {});
|
|
2316
2455
|
const responseHeaders = HeaderManager.normalize(
|
|
2317
|
-
call.response
|
|
2456
|
+
call.response?.headers ?? {}
|
|
2318
2457
|
);
|
|
2319
2458
|
const requestBody = call.request.body ?? "";
|
|
2320
|
-
const responseBody = call.response
|
|
2459
|
+
const responseBody = call.response?.body ?? "";
|
|
2321
2460
|
const requestMethod = call.request.method ?? "GET";
|
|
2322
|
-
const responseStatus = call.response
|
|
2323
|
-
const responseStatusText = call.response
|
|
2461
|
+
const responseStatus = call.response?.status ?? 200;
|
|
2462
|
+
const responseStatusText = call.response?.statusText ?? "OK";
|
|
2324
2463
|
this.propertyResolver.setProperties({ ...call.properties ?? {} });
|
|
2325
2464
|
this.propertyResolver.setRequestMetadata(
|
|
2326
2465
|
requestHeaders,
|
|
@@ -2407,7 +2546,7 @@ var ProxyWasmRunner = class {
|
|
|
2407
2546
|
for (const [k, v] of Object.entries(pending.headers)) {
|
|
2408
2547
|
if (!k.startsWith(":")) fetchHeaders[k] = v;
|
|
2409
2548
|
}
|
|
2410
|
-
let responseHeaders2 =
|
|
2549
|
+
let responseHeaders2 = [];
|
|
2411
2550
|
let responseBody2 = new Uint8Array(0);
|
|
2412
2551
|
try {
|
|
2413
2552
|
const resp = await fetch(url, {
|
|
@@ -2417,20 +2556,23 @@ var ProxyWasmRunner = class {
|
|
|
2417
2556
|
signal: AbortSignal.timeout(pending.timeoutMs)
|
|
2418
2557
|
});
|
|
2419
2558
|
resp.headers.forEach((v, k) => {
|
|
2420
|
-
responseHeaders2[k
|
|
2559
|
+
if (k.toLowerCase() !== "set-cookie") responseHeaders2.push([k, v]);
|
|
2421
2560
|
});
|
|
2561
|
+
for (const cookie of resp.headers.getSetCookie()) {
|
|
2562
|
+
responseHeaders2.push(["set-cookie", cookie]);
|
|
2563
|
+
}
|
|
2422
2564
|
responseBody2 = new Uint8Array(await resp.arrayBuffer());
|
|
2423
2565
|
this.logDebug(
|
|
2424
|
-
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${
|
|
2566
|
+
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${responseHeaders2.length} bodySize=${responseBody2.byteLength}`
|
|
2425
2567
|
);
|
|
2426
2568
|
} catch (err) {
|
|
2427
2569
|
const errMsg = `http_call failed for ${url}: ${String(err)}`;
|
|
2428
2570
|
this.logDebug(errMsg);
|
|
2429
2571
|
this.logs.push({ level: 3, message: `[host] ${errMsg}` });
|
|
2430
|
-
responseHeaders2 =
|
|
2572
|
+
responseHeaders2 = [];
|
|
2431
2573
|
responseBody2 = new Uint8Array(0);
|
|
2432
2574
|
}
|
|
2433
|
-
const numHeaders =
|
|
2575
|
+
const numHeaders = responseHeaders2.length;
|
|
2434
2576
|
const bodySize = responseBody2.byteLength;
|
|
2435
2577
|
this.hostFunctions.setHttpCallResponse(pending.tokenId, responseHeaders2, responseBody2);
|
|
2436
2578
|
this.hostFunctions.resetStreamClosed();
|
|
@@ -2532,11 +2674,18 @@ var ProxyWasmRunner = class {
|
|
|
2532
2674
|
this.isInitializing = false;
|
|
2533
2675
|
}
|
|
2534
2676
|
buildHookInvocation(hook, requestHeaders, responseHeaders, requestBody, responseBody) {
|
|
2677
|
+
const countEntries = (h) => {
|
|
2678
|
+
let n = 0;
|
|
2679
|
+
for (const v of Object.values(h)) {
|
|
2680
|
+
n += Array.isArray(v) ? v.length : 1;
|
|
2681
|
+
}
|
|
2682
|
+
return n;
|
|
2683
|
+
};
|
|
2535
2684
|
switch (hook) {
|
|
2536
2685
|
case "onRequestHeaders":
|
|
2537
2686
|
return {
|
|
2538
2687
|
exportName: "proxy_on_request_headers",
|
|
2539
|
-
args: [this.currentContextId,
|
|
2688
|
+
args: [this.currentContextId, countEntries(requestHeaders), 0]
|
|
2540
2689
|
};
|
|
2541
2690
|
case "onRequestBody":
|
|
2542
2691
|
return {
|
|
@@ -2546,7 +2695,7 @@ var ProxyWasmRunner = class {
|
|
|
2546
2695
|
case "onResponseHeaders":
|
|
2547
2696
|
return {
|
|
2548
2697
|
exportName: "proxy_on_response_headers",
|
|
2549
|
-
args: [this.currentContextId,
|
|
2698
|
+
args: [this.currentContextId, countEntries(responseHeaders), 0]
|
|
2550
2699
|
};
|
|
2551
2700
|
case "onResponseBody":
|
|
2552
2701
|
return {
|
|
@@ -2661,9 +2810,12 @@ var ProxyWasmRunner = class {
|
|
|
2661
2810
|
console.warn(entry.message);
|
|
2662
2811
|
}
|
|
2663
2812
|
/**
|
|
2664
|
-
* Interface-compliant callFullFlow method
|
|
2813
|
+
* Interface-compliant callFullFlow method.
|
|
2814
|
+
*
|
|
2815
|
+
* The upstream response is generated at runtime by a real HTTP fetch
|
|
2816
|
+
* against `url` or by the built-in responder when `url === "built-in"`.
|
|
2665
2817
|
*/
|
|
2666
|
-
async callFullFlow(url, method, headers, body,
|
|
2818
|
+
async callFullFlow(url, method, headers, body, properties, enforceProductionPropertyRules) {
|
|
2667
2819
|
const call = {
|
|
2668
2820
|
hook: "",
|
|
2669
2821
|
// Not used in fullFlow
|
|
@@ -2672,12 +2824,6 @@ var ProxyWasmRunner = class {
|
|
|
2672
2824
|
body,
|
|
2673
2825
|
method
|
|
2674
2826
|
},
|
|
2675
|
-
response: {
|
|
2676
|
-
headers: responseHeaders,
|
|
2677
|
-
body: responseBody,
|
|
2678
|
-
status: responseStatus,
|
|
2679
|
-
statusText: responseStatusText
|
|
2680
|
-
},
|
|
2681
2827
|
properties,
|
|
2682
2828
|
enforceProductionPropertyRules
|
|
2683
2829
|
};
|
|
@@ -2686,7 +2832,7 @@ var ProxyWasmRunner = class {
|
|
|
2686
2832
|
/**
|
|
2687
2833
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
2688
2834
|
*/
|
|
2689
|
-
async execute(
|
|
2835
|
+
async execute(_request) {
|
|
2690
2836
|
throw new Error(
|
|
2691
2837
|
"execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
|
|
2692
2838
|
);
|
|
@@ -2850,21 +2996,22 @@ async function isLegacySyncWasm(bufferOrPath) {
|
|
|
2850
2996
|
|
|
2851
2997
|
// server/runner/HttpWasmRunner.ts
|
|
2852
2998
|
var HttpWasmRunner = class {
|
|
2999
|
+
process = null;
|
|
3000
|
+
port = null;
|
|
3001
|
+
cliPath = null;
|
|
3002
|
+
tempWasmPath = null;
|
|
3003
|
+
currentWasmPath = null;
|
|
3004
|
+
// resolved path used when spawning
|
|
3005
|
+
logs = [];
|
|
3006
|
+
stateManager = null;
|
|
3007
|
+
portManager;
|
|
3008
|
+
dotenvEnabled = true;
|
|
3009
|
+
dotenvPath = null;
|
|
3010
|
+
/** Pinned ports bypass PortManager allocation and must not be released back to it. */
|
|
3011
|
+
isPinnedPort = false;
|
|
3012
|
+
/** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
|
|
3013
|
+
isLegacySync = false;
|
|
2853
3014
|
constructor(portManager, dotenvEnabled = true) {
|
|
2854
|
-
this.process = null;
|
|
2855
|
-
this.port = null;
|
|
2856
|
-
this.cliPath = null;
|
|
2857
|
-
this.tempWasmPath = null;
|
|
2858
|
-
this.currentWasmPath = null;
|
|
2859
|
-
// resolved path used when spawning
|
|
2860
|
-
this.logs = [];
|
|
2861
|
-
this.stateManager = null;
|
|
2862
|
-
this.dotenvEnabled = true;
|
|
2863
|
-
this.dotenvPath = null;
|
|
2864
|
-
/** Pinned ports bypass PortManager allocation and must not be released back to it. */
|
|
2865
|
-
this.isPinnedPort = false;
|
|
2866
|
-
/** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
|
|
2867
|
-
this.isLegacySync = false;
|
|
2868
3015
|
this.portManager = portManager;
|
|
2869
3016
|
this.dotenvEnabled = dotenvEnabled;
|
|
2870
3017
|
}
|
|
@@ -2991,7 +3138,7 @@ var HttpWasmRunner = class {
|
|
|
2991
3138
|
/**
|
|
2992
3139
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2993
3140
|
*/
|
|
2994
|
-
async callFullFlow(_url, _method, _headers, _body,
|
|
3141
|
+
async callFullFlow(_url, _method, _headers, _body, _properties, _enforceProductionPropertyRules) {
|
|
2995
3142
|
throw new Error(
|
|
2996
3143
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2997
3144
|
);
|
|
@@ -3243,27 +3390,31 @@ ${recentLogs || "(no logs)"}`
|
|
|
3243
3390
|
];
|
|
3244
3391
|
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
3245
3392
|
}
|
|
3246
|
-
/**
|
|
3247
|
-
* Parse headers from fetch Headers object
|
|
3248
|
-
*/
|
|
3249
3393
|
parseHeaders(headers) {
|
|
3250
|
-
|
|
3251
|
-
headers.forEach((value, key) => {
|
|
3252
|
-
result[key] = value;
|
|
3253
|
-
});
|
|
3254
|
-
return result;
|
|
3394
|
+
return parseFetchHeaders(headers);
|
|
3255
3395
|
}
|
|
3256
3396
|
};
|
|
3397
|
+
function parseFetchHeaders(headers) {
|
|
3398
|
+
const result = {};
|
|
3399
|
+
headers.forEach((value, key) => {
|
|
3400
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
3401
|
+
result[key] = value;
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
const setCookies = headers.getSetCookie();
|
|
3405
|
+
if (setCookies.length > 0) {
|
|
3406
|
+
result["set-cookie"] = setCookies;
|
|
3407
|
+
}
|
|
3408
|
+
return result;
|
|
3409
|
+
}
|
|
3257
3410
|
|
|
3258
3411
|
// server/runner/PortManager.ts
|
|
3259
3412
|
var import_net = require("net");
|
|
3260
3413
|
var PortManager = class {
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
this.lastAllocatedPort = this.minPort - 1;
|
|
3266
|
-
}
|
|
3414
|
+
minPort = 8100;
|
|
3415
|
+
maxPort = 8199;
|
|
3416
|
+
allocatedPorts = /* @__PURE__ */ new Set();
|
|
3417
|
+
lastAllocatedPort = this.minPort - 1;
|
|
3267
3418
|
/**
|
|
3268
3419
|
* Check whether a port is actually free at the OS level.
|
|
3269
3420
|
* This is necessary when multiple server processes run simultaneously —
|
|
@@ -3323,6 +3474,7 @@ var PortManager = class {
|
|
|
3323
3474
|
|
|
3324
3475
|
// server/runner/WasmRunnerFactory.ts
|
|
3325
3476
|
var WasmRunnerFactory = class {
|
|
3477
|
+
portManager;
|
|
3326
3478
|
constructor() {
|
|
3327
3479
|
this.portManager = new PortManager();
|
|
3328
3480
|
}
|