@gcoredev/fastedge-test 0.1.6 → 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-BpdzhbRl.js → index-CiqeJ9rz.js} +24 -24
- package/dist/frontend/index.html +1 -1
- package/dist/lib/index.cjs +164 -66
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +164 -66
- 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 +22 -5
- package/dist/lib/runner/IStateManager.d.ts +7 -7
- package/dist/lib/runner/IWasmRunner.d.ts +42 -10
- package/dist/lib/runner/PortManager.d.ts +4 -1
- 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 +2 -8
- package/dist/lib/schemas/config.d.ts +2 -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 +18634 -115
- package/dist/lib/test-framework/index.d.ts +2 -0
- package/dist/lib/test-framework/index.js +18651 -104
- package/dist/lib/test-framework/mock-origins.d.ts +56 -0
- package/dist/lib/test-framework/suite-runner.d.ts +16 -0
- package/dist/lib/test-framework/types.d.ts +1 -5
- package/dist/server.js +33 -33
- package/docs/API.md +48 -54
- package/docs/DEBUGGER.md +7 -8
- package/docs/INDEX.md +4 -1
- package/docs/RUNNER.md +126 -74
- package/docs/TEST_CONFIG.md +79 -40
- package/docs/TEST_FRAMEWORK.md +235 -36
- package/docs/WEBSOCKET.md +25 -21
- package/docs/quickstart.md +1 -13
- package/package.json +4 -1
- package/schemas/api-config.schema.json +5 -24
- package/schemas/api-load.schema.json +5 -0
- package/schemas/api-send.schema.json +0 -20
- package/schemas/fastedge-config.test.schema.json +5 -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/frontend/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Proxy Runner</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CiqeJ9rz.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-DdlINQc_.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/dist/lib/index.cjs
CHANGED
|
@@ -219,10 +219,36 @@ var MemoryManager = class {
|
|
|
219
219
|
// server/runner/HeaderManager.ts
|
|
220
220
|
var textEncoder2 = new TextEncoder();
|
|
221
221
|
var HeaderManager = class {
|
|
222
|
+
// Read a header value as a single string. For multi-valued headers (string[]) returns the first.
|
|
223
|
+
// Use when callers know the header is conventionally single-valued (content-type, host, location, etc.)
|
|
224
|
+
// and need to satisfy APIs that take a string.
|
|
225
|
+
static firstValue(v) {
|
|
226
|
+
return Array.isArray(v) ? v[0] : v;
|
|
227
|
+
}
|
|
228
|
+
// Flatten a HeaderRecord to a HeaderMap (single string per key) for consumers
|
|
229
|
+
// that can't handle multi-valued headers (e.g. fetch's HeadersInit).
|
|
230
|
+
// Multi-valued entries are joined with ", " — caller must be sure this is acceptable
|
|
231
|
+
// (NOT valid for Set-Cookie; route those through a separate channel).
|
|
232
|
+
static flattenToMap(headers) {
|
|
233
|
+
const flat = {};
|
|
234
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
235
|
+
if (Array.isArray(v)) {
|
|
236
|
+
flat[k] = v.join(", ");
|
|
237
|
+
} else if (v !== void 0) {
|
|
238
|
+
flat[k] = String(v);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return flat;
|
|
242
|
+
}
|
|
222
243
|
static normalize(headers) {
|
|
223
244
|
const normalized = {};
|
|
224
245
|
for (const [key, value] of Object.entries(headers)) {
|
|
225
|
-
|
|
246
|
+
const k = key.toLowerCase();
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
normalized[k] = value.map(String);
|
|
249
|
+
} else {
|
|
250
|
+
normalized[k] = String(value);
|
|
251
|
+
}
|
|
226
252
|
}
|
|
227
253
|
return normalized;
|
|
228
254
|
}
|
|
@@ -301,13 +327,31 @@ var HeaderManager = class {
|
|
|
301
327
|
}
|
|
302
328
|
// --- Tuple-based methods for multi-valued header support ---
|
|
303
329
|
static recordToTuples(headers) {
|
|
304
|
-
|
|
330
|
+
const tuples = [];
|
|
331
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
332
|
+
const key = k.toLowerCase();
|
|
333
|
+
if (Array.isArray(v)) {
|
|
334
|
+
for (const val of v) tuples.push([key, String(val)]);
|
|
335
|
+
} else if (v !== void 0) {
|
|
336
|
+
tuples.push([key, String(v)]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return tuples;
|
|
305
340
|
}
|
|
341
|
+
// Lossless projection of tuples to a Record: single-valued keys are string,
|
|
342
|
+
// multi-valued keys are string[] — matching Node's IncomingHttpHeaders shape.
|
|
343
|
+
// Set-Cookie and other legitimately-repeatable headers are preserved across duplicates.
|
|
306
344
|
static tuplesToRecord(tuples) {
|
|
307
345
|
const record = {};
|
|
308
346
|
for (const [key, value] of tuples) {
|
|
309
347
|
const existing = record[key];
|
|
310
|
-
|
|
348
|
+
if (existing === void 0) {
|
|
349
|
+
record[key] = value;
|
|
350
|
+
} else if (Array.isArray(existing)) {
|
|
351
|
+
existing.push(value);
|
|
352
|
+
} else {
|
|
353
|
+
record[key] = [existing, value];
|
|
354
|
+
}
|
|
311
355
|
}
|
|
312
356
|
return record;
|
|
313
357
|
}
|
|
@@ -453,10 +497,11 @@ var PropertyResolver = class {
|
|
|
453
497
|
const url = new URL(targetUrl);
|
|
454
498
|
this.requestUrl = targetUrl;
|
|
455
499
|
this.requestHost = url.hostname + (url.port ? `:${url.port}` : "");
|
|
456
|
-
this.requestPath = url.pathname || "/";
|
|
500
|
+
this.requestPath = (url.pathname || "/") + url.search;
|
|
457
501
|
this.requestQuery = url.search.startsWith("?") ? url.search.substring(1) : url.search;
|
|
458
502
|
this.requestScheme = url.protocol.replace(":", "");
|
|
459
|
-
const
|
|
503
|
+
const pathOnly = url.pathname || "/";
|
|
504
|
+
const pathParts = pathOnly.split("/");
|
|
460
505
|
const lastPart = pathParts[pathParts.length - 1];
|
|
461
506
|
const dotIndex = lastPart.lastIndexOf(".");
|
|
462
507
|
if (dotIndex > 0 && dotIndex < lastPart.length - 1) {
|
|
@@ -526,28 +571,28 @@ var PropertyResolver = class {
|
|
|
526
571
|
if (path2 === "request.url")
|
|
527
572
|
return this.requestUrl || `${this.requestScheme}://${this.requestHost}${this.requestPath}`;
|
|
528
573
|
if (path2 === "request.host")
|
|
529
|
-
return this.requestHost || this.requestHeaders["host"] || "localhost";
|
|
574
|
+
return this.requestHost || HeaderManager.firstValue(this.requestHeaders["host"]) || "localhost";
|
|
530
575
|
if (path2 === "request.scheme") return this.requestScheme;
|
|
531
576
|
if (path2 === "request.protocol") return this.requestScheme;
|
|
532
577
|
if (path2 === "request.query") return this.requestQuery;
|
|
533
578
|
if (path2 === "request.extension") return this.requestExtension;
|
|
534
579
|
if (path2 === "request.content_type") {
|
|
535
|
-
return this.requestHeaders["content-type"] || "";
|
|
580
|
+
return HeaderManager.firstValue(this.requestHeaders["content-type"]) || "";
|
|
536
581
|
}
|
|
537
582
|
if (path2.startsWith("request.headers.")) {
|
|
538
583
|
const headerName = path2.substring("request.headers.".length).toLowerCase();
|
|
539
|
-
return this.requestHeaders[headerName] || "";
|
|
584
|
+
return HeaderManager.firstValue(this.requestHeaders[headerName]) || "";
|
|
540
585
|
}
|
|
541
586
|
if (path2 === "response.code") return this.responseStatus;
|
|
542
587
|
if (path2 === "response.status") return this.responseStatus;
|
|
543
588
|
if (path2 === "response.status_code") return this.responseStatus;
|
|
544
589
|
if (path2 === "response.code_details") return this.responseStatusText;
|
|
545
590
|
if (path2 === "response.content_type") {
|
|
546
|
-
return this.responseHeaders["content-type"] || "";
|
|
591
|
+
return HeaderManager.firstValue(this.responseHeaders["content-type"]) || "";
|
|
547
592
|
}
|
|
548
593
|
if (path2.startsWith("response.headers.")) {
|
|
549
594
|
const headerName = path2.substring("response.headers.".length).toLowerCase();
|
|
550
|
-
return this.responseHeaders[headerName] || "";
|
|
595
|
+
return HeaderManager.firstValue(this.responseHeaders[headerName]) || "";
|
|
551
596
|
}
|
|
552
597
|
return void 0;
|
|
553
598
|
}
|
|
@@ -902,7 +947,8 @@ var HostFunctions = class {
|
|
|
902
947
|
return call;
|
|
903
948
|
}
|
|
904
949
|
setHttpCallResponse(tokenId, headers, body) {
|
|
905
|
-
|
|
950
|
+
const tuples = Array.isArray(headers) ? headers : HeaderManager.recordToTuples(headers);
|
|
951
|
+
this.httpCallResponse = { tokenId, headers: tuples, body };
|
|
906
952
|
}
|
|
907
953
|
clearHttpCallResponse() {
|
|
908
954
|
this.httpCallResponse = null;
|
|
@@ -1332,7 +1378,7 @@ var HostFunctions = class {
|
|
|
1332
1378
|
return this.responseHeaders;
|
|
1333
1379
|
}
|
|
1334
1380
|
if (mapType === 6 /* HttpCallResponseHeaders */ || mapType === 7 /* HttpCallResponseTrailers */) {
|
|
1335
|
-
return
|
|
1381
|
+
return this.httpCallResponse?.headers ?? [];
|
|
1336
1382
|
}
|
|
1337
1383
|
return this.requestHeaders;
|
|
1338
1384
|
}
|
|
@@ -1992,7 +2038,7 @@ var ProxyWasmRunner = class {
|
|
|
1992
2038
|
const local = this.hostFunctions.getLocalResponse();
|
|
1993
2039
|
const responseHeaders2 = results.onRequestHeaders.output.response.headers;
|
|
1994
2040
|
this.hostFunctions.resetLocalResponse();
|
|
1995
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
2041
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
1996
2042
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
1997
2043
|
return {
|
|
1998
2044
|
hookResults: results,
|
|
@@ -2035,7 +2081,7 @@ var ProxyWasmRunner = class {
|
|
|
2035
2081
|
const local = this.hostFunctions.getLocalResponse();
|
|
2036
2082
|
const responseHeaders2 = results.onRequestBody.output.response.headers;
|
|
2037
2083
|
this.hostFunctions.resetLocalResponse();
|
|
2038
|
-
const contentType2 = responseHeaders2["content-type"] || "text/plain";
|
|
2084
|
+
const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
|
|
2039
2085
|
const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
|
|
2040
2086
|
return {
|
|
2041
2087
|
hookResults: results,
|
|
@@ -2066,7 +2112,7 @@ var ProxyWasmRunner = class {
|
|
|
2066
2112
|
try {
|
|
2067
2113
|
if (isBuiltIn) {
|
|
2068
2114
|
this.logDebug("Using built-in responder");
|
|
2069
|
-
const rawStatus = (modifiedRequestHeaders["x-debugger-status"] || "").trim();
|
|
2115
|
+
const rawStatus = (HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-status"]) || "").trim();
|
|
2070
2116
|
if (rawStatus === "") {
|
|
2071
2117
|
responseStatus = 200;
|
|
2072
2118
|
} else {
|
|
@@ -2077,7 +2123,7 @@ var ProxyWasmRunner = class {
|
|
|
2077
2123
|
}
|
|
2078
2124
|
}
|
|
2079
2125
|
responseStatusText = responseStatus === 200 ? "OK" : String(responseStatus);
|
|
2080
|
-
const responseContentMode = modifiedRequestHeaders["x-debugger-content"] || "";
|
|
2126
|
+
const responseContentMode = HeaderManager.firstValue(modifiedRequestHeaders["x-debugger-content"]) || "";
|
|
2081
2127
|
delete modifiedRequestHeaders["x-debugger-status"];
|
|
2082
2128
|
delete modifiedRequestHeaders["x-debugger-content"];
|
|
2083
2129
|
if (responseContentMode === "status-only") {
|
|
@@ -2085,14 +2131,14 @@ var ProxyWasmRunner = class {
|
|
|
2085
2131
|
contentType = "text/plain";
|
|
2086
2132
|
} else if (responseContentMode === "body-only") {
|
|
2087
2133
|
responseBody = modifiedRequestBody || "";
|
|
2088
|
-
contentType = modifiedRequestHeaders["content-type"] || "text/plain";
|
|
2134
|
+
contentType = HeaderManager.firstValue(modifiedRequestHeaders["content-type"]) || "text/plain";
|
|
2089
2135
|
} else {
|
|
2090
2136
|
contentType = "application/json";
|
|
2091
2137
|
responseBody = JSON.stringify({
|
|
2092
2138
|
method: requestMethod,
|
|
2093
2139
|
reqHeaders: modifiedRequestHeaders,
|
|
2094
2140
|
reqBody: modifiedRequestBody || "",
|
|
2095
|
-
requestUrl: BUILTIN_URL
|
|
2141
|
+
requestUrl: propertiesAfterRequestBody["request.url"] || BUILTIN_URL
|
|
2096
2142
|
});
|
|
2097
2143
|
}
|
|
2098
2144
|
responseHeaders = {
|
|
@@ -2103,27 +2149,30 @@ var ProxyWasmRunner = class {
|
|
|
2103
2149
|
`Built-in responder: ${responseStatus} ${responseStatusText}, mode=${responseContentMode || "full"}`
|
|
2104
2150
|
);
|
|
2105
2151
|
} else {
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2108
|
-
const modifiedPath = propertiesAfterRequestBody["request.path"] || "/";
|
|
2109
|
-
const modifiedQuery = propertiesAfterRequestBody["request.query"] || "";
|
|
2110
|
-
const actualTargetUrl = `${modifiedScheme}://${modifiedHost}${modifiedPath}${modifiedQuery ? "?" + modifiedQuery : ""}`;
|
|
2152
|
+
const actualTargetUrl = propertiesAfterRequestBody["request.url"] || targetUrl;
|
|
2153
|
+
const actualScheme = new URL(actualTargetUrl).protocol.replace(":", "");
|
|
2111
2154
|
this.logDebug(`Original URL: ${targetUrl}`);
|
|
2112
|
-
this.logDebug(`
|
|
2155
|
+
this.logDebug(`Effective URL: ${actualTargetUrl}`);
|
|
2113
2156
|
this.logDebug(`Fetching ${requestMethod} ${actualTargetUrl}`);
|
|
2114
|
-
const fetchHeaders =
|
|
2115
|
-
|
|
2116
|
-
|
|
2157
|
+
const fetchHeaders = HeaderManager.flattenToMap(
|
|
2158
|
+
modifiedRequestHeaders
|
|
2159
|
+
);
|
|
2160
|
+
for (const key of Object.keys(fetchHeaders)) {
|
|
2161
|
+
if (key.startsWith(":")) {
|
|
2162
|
+
delete fetchHeaders[key];
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2117
2165
|
const hostHeader = Object.entries(modifiedRequestHeaders).find(
|
|
2118
2166
|
([key]) => key.toLowerCase() === "host"
|
|
2119
2167
|
);
|
|
2120
2168
|
if (hostHeader) {
|
|
2121
|
-
|
|
2122
|
-
|
|
2169
|
+
const hostValue = HeaderManager.firstValue(hostHeader[1]) ?? "";
|
|
2170
|
+
fetchHeaders["x-forwarded-host"] = hostValue;
|
|
2171
|
+
this.logDebug(`Adding x-forwarded-host: ${hostValue}`);
|
|
2123
2172
|
}
|
|
2124
|
-
fetchHeaders["x-forwarded-proto"] =
|
|
2125
|
-
this.logDebug(`Adding x-forwarded-proto: ${
|
|
2126
|
-
fetchHeaders["x-forwarded-port"] =
|
|
2173
|
+
fetchHeaders["x-forwarded-proto"] = actualScheme;
|
|
2174
|
+
this.logDebug(`Adding x-forwarded-proto: ${actualScheme}`);
|
|
2175
|
+
fetchHeaders["x-forwarded-port"] = actualScheme === "https" ? "443" : "80";
|
|
2127
2176
|
this.logDebug(
|
|
2128
2177
|
`Adding x-forwarded-port: ${fetchHeaders["x-forwarded-port"]}`
|
|
2129
2178
|
);
|
|
@@ -2143,8 +2192,14 @@ var ProxyWasmRunner = class {
|
|
|
2143
2192
|
const response = await fetch(actualTargetUrl, fetchOptions);
|
|
2144
2193
|
responseHeaders = {};
|
|
2145
2194
|
response.headers.forEach((value, key) => {
|
|
2146
|
-
|
|
2195
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
2196
|
+
responseHeaders[key] = value;
|
|
2197
|
+
}
|
|
2147
2198
|
});
|
|
2199
|
+
const setCookies = response.headers.getSetCookie();
|
|
2200
|
+
if (setCookies.length > 0) {
|
|
2201
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
2202
|
+
}
|
|
2148
2203
|
contentType = response.headers.get("content-type") || "text/plain";
|
|
2149
2204
|
responseStatus = response.status;
|
|
2150
2205
|
responseStatusText = response.statusText;
|
|
@@ -2221,7 +2276,7 @@ var ProxyWasmRunner = class {
|
|
|
2221
2276
|
const finalBody = results.onResponseBody.output.response.body;
|
|
2222
2277
|
this.logDebug(`Final response body length: ${finalBody.length}`);
|
|
2223
2278
|
const calculatedProperties = this.propertyResolver.getCalculatedProperties();
|
|
2224
|
-
const finalContentType = finalHeaders["content-type"] || contentType;
|
|
2279
|
+
const finalContentType = HeaderManager.firstValue(finalHeaders["content-type"]) || contentType;
|
|
2225
2280
|
return {
|
|
2226
2281
|
hookResults: results,
|
|
2227
2282
|
finalResponse: {
|
|
@@ -2314,13 +2369,13 @@ var ProxyWasmRunner = class {
|
|
|
2314
2369
|
this.hostFunctions.setLogLevel(0);
|
|
2315
2370
|
const requestHeaders = HeaderManager.normalize(call.request.headers ?? {});
|
|
2316
2371
|
const responseHeaders = HeaderManager.normalize(
|
|
2317
|
-
call.response
|
|
2372
|
+
call.response?.headers ?? {}
|
|
2318
2373
|
);
|
|
2319
2374
|
const requestBody = call.request.body ?? "";
|
|
2320
|
-
const responseBody = call.response
|
|
2375
|
+
const responseBody = call.response?.body ?? "";
|
|
2321
2376
|
const requestMethod = call.request.method ?? "GET";
|
|
2322
|
-
const responseStatus = call.response
|
|
2323
|
-
const responseStatusText = call.response
|
|
2377
|
+
const responseStatus = call.response?.status ?? 200;
|
|
2378
|
+
const responseStatusText = call.response?.statusText ?? "OK";
|
|
2324
2379
|
this.propertyResolver.setProperties({ ...call.properties ?? {} });
|
|
2325
2380
|
this.propertyResolver.setRequestMetadata(
|
|
2326
2381
|
requestHeaders,
|
|
@@ -2407,7 +2462,7 @@ var ProxyWasmRunner = class {
|
|
|
2407
2462
|
for (const [k, v] of Object.entries(pending.headers)) {
|
|
2408
2463
|
if (!k.startsWith(":")) fetchHeaders[k] = v;
|
|
2409
2464
|
}
|
|
2410
|
-
let responseHeaders2 =
|
|
2465
|
+
let responseHeaders2 = [];
|
|
2411
2466
|
let responseBody2 = new Uint8Array(0);
|
|
2412
2467
|
try {
|
|
2413
2468
|
const resp = await fetch(url, {
|
|
@@ -2417,20 +2472,23 @@ var ProxyWasmRunner = class {
|
|
|
2417
2472
|
signal: AbortSignal.timeout(pending.timeoutMs)
|
|
2418
2473
|
});
|
|
2419
2474
|
resp.headers.forEach((v, k) => {
|
|
2420
|
-
responseHeaders2[k
|
|
2475
|
+
if (k.toLowerCase() !== "set-cookie") responseHeaders2.push([k, v]);
|
|
2421
2476
|
});
|
|
2477
|
+
for (const cookie of resp.headers.getSetCookie()) {
|
|
2478
|
+
responseHeaders2.push(["set-cookie", cookie]);
|
|
2479
|
+
}
|
|
2422
2480
|
responseBody2 = new Uint8Array(await resp.arrayBuffer());
|
|
2423
2481
|
this.logDebug(
|
|
2424
|
-
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${
|
|
2482
|
+
`http_call response: ${resp.status} ${resp.statusText} numHeaders=${responseHeaders2.length} bodySize=${responseBody2.byteLength}`
|
|
2425
2483
|
);
|
|
2426
2484
|
} catch (err) {
|
|
2427
2485
|
const errMsg = `http_call failed for ${url}: ${String(err)}`;
|
|
2428
2486
|
this.logDebug(errMsg);
|
|
2429
2487
|
this.logs.push({ level: 3, message: `[host] ${errMsg}` });
|
|
2430
|
-
responseHeaders2 =
|
|
2488
|
+
responseHeaders2 = [];
|
|
2431
2489
|
responseBody2 = new Uint8Array(0);
|
|
2432
2490
|
}
|
|
2433
|
-
const numHeaders =
|
|
2491
|
+
const numHeaders = responseHeaders2.length;
|
|
2434
2492
|
const bodySize = responseBody2.byteLength;
|
|
2435
2493
|
this.hostFunctions.setHttpCallResponse(pending.tokenId, responseHeaders2, responseBody2);
|
|
2436
2494
|
this.hostFunctions.resetStreamClosed();
|
|
@@ -2532,11 +2590,18 @@ var ProxyWasmRunner = class {
|
|
|
2532
2590
|
this.isInitializing = false;
|
|
2533
2591
|
}
|
|
2534
2592
|
buildHookInvocation(hook, requestHeaders, responseHeaders, requestBody, responseBody) {
|
|
2593
|
+
const countEntries = (h) => {
|
|
2594
|
+
let n = 0;
|
|
2595
|
+
for (const v of Object.values(h)) {
|
|
2596
|
+
n += Array.isArray(v) ? v.length : 1;
|
|
2597
|
+
}
|
|
2598
|
+
return n;
|
|
2599
|
+
};
|
|
2535
2600
|
switch (hook) {
|
|
2536
2601
|
case "onRequestHeaders":
|
|
2537
2602
|
return {
|
|
2538
2603
|
exportName: "proxy_on_request_headers",
|
|
2539
|
-
args: [this.currentContextId,
|
|
2604
|
+
args: [this.currentContextId, countEntries(requestHeaders), 0]
|
|
2540
2605
|
};
|
|
2541
2606
|
case "onRequestBody":
|
|
2542
2607
|
return {
|
|
@@ -2546,7 +2611,7 @@ var ProxyWasmRunner = class {
|
|
|
2546
2611
|
case "onResponseHeaders":
|
|
2547
2612
|
return {
|
|
2548
2613
|
exportName: "proxy_on_response_headers",
|
|
2549
|
-
args: [this.currentContextId,
|
|
2614
|
+
args: [this.currentContextId, countEntries(responseHeaders), 0]
|
|
2550
2615
|
};
|
|
2551
2616
|
case "onResponseBody":
|
|
2552
2617
|
return {
|
|
@@ -2661,9 +2726,12 @@ var ProxyWasmRunner = class {
|
|
|
2661
2726
|
console.warn(entry.message);
|
|
2662
2727
|
}
|
|
2663
2728
|
/**
|
|
2664
|
-
* Interface-compliant callFullFlow method
|
|
2729
|
+
* Interface-compliant callFullFlow method.
|
|
2730
|
+
*
|
|
2731
|
+
* The upstream response is generated at runtime by a real HTTP fetch
|
|
2732
|
+
* against `url` or by the built-in responder when `url === "built-in"`.
|
|
2665
2733
|
*/
|
|
2666
|
-
async callFullFlow(url, method, headers, body,
|
|
2734
|
+
async callFullFlow(url, method, headers, body, properties, enforceProductionPropertyRules) {
|
|
2667
2735
|
const call = {
|
|
2668
2736
|
hook: "",
|
|
2669
2737
|
// Not used in fullFlow
|
|
@@ -2672,12 +2740,6 @@ var ProxyWasmRunner = class {
|
|
|
2672
2740
|
body,
|
|
2673
2741
|
method
|
|
2674
2742
|
},
|
|
2675
|
-
response: {
|
|
2676
|
-
headers: responseHeaders,
|
|
2677
|
-
body: responseBody,
|
|
2678
|
-
status: responseStatus,
|
|
2679
|
-
statusText: responseStatusText
|
|
2680
|
-
},
|
|
2681
2743
|
properties,
|
|
2682
2744
|
enforceProductionPropertyRules
|
|
2683
2745
|
};
|
|
@@ -2861,6 +2923,8 @@ var HttpWasmRunner = class {
|
|
|
2861
2923
|
this.stateManager = null;
|
|
2862
2924
|
this.dotenvEnabled = true;
|
|
2863
2925
|
this.dotenvPath = null;
|
|
2926
|
+
/** Pinned ports bypass PortManager allocation and must not be released back to it. */
|
|
2927
|
+
this.isPinnedPort = false;
|
|
2864
2928
|
/** @deprecated Legacy sync support — remove when #[fastedge::http] is retired */
|
|
2865
2929
|
this.isLegacySync = false;
|
|
2866
2930
|
this.portManager = portManager;
|
|
@@ -2887,7 +2951,19 @@ var HttpWasmRunner = class {
|
|
|
2887
2951
|
this.tempWasmPath = wasmPath;
|
|
2888
2952
|
}
|
|
2889
2953
|
this.isLegacySync = await isLegacySyncWasm(bufferOrPath);
|
|
2890
|
-
|
|
2954
|
+
if (config?.httpPort !== void 0) {
|
|
2955
|
+
const pinned = config.httpPort;
|
|
2956
|
+
if (!await this.portManager.isPortFree(pinned)) {
|
|
2957
|
+
throw new Error(
|
|
2958
|
+
`fastedge-run port ${pinned} is not available \u2014 release it or choose a different httpPort in fastedge-config.test.json`
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
this.port = pinned;
|
|
2962
|
+
this.isPinnedPort = true;
|
|
2963
|
+
} else {
|
|
2964
|
+
this.port = await this.portManager.allocate();
|
|
2965
|
+
this.isPinnedPort = false;
|
|
2966
|
+
}
|
|
2891
2967
|
const wasi_http = !this.isLegacySync;
|
|
2892
2968
|
const args = [
|
|
2893
2969
|
"http",
|
|
@@ -2919,7 +2995,13 @@ var HttpWasmRunner = class {
|
|
|
2919
2995
|
await this.waitForServerReady(this.port, timeout);
|
|
2920
2996
|
}
|
|
2921
2997
|
/**
|
|
2922
|
-
* Execute an HTTP request through the WASM module
|
|
2998
|
+
* Execute an HTTP request through the WASM module.
|
|
2999
|
+
*
|
|
3000
|
+
* Redirects are surfaced verbatim — `fetch` is called with
|
|
3001
|
+
* `redirect: "manual"` so 3xx responses (status + `Location`) reach the
|
|
3002
|
+
* caller intact. This matches FastEdge edge behaviour, which returns
|
|
3003
|
+
* redirects to the client rather than following them server-side. See
|
|
3004
|
+
* `IWasmRunner.execute` for the public contract.
|
|
2923
3005
|
*/
|
|
2924
3006
|
async execute(request) {
|
|
2925
3007
|
if (!this.port || !this.process) {
|
|
@@ -2932,8 +3014,12 @@ var HttpWasmRunner = class {
|
|
|
2932
3014
|
method: request.method,
|
|
2933
3015
|
headers: request.headers,
|
|
2934
3016
|
body: request.body || void 0,
|
|
2935
|
-
signal: AbortSignal.timeout(3e4)
|
|
3017
|
+
signal: AbortSignal.timeout(3e4),
|
|
2936
3018
|
// 30 second timeout
|
|
3019
|
+
// Surface 3xx responses verbatim so tests can assert on status/Location.
|
|
3020
|
+
// A FastEdge edge returns redirects to the client rather than following
|
|
3021
|
+
// them server-side; production parity requires the same here.
|
|
3022
|
+
redirect: "manual"
|
|
2937
3023
|
});
|
|
2938
3024
|
const arrayBuffer = await response.arrayBuffer();
|
|
2939
3025
|
const bodyBuffer = Buffer.from(arrayBuffer);
|
|
@@ -2967,7 +3053,7 @@ var HttpWasmRunner = class {
|
|
|
2967
3053
|
/**
|
|
2968
3054
|
* Not supported for HTTP WASM (proxy-wasm only)
|
|
2969
3055
|
*/
|
|
2970
|
-
async callFullFlow(_url, _method, _headers, _body,
|
|
3056
|
+
async callFullFlow(_url, _method, _headers, _body, _properties, _enforceProductionPropertyRules) {
|
|
2971
3057
|
throw new Error(
|
|
2972
3058
|
"callFullFlow() is not supported for HTTP WASM. Use execute() instead."
|
|
2973
3059
|
);
|
|
@@ -3024,8 +3110,11 @@ var HttpWasmRunner = class {
|
|
|
3024
3110
|
this.process = null;
|
|
3025
3111
|
}
|
|
3026
3112
|
if (this.port !== null) {
|
|
3027
|
-
|
|
3113
|
+
if (!this.isPinnedPort) {
|
|
3114
|
+
this.portManager.release(this.port);
|
|
3115
|
+
}
|
|
3028
3116
|
this.port = null;
|
|
3117
|
+
this.isPinnedPort = false;
|
|
3029
3118
|
}
|
|
3030
3119
|
if (this.tempWasmPath) {
|
|
3031
3120
|
await removeTempWasmFile(this.tempWasmPath);
|
|
@@ -3216,17 +3305,23 @@ ${recentLogs || "(no logs)"}`
|
|
|
3216
3305
|
];
|
|
3217
3306
|
return binaryTypes.some((type) => contentType.toLowerCase().includes(type));
|
|
3218
3307
|
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Parse headers from fetch Headers object
|
|
3221
|
-
*/
|
|
3222
3308
|
parseHeaders(headers) {
|
|
3223
|
-
|
|
3224
|
-
headers.forEach((value, key) => {
|
|
3225
|
-
result[key] = value;
|
|
3226
|
-
});
|
|
3227
|
-
return result;
|
|
3309
|
+
return parseFetchHeaders(headers);
|
|
3228
3310
|
}
|
|
3229
3311
|
};
|
|
3312
|
+
function parseFetchHeaders(headers) {
|
|
3313
|
+
const result = {};
|
|
3314
|
+
headers.forEach((value, key) => {
|
|
3315
|
+
if (key.toLowerCase() !== "set-cookie") {
|
|
3316
|
+
result[key] = value;
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
const setCookies = headers.getSetCookie();
|
|
3320
|
+
if (setCookies.length > 0) {
|
|
3321
|
+
result["set-cookie"] = setCookies;
|
|
3322
|
+
}
|
|
3323
|
+
return result;
|
|
3324
|
+
}
|
|
3230
3325
|
|
|
3231
3326
|
// server/runner/PortManager.ts
|
|
3232
3327
|
var import_net = require("net");
|
|
@@ -3242,6 +3337,9 @@ var PortManager = class {
|
|
|
3242
3337
|
* This is necessary when multiple server processes run simultaneously —
|
|
3243
3338
|
* each has its own PortManager with independent in-memory state, so
|
|
3244
3339
|
* in-memory tracking alone is not enough to prevent cross-process conflicts.
|
|
3340
|
+
*
|
|
3341
|
+
* Public so pinned-port callers (HttpWasmRunner with RunnerConfig.httpPort)
|
|
3342
|
+
* can reuse the same OS-level check without going through allocate().
|
|
3245
3343
|
*/
|
|
3246
3344
|
isPortFree(port) {
|
|
3247
3345
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./runner/index.js";
|