@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
package/dist/lib/index.js CHANGED
@@ -5,13 +5,11 @@ import { WASI } from "node:wasi";
5
5
  var textEncoder = new TextEncoder();
6
6
  var textDecoder = new TextDecoder();
7
7
  var MemoryManager = class {
8
- constructor() {
9
- this.memory = null;
10
- this.instance = null;
11
- this.hostAllocOffset = 0;
12
- this.logCallback = null;
13
- this.isInitializing = false;
14
- }
8
+ memory = null;
9
+ instance = null;
10
+ hostAllocOffset = 0;
11
+ logCallback = null;
12
+ isInitializing = false;
15
13
  setMemory(memory) {
16
14
  this.memory = memory;
17
15
  }
@@ -174,14 +172,64 @@ var MemoryManager = class {
174
172
 
175
173
  // server/runner/HeaderManager.ts
176
174
  var textEncoder2 = new TextEncoder();
177
- var HeaderManager = class {
175
+ var HeaderManager = class _HeaderManager {
176
+ // Read a header value as a single string. For multi-valued headers (string[]) returns the first.
177
+ // Use when callers know the header is conventionally single-valued (content-type, host, location, etc.)
178
+ // and need to satisfy APIs that take a string.
179
+ static firstValue(v) {
180
+ return Array.isArray(v) ? v[0] : v;
181
+ }
182
+ // Flatten a HeaderRecord to a HeaderMap (single string per key) for consumers
183
+ // that can't handle multi-valued headers (e.g. fetch's HeadersInit).
184
+ // Multi-valued entries are joined with ", " — caller must be sure this is acceptable
185
+ // (NOT valid for Set-Cookie; route those through a separate channel).
186
+ static flattenToMap(headers) {
187
+ const flat = {};
188
+ for (const [k, v] of Object.entries(headers)) {
189
+ if (Array.isArray(v)) {
190
+ flat[k] = v.join(", ");
191
+ } else if (v !== void 0) {
192
+ flat[k] = String(v);
193
+ }
194
+ }
195
+ return flat;
196
+ }
178
197
  static normalize(headers) {
179
- const normalized = {};
198
+ const normalized = /* @__PURE__ */ Object.create(null);
180
199
  for (const [key, value] of Object.entries(headers)) {
181
- normalized[key.toLowerCase()] = String(value);
200
+ const k = key.toLowerCase();
201
+ if (Array.isArray(value)) {
202
+ normalized[k] = value.map(String);
203
+ } else {
204
+ normalized[k] = String(value);
205
+ }
182
206
  }
183
207
  return normalized;
184
208
  }
209
+ // Append-merge two header records: for keys present in both, values are
210
+ // concatenated into a string[] rather than the right-hand side overwriting
211
+ // the left. Keys are lowercased on the way in (consistent with `normalize`).
212
+ //
213
+ // Used by the runner to combine request-phase response-header state with
214
+ // the origin's response headers, mirroring how Envoy serves
215
+ // `add_http_response_header` calls issued during onRequestHeaders /
216
+ // onRequestBody against the actual upstream response — both values survive
217
+ // as a multi-value list (preserving the proxy-wasm cross-phase pattern).
218
+ static appendMerge(left, right) {
219
+ const result = _HeaderManager.normalize(left);
220
+ for (const [key, value] of Object.entries(right)) {
221
+ const k = key.toLowerCase();
222
+ const incoming = Array.isArray(value) ? value.map(String) : [String(value)];
223
+ if (Object.hasOwn(result, k)) {
224
+ const existing = result[k];
225
+ const existingArr = Array.isArray(existing) ? existing : [existing];
226
+ result[k] = [...existingArr, ...incoming];
227
+ } else {
228
+ result[k] = incoming.length === 1 ? incoming[0] : incoming;
229
+ }
230
+ }
231
+ return result;
232
+ }
185
233
  static serialize(headers) {
186
234
  const pairs = Object.entries(headers);
187
235
  const numPairs = pairs.length;
@@ -257,13 +305,31 @@ var HeaderManager = class {
257
305
  }
258
306
  // --- Tuple-based methods for multi-valued header support ---
259
307
  static recordToTuples(headers) {
260
- return Object.entries(headers).map(([k, v]) => [k.toLowerCase(), String(v)]);
308
+ const tuples = [];
309
+ for (const [k, v] of Object.entries(headers)) {
310
+ const key = k.toLowerCase();
311
+ if (Array.isArray(v)) {
312
+ for (const val of v) tuples.push([key, String(val)]);
313
+ } else if (v !== void 0) {
314
+ tuples.push([key, String(v)]);
315
+ }
316
+ }
317
+ return tuples;
261
318
  }
319
+ // Lossless projection of tuples to a Record: single-valued keys are string,
320
+ // multi-valued keys are string[] — matching Node's IncomingHttpHeaders shape.
321
+ // Set-Cookie and other legitimately-repeatable headers are preserved across duplicates.
262
322
  static tuplesToRecord(tuples) {
263
323
  const record = {};
264
324
  for (const [key, value] of tuples) {
265
325
  const existing = record[key];
266
- record[key] = existing !== void 0 ? `${existing},${value}` : value;
326
+ if (existing === void 0) {
327
+ record[key] = value;
328
+ } else if (Array.isArray(existing)) {
329
+ existing.push(value);
330
+ } else {
331
+ record[key] = [existing, value];
332
+ }
267
333
  }
268
334
  return record;
269
335
  }
@@ -342,20 +408,18 @@ var HeaderManager = class {
342
408
 
343
409
  // server/runner/PropertyResolver.ts
344
410
  var PropertyResolver = class {
345
- constructor() {
346
- this.properties = {};
347
- this.requestHeaders = {};
348
- this.requestMethod = "GET";
349
- this.requestPath = "/";
350
- this.requestScheme = "https";
351
- this.requestUrl = "";
352
- this.requestHost = "";
353
- this.requestQuery = "";
354
- this.requestExtension = "";
355
- this.responseHeaders = {};
356
- this.responseStatus = 200;
357
- this.responseStatusText = "OK";
358
- }
411
+ properties = {};
412
+ requestHeaders = {};
413
+ requestMethod = "GET";
414
+ requestPath = "/";
415
+ requestScheme = "https";
416
+ requestUrl = "";
417
+ requestHost = "";
418
+ requestQuery = "";
419
+ requestExtension = "";
420
+ responseHeaders = {};
421
+ responseStatus = 200;
422
+ responseStatusText = "OK";
359
423
  setProperties(properties) {
360
424
  this.properties = properties;
361
425
  }
@@ -409,10 +473,11 @@ var PropertyResolver = class {
409
473
  const url = new URL(targetUrl);
410
474
  this.requestUrl = targetUrl;
411
475
  this.requestHost = url.hostname + (url.port ? `:${url.port}` : "");
412
- this.requestPath = url.pathname || "/";
476
+ this.requestPath = (url.pathname || "/") + url.search;
413
477
  this.requestQuery = url.search.startsWith("?") ? url.search.substring(1) : url.search;
414
478
  this.requestScheme = url.protocol.replace(":", "");
415
- const pathParts = this.requestPath.split("/");
479
+ const pathOnly = url.pathname || "/";
480
+ const pathParts = pathOnly.split("/");
416
481
  const lastPart = pathParts[pathParts.length - 1];
417
482
  const dotIndex = lastPart.lastIndexOf(".");
418
483
  if (dotIndex > 0 && dotIndex < lastPart.length - 1) {
@@ -482,28 +547,28 @@ var PropertyResolver = class {
482
547
  if (path2 === "request.url")
483
548
  return this.requestUrl || `${this.requestScheme}://${this.requestHost}${this.requestPath}`;
484
549
  if (path2 === "request.host")
485
- return this.requestHost || this.requestHeaders["host"] || "localhost";
550
+ return this.requestHost || HeaderManager.firstValue(this.requestHeaders["host"]) || "localhost";
486
551
  if (path2 === "request.scheme") return this.requestScheme;
487
552
  if (path2 === "request.protocol") return this.requestScheme;
488
553
  if (path2 === "request.query") return this.requestQuery;
489
554
  if (path2 === "request.extension") return this.requestExtension;
490
555
  if (path2 === "request.content_type") {
491
- return this.requestHeaders["content-type"] || "";
556
+ return HeaderManager.firstValue(this.requestHeaders["content-type"]) || "";
492
557
  }
493
558
  if (path2.startsWith("request.headers.")) {
494
559
  const headerName = path2.substring("request.headers.".length).toLowerCase();
495
- return this.requestHeaders[headerName] || "";
560
+ return HeaderManager.firstValue(this.requestHeaders[headerName]) || "";
496
561
  }
497
562
  if (path2 === "response.code") return this.responseStatus;
498
563
  if (path2 === "response.status") return this.responseStatus;
499
564
  if (path2 === "response.status_code") return this.responseStatus;
500
565
  if (path2 === "response.code_details") return this.responseStatusText;
501
566
  if (path2 === "response.content_type") {
502
- return this.responseHeaders["content-type"] || "";
567
+ return HeaderManager.firstValue(this.responseHeaders["content-type"]) || "";
503
568
  }
504
569
  if (path2.startsWith("response.headers.")) {
505
570
  const headerName = path2.substring("response.headers.".length).toLowerCase();
506
- return this.responseHeaders[headerName] || "";
571
+ return HeaderManager.firstValue(this.responseHeaders[headerName]) || "";
507
572
  }
508
573
  return void 0;
509
574
  }
@@ -546,6 +611,7 @@ var PropertyResolver = class {
546
611
 
547
612
  // server/fastedge-host/SecretStore.ts
548
613
  var SecretStore = class {
614
+ secrets;
549
615
  constructor(initialSecrets) {
550
616
  this.secrets = /* @__PURE__ */ new Map();
551
617
  if (initialSecrets) {
@@ -628,6 +694,7 @@ var SecretStore = class {
628
694
 
629
695
  // server/fastedge-host/Dictionary.ts
630
696
  var Dictionary = class {
697
+ data;
631
698
  constructor(initialData) {
632
699
  this.data = /* @__PURE__ */ new Map();
633
700
  if (initialData) {
@@ -795,26 +862,33 @@ function createFastEdgeHostFunctions(memory, secretStore, dictionary, logDebug)
795
862
  // server/runner/HostFunctions.ts
796
863
  var textEncoder3 = new TextEncoder();
797
864
  var HostFunctions = class {
865
+ memory;
866
+ propertyResolver;
867
+ propertyAccessControl;
868
+ getCurrentHook;
869
+ logs = [];
870
+ requestHeaders = [];
871
+ responseHeaders = [];
872
+ requestBody = "";
873
+ responseBody = "";
874
+ vmConfig = "";
875
+ pluginConfig = "";
876
+ currentContextId = 1;
877
+ lastHostCall = null;
878
+ debug = false;
879
+ currentLogLevel = 0 /* Trace */;
880
+ // Default to show all logs
881
+ // http_call state
882
+ nextTokenId = 0;
883
+ pendingHttpCall = null;
884
+ httpCallResponse = null;
885
+ streamClosed = false;
886
+ // Local response state (from proxy_send_local_response / send_http_response)
887
+ localResponse = null;
888
+ // FastEdge extensions
889
+ secretStore;
890
+ dictionary;
798
891
  constructor(memory, propertyResolver, propertyAccessControl, getCurrentHook, debug = false, secretStore, dictionary) {
799
- this.logs = [];
800
- this.requestHeaders = [];
801
- this.responseHeaders = [];
802
- this.requestBody = "";
803
- this.responseBody = "";
804
- this.vmConfig = "";
805
- this.pluginConfig = "";
806
- this.currentContextId = 1;
807
- this.lastHostCall = null;
808
- this.debug = false;
809
- this.currentLogLevel = 0 /* Trace */;
810
- // Default to show all logs
811
- // http_call state
812
- this.nextTokenId = 0;
813
- this.pendingHttpCall = null;
814
- this.httpCallResponse = null;
815
- this.streamClosed = false;
816
- // Local response state (from proxy_send_local_response / send_http_response)
817
- this.localResponse = null;
818
892
  this.memory = memory;
819
893
  this.propertyResolver = propertyResolver;
820
894
  this.propertyAccessControl = propertyAccessControl;
@@ -858,7 +932,8 @@ var HostFunctions = class {
858
932
  return call;
859
933
  }
860
934
  setHttpCallResponse(tokenId, headers, body) {
861
- this.httpCallResponse = { tokenId, headers, body };
935
+ const tuples = Array.isArray(headers) ? headers : HeaderManager.recordToTuples(headers);
936
+ this.httpCallResponse = { tokenId, headers: tuples, body };
862
937
  }
863
938
  clearHttpCallResponse() {
864
939
  this.httpCallResponse = null;
@@ -1288,7 +1363,7 @@ var HostFunctions = class {
1288
1363
  return this.responseHeaders;
1289
1364
  }
1290
1365
  if (mapType === 6 /* HttpCallResponseHeaders */ || mapType === 7 /* HttpCallResponseTrailers */) {
1291
- return HeaderManager.recordToTuples(this.httpCallResponse?.headers ?? {});
1366
+ return this.httpCallResponse?.headers ?? [];
1292
1367
  }
1293
1368
  return this.requestHeaders;
1294
1369
  }
@@ -1544,6 +1619,8 @@ var BUILT_IN_PROPERTIES = [
1544
1619
  }
1545
1620
  ];
1546
1621
  var PropertyAccessControl = class {
1622
+ builtInProperties;
1623
+ customProperties;
1547
1624
  constructor() {
1548
1625
  this.builtInProperties = /* @__PURE__ */ new Map();
1549
1626
  this.customProperties = /* @__PURE__ */ new Map();
@@ -1761,21 +1838,28 @@ var textEncoder4 = new TextEncoder();
1761
1838
  var BUILTIN_URL = "http://fastedge-builtin.debug";
1762
1839
  var BUILTIN_SHORTHAND = "built-in";
1763
1840
  var ProxyWasmRunner = class {
1841
+ module = null;
1842
+ // Compiled module (reused)
1843
+ instance = null;
1844
+ // Current instance (transient per hook)
1845
+ memory;
1846
+ propertyResolver;
1847
+ propertyAccessControl;
1848
+ currentHook = null;
1849
+ hostFunctions;
1850
+ logs = [];
1851
+ rootContextId = 1;
1852
+ nextContextId = 2;
1853
+ currentContextId = 1;
1854
+ isInitializing = false;
1855
+ debug = process.env.PROXY_RUNNER_DEBUG === "1";
1856
+ stateManager = null;
1857
+ secretStore;
1858
+ dictionary;
1859
+ dotenvEnabled = true;
1860
+ // Default to enabled
1861
+ dotenvPath = ".";
1764
1862
  constructor(fastEdgeConfig, dotenvEnabled = true) {
1765
- this.module = null;
1766
- // Compiled module (reused)
1767
- this.instance = null;
1768
- this.currentHook = null;
1769
- this.logs = [];
1770
- this.rootContextId = 1;
1771
- this.nextContextId = 2;
1772
- this.currentContextId = 1;
1773
- this.isInitializing = false;
1774
- this.debug = process.env.PROXY_RUNNER_DEBUG === "1";
1775
- this.stateManager = null;
1776
- this.dotenvEnabled = true;
1777
- // Default to enabled
1778
- this.dotenvPath = ".";
1779
1863
  this.memory = new MemoryManager();
1780
1864
  this.propertyResolver = new PropertyResolver();
1781
1865
  this.propertyAccessControl = new PropertyAccessControl();
@@ -1948,7 +2032,7 @@ var ProxyWasmRunner = class {
1948
2032
  const local = this.hostFunctions.getLocalResponse();
1949
2033
  const responseHeaders2 = results.onRequestHeaders.output.response.headers;
1950
2034
  this.hostFunctions.resetLocalResponse();
1951
- const contentType2 = responseHeaders2["content-type"] || "text/plain";
2035
+ const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
1952
2036
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
1953
2037
  return {
1954
2038
  hookResults: results,
@@ -1991,7 +2075,7 @@ var ProxyWasmRunner = class {
1991
2075
  const local = this.hostFunctions.getLocalResponse();
1992
2076
  const responseHeaders2 = results.onRequestBody.output.response.headers;
1993
2077
  this.hostFunctions.resetLocalResponse();
1994
- const contentType2 = responseHeaders2["content-type"] || "text/plain";
2078
+ const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
1995
2079
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
1996
2080
  return {
1997
2081
  hookResults: results,
@@ -2022,7 +2106,7 @@ var ProxyWasmRunner = class {
2022
2106
  try {
2023
2107
  if (isBuiltIn) {
2024
2108
  this.logDebug("Using built-in responder");
2025
- const rawStatus = (modifiedRequestHeaders["x-debugger-status"] || "").trim();
2109
+ const rawStatus = (HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-status"]) || "").trim();
2026
2110
  if (rawStatus === "") {
2027
2111
  responseStatus = 200;
2028
2112
  } else {
@@ -2033,7 +2117,7 @@ var ProxyWasmRunner = class {
2033
2117
  }
2034
2118
  }
2035
2119
  responseStatusText = responseStatus === 200 ? "OK" : String(responseStatus);
2036
- const responseContentMode = modifiedRequestHeaders["x-debugger-content"] || "";
2120
+ const responseContentMode = HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-content"]) || "";
2037
2121
  delete modifiedRequestHeaders["x-debugger-status"];
2038
2122
  delete modifiedRequestHeaders["x-debugger-content"];
2039
2123
  if (responseContentMode === "status-only") {
@@ -2041,14 +2125,14 @@ var ProxyWasmRunner = class {
2041
2125
  contentType = "text/plain";
2042
2126
  } else if (responseContentMode === "body-only") {
2043
2127
  responseBody = modifiedRequestBody || "";
2044
- contentType = modifiedRequestHeaders["content-type"] || "text/plain";
2128
+ contentType = HeaderManager.firstValue(modifiedRequestHeaders["content-type"]) || "text/plain";
2045
2129
  } else {
2046
2130
  contentType = "application/json";
2047
2131
  responseBody = JSON.stringify({
2048
2132
  method: requestMethod,
2049
2133
  reqHeaders: modifiedRequestHeaders,
2050
2134
  reqBody: modifiedRequestBody || "",
2051
- requestUrl: BUILTIN_URL
2135
+ requestUrl: propertiesAfterRequestBody["request.url"] || BUILTIN_URL
2052
2136
  });
2053
2137
  }
2054
2138
  responseHeaders = {
@@ -2059,27 +2143,30 @@ var ProxyWasmRunner = class {
2059
2143
  `Built-in responder: ${responseStatus} ${responseStatusText}, mode=${responseContentMode || "full"}`
2060
2144
  );
2061
2145
  } else {
2062
- const modifiedScheme = propertiesAfterRequestBody["request.scheme"] || "https";
2063
- const modifiedHost = propertiesAfterRequestBody["request.host"] || "localhost";
2064
- const modifiedPath = propertiesAfterRequestBody["request.path"] || "/";
2065
- const modifiedQuery = propertiesAfterRequestBody["request.query"] || "";
2066
- const actualTargetUrl = `${modifiedScheme}://${modifiedHost}${modifiedPath}${modifiedQuery ? "?" + modifiedQuery : ""}`;
2146
+ const actualTargetUrl = propertiesAfterRequestBody["request.url"] || targetUrl;
2147
+ const actualScheme = new URL(actualTargetUrl).protocol.replace(":", "");
2067
2148
  this.logDebug(`Original URL: ${targetUrl}`);
2068
- this.logDebug(`Modified URL: ${actualTargetUrl}`);
2149
+ this.logDebug(`Effective URL: ${actualTargetUrl}`);
2069
2150
  this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
2070
- const fetchHeaders = {
2071
- ...modifiedRequestHeaders
2072
- };
2151
+ const fetchHeaders = HeaderManager.flattenToMap(
2152
+ modifiedRequestHeaders
2153
+ );
2154
+ for (const key of Object.keys(fetchHeaders)) {
2155
+ if (key.startsWith(":")) {
2156
+ delete fetchHeaders[key];
2157
+ }
2158
+ }
2073
2159
  const hostHeader = Object.entries(modifiedRequestHeaders).find(
2074
2160
  ([key]) => key.toLowerCase() === "host"
2075
2161
  );
2076
2162
  if (hostHeader) {
2077
- fetchHeaders["x-forwarded-host"] = hostHeader[1];
2078
- this.logDebug(`Adding x-forwarded-host: ${hostHeader[1]}`);
2163
+ const hostValue = HeaderManager.firstValue(hostHeader[1]) ?? "";
2164
+ fetchHeaders["x-forwarded-host"] = hostValue;
2165
+ this.logDebug(`Adding x-forwarded-host: ${hostValue}`);
2079
2166
  }
2080
- fetchHeaders["x-forwarded-proto"] = modifiedScheme;
2081
- this.logDebug(`Adding x-forwarded-proto: ${modifiedScheme}`);
2082
- fetchHeaders["x-forwarded-port"] = modifiedScheme === "https" ? "443" : "80";
2167
+ fetchHeaders["x-forwarded-proto"] = actualScheme;
2168
+ this.logDebug(`Adding x-forwarded-proto: ${actualScheme}`);
2169
+ fetchHeaders["x-forwarded-port"] = actualScheme === "https" ? "443" : "80";
2083
2170
  this.logDebug(
2084
2171
  `Adding x-forwarded-port: ${fetchHeaders["x-forwarded-port"]}`
2085
2172
  );
@@ -2099,8 +2186,14 @@ var ProxyWasmRunner = class {
2099
2186
  const response = await fetch(actualTargetUrl, fetchOptions);
2100
2187
  responseHeaders = {};
2101
2188
  response.headers.forEach((value, key) => {
2102
- responseHeaders[key] = value;
2189
+ if (key.toLowerCase() !== "set-cookie") {
2190
+ responseHeaders[key] = value;
2191
+ }
2103
2192
  });
2193
+ const setCookies = response.headers.getSetCookie();
2194
+ if (setCookies.length > 0) {
2195
+ responseHeaders["set-cookie"] = setCookies;
2196
+ }
2104
2197
  contentType = response.headers.get("content-type") || "text/plain";
2105
2198
  responseStatus = response.status;
2106
2199
  responseStatusText = response.statusText;
@@ -2119,6 +2212,14 @@ var ProxyWasmRunner = class {
2119
2212
  `Fetch completed: ${responseStatus} ${responseStatusText}`
2120
2213
  );
2121
2214
  }
2215
+ const requestPhaseResponseHeaders = {
2216
+ ...results.onRequestHeaders.output.response.headers ?? {},
2217
+ ...results.onRequestBody.output.response.headers ?? {}
2218
+ };
2219
+ const mergedResponseHeaders = HeaderManager.appendMerge(
2220
+ requestPhaseResponseHeaders,
2221
+ responseHeaders
2222
+ );
2122
2223
  const responseCall = {
2123
2224
  ...call,
2124
2225
  request: {
@@ -2127,7 +2228,7 @@ var ProxyWasmRunner = class {
2127
2228
  body: modifiedRequestBody
2128
2229
  },
2129
2230
  response: {
2130
- headers: responseHeaders,
2231
+ headers: mergedResponseHeaders,
2131
2232
  body: responseBody,
2132
2233
  status: responseStatus,
2133
2234
  statusText: responseStatusText
@@ -2149,6 +2250,25 @@ var ProxyWasmRunner = class {
2149
2250
  "system"
2150
2251
  );
2151
2252
  }
2253
+ if (results.onResponseHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2254
+ const local = this.hostFunctions.getLocalResponse();
2255
+ const headers = results.onResponseHeaders.output.response.headers;
2256
+ this.hostFunctions.resetLocalResponse();
2257
+ const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2258
+ const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2259
+ return {
2260
+ hookResults: results,
2261
+ finalResponse: {
2262
+ status: local.statusCode,
2263
+ statusText: local.statusText,
2264
+ headers,
2265
+ body,
2266
+ contentType: contentType2,
2267
+ isBase64: isBase642
2268
+ },
2269
+ calculatedProperties: this.propertyResolver.getCalculatedProperties()
2270
+ };
2271
+ }
2152
2272
  const headersAfterResponseHeaders = results.onResponseHeaders.output.response.headers;
2153
2273
  const propertiesAfterResponseHeaders = results.onResponseHeaders.properties;
2154
2274
  this.logDebug(
@@ -2173,11 +2293,30 @@ var ProxyWasmRunner = class {
2173
2293
  "system"
2174
2294
  );
2175
2295
  }
2296
+ if (results.onResponseBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2297
+ const local = this.hostFunctions.getLocalResponse();
2298
+ const headers = results.onResponseBody.output.response.headers;
2299
+ this.hostFunctions.resetLocalResponse();
2300
+ const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2301
+ const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2302
+ return {
2303
+ hookResults: results,
2304
+ finalResponse: {
2305
+ status: local.statusCode,
2306
+ statusText: local.statusText,
2307
+ headers,
2308
+ body,
2309
+ contentType: contentType2,
2310
+ isBase64: isBase642
2311
+ },
2312
+ calculatedProperties: this.propertyResolver.getCalculatedProperties()
2313
+ };
2314
+ }
2176
2315
  const finalHeaders = results.onResponseBody.output.response.headers;
2177
2316
  const finalBody = results.onResponseBody.output.response.body;
2178
2317
  this.logDebug(`Final response body length: ${finalBody.length}`);
2179
2318
  const calculatedProperties = this.propertyResolver.getCalculatedProperties();
2180
- const finalContentType = finalHeaders["content-type"] || contentType;
2319
+ const finalContentType = HeaderManager.firstValue(finalHeaders["content-type"]) || contentType;
2181
2320
  return {
2182
2321
  hookResults: results,
2183
2322
  finalResponse: {
@@ -2270,13 +2409,13 @@ var ProxyWasmRunner = class {
2270
2409
  this.hostFunctions.setLogLevel(0);
2271
2410
  const requestHeaders = HeaderManager.normalize(call.request.headers ?? {});
2272
2411
  const responseHeaders = HeaderManager.normalize(
2273
- call.response.headers ?? {}
2412
+ call.response?.headers ?? {}
2274
2413
  );
2275
2414
  const requestBody = call.request.body ?? "";
2276
- const responseBody = call.response.body ?? "";
2415
+ const responseBody = call.response?.body ?? "";
2277
2416
  const requestMethod = call.request.method ?? "GET";
2278
- const responseStatus = call.response.status ?? 200;
2279
- const responseStatusText = call.response.statusText ?? "OK";
2417
+ const responseStatus = call.response?.status ?? 200;
2418
+ const responseStatusText = call.response?.statusText ?? "OK";
2280
2419
  this.propertyResolver.setProperties({ ...call.properties ?? {} });
2281
2420
  this.propertyResolver.setRequestMetadata(
2282
2421
  requestHeaders,
@@ -2363,7 +2502,7 @@ var ProxyWasmRunner = class {
2363
2502
  for (const [k, v] of Object.entries(pending.headers)) {
2364
2503
  if (!k.startsWith(":")) fetchHeaders[k] = v;
2365
2504
  }
2366
- let responseHeaders2 = {};
2505
+ let responseHeaders2 = [];
2367
2506
  let responseBody2 = new Uint8Array(0);
2368
2507
  try {
2369
2508
  const resp = await fetch(url, {
@@ -2373,20 +2512,23 @@ var ProxyWasmRunner = class {
2373
2512
  signal: AbortSignal.timeout(pending.timeoutMs)
2374
2513
  });
2375
2514
  resp.headers.forEach((v, k) => {
2376
- responseHeaders2[k] = v;
2515
+ if (k.toLowerCase() !== "set-cookie") responseHeaders2.push([k, v]);
2377
2516
  });
2517
+ for (const cookie of resp.headers.getSetCookie()) {
2518
+ responseHeaders2.push(["set-cookie", cookie]);
2519
+ }
2378
2520
  responseBody2 = new Uint8Array(await resp.arrayBuffer());
2379
2521
  this.logDebug(
2380
- `http_call response: ${resp.status} ${resp.statusText} numHeaders=${Object.keys(responseHeaders2).length} bodySize=${responseBody2.byteLength}`
2522
+ `http_call response: ${resp.status} ${resp.statusText} numHeaders=${responseHeaders2.length} bodySize=${responseBody2.byteLength}`
2381
2523
  );
2382
2524
  } catch (err) {
2383
2525
  const errMsg = `http_call failed for ${url}: ${String(err)}`;
2384
2526
  this.logDebug(errMsg);
2385
2527
  this.logs.push({ level: 3, message: `[host] ${errMsg}` });
2386
- responseHeaders2 = {};
2528
+ responseHeaders2 = [];
2387
2529
  responseBody2 = new Uint8Array(0);
2388
2530
  }
2389
- const numHeaders = Object.keys(responseHeaders2).length;
2531
+ const numHeaders = responseHeaders2.length;
2390
2532
  const bodySize = responseBody2.byteLength;
2391
2533
  this.hostFunctions.setHttpCallResponse(pending.tokenId, responseHeaders2, responseBody2);
2392
2534
  this.hostFunctions.resetStreamClosed();
@@ -2488,11 +2630,18 @@ var ProxyWasmRunner = class {
2488
2630
  this.isInitializing = false;
2489
2631
  }
2490
2632
  buildHookInvocation(hook, requestHeaders, responseHeaders, requestBody, responseBody) {
2633
+ const countEntries = (h) => {
2634
+ let n = 0;
2635
+ for (const v of Object.values(h)) {
2636
+ n += Array.isArray(v) ? v.length : 1;
2637
+ }
2638
+ return n;
2639
+ };
2491
2640
  switch (hook) {
2492
2641
  case "onRequestHeaders":
2493
2642
  return {
2494
2643
  exportName: "proxy_on_request_headers",
2495
- args: [this.currentContextId, Object.keys(requestHeaders).length, 0]
2644
+ args: [this.currentContextId, countEntries(requestHeaders), 0]
2496
2645
  };
2497
2646
  case "onRequestBody":
2498
2647
  return {
@@ -2502,7 +2651,7 @@ var ProxyWasmRunner = class {
2502
2651
  case "onResponseHeaders":
2503
2652
  return {
2504
2653
  exportName: "proxy_on_response_headers",
2505
- args: [this.currentContextId, Object.keys(responseHeaders).length, 0]
2654
+ args: [this.currentContextId, countEntries(responseHeaders), 0]
2506
2655
  };
2507
2656
  case "onResponseBody":
2508
2657
  return {
@@ -2617,9 +2766,12 @@ var ProxyWasmRunner = class {
2617
2766
  console.warn(entry.message);
2618
2767
  }
2619
2768
  /**
2620
- * Interface-compliant callFullFlow method
2769
+ * Interface-compliant callFullFlow method.
2770
+ *
2771
+ * The upstream response is generated at runtime by a real HTTP fetch
2772
+ * against `url` or by the built-in responder when `url === "built-in"`.
2621
2773
  */
2622
- async callFullFlow(url, method, headers, body, responseHeaders, responseBody, responseStatus, responseStatusText, properties, enforceProductionPropertyRules) {
2774
+ async callFullFlow(url, method, headers, body, properties, enforceProductionPropertyRules) {
2623
2775
  const call = {
2624
2776
  hook: "",
2625
2777
  // Not used in fullFlow
@@ -2628,12 +2780,6 @@ var ProxyWasmRunner = class {
2628
2780
  body,
2629
2781
  method
2630
2782
  },
2631
- response: {
2632
- headers: responseHeaders,
2633
- body: responseBody,
2634
- status: responseStatus,
2635
- statusText: responseStatusText
2636
- },
2637
2783
  properties,
2638
2784
  enforceProductionPropertyRules
2639
2785
  };
@@ -2642,7 +2788,7 @@ var ProxyWasmRunner = class {
2642
2788
  /**
2643
2789
  * Not supported for Proxy-WASM (HTTP WASM only)
2644
2790
  */
2645
- async execute(request) {
2791
+ async execute(_request) {
2646
2792
  throw new Error(
2647
2793
  "execute() is not supported for Proxy-WASM. Use callHook() or callFullFlow() instead."
2648
2794
  );
@@ -2806,21 +2952,22 @@ async function isLegacySyncWasm(bufferOrPath) {
2806
2952
 
2807
2953
  // server/runner/HttpWasmRunner.ts
2808
2954
  var HttpWasmRunner = class {
2955
+ process = null;
2956
+ port = null;
2957
+ cliPath = null;
2958
+ tempWasmPath = null;
2959
+ currentWasmPath = null;
2960
+ // resolved path used when spawning
2961
+ logs = [];
2962
+ stateManager = null;
2963
+ portManager;
2964
+ dotenvEnabled = true;
2965
+ dotenvPath = null;
2966
+ /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2967
+ isPinnedPort = false;
2968
+ /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2969
+ isLegacySync = false;
2809
2970
  constructor(portManager, dotenvEnabled = true) {
2810
- this.process = null;
2811
- this.port = null;
2812
- this.cliPath = null;
2813
- this.tempWasmPath = null;
2814
- this.currentWasmPath = null;
2815
- // resolved path used when spawning
2816
- this.logs = [];
2817
- this.stateManager = null;
2818
- this.dotenvEnabled = true;
2819
- this.dotenvPath = null;
2820
- /** Pinned ports bypass PortManager allocation and must not be released back to it. */
2821
- this.isPinnedPort = false;
2822
- /** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
2823
- this.isLegacySync = false;
2824
2971
  this.portManager = portManager;
2825
2972
  this.dotenvEnabled = dotenvEnabled;
2826
2973
  }
@@ -2947,7 +3094,7 @@ var HttpWasmRunner = class {
2947
3094
  /**
2948
3095
  * Not supported for HTTP WASM (proxy-wasm only)
2949
3096
  */
2950
- async callFullFlow(_url, _method, _headers, _body, _responseHeaders, _responseBody, _responseStatus, _responseStatusText, _properties, _enforceProductionPropertyRules) {
3097
+ async callFullFlow(_url, _method, _headers, _body, _properties, _enforceProductionPropertyRules) {
2951
3098
  throw new Error(
2952
3099
  "callFullFlow() is not supported for HTTP WASM. Use execute() instead."
2953
3100
  );
@@ -3199,27 +3346,31 @@ ${recentLogs || "(no logs)"}`
3199
3346
  ];
3200
3347
  return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
3201
3348
  }
3202
- /**
3203
- * Parse headers from fetch Headers object
3204
- */
3205
3349
  parseHeaders(headers) {
3206
- const result = {};
3207
- headers.forEach((value, key) => {
3208
- result[key] = value;
3209
- });
3210
- return result;
3350
+ return parseFetchHeaders(headers);
3211
3351
  }
3212
3352
  };
3353
+ function parseFetchHeaders(headers) {
3354
+ const result = {};
3355
+ headers.forEach((value, key) => {
3356
+ if (key.toLowerCase() !== "set-cookie") {
3357
+ result[key] = value;
3358
+ }
3359
+ });
3360
+ const setCookies = headers.getSetCookie();
3361
+ if (setCookies.length > 0) {
3362
+ result["set-cookie"] = setCookies;
3363
+ }
3364
+ return result;
3365
+ }
3213
3366
 
3214
3367
  // server/runner/PortManager.ts
3215
3368
  import { createServer } from "net";
3216
3369
  var PortManager = class {
3217
- constructor() {
3218
- this.minPort = 8100;
3219
- this.maxPort = 8199;
3220
- this.allocatedPorts = /* @__PURE__ */ new Set();
3221
- this.lastAllocatedPort = this.minPort - 1;
3222
- }
3370
+ minPort = 8100;
3371
+ maxPort = 8199;
3372
+ allocatedPorts = /* @__PURE__ */ new Set();
3373
+ lastAllocatedPort = this.minPort - 1;
3223
3374
  /**
3224
3375
  * Check whether a port is actually free at the OS level.
3225
3376
  * This is necessary when multiple server processes run simultaneously —
@@ -3279,6 +3430,7 @@ var PortManager = class {
3279
3430
 
3280
3431
  // server/runner/WasmRunnerFactory.ts
3281
3432
  var WasmRunnerFactory = class {
3433
+ portManager;
3282
3434
  constructor() {
3283
3435
  this.portManager = new PortManager();
3284
3436
  }