@gcoredev/fastedge-test 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frontend/assets/{index-BCXfEMSq.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +130 -62
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +130 -62
- package/dist/lib/runner/HeaderManager.d.ts +6 -4
- package/dist/lib/runner/HostFunctions.d.ts +5 -5
- package/dist/lib/runner/HttpWasmRunner.d.ts +13 -4
- package/dist/lib/runner/IStateManager.d.ts +7 -7
- package/dist/lib/runner/IWasmRunner.d.ts +17 -9
- package/dist/lib/runner/PropertyResolver.d.ts +3 -3
- package/dist/lib/runner/ProxyWasmRunner.d.ts +5 -2
- package/dist/lib/runner/standalone.d.ts +1 -1
- package/dist/lib/runner/types.d.ts +17 -8
- package/dist/lib/schemas/api.d.ts +0 -8
- package/dist/lib/schemas/config.d.ts +0 -13
- package/dist/lib/schemas/index.d.ts +2 -2
- package/dist/lib/test-framework/assertions.d.ts +18 -4
- package/dist/lib/test-framework/index.cjs +18593 -111
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18610 -100
- package/dist/lib/test-framework/mock-origins.d.ts +56 -0
- package/dist/lib/test-framework/types.d.ts +1 -5
- package/dist/server.js +33 -33
- package/docs/API.md +19 -49
- package/docs/DEBUGGER.md +6 -7
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +96 -81
- package/docs/TEST_CONFIG.md +9 -22
- package/docs/TEST_FRAMEWORK.md +206 -31
- package/docs/WEBSOCKET.md +25 -21
- package/docs/quickstart.md +1 -13
- package/package.json +4 -1
- package/schemas/api-config.schema.json +0 -24
- package/schemas/api-send.schema.json +0 -20
- package/schemas/fastedge-config.test.schema.json +0 -24
- package/schemas/full-flow-result.schema.json +17 -7
- package/schemas/hook-call.schema.json +16 -6
- package/schemas/hook-result.schema.json +16 -6
- package/schemas/http-response.schema.json +227 -5
package/dist/lib/index.js
CHANGED
|
@@ -175,10 +175,36 @@ var MemoryManager = class {
|
|
|
175
175
|
// server/runner/HeaderManager.ts
|
|
176
176
|
var textEncoder2 = new TextEncoder();
|
|
177
177
|
var HeaderManager = class {
|
|
178
|
+
// Read a header value as a single string. For multi-valued headers (string[]) returns the first.
|
|
179
|
+
// Use when callers know the header is conventionally single-valued (content-type, host, location, etc.)
|
|
180
|
+
// and need to satisfy APIs that take a string.
|
|
181
|
+
static firstValue(v) {
|
|
182
|
+
return Array.isArray(v) ? v[0] : v;
|
|
183
|
+
}
|
|
184
|
+
// Flatten a HeaderRecord to a HeaderMap (single string per key) for consumers
|
|
185
|
+
// that can't handle multi-valued headers (e.g. fetch's HeadersInit).
|
|
186
|
+
// Multi-valued entries are joined with ", " — caller must be sure this is acceptable
|
|
187
|
+
// (NOT valid for Set-Cookie; route those through a separate channel).
|
|
188
|
+
static flattenToMap(headers) {
|
|
189
|
+
const flat = {};
|
|
190
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
191
|
+
if (Array.isArray(v)) {
|
|
192
|
+
flat[k] = v.join(", ");
|
|
193
|
+
} else if (v !== void 0) {
|
|
194
|
+
flat[k] = String(v);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return flat;
|
|
198
|
+
}
|
|
178
199
|
static normalize(headers) {
|
|
179
200
|
const normalized = {};
|
|
180
201
|
for (const [key, value] of Object.entries(headers)) {
|
|
181
|
-
|
|
202
|
+
const k = key.toLowerCase();
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
normalized[k] = value.map(String);
|
|
205
|
+
} else {
|
|
206
|
+
normalized[k] = String(value);
|
|
207
|
+
}
|
|
182
208
|
}
|
|
183
209
|
return normalized;
|
|
184
210
|
}
|
|
@@ -257,13 +283,31 @@ var HeaderManager = class {
|
|
|
257
283
|
}
|
|
258
284
|
// --- Tuple-based methods for multi-valued header support ---
|
|
259
285
|
static recordToTuples(headers) {
|
|
260
|
-
|
|
286
|
+
const tuples = [];
|
|
287
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
288
|
+
const key = k.toLowerCase();
|
|
289
|
+
if (Array.isArray(v)) {
|
|
290
|
+
for (const val of v) tuples.push([key, String(val)]);
|
|
291
|
+
} else if (v !== void 0) {
|
|
292
|
+
tuples.push([key, String(v)]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return tuples;
|
|
261
296
|
}
|
|
297
|
+
// Lossless projection of tuples to a Record: single-valued keys are string,
|
|
298
|
+
// multi-valued keys are string[] — matching Node's IncomingHttpHeaders shape.
|
|
299
|
+
// Set-Cookie and other legitimately-repeatable headers are preserved across duplicates.
|
|
262
300
|
static tuplesToRecord(tuples) {
|
|
263
301
|
const record = {};
|
|
264
302
|
for (const [key, value] of tuples) {
|
|
265
303
|
const existing = record[key];
|
|
266
|
-
|
|
304
|
+
if (existing === void 0) {
|
|
305
|
+
record[key] = value;
|
|
306
|
+
} else if (Array.isArray(existing)) {
|
|
307
|
+
existing.push(value);
|
|
308
|
+
} else {
|
|
309
|
+
record[key] = [existing, value];
|
|
310
|
+
}
|
|
267
311
|
}
|
|
268
312
|
return record;
|
|
269
313
|
}
|
|
@@ -409,10 +453,11 @@ var PropertyResolver = class {
|
|
|
409
453
|
const url = new URL(targetUrl);
|
|
410
454
|
this.requestUrl = targetUrl;
|
|
411
455
|
this.requestHost = url.hostname + (url.port ? `:${url.port}` : "");
|
|
412
|
-
this.requestPath = url.pathname || "/";
|
|
456
|
+
this.requestPath = (url.pathname || "/") + url.search;
|
|
413
457
|
this.requestQuery = url.search.startsWith("?") ? url.search.substring(1) : url.search;
|
|
414
458
|
this.requestScheme = url.protocol.replace(":", "");
|
|
415
|
-
const
|
|
459
|
+
const pathOnly = url.pathname || "/";
|
|
460
|
+
const pathParts = pathOnly.split("/");
|
|
416
461
|
const lastPart = pathParts[pathParts.length - 1];
|
|
417
462
|
const dotIndex = lastPart.lastIndexOf(".");
|
|
418
463
|
if (dotIndex > 0 && dotIndex < lastPart.length - 1) {
|
|
@@ -482,28 +527,28 @@ var PropertyResolver = class {
|
|
|
482
527
|
if (path2 === "request.url")
|
|
483
528
|
return this.requestUrl || `${this.requestScheme}://${this.requestHost}${this.requestPath}`;
|
|
484
529
|
if (path2 === "request.host")
|
|
485
|
-
return this.requestHost || this.requestHeaders["host"] || "localhost";
|
|
530
|
+
return this.requestHost || HeaderManager.firstValue(this.requestHeaders["host"]) || "localhost";
|
|
486
531
|
if (path2 === "request.scheme") return this.requestScheme;
|
|
487
532
|
if (path2 === "request.protocol") return this.requestScheme;
|
|
488
533
|
if (path2 === "request.query") return this.requestQuery;
|
|
489
534
|
if (path2 === "request.extension") return this.requestExtension;
|
|
490
535
|
if (path2 === "request.content_type") {
|
|
491
|
-
return this.requestHeaders["content-type"] || "";
|
|
536
|
+
return HeaderManager.firstValue(this.requestHeaders["content-type"]) || "";
|
|
492
537
|
}
|
|
493
538
|
if (path2.startsWith("request.headers.")) {
|
|
494
539
|
const headerName = path2.substring("request.headers.".length).toLowerCase();
|
|
495
|
-
return this.requestHeaders[headerName] || "";
|
|
540
|
+
return HeaderManager.firstValue(this.requestHeaders[headerName]) || "";
|
|
496
541
|
}
|
|
497
542
|
if (path2 === "response.code") return this.responseStatus;
|
|
498
543
|
if (path2 === "response.status") return this.responseStatus;
|
|
499
544
|
if (path2 === "response.status_code") return this.responseStatus;
|
|
500
545
|
if (path2 === "response.code_details") return this.responseStatusText;
|
|
501
546
|
if (path2 === "response.content_type") {
|
|
502
|
-
return this.responseHeaders["content-type"] || "";
|
|
547
|
+
return HeaderManager.firstValue(this.responseHeaders["content-type"]) || "";
|
|
503
548
|
}
|
|
504
549
|
if (path2.startsWith("response.headers.")) {
|
|
505
550
|
const headerName = path2.substring("response.headers.".length).toLowerCase();
|
|
506
|
-
return this.responseHeaders[headerName] || "";
|
|
551
|
+
return HeaderManager.firstValue(this.responseHeaders[headerName]) || "";
|
|
507
552
|
}
|
|
508
553
|
return void 0;
|
|
509
554
|
}
|
|
@@ -858,7 +903,8 @@ var HostFunctions = class {
|
|
|
858
903
|
return call;
|
|
859
904
|
}
|
|
860
905
|
setHttpCallResponse(tokenId, headers, body) {
|
|
861
|
-
|
|
906
|
+
const tuples = Array.isArray(headers) ? headers : HeaderManager.recordToTuples(headers);
|
|
907
|
+
this.httpCallResponse = { tokenId, headers: tuples, body };
|
|
862
908
|
}
|
|
863
909
|
clearHttpCallResponse() {
|
|
864
910
|
this.httpCallResponse = null;
|
|
@@ -1288,7 +1334,7 @@ var HostFunctions = class {
|
|
|
1288
1334
|
return this.responseHeaders;
|
|
1289
1335
|
}
|
|
1290
1336
|
if (mapType === 6 /* HttpCallResponseHeaders */ || mapType === 7 /* HttpCallResponseTrailers */) {
|
|
1291
|
-
return
|
|
1337
|
+
return this.httpCallResponse?.headers ?? [];
|
|
1292
1338
|
}
|
|
1293
1339
|
return this.requestHeaders;
|
|
1294
1340
|
}
|
|
@@ -1948,7 +1994,7 @@ var ProxyWasmRunner = class {
|
|
|
1948
1994
|
const local = this.hostFunctions.getLocalResponse();
|
|
1949
1995
|
const responseHeaders2 = results.onRequestHeaders.output.response.headers;
|
|
1950
1996
|
this.hostFunctions.resetLocalResponse();
|
|
1951
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
1997
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
1952
1998
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
1953
1999
|
return {
|
|
1954
2000
|
hookResults: results,
|
|
@@ -1991,7 +2037,7 @@ var ProxyWasmRunner = class {
|
|
|
1991
2037
|
const local = this.hostFunctions.getLocalResponse();
|
|
1992
2038
|
const responseHeaders2 = results.onRequestBody.output.response.headers;
|
|
1993
2039
|
this.hostFunctions.resetLocalResponse();
|
|
1994
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
2040
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
1995
2041
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
1996
2042
|
return {
|
|
1997
2043
|
hookResults: results,
|
|
@@ -2022,7 +2068,7 @@ var ProxyWasmRunner = class {
|
|
|
2022
2068
|
try {
|
|
2023
2069
|
if (isBuiltIn) {
|
|
2024
2070
|
this.logDebug("Using built-in responder");
|
|
2025
|
-
const rawStatus = (modifiedRequestHeaders["x-debugger-status"] || "").trim();
|
|
2071
|
+
const rawStatus = (HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-status"]) || "").trim();
|
|
2026
2072
|
if (rawStatus === "") {
|
|
2027
2073
|
responseStatus = 200;
|
|
2028
2074
|
} else {
|
|
@@ -2033,7 +2079,7 @@ var ProxyWasmRunner = class {
|
|
|
2033
2079
|
}
|
|
2034
2080
|
}
|
|
2035
2081
|
responseStatusText = responseStatus === 200 ? "OK" : String(responseStatus);
|
|
2036
|
-
const responseContentMode = modifiedRequestHeaders["x-debugger-content"] || "";
|
|
2082
|
+
const responseContentMode = HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-content"]) || "";
|
|
2037
2083
|
delete modifiedRequestHeaders["x-debugger-status"];
|
|
2038
2084
|
delete modifiedRequestHeaders["x-debugger-content"];
|
|
2039
2085
|
if (responseContentMode === "status-only") {
|
|
@@ -2041,14 +2087,14 @@ var ProxyWasmRunner = class {
|
|
|
2041
2087
|
contentType = "text/plain";
|
|
2042
2088
|
} else if (responseContentMode === "body-only") {
|
|
2043
2089
|
responseBody = modifiedRequestBody || "";
|
|
2044
|
-
contentType = modifiedRequestHeaders["content-type"] || "text/plain";
|
|
2090
|
+
contentType = HeaderManager.firstValue(modifiedRequestHeaders["content-type"]) || "text/plain";
|
|
2045
2091
|
} else {
|
|
2046
2092
|
contentType = "application/json";
|
|
2047
2093
|
responseBody = JSON.stringify({
|
|
2048
2094
|
method: requestMethod,
|
|
2049
2095
|
reqHeaders: modifiedRequestHeaders,
|
|
2050
2096
|
reqBody: modifiedRequestBody || "",
|
|
2051
|
-
requestUrl: BUILTIN_URL
|
|
2097
|
+
requestUrl: propertiesAfterRequestBody["request.url"] || BUILTIN_URL
|
|
2052
2098
|
});
|
|
2053
2099
|
}
|
|
2054
2100
|
responseHeaders = {
|
|
@@ -2059,27 +2105,30 @@ var ProxyWasmRunner = class {
|
|
|
2059
2105
|
`Built-in responder: ${responseStatus} ${responseStatusText}, mode=${responseContentMode || "full"}`
|
|
2060
2106
|
);
|
|
2061
2107
|
} 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 : ""}`;
|
|
2108
|
+
const actualTargetUrl = propertiesAfterRequestBody["request.url"] || targetUrl;
|
|
2109
|
+
const actualScheme = new URL(actualTargetUrl).protocol.replace(":", "");
|
|
2067
2110
|
this.logDebug(`Original URL: ${targetUrl}`);
|
|
2068
|
-
this.logDebug(`
|
|
2111
|
+
this.logDebug(`Effective URL: ${actualTargetUrl}`);
|
|
2069
2112
|
this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
|
|
2070
|
-
const fetchHeaders =
|
|
2071
|
-
|
|
2072
|
-
|
|
2113
|
+
const fetchHeaders = HeaderManager.flattenToMap(
|
|
2114
|
+
modifiedRequestHeaders
|
|
2115
|
+
);
|
|
2116
|
+
for (const key of Object.keys(fetchHeaders)) {
|
|
2117
|
+
if (key.startsWith(":")) {
|
|
2118
|
+
delete fetchHeaders[key];
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2073
2121
|
const hostHeader = Object.entries(modifiedRequestHeaders).find(
|
|
2074
2122
|
([key]) => key.toLowerCase() === "host"
|
|
2075
2123
|
);
|
|
2076
2124
|
if (hostHeader) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2125
|
+
const hostValue = HeaderManager.firstValue(hostHeader[1]) ?? "";
|
|
2126
|
+
fetchHeaders["x-forwarded-host"] = hostValue;
|
|
2127
|
+
this.logDebug(`Adding x-forwarded-host: ${hostValue}`);
|
|
2079
2128
|
}
|
|
2080
|
-
fetchHeaders["x-forwarded-proto"] =
|
|
2081
|
-
this.logDebug(`Adding x-forwarded-proto: ${
|
|
2082
|
-
fetchHeaders["x-forwarded-port"] =
|
|
2129
|
+
fetchHeaders["x-forwarded-proto"] = actualScheme;
|
|
2130
|
+
this.logDebug(`Adding x-forwarded-proto: ${actualScheme}`);
|
|
2131
|
+
fetchHeaders["x-forwarded-port"] = actualScheme === "https" ? "443" : "80";
|
|
2083
2132
|
this.logDebug(
|
|
2084
2133
|
`Adding x-forwarded-port: ${fetchHeaders["x-forwarded-port"]}`
|
|
2085
2134
|
);
|
|
@@ -2099,8 +2148,14 @@ var ProxyWasmRunner = class {
|
|
|
2099
2148
|
const response = await fetch(actualTargetUrl, fetchOptions);
|
|
2100
2149
|
responseHeaders = {};
|
|
2101
2150
|
response.headers.forEach((value, key) => {
|
|
2102
|
-
|
|
2151
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
2152
|
+
responseHeaders[key] = value;
|
|
2153
|
+
}
|
|
2103
2154
|
});
|
|
2155
|
+
const setCookies = response.headers.getSetCookie();
|
|
2156
|
+
if (setCookies.length > 0) {
|
|
2157
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
2158
|
+
}
|
|
2104
2159
|
contentType = response.headers.get("content-type") || "text/plain";
|
|
2105
2160
|
responseStatus = response.status;
|
|
2106
2161
|
responseStatusText = response.statusText;
|
|
@@ -2177,7 +2232,7 @@ var ProxyWasmRunner = class {
|
|
|
2177
2232
|
const finalBody = results.onResponseBody.output.response.body;
|
|
2178
2233
|
this.logDebug(`Final response body length: ${finalBody.length}`);
|
|
2179
2234
|
const calculatedProperties = this.propertyResolver.getCalculatedProperties();
|
|
2180
|
-
const finalContentType = finalHeaders["content-type"] || contentType;
|
|
2235
|
+
const finalContentType = HeaderManager.firstValue(finalHeaders["content-type"]) || contentType;
|
|
2181
2236
|
return {
|
|
2182
2237
|
hookResults: results,
|
|
2183
2238
|
finalResponse: {
|
|
@@ -2270,13 +2325,13 @@ var ProxyWasmRunner = class {
|
|
|
2270
2325
|
this.hostFunctions.setLogLevel(0);
|
|
2271
2326
|
const requestHeaders = HeaderManager.normalize(call.request.headers ?? {});
|
|
2272
2327
|
const responseHeaders = HeaderManager.normalize(
|
|
2273
|
-
call.response
|
|
2328
|
+
call.response?.headers ?? {}
|
|
2274
2329
|
);
|
|
2275
2330
|
const requestBody = call.request.body ?? "";
|
|
2276
|
-
const responseBody = call.response
|
|
2331
|
+
const responseBody = call.response?.body ?? "";
|
|
2277
2332
|
const requestMethod = call.request.method ?? "GET";
|
|
2278
|
-
const responseStatus = call.response
|
|
2279
|
-
const responseStatusText = call.response
|
|
2333
|
+
const responseStatus = call.response?.status ?? 200;
|
|
2334
|
+
const responseStatusText = call.response?.statusText ?? "OK";
|
|
2280
2335
|
this.propertyResolver.setProperties({ ...call.properties ?? {} });
|
|
2281
2336
|
this.propertyResolver.setRequestMetadata(
|
|
2282
2337
|
requestHeaders,
|
|
@@ -2363,7 +2418,7 @@ var ProxyWasmRunner = class {
|
|
|
2363
2418
|
for (const [k, v] of Object.entries(pending.headers)) {
|
|
2364
2419
|
if (!k.startsWith(":")) fetchHeaders[k] = v;
|
|
2365
2420
|
}
|
|
2366
|
-
let responseHeaders2 =
|
|
2421
|
+
let responseHeaders2 = [];
|
|
2367
2422
|
let responseBody2 = new Uint8Array(0);
|
|
2368
2423
|
try {
|
|
2369
2424
|
const resp = await fetch(url, {
|
|
@@ -2373,20 +2428,23 @@ var ProxyWasmRunner = class {
|
|
|
2373
2428
|
signal: AbortSignal.timeout(pending.timeoutMs)
|
|
2374
2429
|
});
|
|
2375
2430
|
resp.headers.forEach((v, k) => {
|
|
2376
|
-
responseHeaders2[k
|
|
2431
|
+
if (k.toLowerCase() !== "set-cookie") responseHeaders2.push([k, v]);
|
|
2377
2432
|
});
|
|
2433
|
+
for (const cookie of resp.headers.getSetCookie()) {
|
|
2434
|
+
responseHeaders2.push(["set-cookie", cookie]);
|
|
2435
|
+
}
|
|
2378
2436
|
responseBody2 = new Uint8Array(await resp.arrayBuffer());
|
|
2379
2437
|
this.logDebug(
|
|
2380
|
-
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${
|
|
2438
|
+
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${responseHeaders2.length} bodySize=${responseBody2.byteLength}`
|
|
2381
2439
|
);
|
|
2382
2440
|
} catch (err) {
|
|
2383
2441
|
const errMsg = `http_call failed for ${url}: ${String(err)}`;
|
|
2384
2442
|
this.logDebug(errMsg);
|
|
2385
2443
|
this.logs.push({ level: 3, message: `[host] ${errMsg}` });
|
|
2386
|
-
responseHeaders2 =
|
|
2444
|
+
responseHeaders2 = [];
|
|
2387
2445
|
responseBody2 = new Uint8Array(0);
|
|
2388
2446
|
}
|
|
2389
|
-
const numHeaders =
|
|
2447
|
+
const numHeaders = responseHeaders2.length;
|
|
2390
2448
|
const bodySize = responseBody2.byteLength;
|
|
2391
2449
|
this.hostFunctions.setHttpCallResponse(pending.tokenId, responseHeaders2, responseBody2);
|
|
2392
2450
|
this.hostFunctions.resetStreamClosed();
|
|
@@ -2488,11 +2546,18 @@ var ProxyWasmRunner = class {
|
|
|
2488
2546
|
this.isInitializing = false;
|
|
2489
2547
|
}
|
|
2490
2548
|
buildHookInvocation(hook, requestHeaders, responseHeaders, requestBody, responseBody) {
|
|
2549
|
+
const countEntries = (h) => {
|
|
2550
|
+
let n = 0;
|
|
2551
|
+
for (const v of Object.values(h)) {
|
|
2552
|
+
n += Array.isArray(v) ? v.length : 1;
|
|
2553
|
+
}
|
|
2554
|
+
return n;
|
|
2555
|
+
};
|
|
2491
2556
|
switch (hook) {
|
|
2492
2557
|
case "onRequestHeaders":
|
|
2493
2558
|
return {
|
|
2494
2559
|
exportName: "proxy_on_request_headers",
|
|
2495
|
-
args: [this.currentContextId,
|
|
2560
|
+
args: [this.currentContextId, countEntries(requestHeaders), 0]
|
|
2496
2561
|
};
|
|
2497
2562
|
case "onRequestBody":
|
|
2498
2563
|
return {
|
|
@@ -2502,7 +2567,7 @@ var ProxyWasmRunner = class {
|
|
|
2502
2567
|
case "onResponseHeaders":
|
|
2503
2568
|
return {
|
|
2504
2569
|
exportName: "proxy_on_response_headers",
|
|
2505
|
-
args: [this.currentContextId,
|
|
2570
|
+
args: [this.currentContextId, countEntries(responseHeaders), 0]
|
|
2506
2571
|
};
|
|
2507
2572
|
case "onResponseBody":
|
|
2508
2573
|
return {
|
|
@@ -2617,9 +2682,12 @@ var ProxyWasmRunner = class {
|
|
|
2617
2682
|
console.warn(entry.message);
|
|
2618
2683
|
}
|
|
2619
2684
|
/**
|
|
2620
|
-
* Interface-compliant callFullFlow method
|
|
2685
|
+
* Interface-compliant callFullFlow method.
|
|
2686
|
+
*
|
|
2687
|
+
* The upstream response is generated at runtime by a real HTTP fetch
|
|
2688
|
+
* against `url` or by the built-in responder when `url === "built-in"`.
|
|
2621
2689
|
*/
|
|
2622
|
-
async callFullFlow(url, method, headers, body,
|
|
2690
|
+
async callFullFlow(url, method, headers, body, properties, enforceProductionPropertyRules) {
|
|
2623
2691
|
const call = {
|
|
2624
2692
|
hook: "",
|
|
2625
2693
|
// Not used in fullFlow
|
|
@@ -2628,12 +2696,6 @@ var ProxyWasmRunner = class {
|
|
|
2628
2696
|
body,
|
|
2629
2697
|
method
|
|
2630
2698
|
},
|
|
2631
|
-
response: {
|
|
2632
|
-
headers: responseHeaders,
|
|
2633
|
-
body: responseBody,
|
|
2634
|
-
status: responseStatus,
|
|
2635
|
-
statusText: responseStatusText
|
|
2636
|
-
},
|
|
2637
2699
|
properties,
|
|
2638
2700
|
enforceProductionPropertyRules
|
|
2639
2701
|
};
|
|
@@ -2947,7 +3009,7 @@ var HttpWasmRunner = class {
|
|
|
2947
3009
|
/**
|
|
2948
3010
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2949
3011
|
*/
|
|
2950
|
-
async callFullFlow(_url, _method, _headers, _body,
|
|
3012
|
+
async callFullFlow(_url, _method, _headers, _body, _properties, _enforceProductionPropertyRules) {
|
|
2951
3013
|
throw new Error(
|
|
2952
3014
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2953
3015
|
);
|
|
@@ -3199,17 +3261,23 @@ ${recentLogs || "(no logs)"}`
|
|
|
3199
3261
|
];
|
|
3200
3262
|
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
3201
3263
|
}
|
|
3202
|
-
/**
|
|
3203
|
-
* Parse headers from fetch Headers object
|
|
3204
|
-
*/
|
|
3205
3264
|
parseHeaders(headers) {
|
|
3206
|
-
|
|
3207
|
-
headers.forEach((value, key) => {
|
|
3208
|
-
result[key] = value;
|
|
3209
|
-
});
|
|
3210
|
-
return result;
|
|
3265
|
+
return parseFetchHeaders(headers);
|
|
3211
3266
|
}
|
|
3212
3267
|
};
|
|
3268
|
+
function parseFetchHeaders(headers) {
|
|
3269
|
+
const result = {};
|
|
3270
|
+
headers.forEach((value, key) => {
|
|
3271
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
3272
|
+
result[key] = value;
|
|
3273
|
+
}
|
|
3274
|
+
});
|
|
3275
|
+
const setCookies = headers.getSetCookie();
|
|
3276
|
+
if (setCookies.length > 0) {
|
|
3277
|
+
result["set-cookie"] = setCookies;
|
|
3278
|
+
}
|
|
3279
|
+
return result;
|
|
3280
|
+
}
|
|
3213
3281
|
|
|
3214
3282
|
// server/runner/PortManager.ts
|
|
3215
3283
|
import { createServer } from "net";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { HeaderMap, HeaderTuples } from "./types";
|
|
1
|
+
import type { HeaderMap, HeaderRecord, HeaderTuples } from "./types";
|
|
2
2
|
export declare class HeaderManager {
|
|
3
|
-
static
|
|
3
|
+
static firstValue(v: string | string[] | undefined): string | undefined;
|
|
4
|
+
static flattenToMap(headers: HeaderMap | HeaderRecord): HeaderMap;
|
|
5
|
+
static normalize(headers: HeaderMap | HeaderRecord): HeaderRecord;
|
|
4
6
|
static serialize(headers: HeaderMap): Uint8Array;
|
|
5
7
|
/**
|
|
6
8
|
* Deserializes a proxy-wasm binary header map format:
|
|
@@ -8,8 +10,8 @@ export declare class HeaderManager {
|
|
|
8
10
|
*/
|
|
9
11
|
static deserializeBinary(bytes: Uint8Array): HeaderMap;
|
|
10
12
|
static deserialize(payload: string): HeaderMap;
|
|
11
|
-
static recordToTuples(headers: HeaderMap): HeaderTuples;
|
|
12
|
-
static tuplesToRecord(tuples: HeaderTuples):
|
|
13
|
+
static recordToTuples(headers: HeaderMap | HeaderRecord): HeaderTuples;
|
|
14
|
+
static tuplesToRecord(tuples: HeaderTuples): HeaderRecord;
|
|
13
15
|
static normalizeTuples(tuples: HeaderTuples): HeaderTuples;
|
|
14
16
|
static serializeTuples(tuples: HeaderTuples): Uint8Array;
|
|
15
17
|
static deserializeBinaryToTuples(bytes: Uint8Array): HeaderTuples;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HeaderMap, LogEntry } from "./types";
|
|
1
|
+
import type { HeaderMap, HeaderRecord, HeaderTuples, LogEntry } from "./types";
|
|
2
2
|
import { MemoryManager } from "./MemoryManager";
|
|
3
3
|
import { PropertyResolver } from "./PropertyResolver";
|
|
4
4
|
import { PropertyAccessControl, HookContext } from "./PropertyAccessControl";
|
|
@@ -36,7 +36,7 @@ export declare class HostFunctions {
|
|
|
36
36
|
private dictionary;
|
|
37
37
|
constructor(memory: MemoryManager, propertyResolver: PropertyResolver, propertyAccessControl: PropertyAccessControl, getCurrentHook: () => HookContext | null, debug?: boolean, secretStore?: SecretStore, dictionary?: Dictionary);
|
|
38
38
|
setLogs(logs: LogEntry[]): void;
|
|
39
|
-
setHeadersAndBodies(reqHeaders: HeaderMap, resHeaders: HeaderMap, reqBody: string, resBody: string): void;
|
|
39
|
+
setHeadersAndBodies(reqHeaders: HeaderMap | HeaderRecord, resHeaders: HeaderMap | HeaderRecord, reqBody: string, resBody: string): void;
|
|
40
40
|
setConfigs(vmConfig: string, pluginConfig: string): void;
|
|
41
41
|
setCurrentContext(contextId: number): void;
|
|
42
42
|
getCurrentLogLevel(): number;
|
|
@@ -50,7 +50,7 @@ export declare class HostFunctions {
|
|
|
50
50
|
body: Uint8Array | null;
|
|
51
51
|
timeoutMs: number;
|
|
52
52
|
} | null;
|
|
53
|
-
setHttpCallResponse(tokenId: number, headers: HeaderMap, body: Uint8Array): void;
|
|
53
|
+
setHttpCallResponse(tokenId: number, headers: HeaderMap | HeaderRecord | HeaderTuples, body: Uint8Array): void;
|
|
54
54
|
clearHttpCallResponse(): void;
|
|
55
55
|
isStreamClosed(): boolean;
|
|
56
56
|
resetStreamClosed(): void;
|
|
@@ -61,8 +61,8 @@ export declare class HostFunctions {
|
|
|
61
61
|
body: Uint8Array;
|
|
62
62
|
} | null;
|
|
63
63
|
resetLocalResponse(): void;
|
|
64
|
-
getRequestHeaders():
|
|
65
|
-
getResponseHeaders():
|
|
64
|
+
getRequestHeaders(): HeaderRecord;
|
|
65
|
+
getResponseHeaders(): HeaderRecord;
|
|
66
66
|
getRequestBody(): string;
|
|
67
67
|
getResponseBody(): string;
|
|
68
68
|
getSecretStore(): SecretStore;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Executes HTTP WASM binaries (component model with wasi-http interface)
|
|
5
5
|
* using the FastEdge-run CLI as a process-based runner.
|
|
6
6
|
*/
|
|
7
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
7
8
|
import type { IWasmRunner, WasmType, RunnerConfig, HttpRequest, HttpResponse } from "./IWasmRunner.js";
|
|
8
9
|
import type { HookCall, HookResult, FullFlowResult } from "./types.js";
|
|
9
10
|
import type { IStateManager } from "./IStateManager.js";
|
|
@@ -50,7 +51,7 @@ export declare class HttpWasmRunner implements IWasmRunner {
|
|
|
50
51
|
/**
|
|
51
52
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
52
53
|
*/
|
|
53
|
-
callFullFlow(_url: string, _method: string, _headers: Record<string, string>, _body: string,
|
|
54
|
+
callFullFlow(_url: string, _method: string, _headers: Record<string, string>, _body: string, _properties: Record<string, unknown>, _enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
|
|
54
55
|
/**
|
|
55
56
|
* Apply dotenv settings by restarting the fastedge-run process.
|
|
56
57
|
* The WASM file is not re-read; only the --dotenv flag changes.
|
|
@@ -111,8 +112,16 @@ export declare class HttpWasmRunner implements IWasmRunner {
|
|
|
111
112
|
* Check if content type is binary
|
|
112
113
|
*/
|
|
113
114
|
private isBinaryContentType;
|
|
114
|
-
/**
|
|
115
|
-
* Parse headers from fetch Headers object
|
|
116
|
-
*/
|
|
117
115
|
private parseHeaders;
|
|
118
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Parse a fetch Headers object into an IncomingHttpHeaders-shaped record.
|
|
119
|
+
*
|
|
120
|
+
* Uses `Headers.getSetCookie()` (Node 19.7+, always available on Node ≥22.12)
|
|
121
|
+
* to preserve multiple Set-Cookie entries as a string[] — RFC 6265 §3 exempts
|
|
122
|
+
* Set-Cookie from the "combine duplicates with commas" rule, and real browsers
|
|
123
|
+
* process each Set-Cookie independently.
|
|
124
|
+
*
|
|
125
|
+
* Exported for unit testing; in production use it via HttpWasmRunner.
|
|
126
|
+
*/
|
|
127
|
+
export declare function parseFetchHeaders(headers: Headers): IncomingHttpHeaders;
|
|
@@ -7,30 +7,30 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type EventSource = "ui" | "ai_agent" | "api" | "system";
|
|
9
9
|
export interface IStateManager {
|
|
10
|
-
emitRequestStarted(url: string, method: string, headers: Record<string, string>, source?: EventSource): void;
|
|
10
|
+
emitRequestStarted(url: string, method: string, headers: Record<string, string | string[]>, source?: EventSource): void;
|
|
11
11
|
emitHookExecuted(hook: string, returnCode: number | null, logCount: number, input: {
|
|
12
12
|
request: {
|
|
13
|
-
headers: Record<string, string>;
|
|
13
|
+
headers: Record<string, string | string[]>;
|
|
14
14
|
body: string;
|
|
15
15
|
};
|
|
16
16
|
response: {
|
|
17
|
-
headers: Record<string, string>;
|
|
17
|
+
headers: Record<string, string | string[]>;
|
|
18
18
|
body: string;
|
|
19
19
|
};
|
|
20
20
|
}, output: {
|
|
21
21
|
request: {
|
|
22
|
-
headers: Record<string, string>;
|
|
22
|
+
headers: Record<string, string | string[]>;
|
|
23
23
|
body: string;
|
|
24
24
|
};
|
|
25
25
|
response: {
|
|
26
|
-
headers: Record<string, string>;
|
|
26
|
+
headers: Record<string, string | string[]>;
|
|
27
27
|
body: string;
|
|
28
28
|
};
|
|
29
29
|
}, source?: EventSource): void;
|
|
30
30
|
emitRequestCompleted(hookResults: Record<string, unknown>, finalResponse: {
|
|
31
31
|
status: number;
|
|
32
32
|
statusText: string;
|
|
33
|
-
headers: Record<string, string>;
|
|
33
|
+
headers: Record<string, string | string[]>;
|
|
34
34
|
body: string;
|
|
35
35
|
contentType: string;
|
|
36
36
|
isBase64?: boolean;
|
|
@@ -41,7 +41,7 @@ export interface IStateManager {
|
|
|
41
41
|
emitHttpWasmRequestCompleted(response: {
|
|
42
42
|
status: number;
|
|
43
43
|
statusText: string;
|
|
44
|
-
headers: Record<string, string>;
|
|
44
|
+
headers: Record<string, string | string[] | undefined>;
|
|
45
45
|
body: string;
|
|
46
46
|
contentType: string | null;
|
|
47
47
|
isBase64?: boolean;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Defines the common contract for both ProxyWasmRunner and HttpWasmRunner
|
|
5
5
|
*/
|
|
6
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
6
7
|
import type { IStateManager } from "./IStateManager.js";
|
|
7
8
|
import type { HookCall, HookResult, FullFlowResult } from "./types.js";
|
|
8
9
|
export type WasmType = "http-wasm" | "proxy-wasm";
|
|
@@ -38,12 +39,18 @@ export interface HttpRequest {
|
|
|
38
39
|
body?: string;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* HTTP Response type for HTTP WASM runner
|
|
42
|
+
* HTTP Response type for HTTP WASM runner.
|
|
43
|
+
*
|
|
44
|
+
* `headers` uses Node's `IncomingHttpHeaders` shape — common single-valued
|
|
45
|
+
* headers (`content-type`, `location`, `etag`, …) are typed as `string`;
|
|
46
|
+
* `set-cookie` is `string[]`; unknown keys are `string | string[] | undefined`.
|
|
47
|
+
* This preserves RFC 6265 Set-Cookie semantics (each cookie kept separate)
|
|
48
|
+
* and matches consumer expectations for Node's fetch/http ecosystem.
|
|
42
49
|
*/
|
|
43
50
|
export interface HttpResponse {
|
|
44
51
|
status: number;
|
|
45
52
|
statusText: string;
|
|
46
|
-
headers:
|
|
53
|
+
headers: IncomingHttpHeaders;
|
|
47
54
|
body: string;
|
|
48
55
|
contentType: string | null;
|
|
49
56
|
isBase64?: boolean;
|
|
@@ -90,20 +97,21 @@ export interface IWasmRunner {
|
|
|
90
97
|
*/
|
|
91
98
|
callHook(hookCall: HookCall): Promise<HookResult>;
|
|
92
99
|
/**
|
|
93
|
-
* Execute full request/response flow (Proxy-WASM only)
|
|
94
|
-
*
|
|
100
|
+
* Execute full request/response flow (Proxy-WASM only).
|
|
101
|
+
*
|
|
102
|
+
* The upstream response is generated at runtime — either by a real HTTP
|
|
103
|
+
* fetch against `url`, or by the built-in responder when
|
|
104
|
+
* `url === "built-in"`. There is no fixture-level mock response.
|
|
105
|
+
*
|
|
106
|
+
* @param url Request URL, or `"built-in"` to use the built-in responder
|
|
95
107
|
* @param method HTTP method
|
|
96
108
|
* @param headers Request headers
|
|
97
109
|
* @param body Request body
|
|
98
|
-
* @param responseHeaders Response headers
|
|
99
|
-
* @param responseBody Response body
|
|
100
|
-
* @param responseStatus Response status code
|
|
101
|
-
* @param responseStatusText Response status text
|
|
102
110
|
* @param properties Shared properties
|
|
103
111
|
* @param enforceProductionPropertyRules Whether to enforce property access rules
|
|
104
112
|
* @returns Full flow execution result
|
|
105
113
|
*/
|
|
106
|
-
callFullFlow(url: string, method: string, headers: Record<string, string>, body: string,
|
|
114
|
+
callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
|
|
107
115
|
/**
|
|
108
116
|
* Apply dotenv settings to the current runner without reloading the WASM.
|
|
109
117
|
* For ProxyWasmRunner: resets stores and re-loads dotenv files in-place.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HeaderMap } from "./types";
|
|
1
|
+
import type { HeaderMap, HeaderRecord } from "./types";
|
|
2
2
|
export declare class PropertyResolver {
|
|
3
3
|
private properties;
|
|
4
4
|
private requestHeaders;
|
|
@@ -28,13 +28,13 @@ export declare class PropertyResolver {
|
|
|
28
28
|
* User properties take precedence over calculated ones
|
|
29
29
|
*/
|
|
30
30
|
getAllProperties(): Record<string, unknown>;
|
|
31
|
-
setRequestMetadata(headers: HeaderMap, method: string, path?: string, scheme?: string): void;
|
|
31
|
+
setRequestMetadata(headers: HeaderMap | HeaderRecord, method: string, path?: string, scheme?: string): void;
|
|
32
32
|
/**
|
|
33
33
|
* Extract runtime properties from target URL
|
|
34
34
|
* This parses the URL to populate request.url, request.host, request.path, etc.
|
|
35
35
|
*/
|
|
36
36
|
extractRuntimePropertiesFromUrl(targetUrl: string): void;
|
|
37
|
-
setResponseMetadata(headers: HeaderMap, status: number, statusText: string): void;
|
|
37
|
+
setResponseMetadata(headers: HeaderMap | HeaderRecord, status: number, statusText: string): void;
|
|
38
38
|
resolve(path: string): unknown;
|
|
39
39
|
private resolveStandard;
|
|
40
40
|
private resolvePathSegments;
|
|
@@ -59,9 +59,12 @@ export declare class ProxyWasmRunner implements IWasmRunner {
|
|
|
59
59
|
private getHookContext;
|
|
60
60
|
private logDebug;
|
|
61
61
|
/**
|
|
62
|
-
* Interface-compliant callFullFlow method
|
|
62
|
+
* Interface-compliant callFullFlow method.
|
|
63
|
+
*
|
|
64
|
+
* The upstream response is generated at runtime by a real HTTP fetch
|
|
65
|
+
* against `url` or by the built-in responder when `url === "built-in"`.
|
|
63
66
|
*/
|
|
64
|
-
callFullFlow(url: string, method: string, headers: Record<string, string>, body: string,
|
|
67
|
+
callFullFlow(url: string, method: string, headers: Record<string, string>, body: string, properties: Record<string, unknown>, enforceProductionPropertyRules: boolean): Promise<FullFlowResult>;
|
|
65
68
|
/**
|
|
66
69
|
* Not supported for Proxy-WASM (HTTP WASM only)
|
|
67
70
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Usage:
|
|
8
8
|
* import { createRunner } from './server/runner/standalone.js';
|
|
9
9
|
* const runner = await createRunner('./path/to/wasm.wasm');
|
|
10
|
-
* const result = await runner.callFullFlow('https://example.com', 'GET', {}, '', {},
|
|
10
|
+
* const result = await runner.callFullFlow('https://example.com', 'GET', {}, '', {}, true);
|
|
11
11
|
*/
|
|
12
12
|
import type { IWasmRunner, RunnerConfig } from "./IWasmRunner.js";
|
|
13
13
|
/**
|