@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.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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2063
|
-
const
|
|
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(`
|
|
2149
|
+
this.logDebug(`Effective URL: ${actualTargetUrl}`);
|
|
2069
2150
|
this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
|
|
2070
|
-
const fetchHeaders =
|
|
2071
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
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"] =
|
|
2081
|
-
this.logDebug(`Adding x-forwarded-proto: ${
|
|
2082
|
-
fetchHeaders["x-forwarded-port"] =
|
|
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
|
-
|
|
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:
|
|
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
|
|
2412
|
+
call.response?.headers ?? {}
|
|
2274
2413
|
);
|
|
2275
2414
|
const requestBody = call.request.body ?? "";
|
|
2276
|
-
const responseBody = call.response
|
|
2415
|
+
const responseBody = call.response?.body ?? "";
|
|
2277
2416
|
const requestMethod = call.request.method ?? "GET";
|
|
2278
|
-
const responseStatus = call.response
|
|
2279
|
-
const responseStatusText = call.response
|
|
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
|
|
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=${
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
}
|