@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.
Files changed (44) hide show
  1. package/dist/fastedge-cli/METADATA.json +1 -1
  2. package/dist/fastedge-cli/fastedge-run-darwin-arm64 +0 -0
  3. package/dist/fastedge-cli/fastedge-run-linux-x64 +0 -0
  4. package/dist/fastedge-cli/fastedge-run.exe +0 -0
  5. package/dist/frontend/assets/{index-BCXfEMSq.js → index-CiqeJ9rz.js} +24 -24
  6. package/dist/frontend/index.html +1 -1
  7. package/dist/lib/index.cjs +292 -140
  8. package/dist/lib/index.d.ts +1 -0
  9. package/dist/lib/index.js +292 -140
  10. package/dist/lib/runner/HeaderManager.d.ts +7 -4
  11. package/dist/lib/runner/HostFunctions.d.ts +5 -5
  12. package/dist/lib/runner/HttpWasmRunner.d.ts +13 -4
  13. package/dist/lib/runner/IStateManager.d.ts +7 -7
  14. package/dist/lib/runner/IWasmRunner.d.ts +17 -9
  15. package/dist/lib/runner/PropertyResolver.d.ts +3 -3
  16. package/dist/lib/runner/ProxyWasmRunner.d.ts +6 -3
  17. package/dist/lib/runner/standalone.d.ts +1 -1
  18. package/dist/lib/runner/types.d.ts +17 -8
  19. package/dist/lib/schemas/api.d.ts +0 -8
  20. package/dist/lib/schemas/config.d.ts +0 -13
  21. package/dist/lib/schemas/index.d.ts +2 -2
  22. package/dist/lib/test-framework/assertions.d.ts +18 -4
  23. package/dist/lib/test-framework/index.cjs +18754 -189
  24. package/dist/lib/test-framework/index.d.ts +2 -0
  25. package/dist/lib/test-framework/index.js +18771 -178
  26. package/dist/lib/test-framework/mock-origins.d.ts +56 -0
  27. package/dist/lib/test-framework/types.d.ts +1 -5
  28. package/dist/server.js +33 -33
  29. package/docs/API.md +23 -53
  30. package/docs/DEBUGGER.md +7 -7
  31. package/docs/INDEX.md +4 -1
  32. package/docs/RUNNER.md +79 -64
  33. package/docs/TEST_CONFIG.md +28 -41
  34. package/docs/TEST_FRAMEWORK.md +205 -32
  35. package/docs/WEBSOCKET.md +25 -21
  36. package/docs/quickstart.md +1 -13
  37. package/package.json +4 -1
  38. package/schemas/api-config.schema.json +0 -24
  39. package/schemas/api-send.schema.json +0 -20
  40. package/schemas/fastedge-config.test.schema.json +0 -24
  41. package/schemas/full-flow-result.schema.json +17 -7
  42. package/schemas/hook-call.schema.json +16 -6
  43. package/schemas/hook-result.schema.json +16 -6
  44. package/schemas/http-response.schema.json +227 -5
@@ -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
- constructor() {
53
- this.memory = null;
54
- this.instance = null;
55
- this.hostAllocOffset = 0;
56
- this.logCallback = null;
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
- normalized[key.toLowerCase()] = String(value);
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
- return Object.entries(headers).map(([k, v]) => [k.toLowerCase(), String(v)]);
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
- record[key] = existing !== void 0 ? `${existing},${value}` : value;
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
- constructor() {
390
- this.properties = {};
391
- this.requestHeaders = {};
392
- this.requestMethod = "GET";
393
- this.requestPath = "/";
394
- this.requestScheme = "https";
395
- this.requestUrl = "";
396
- this.requestHost = "";
397
- this.requestQuery = "";
398
- this.requestExtension = "";
399
- this.responseHeaders = {};
400
- this.responseStatus = 200;
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 pathParts = this.requestPath.split("/");
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
- this.httpCallResponse = { tokenId, headers, body };
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 HeaderManager.recordToTuples(this.httpCallResponse?.headers ?? {});
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 modifiedScheme = propertiesAfterRequestBody["request.scheme"] || "https";
2107
- const modifiedHost = propertiesAfterRequestBody["request.host"] || "localhost";
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(`Modified URL: ${actualTargetUrl}`);
2193
+ this.logDebug(`Effective URL: ${actualTargetUrl}`);
2113
2194
  this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
2114
- const fetchHeaders = {
2115
- ...modifiedRequestHeaders
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
- fetchHeaders["x-forwarded-host"] = hostHeader[1];
2122
- this.logDebug(`Adding x-forwarded-host: ${hostHeader[1]}`);
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"] = modifiedScheme;
2125
- this.logDebug(`Adding x-forwarded-proto: ${modifiedScheme}`);
2126
- fetchHeaders["x-forwarded-port"] = modifiedScheme === "https" ? "443" : "80";
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
- responseHeaders[key] = value;
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: responseHeaders,
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.headers ?? {}
2456
+ call.response?.headers ?? {}
2318
2457
  );
2319
2458
  const requestBody = call.request.body ?? "";
2320
- const responseBody = call.response.body ?? "";
2459
+ const responseBody = call.response?.body ?? "";
2321
2460
  const requestMethod = call.request.method ?? "GET";
2322
- const responseStatus = call.response.status ?? 200;
2323
- const responseStatusText = call.response.statusText ?? "OK";
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] = v;
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=${Object.keys(responseHeaders2).length} bodySize=${responseBody2.byteLength}`
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 = Object.keys(responseHeaders2).length;
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, Object.keys(requestHeaders).length, 0]
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, Object.keys(responseHeaders).length, 0]
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, responseHeaders, responseBody, responseStatus, responseStatusText, properties, enforceProductionPropertyRules) {
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(request) {
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, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
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
- const result = {};
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
- constructor() {
3262
- this.minPort = 8100;
3263
- this.maxPort = 8199;
3264
- this.allocatedPorts = /* @__PURE__ */ new Set();
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
  }