@gcoredev/fastedge-test 0.2.1 → 0.2.3

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.
@@ -34,8 +34,43 @@ function resolveAppRoot(startPath) {
34
34
  return dir;
35
35
  }
36
36
 
37
- process.env.WORKSPACE_PATH = resolveAppRoot(
38
- process.argv[2] ? resolve(process.argv[2]) : process.cwd()
39
- );
37
+ // Parse `--project-dir <path>` / `--project-dir=<path>` and strip it from argv
38
+ // before the server import, so the server's own arg handling doesn't see it.
39
+ // When set, it overrides the positional fallback for resolveAppRoot — useful
40
+ // when running from a nested sandbox (e.g. `cd fastedge-test && npm run debug`
41
+ // with the project root one directory up).
42
+ function extractProjectDirFlag(argv) {
43
+ for (let i = 2; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ if (a === "--project-dir" || a === "-C") {
46
+ const value = argv[i + 1];
47
+ if (!value) {
48
+ console.error(`Error: ${a} requires a path argument.`);
49
+ process.exit(2);
50
+ }
51
+ argv.splice(i, 2);
52
+ return value;
53
+ }
54
+ const eq = a.startsWith("--project-dir=")
55
+ ? a.slice("--project-dir=".length)
56
+ : a.startsWith("-C=")
57
+ ? a.slice("-C=".length)
58
+ : null;
59
+ if (eq !== null) {
60
+ argv.splice(i, 1);
61
+ return eq;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ const projectDirFlag = extractProjectDirFlag(process.argv);
68
+ const startPath = projectDirFlag
69
+ ? resolve(projectDirFlag)
70
+ : process.argv[2]
71
+ ? resolve(process.argv[2])
72
+ : process.cwd();
73
+
74
+ process.env.WORKSPACE_PATH = resolveAppRoot(startPath);
40
75
 
41
76
  import("../dist/server.js");
@@ -1 +1 @@
1
- {"fastedge_run_version": "v0.16.5"}
1
+ {"fastedge_run_version": "v0.16.7"}
Binary file
@@ -927,7 +927,13 @@ var HostFunctions = class {
927
927
  pendingHttpCall = null;
928
928
  httpCallResponse = null;
929
929
  streamClosed = false;
930
- // Local response state (from proxy_send_local_response / send_http_response)
930
+ // Local response state (from proxy_send_local_response / send_http_response).
931
+ // `headers` carries the additional headers passed as the 4th argument of
932
+ // `send_http_response`; ProxyWasmRunner merges these into finalResponse at
933
+ // the short-circuit points so they reach the caller alongside any headers
934
+ // accumulated via stream_context.headers.response.add() in the same hook.
935
+ // Stored as tuples to preserve order and duplicate-name semantics (e.g. for
936
+ // multi-value Set-Cookie additions).
931
937
  localResponse = null;
932
938
  // FastEdge extensions
933
939
  secretStore;
@@ -1277,12 +1283,13 @@ var HostFunctions = class {
1277
1283
  );
1278
1284
  const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
1279
1285
  const body = this.memory.readBytes(bodyPtr, bodyLen);
1286
+ let headers = [];
1280
1287
  if (headerPairsLen > 0) {
1281
1288
  const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
1282
- const headers = HeaderManager.deserializeBinary(headerBytes);
1283
- this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
1289
+ headers = HeaderManager.deserializeBinaryToTuples(headerBytes);
1290
+ this.logDebug(`send_local_response headers: ${JSON.stringify(headers)}`);
1284
1291
  }
1285
- this.localResponse = { statusCode, statusText, body };
1292
+ this.localResponse = { statusCode, statusText, body, headers };
1286
1293
  this.logs.push({
1287
1294
  level: 1,
1288
1295
  message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
@@ -2076,14 +2083,15 @@ var ProxyWasmRunner = class {
2076
2083
  const local = this.hostFunctions.getLocalResponse();
2077
2084
  const responseHeaders2 = results.onRequestHeaders.output.response.headers;
2078
2085
  this.hostFunctions.resetLocalResponse();
2079
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
2086
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2087
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2080
2088
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2081
2089
  return {
2082
2090
  hookResults: results,
2083
2091
  finalResponse: {
2084
2092
  status: local.statusCode,
2085
2093
  statusText: local.statusText,
2086
- headers: responseHeaders2,
2094
+ headers: mergedHeaders,
2087
2095
  body,
2088
2096
  contentType: contentType2,
2089
2097
  isBase64: isBase642
@@ -2119,14 +2127,15 @@ var ProxyWasmRunner = class {
2119
2127
  const local = this.hostFunctions.getLocalResponse();
2120
2128
  const responseHeaders2 = results.onRequestBody.output.response.headers;
2121
2129
  this.hostFunctions.resetLocalResponse();
2122
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
2130
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2131
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2123
2132
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2124
2133
  return {
2125
2134
  hookResults: results,
2126
2135
  finalResponse: {
2127
2136
  status: local.statusCode,
2128
2137
  statusText: local.statusText,
2129
- headers: responseHeaders2,
2138
+ headers: mergedHeaders,
2130
2139
  body,
2131
2140
  contentType: contentType2,
2132
2141
  isBase64: isBase642
@@ -2296,16 +2305,17 @@ var ProxyWasmRunner = class {
2296
2305
  }
2297
2306
  if (results.onResponseHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2298
2307
  const local = this.hostFunctions.getLocalResponse();
2299
- const headers = results.onResponseHeaders.output.response.headers;
2308
+ const responseHeaders2 = results.onResponseHeaders.output.response.headers;
2300
2309
  this.hostFunctions.resetLocalResponse();
2301
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2310
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2311
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2302
2312
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2303
2313
  return {
2304
2314
  hookResults: results,
2305
2315
  finalResponse: {
2306
2316
  status: local.statusCode,
2307
2317
  statusText: local.statusText,
2308
- headers,
2318
+ headers: mergedHeaders,
2309
2319
  body,
2310
2320
  contentType: contentType2,
2311
2321
  isBase64: isBase642
@@ -2339,16 +2349,17 @@ var ProxyWasmRunner = class {
2339
2349
  }
2340
2350
  if (results.onResponseBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2341
2351
  const local = this.hostFunctions.getLocalResponse();
2342
- const headers = results.onResponseBody.output.response.headers;
2352
+ const responseHeaders2 = results.onResponseBody.output.response.headers;
2343
2353
  this.hostFunctions.resetLocalResponse();
2344
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2354
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2355
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2345
2356
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2346
2357
  return {
2347
2358
  hookResults: results,
2348
2359
  finalResponse: {
2349
2360
  status: local.statusCode,
2350
2361
  statusText: local.statusText,
2351
- headers,
2362
+ headers: mergedHeaders,
2352
2363
  body,
2353
2364
  contentType: contentType2,
2354
2365
  isBase64: isBase642
@@ -2723,7 +2734,9 @@ var ProxyWasmRunner = class {
2723
2734
  }
2724
2735
  }
2725
2736
  createImports() {
2726
- const wasi = new import_node_wasi.WASI({ version: "preview1", env: this.dictionary.getAll() });
2737
+ const dictEnv = this.dictionary.getAll();
2738
+ const wasiEnv = Object.keys(dictEnv).length === 0 ? { __FASTEDGE_RUNNER__: "1" } : dictEnv;
2739
+ const wasi = new import_node_wasi.WASI({ version: "preview1", env: wasiEnv });
2727
2740
  const wasiImport = wasi.wasiImport;
2728
2741
  return {
2729
2742
  env: this.hostFunctions.createImports(),
@@ -2897,19 +2910,37 @@ function getCliBinaryName() {
2897
2910
  throw new Error(`Unsupported platform: ${import_os.default.platform()}`);
2898
2911
  }
2899
2912
  }
2900
- function getBundledCliPaths() {
2913
+ function getPackageRoot(startDir = _currentDir) {
2914
+ let dir = startDir;
2915
+ while (true) {
2916
+ const pkgPath = (0, import_path2.join)(dir, "package.json");
2917
+ if ((0, import_fs.existsSync)(pkgPath)) {
2918
+ try {
2919
+ const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf8"));
2920
+ if (pkg.name === "@gcoredev/fastedge-test") return dir;
2921
+ } catch {
2922
+ }
2923
+ }
2924
+ const parent = (0, import_path2.dirname)(dir);
2925
+ if (parent === dir) return null;
2926
+ dir = parent;
2927
+ }
2928
+ }
2929
+ function getBundledCliPaths(startDir = _currentDir) {
2901
2930
  const binaryName = getCliBinaryName();
2902
- return [
2903
- // Installed npm package: dist/lib/index.js → dist/fastedge-cli/
2904
- (0, import_path2.join)(_currentDir, "..", "fastedge-cli", binaryName),
2905
- // Production: bundled server at dist/server.js → dist/fastedge-cli/
2906
- (0, import_path2.join)(_currentDir, "fastedge-cli", binaryName),
2907
- // Development/Tests: running from source
2908
- // _currentDir might be server/utils/, so go up to project root
2909
- (0, import_path2.join)(_currentDir, "..", "..", "fastedge-run", binaryName),
2910
- // Alternative: if _currentDir is already at project root
2911
- (0, import_path2.join)(_currentDir, "fastedge-run", binaryName)
2912
- ];
2931
+ const candidates = [];
2932
+ const root = getPackageRoot(startDir);
2933
+ if (root) {
2934
+ candidates.push(
2935
+ (0, import_path2.join)(root, "dist", "fastedge-cli", binaryName),
2936
+ (0, import_path2.join)(root, "fastedge-run", binaryName)
2937
+ );
2938
+ }
2939
+ candidates.push(
2940
+ (0, import_path2.join)(startDir, "fastedge-cli", binaryName),
2941
+ (0, import_path2.join)(startDir, "..", "fastedge-cli", binaryName)
2942
+ );
2943
+ return candidates;
2913
2944
  }
2914
2945
  function ensureExecutable(binaryPath) {
2915
2946
  if (process.platform !== "win32") {
@@ -2947,7 +2978,7 @@ async function findFastEdgeRunCli() {
2947
2978
  } catch (error) {
2948
2979
  }
2949
2980
  throw new Error(
2950
- "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled binary in fastedge-cli/ (project root)\n 3. System PATH\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH environment variable, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Place the binary in fastedge-cli/ at project root (platform-specific filename)"
2981
+ "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled inside the @gcoredev/fastedge-test package (dist/fastedge-cli/<binary> when installed, fastedge-run/<binary> in the source tree)\n 3. System PATH (which/where fastedge-run)\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH to a fastedge-run binary you have locally, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Reinstall @gcoredev/fastedge-test to restore the bundled binary (or, when developing this repo, place the platform binary in fastedge-run/)"
2951
2982
  );
2952
2983
  }
2953
2984
 
package/dist/lib/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
2
+
1
3
  // server/runner/ProxyWasmRunner.ts
2
4
  import { WASI } from "node:wasi";
3
5
 
@@ -883,7 +885,13 @@ var HostFunctions = class {
883
885
  pendingHttpCall = null;
884
886
  httpCallResponse = null;
885
887
  streamClosed = false;
886
- // Local response state (from proxy_send_local_response / send_http_response)
888
+ // Local response state (from proxy_send_local_response / send_http_response).
889
+ // `headers` carries the additional headers passed as the 4th argument of
890
+ // `send_http_response`; ProxyWasmRunner merges these into finalResponse at
891
+ // the short-circuit points so they reach the caller alongside any headers
892
+ // accumulated via stream_context.headers.response.add() in the same hook.
893
+ // Stored as tuples to preserve order and duplicate-name semantics (e.g. for
894
+ // multi-value Set-Cookie additions).
887
895
  localResponse = null;
888
896
  // FastEdge extensions
889
897
  secretStore;
@@ -1233,12 +1241,13 @@ var HostFunctions = class {
1233
1241
  );
1234
1242
  const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
1235
1243
  const body = this.memory.readBytes(bodyPtr, bodyLen);
1244
+ let headers = [];
1236
1245
  if (headerPairsLen > 0) {
1237
1246
  const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
1238
- const headers = HeaderManager.deserializeBinary(headerBytes);
1239
- this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
1247
+ headers = HeaderManager.deserializeBinaryToTuples(headerBytes);
1248
+ this.logDebug(`send_local_response headers: ${JSON.stringify(headers)}`);
1240
1249
  }
1241
- this.localResponse = { statusCode, statusText, body };
1250
+ this.localResponse = { statusCode, statusText, body, headers };
1242
1251
  this.logs.push({
1243
1252
  level: 1,
1244
1253
  message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
@@ -2032,14 +2041,15 @@ var ProxyWasmRunner = class {
2032
2041
  const local = this.hostFunctions.getLocalResponse();
2033
2042
  const responseHeaders2 = results.onRequestHeaders.output.response.headers;
2034
2043
  this.hostFunctions.resetLocalResponse();
2035
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
2044
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2045
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2036
2046
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2037
2047
  return {
2038
2048
  hookResults: results,
2039
2049
  finalResponse: {
2040
2050
  status: local.statusCode,
2041
2051
  statusText: local.statusText,
2042
- headers: responseHeaders2,
2052
+ headers: mergedHeaders,
2043
2053
  body,
2044
2054
  contentType: contentType2,
2045
2055
  isBase64: isBase642
@@ -2075,14 +2085,15 @@ var ProxyWasmRunner = class {
2075
2085
  const local = this.hostFunctions.getLocalResponse();
2076
2086
  const responseHeaders2 = results.onRequestBody.output.response.headers;
2077
2087
  this.hostFunctions.resetLocalResponse();
2078
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
2088
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2089
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2079
2090
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2080
2091
  return {
2081
2092
  hookResults: results,
2082
2093
  finalResponse: {
2083
2094
  status: local.statusCode,
2084
2095
  statusText: local.statusText,
2085
- headers: responseHeaders2,
2096
+ headers: mergedHeaders,
2086
2097
  body,
2087
2098
  contentType: contentType2,
2088
2099
  isBase64: isBase642
@@ -2252,16 +2263,17 @@ var ProxyWasmRunner = class {
2252
2263
  }
2253
2264
  if (results.onResponseHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2254
2265
  const local = this.hostFunctions.getLocalResponse();
2255
- const headers = results.onResponseHeaders.output.response.headers;
2266
+ const responseHeaders2 = results.onResponseHeaders.output.response.headers;
2256
2267
  this.hostFunctions.resetLocalResponse();
2257
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2268
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2269
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2258
2270
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2259
2271
  return {
2260
2272
  hookResults: results,
2261
2273
  finalResponse: {
2262
2274
  status: local.statusCode,
2263
2275
  statusText: local.statusText,
2264
- headers,
2276
+ headers: mergedHeaders,
2265
2277
  body,
2266
2278
  contentType: contentType2,
2267
2279
  isBase64: isBase642
@@ -2295,16 +2307,17 @@ var ProxyWasmRunner = class {
2295
2307
  }
2296
2308
  if (results.onResponseBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
2297
2309
  const local = this.hostFunctions.getLocalResponse();
2298
- const headers = results.onResponseBody.output.response.headers;
2310
+ const responseHeaders2 = results.onResponseBody.output.response.headers;
2299
2311
  this.hostFunctions.resetLocalResponse();
2300
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
2312
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
2313
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
2301
2314
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
2302
2315
  return {
2303
2316
  hookResults: results,
2304
2317
  finalResponse: {
2305
2318
  status: local.statusCode,
2306
2319
  statusText: local.statusText,
2307
- headers,
2320
+ headers: mergedHeaders,
2308
2321
  body,
2309
2322
  contentType: contentType2,
2310
2323
  isBase64: isBase642
@@ -2679,7 +2692,9 @@ var ProxyWasmRunner = class {
2679
2692
  }
2680
2693
  }
2681
2694
  createImports() {
2682
- const wasi = new WASI({ version: "preview1", env: this.dictionary.getAll() });
2695
+ const dictEnv = this.dictionary.getAll();
2696
+ const wasiEnv = Object.keys(dictEnv).length === 0 ? { __FASTEDGE_RUNNER__: "1" } : dictEnv;
2697
+ const wasi = new WASI({ version: "preview1", env: wasiEnv });
2683
2698
  const wasiImport = wasi.wasiImport;
2684
2699
  return {
2685
2700
  env: this.hostFunctions.createImports(),
@@ -2836,7 +2851,7 @@ import { spawn, execSync as execSync2 } from "child_process";
2836
2851
 
2837
2852
  // server/utils/fastedge-cli.ts
2838
2853
  import { execSync } from "child_process";
2839
- import { existsSync, chmodSync } from "fs";
2854
+ import { existsSync, chmodSync, readFileSync } from "fs";
2840
2855
  import { join, dirname } from "path";
2841
2856
  import { fileURLToPath } from "url";
2842
2857
  import os from "os";
@@ -2853,19 +2868,37 @@ function getCliBinaryName() {
2853
2868
  throw new Error(`Unsupported platform: ${os.platform()}`);
2854
2869
  }
2855
2870
  }
2856
- function getBundledCliPaths() {
2871
+ function getPackageRoot(startDir = _currentDir) {
2872
+ let dir = startDir;
2873
+ while (true) {
2874
+ const pkgPath = join(dir, "package.json");
2875
+ if (existsSync(pkgPath)) {
2876
+ try {
2877
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
2878
+ if (pkg.name === "@gcoredev/fastedge-test") return dir;
2879
+ } catch {
2880
+ }
2881
+ }
2882
+ const parent = dirname(dir);
2883
+ if (parent === dir) return null;
2884
+ dir = parent;
2885
+ }
2886
+ }
2887
+ function getBundledCliPaths(startDir = _currentDir) {
2857
2888
  const binaryName = getCliBinaryName();
2858
- return [
2859
- // Installed npm package: dist/lib/index.js → dist/fastedge-cli/
2860
- join(_currentDir, "..", "fastedge-cli", binaryName),
2861
- // Production: bundled server at dist/server.js → dist/fastedge-cli/
2862
- join(_currentDir, "fastedge-cli", binaryName),
2863
- // Development/Tests: running from source
2864
- // _currentDir might be server/utils/, so go up to project root
2865
- join(_currentDir, "..", "..", "fastedge-run", binaryName),
2866
- // Alternative: if _currentDir is already at project root
2867
- join(_currentDir, "fastedge-run", binaryName)
2868
- ];
2889
+ const candidates = [];
2890
+ const root = getPackageRoot(startDir);
2891
+ if (root) {
2892
+ candidates.push(
2893
+ join(root, "dist", "fastedge-cli", binaryName),
2894
+ join(root, "fastedge-run", binaryName)
2895
+ );
2896
+ }
2897
+ candidates.push(
2898
+ join(startDir, "fastedge-cli", binaryName),
2899
+ join(startDir, "..", "fastedge-cli", binaryName)
2900
+ );
2901
+ return candidates;
2869
2902
  }
2870
2903
  function ensureExecutable(binaryPath) {
2871
2904
  if (process.platform !== "win32") {
@@ -2903,7 +2936,7 @@ async function findFastEdgeRunCli() {
2903
2936
  } catch (error) {
2904
2937
  }
2905
2938
  throw new Error(
2906
- "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled binary in fastedge-cli/ (project root)\n 3. System PATH\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH environment variable, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Place the binary in fastedge-cli/ at project root (platform-specific filename)"
2939
+ "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled inside the @gcoredev/fastedge-test package (dist/fastedge-cli/<binary> when installed, fastedge-run/<binary> in the source tree)\n 3. System PATH (which/where fastedge-run)\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH to a fastedge-run binary you have locally, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Reinstall @gcoredev/fastedge-test to restore the bundled binary (or, when developing this repo, place the platform binary in fastedge-run/)"
2907
2940
  );
2908
2941
  }
2909
2942
 
@@ -59,6 +59,7 @@ export declare class HostFunctions {
59
59
  statusCode: number;
60
60
  statusText: string;
61
61
  body: Uint8Array;
62
+ headers: HeaderTuples;
62
63
  } | null;
63
64
  resetLocalResponse(): void;
64
65
  getRequestHeaders(): HeaderRecord;
@@ -19324,7 +19324,13 @@ var HostFunctions = class {
19324
19324
  pendingHttpCall = null;
19325
19325
  httpCallResponse = null;
19326
19326
  streamClosed = false;
19327
- // Local response state (from proxy_send_local_response / send_http_response)
19327
+ // Local response state (from proxy_send_local_response / send_http_response).
19328
+ // `headers` carries the additional headers passed as the 4th argument of
19329
+ // `send_http_response`; ProxyWasmRunner merges these into finalResponse at
19330
+ // the short-circuit points so they reach the caller alongside any headers
19331
+ // accumulated via stream_context.headers.response.add() in the same hook.
19332
+ // Stored as tuples to preserve order and duplicate-name semantics (e.g. for
19333
+ // multi-value Set-Cookie additions).
19328
19334
  localResponse = null;
19329
19335
  // FastEdge extensions
19330
19336
  secretStore;
@@ -19674,12 +19680,13 @@ var HostFunctions = class {
19674
19680
  );
19675
19681
  const statusText = this.memory.readString(statusCodePtr, statusCodeLen);
19676
19682
  const body = this.memory.readBytes(bodyPtr, bodyLen);
19683
+ let headers = [];
19677
19684
  if (headerPairsLen > 0) {
19678
19685
  const headerBytes = this.memory.readBytes(headerPairsPtr, headerPairsLen);
19679
- const headers = HeaderManager.deserializeBinary(headerBytes);
19680
- this.logDebug(`send_local_response headers (not merged): ${JSON.stringify(headers)}`);
19686
+ headers = HeaderManager.deserializeBinaryToTuples(headerBytes);
19687
+ this.logDebug(`send_local_response headers: ${JSON.stringify(headers)}`);
19681
19688
  }
19682
- this.localResponse = { statusCode, statusText, body };
19689
+ this.localResponse = { statusCode, statusText, body, headers };
19683
19690
  this.logs.push({
19684
19691
  level: 1,
19685
19692
  message: `local_response status=${statusCode} ${statusText} bodyLen=${body.byteLength} grpc=${grpcStatus}`
@@ -20473,14 +20480,15 @@ var ProxyWasmRunner = class {
20473
20480
  const local = this.hostFunctions.getLocalResponse();
20474
20481
  const responseHeaders2 = results.onRequestHeaders.output.response.headers;
20475
20482
  this.hostFunctions.resetLocalResponse();
20476
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
20483
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
20484
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
20477
20485
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
20478
20486
  return {
20479
20487
  hookResults: results,
20480
20488
  finalResponse: {
20481
20489
  status: local.statusCode,
20482
20490
  statusText: local.statusText,
20483
- headers: responseHeaders2,
20491
+ headers: mergedHeaders,
20484
20492
  body,
20485
20493
  contentType: contentType2,
20486
20494
  isBase64: isBase642
@@ -20516,14 +20524,15 @@ var ProxyWasmRunner = class {
20516
20524
  const local = this.hostFunctions.getLocalResponse();
20517
20525
  const responseHeaders2 = results.onRequestBody.output.response.headers;
20518
20526
  this.hostFunctions.resetLocalResponse();
20519
- const contentType2 = HeaderManager.firstValue(responseHeaders2["content-type"]) || "text/plain";
20527
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
20528
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
20520
20529
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
20521
20530
  return {
20522
20531
  hookResults: results,
20523
20532
  finalResponse: {
20524
20533
  status: local.statusCode,
20525
20534
  statusText: local.statusText,
20526
- headers: responseHeaders2,
20535
+ headers: mergedHeaders,
20527
20536
  body,
20528
20537
  contentType: contentType2,
20529
20538
  isBase64: isBase642
@@ -20693,16 +20702,17 @@ var ProxyWasmRunner = class {
20693
20702
  }
20694
20703
  if (results.onResponseHeaders.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
20695
20704
  const local = this.hostFunctions.getLocalResponse();
20696
- const headers = results.onResponseHeaders.output.response.headers;
20705
+ const responseHeaders2 = results.onResponseHeaders.output.response.headers;
20697
20706
  this.hostFunctions.resetLocalResponse();
20698
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
20707
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
20708
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
20699
20709
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
20700
20710
  return {
20701
20711
  hookResults: results,
20702
20712
  finalResponse: {
20703
20713
  status: local.statusCode,
20704
20714
  statusText: local.statusText,
20705
- headers,
20715
+ headers: mergedHeaders,
20706
20716
  body,
20707
20717
  contentType: contentType2,
20708
20718
  isBase64: isBase642
@@ -20736,16 +20746,17 @@ var ProxyWasmRunner = class {
20736
20746
  }
20737
20747
  if (results.onResponseBody.returnCode === 1 && this.hostFunctions.hasLocalResponse()) {
20738
20748
  const local = this.hostFunctions.getLocalResponse();
20739
- const headers = results.onResponseBody.output.response.headers;
20749
+ const responseHeaders2 = results.onResponseBody.output.response.headers;
20740
20750
  this.hostFunctions.resetLocalResponse();
20741
- const contentType2 = HeaderManager.firstValue(headers["content-type"]) || "text/plain";
20751
+ const mergedHeaders = local.headers.length > 0 ? HeaderManager.appendMerge(responseHeaders2, HeaderManager.tuplesToRecord(local.headers)) : responseHeaders2;
20752
+ const contentType2 = HeaderManager.firstValue(mergedHeaders["content-type"]) || "text/plain";
20742
20753
  const { body, isBase64: isBase642 } = encodeLocalResponseBody(local.body, contentType2);
20743
20754
  return {
20744
20755
  hookResults: results,
20745
20756
  finalResponse: {
20746
20757
  status: local.statusCode,
20747
20758
  statusText: local.statusText,
20748
- headers,
20759
+ headers: mergedHeaders,
20749
20760
  body,
20750
20761
  contentType: contentType2,
20751
20762
  isBase64: isBase642
@@ -21120,7 +21131,9 @@ var ProxyWasmRunner = class {
21120
21131
  }
21121
21132
  }
21122
21133
  createImports() {
21123
- const wasi = new import_node_wasi.WASI({ version: "preview1", env: this.dictionary.getAll() });
21134
+ const dictEnv = this.dictionary.getAll();
21135
+ const wasiEnv = Object.keys(dictEnv).length === 0 ? { __FASTEDGE_RUNNER__: "1" } : dictEnv;
21136
+ const wasi = new import_node_wasi.WASI({ version: "preview1", env: wasiEnv });
21124
21137
  const wasiImport = wasi.wasiImport;
21125
21138
  return {
21126
21139
  env: this.hostFunctions.createImports(),
@@ -21294,19 +21307,37 @@ function getCliBinaryName() {
21294
21307
  throw new Error(`Unsupported platform: ${import_os.default.platform()}`);
21295
21308
  }
21296
21309
  }
21297
- function getBundledCliPaths() {
21310
+ function getPackageRoot(startDir = _currentDir) {
21311
+ let dir = startDir;
21312
+ while (true) {
21313
+ const pkgPath = (0, import_path2.join)(dir, "package.json");
21314
+ if ((0, import_fs.existsSync)(pkgPath)) {
21315
+ try {
21316
+ const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf8"));
21317
+ if (pkg.name === "@gcoredev/fastedge-test") return dir;
21318
+ } catch {
21319
+ }
21320
+ }
21321
+ const parent = (0, import_path2.dirname)(dir);
21322
+ if (parent === dir) return null;
21323
+ dir = parent;
21324
+ }
21325
+ }
21326
+ function getBundledCliPaths(startDir = _currentDir) {
21298
21327
  const binaryName = getCliBinaryName();
21299
- return [
21300
- // Installed npm package: dist/lib/index.js → dist/fastedge-cli/
21301
- (0, import_path2.join)(_currentDir, "..", "fastedge-cli", binaryName),
21302
- // Production: bundled server at dist/server.js → dist/fastedge-cli/
21303
- (0, import_path2.join)(_currentDir, "fastedge-cli", binaryName),
21304
- // Development/Tests: running from source
21305
- // _currentDir might be server/utils/, so go up to project root
21306
- (0, import_path2.join)(_currentDir, "..", "..", "fastedge-run", binaryName),
21307
- // Alternative: if _currentDir is already at project root
21308
- (0, import_path2.join)(_currentDir, "fastedge-run", binaryName)
21309
- ];
21328
+ const candidates = [];
21329
+ const root = getPackageRoot(startDir);
21330
+ if (root) {
21331
+ candidates.push(
21332
+ (0, import_path2.join)(root, "dist", "fastedge-cli", binaryName),
21333
+ (0, import_path2.join)(root, "fastedge-run", binaryName)
21334
+ );
21335
+ }
21336
+ candidates.push(
21337
+ (0, import_path2.join)(startDir, "fastedge-cli", binaryName),
21338
+ (0, import_path2.join)(startDir, "..", "fastedge-cli", binaryName)
21339
+ );
21340
+ return candidates;
21310
21341
  }
21311
21342
  function ensureExecutable(binaryPath) {
21312
21343
  if (process.platform !== "win32") {
@@ -21344,7 +21375,7 @@ async function findFastEdgeRunCli() {
21344
21375
  } catch (error) {
21345
21376
  }
21346
21377
  throw new Error(
21347
- "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled binary in fastedge-cli/ (project root)\n 3. System PATH\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH environment variable, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Place the binary in fastedge-cli/ at project root (platform-specific filename)"
21378
+ "fastedge-run CLI not found in any of these locations:\n 1. FASTEDGE_RUN_PATH environment variable\n 2. Bundled inside the @gcoredev/fastedge-test package (dist/fastedge-cli/<binary> when installed, fastedge-run/<binary> in the source tree)\n 3. System PATH (which/where fastedge-run)\n\nTo fix this:\n - Set FASTEDGE_RUN_PATH to a fastedge-run binary you have locally, or\n - Install fastedge-run in PATH: cargo install fastedge-run, or\n - Reinstall @gcoredev/fastedge-test to restore the bundled binary (or, when developing this repo, place the platform binary in fastedge-run/)"
21348
21379
  );
21349
21380
  }
21350
21381