Skip to content

Latest commit

 

History

History
1010 lines (809 loc) · 29 KB

File metadata and controls

1010 lines (809 loc) · 29 KB

REST API Reference

The @gcoredev/fastedge-test debugger server exposes a REST API for loading WASM modules, executing requests, and managing test configuration.

Note on header values. Response-side and hook-result headers use Record<string, string | string[]> — single-valued headers are a string, multi-valued headers (notably Set-Cookie per RFC 6265) are a string[]. Request-side header inputs are single-valued Record<string, string>.

Base URL

http://localhost:5179

The port can be overridden via the PORT environment variable. The active port is written to .fastedge-debug/.debug-port (relative to WORKSPACE_PATH if set, otherwise the current working directory) on startup and deleted on shutdown. The file contains PORT:SHA256_HASH, where PORT is the decimal port number and SHA256_HASH is the hex-encoded SHA-256 of the session token. Consumers that only need the port should parse the prefix up to the first colon (e.g. parseInt(content.split(":")[0], 10)).

Authentication

The debugger server requires a session token on all /api/* requests. /health is the only unauthenticated endpoint.

When started from the CLI (npx fastedge-debug), the server generates a random 32-byte hex token and prints the full browser URL to stderr:

Open: http://localhost:5179/#token=<hex>

The fragment after #token= is your session token. Copy it for use in API calls or WebSocket connections.

HTTP requests — send the token as a header on every /api/* call:

x-fastedge-token: <token>

WebSocket — two mechanisms exist, tried in preference order:

  1. Sec-WebSocket-Protocol: fastedge-token.<token> (preferred) — the token is embedded in the subprotocol list rather than the URL, keeping it out of proxy and server access logs. The server echoes the subprotocol back on accept.
  2. ?token=<token> query parameter (fallback) — for legacy or non-browser tooling that cannot set subprotocols on the WebSocket handshake. Avoid this form in new consumers: the token appears in server and proxy logs.
const ws = new WebSocket(
  `ws://127.0.0.1:5179/ws`,
  [`fastedge-token.${token}`],
);
ws://127.0.0.1:<port>/ws?token=<token>

Environment variables related to authentication and binding:

Variable Default Description
FASTEDGE_DEBUG_TOKEN unset Inject a known token instead of generating one. When set, the Open: URL is not printed to stderr (the VSCode extension uses this path).
FASTEDGE_BIND_HOST 127.0.0.1 Interface the HTTP server binds to.
FASTEDGE_EXPECTED_HOST unset Extra hostname (suffix match) allowed in Host / Origin headers — for Codespaces forwarded URLs.
WORKSPACE_PATH process.cwd() Workspace root; affects .env resolution, port file, and config file placement.

Open question — token file for local tooling: Should the server also write the session token to .fastedge-debug/.debug-token (mode 0600, same trust boundary as .env) alongside .fastedge-debug/.debug-port, so local tooling can find the token without parsing stderr? Decision pending from repo owner — document the answer here when made. If yes, implement in writePortFile and update this section; if no, note that tooling must capture the Open: stderr line.


Common Headers

X-Source Header

The POST /api/execute, POST /api/send, and POST /api/config endpoints accept an optional X-Source request header that tags the origin of the operation in WebSocket broadcast events.

Value Description
ui Request originated from the web UI (default if omitted)
ai_agent Request originated from an AI agent
api Request originated from direct API usage
system Request originated from an automated system
X-Source: ai_agent

Health

GET /health

Returns the server status and service identity.

Response

{
  status: "ok";
  service: "fastedge-debugger";
}

Example

curl http://localhost:5179/health
{
  "status": "ok",
  "service": "fastedge-debugger"
}

GET /api/client-count

Returns the number of currently connected WebSocket clients. Useful in CI tooling to wait until the UI has connected before proceeding.

Response

{
  count: number;
}

Example

curl -H "x-fastedge-token: <token>" http://localhost:5179/api/client-count
{
  "count": 1
}

WASM Loading

POST /api/load

Loads a WASM binary into the runner. Accepts either a file path or a base64-encoded binary. Automatically detects whether the module is HTTP-WASM or Proxy-WASM.

Request Body

Exactly one of wasmPath or wasmBase64 must be provided; providing both is an error.

{
  wasmPath?: string;    // Absolute path to a .wasm file on the server filesystem
  wasmBase64?: string;  // Base64-encoded WASM binary; mutually exclusive with wasmPath
  dotenv?: {
    enabled?: boolean;  // Whether to load .env files for this module
    path?: string;      // Directory containing .env files (defaults to server CWD)
  };
  httpPort?: number;    // HTTP-WASM only. Pin the runner subprocess to this port (1024–65535).
                        // Load fails immediately if the port is already in use.
                        // Ignored for proxy-wasm modules.
}

Response

{
  ok: true;
  wasmType: "http-wasm" | "proxy-wasm";
  resolvedPath?: string; // Absolute path used when wasmPath was provided
}

Example — load from path

curl -X POST http://localhost:5179/api/load \
  -H "Content-Type: application/json" \
  -d '{
    "wasmPath": "/home/user/project/build/module.wasm",
    "dotenv": { "enabled": true }
  }'
{
  "ok": true,
  "wasmType": "proxy-wasm",
  "resolvedPath": "/home/user/project/build/module.wasm"
}

Example — load from base64

curl -X POST http://localhost:5179/api/load \
  -H "Content-Type: application/json" \
  -d '{
    "wasmBase64": "AGFzbQEAAAA...",
    "dotenv": { "enabled": false }
  }'
{
  "ok": true,
  "wasmType": "http-wasm"
}

Example — pin HTTP-WASM to a specific port

curl -X POST http://localhost:5179/api/load \
  -H "Content-Type: application/json" \
  -d '{
    "wasmPath": "/home/user/project/build/app.wasm",
    "httpPort": 8100
  }'
{
  "ok": true,
  "wasmType": "http-wasm",
  "resolvedPath": "/home/user/project/build/app.wasm"
}

Error Responses

Status Condition
400 Validation failed, missing both wasmPath and wasmBase64, invalid path, or path does not end in .wasm
400 httpPort is specified and already in use (HTTP-WASM only)
500 WASM load failed or runner initialization error

PATCH /api/dotenv

Applies updated dotenv settings to the currently loaded WASM module without reloading the binary. For Proxy-WASM, this resets stores and reloads dotenv files in-place. For HTTP-WASM, this restarts the underlying process with updated flags.

Requires a WASM module to already be loaded via POST /api/load.

Request Body

{
  dotenv: {
    enabled: boolean; // Whether dotenv loading should be enabled
    path?: string;    // Directory containing .env files (defaults to server CWD)
  };
}

Response

{
  ok: true;
}

Example

curl -X PATCH http://localhost:5179/api/dotenv \
  -H "Content-Type: application/json" \
  -d '{
    "dotenv": { "enabled": true, "path": "/home/user/project" }
  }'
{
  "ok": true
}

Error Responses

Status Condition
400 dotenv.enabled is not a boolean, or no WASM module is loaded
500 Failed to apply dotenv settings

Test Execution

POST /api/execute

Executes a request through the loaded WASM module. Behavior differs based on the detected runner type. This endpoint does not use schema validation — fields are read directly from the request body.

Requires a WASM module to be loaded via POST /api/load. Accepts an optional X-Source request header.

Request Body

For HTTP-WASM, provide either path (preferred) or url (legacy). When path is given, it is used directly as the request path (e.g. /api/hello?q=1). When only url is given, the path and query string are extracted from it.

{
  path?: string;                     // Request path and query string (preferred)
  url?: string;                      // Full URL — path and query extracted (legacy fallback)
  method?: string;                   // HTTP method (default: "GET")
  headers?: Record<string, string>;  // Request headers (default: {})
  body?: string;                     // Request body (default: "")
}

For Proxy-WASM, the top-level url field is required. The full CDN flow is controlled via nested request and properties fields. The upstream response is generated at runtime — either by a real fetch against url, or by the built-in responder when url === "built-in":

{
  url: string;                          // Request URL, or "built-in" (required)
  request?: {
    method?: string;                    // HTTP method (default: "GET")
    headers?: Record<string, string>;   // Request headers (default: {})
    body?: string;                      // Request body (default: "")
  };
  properties?: Record<string, unknown>; // CDN properties (default: {})
}

Response — HTTP-WASM

{
  ok: true;
  result: {
    status: number;
    statusText: string;
    headers: Record<string, string | string[]>;
    body: string;
    contentType: string | null;
    isBase64?: boolean;
    logs: Array<{ level: number; message: string }>;
  };
}

Response — Proxy-WASM

{
  ok: true;
  hookResults: Record<string, HookResult>;
  finalResponse: {
    status: number;
    statusText: string;
    headers: Record<string, string | string[]>;
    body: string;
    contentType: string;
    isBase64?: boolean;
  };
  calculatedProperties?: Record<string, unknown>;
}

Where HookResult is:

type HookResult = {
  returnCode: number | null;
  logs: Array<{ level: number; message: string }>;
  input: {
    request: { headers: Record<string, string | string[]>; body: string };
    response: { headers: Record<string, string | string[]>; body: string };
    properties?: Record<string, unknown>;
  };
  output: {
    request: { headers: Record<string, string | string[]>; body: string };
    response: { headers: Record<string, string | string[]>; body: string };
    properties?: Record<string, unknown>;
  };
  properties: Record<string, unknown>;
};

hookResults is keyed by hook name (e.g. "onRequestHeaders", "onResponseHeaders"). calculatedProperties is present only when the runner derives request-derived properties; read PropertyResolver.getCalculatedProperties() in the runner source for the current list of derived request.* keys.

Example — HTTP-WASM

curl -X POST http://localhost:5179/api/execute \
  -H "Content-Type: application/json" \
  -H "X-Source: api" \
  -d '{
    "path": "/api/data?format=json",
    "method": "GET",
    "headers": { "accept": "application/json" }
  }'
{
  "ok": true,
  "result": {
    "status": 200,
    "statusText": "OK",
    "headers": { "content-type": "application/json" },
    "body": "{\"hello\":\"world\"}",
    "contentType": "application/json",
    "isBase64": false,
    "logs": [
      { "level": 2, "message": "request received" }
    ]
  }
}

Example — Proxy-WASM

curl -X POST http://localhost:5179/api/execute \
  -H "Content-Type: application/json" \
  -H "X-Source: api" \
  -d '{
    "url": "https://example.com/page",
    "request": {
      "method": "GET",
      "headers": { "host": "example.com" },
      "body": ""
    },
    "properties": {}
  }'
{
  "ok": true,
  "hookResults": {
    "onRequestHeaders": {
      "returnCode": 0,
      "logs": [{ "level": 2, "message": "onRequestHeaders called" }],
      "input": {
        "request": { "headers": { "host": "example.com" }, "body": "" },
        "response": { "headers": {}, "body": "" },
        "properties": {}
      },
      "output": {
        "request": { "headers": { "host": "example.com", "x-added": "1" }, "body": "" },
        "response": { "headers": {}, "body": "" }
      },
      "properties": {}
    }
  },
  "finalResponse": {
    "status": 200,
    "statusText": "OK",
    "headers": { "content-type": "text/html" },
    "body": "<html/>",
    "contentType": "text/html"
  },
  "calculatedProperties": {
    "request.url": "https://example.com/page",
    "request.host": "example.com",
    "request.path": "/page"
  }
}

Error Responses

Status Condition
400 No WASM module loaded, or missing path/url for HTTP-WASM, or missing url for Proxy-WASM
500 Execution failed

POST /api/call

Calls a specific Proxy-WASM CDN hook directly. Only valid for Proxy-WASM modules.

Requires a WASM module to be loaded via POST /api/load.

Request Body

{
  hook: "onRequestHeaders" | "onRequestBody" | "onResponseHeaders" | "onResponseBody";
  request?: {
    headers: Record<string, string>;
    body: string;
  };
  response?: {
    headers: Record<string, string>;
    body: string;
  };
  properties: Record<string, unknown>; // Required; use {} if none
}

request and response default to { headers: {}, body: "" } if omitted.

Response

{
  ok: true;
  result: HookResult;
}

Where HookResult is:

type HookResult = {
  returnCode: number | null;
  logs: Array<{ level: number; message: string }>;
  input: {
    request: { headers: Record<string, string | string[]>; body: string };
    response: { headers: Record<string, string | string[]>; body: string };
    properties?: Record<string, unknown>;
  };
  output: {
    request: { headers: Record<string, string | string[]>; body: string };
    response: { headers: Record<string, string | string[]>; body: string };
    properties?: Record<string, unknown>;
  };
  properties: Record<string, unknown>;
};

Example

curl -X POST http://localhost:5179/api/call \
  -H "Content-Type: application/json" \
  -d '{
    "hook": "onRequestHeaders",
    "request": {
      "headers": { "host": "example.com", "user-agent": "curl/8.0" },
      "body": ""
    },
    "response": {
      "headers": {},
      "body": ""
    },
    "properties": {
      "client.geo.country": "US"
    }
  }'
{
  "ok": true,
  "result": {
    "returnCode": 0,
    "logs": [
      { "level": 2, "message": "processing request headers" }
    ],
    "input": {
      "request": {
        "headers": { "host": "example.com", "user-agent": "curl/8.0" },
        "body": ""
      },
      "response": { "headers": {}, "body": "" },
      "properties": { "client.geo.country": "US" }
    },
    "output": {
      "request": {
        "headers": { "host": "example.com", "user-agent": "curl/8.0", "x-country": "US" },
        "body": ""
      },
      "response": { "headers": {}, "body": "" }
    },
    "properties": { "client.geo.country": "US" }
  }
}

Error Responses

Status Condition
400 Validation failed (invalid hook name, missing properties), or no WASM module loaded
500 Hook execution failed

POST /api/send

Executes the full Proxy-WASM CDN request/response flow. Equivalent to POST /api/execute for Proxy-WASM, but uses stricter Zod schema validation on the request body. Only valid for Proxy-WASM modules.

Requires a WASM module to be loaded via POST /api/load. Accepts an optional X-Source request header.

Request Body

{
  url: string | "built-in";            // Full request URL, or "built-in" to use the built-in responder
  request?: {
    method?: string;                   // HTTP method (default: "GET")
    url?: string;
    headers?: Record<string, string>;  // Request headers (default: {})
    body?: string;                     // Request body (default: "")
  };
  properties: Record<string, unknown>; // CDN properties (required; use {} if none)
}

The upstream response is generated at runtime — either by a real fetch against url, or by the built-in responder when url === "built-in".

Response

{
  ok: true;
  hookResults: Record<string, HookResult>;
  finalResponse: {
    status: number;
    statusText: string;
    headers: Record<string, string | string[]>;
    body: string;
    contentType: string;
    isBase64?: boolean;
  };
  calculatedProperties?: Record<string, unknown>;
}

HookResult has the same shape as documented in POST /api/call. hookResults is keyed by hook name. calculatedProperties is optional; keys follow the request.* pattern derived by PropertyResolver.getCalculatedProperties().

Example

curl -X POST http://localhost:5179/api/send \
  -H "Content-Type: application/json" \
  -H "X-Source: ai_agent" \
  -d '{
    "url": "https://example.com/api/resource",
    "request": {
      "method": "POST",
      "headers": { "content-type": "application/json" },
      "body": "{\"key\":\"value\"}"
    },
    "properties": {
      "client.geo.country": "DE"
    }
  }'
{
  "ok": true,
  "hookResults": {
    "onRequestHeaders": {
      "returnCode": 0,
      "logs": [],
      "input": {
        "request": { "headers": { "content-type": "application/json" }, "body": "" },
        "response": { "headers": {}, "body": "" },
        "properties": { "client.geo.country": "DE" }
      },
      "output": {
        "request": { "headers": { "content-type": "application/json" }, "body": "" },
        "response": { "headers": {}, "body": "" }
      },
      "properties": { "client.geo.country": "DE" }
    },
    "onResponseHeaders": {
      "returnCode": 0,
      "logs": [],
      "input": {
        "request": { "headers": { "content-type": "application/json" }, "body": "" },
        "response": { "headers": { "content-type": "application/json" }, "body": "" },
        "properties": { "client.geo.country": "DE" }
      },
      "output": {
        "request": { "headers": { "content-type": "application/json" }, "body": "" },
        "response": { "headers": { "content-type": "application/json" }, "body": "" }
      },
      "properties": { "client.geo.country": "DE" }
    }
  },
  "finalResponse": {
    "status": 200,
    "statusText": "OK",
    "headers": { "content-type": "application/json" },
    "body": "{\"result\":\"ok\"}",
    "contentType": "application/json"
  },
  "calculatedProperties": {
    "request.url": "https://example.com/api/resource",
    "request.host": "example.com",
    "request.path": "/api/resource"
  }
}

Error Responses

Status Condition
400 Validation failed (missing url or properties), or no WASM module loaded
500 Execution failed

Configuration

GET /api/config

Reads fastedge-config.test.json from the .fastedge-debug/ directory (relative to WORKSPACE_PATH if set, otherwise the current working directory) and returns it along with a validation result.

Response

{
  ok: true;
  config: TestConfig;
  valid: boolean;
  validationErrors?: {
    formErrors: string[];
    fieldErrors: Record<string, string[]>;
  };
}

TestConfig is a discriminated union on appType:

// Proxy-WASM config (appType defaults to "proxy-wasm")
type ProxyWasmConfig = {
  $schema?: string;
  description?: string;
  appType: "proxy-wasm";
  wasm?: { path: string; description?: string };
  request: {
    method: string;
    url: string;
    headers: Record<string, string>;
    body: string;
  };
  properties: Record<string, unknown>;
  dotenv?: { enabled?: boolean; path?: string };
};

// HTTP-WASM config
type HttpWasmConfig = {
  $schema?: string;
  description?: string;
  appType: "http-wasm";
  wasm?: { path: string; description?: string };
  httpPort?: number; // Pin the runner subprocess to this port (1024–65535)
  request: {
    method: string;
    path: string;
    headers: Record<string, string>;
    body: string;
  };
  properties: Record<string, unknown>;
  dotenv?: { enabled?: boolean; path?: string };
};

type TestConfig = ProxyWasmConfig | HttpWasmConfig;

Example

curl http://localhost:5179/api/config
{
  "ok": true,
  "config": {
    "$schema": "http://localhost:5179/api/schema/fastedge-config.test",
    "appType": "proxy-wasm",
    "request": {
      "method": "GET",
      "url": "https://example.com/",
      "headers": {},
      "body": ""
    },
    "properties": {}
  },
  "valid": true
}

Error Responses

Status Condition
404 fastedge-config.test.json does not exist

POST /api/config

Saves the provided configuration to .fastedge-debug/fastedge-config.test.json (relative to WORKSPACE_PATH if set, otherwise the current working directory). If the config includes a properties field, a WebSocket event is broadcast to connected clients.

Accepts an optional X-Source request header.

Request Body

{
  config: TestConfig; // See GET /api/config for the TestConfig type
}

The config object must match one of the two TestConfig variants. properties and appType are required in both variants; request is required and its shape depends on appType (path for "http-wasm", url for "proxy-wasm").

Response

{
  ok: true;
}

Example

curl -X POST http://localhost:5179/api/config \
  -H "Content-Type: application/json" \
  -H "X-Source: api" \
  -d '{
    "config": {
      "$schema": "http://localhost:5179/api/schema/fastedge-config.test",
      "appType": "proxy-wasm",
      "request": {
        "method": "GET",
        "url": "https://example.com/",
        "headers": { "accept": "text/html" },
        "body": ""
      },
      "properties": {
        "client.geo.country": "US"
      }
    }
  }'
{
  "ok": true
}

Error Responses

Status Condition
400 Validation failed (missing config.appType, config.request, or config.properties)
500 File write failed

POST /api/config/save-as

Saves the provided configuration to a file path previously vended by the (Electron-only) save dialog. The path must have been registered by /api/config/show-save-dialog first; unregistered paths are rejected. The path is single-use — it is removed from the pending set once consumed. Creates intermediate directories as needed.

Request Body

{
  config: object;    // The configuration object to serialize as JSON
  filePath: string;  // A path previously returned by the save dialog
}

Response

{
  ok: true;
  savedPath: string; // The file path where the config was written
}

Example

curl -X POST http://localhost:5179/api/config/save-as \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "appType": "proxy-wasm",
      "request": {
        "method": "GET",
        "url": "https://example.com/",
        "headers": {},
        "body": ""
      },
      "properties": {}
    },
    "filePath": "/home/user/project/configs/staging.test.json"
  }'
{
  "ok": true,
  "savedPath": "/home/user/project/configs/staging.test.json"
}

Error Responses

Status Condition
400 Missing config or filePath
403 filePath was not vended by the save dialog (or was already used)
500 File write or directory creation failed

Schema

GET /api/schema/:name

Serves a JSON Schema file by name. Use these schemas for request validation in test tooling or editor integrations.

The :name parameter is the schema name without the .schema.json suffix.

Response

Returns the JSON Schema document with Content-Type: application/json.

Available Schemas

Request Schemas

Name Description
api-load Request body schema for POST /api/load
api-send Request body schema for POST /api/send
api-call Request body schema for POST /api/call
api-config Request body schema for POST /api/config

Response / Type Schemas

Name Description
fastedge-config.test Schema for fastedge-config.test.json config files
hook-result Shape of a single HookResult object
hook-call Shape of a HookCall input object
full-flow-result Shape of the FullFlowResult returned by full-flow endpoints
http-request Shape of an HttpRequest for HTTP-WASM execution
http-response Shape of an HttpResponse returned by HTTP-WASM execution

Example

curl http://localhost:5179/api/schema/api-send
curl http://localhost:5179/api/schema/fastedge-config.test

Using the schema in a config file

{
  "$schema": "http://localhost:5179/api/schema/fastedge-config.test",
  "appType": "proxy-wasm",
  "request": {
    "method": "GET",
    "url": "https://example.com/",
    "headers": {},
    "body": ""
  },
  "properties": {}
}

Error Responses

Status Condition
404 Schema name not found

Error Handling

All error responses follow a consistent shape:

{
  ok: false;
  error: string | { formErrors: string[]; fieldErrors: Record<string, string[]> };
}

When a request body fails schema validation (Zod), error is the flattened Zod error object with formErrors and fieldErrors. For runtime errors, error is a plain string.

Common status codes

Status Meaning
400 Invalid request body, missing required fields, or precondition not met (e.g. no WASM loaded)
403 Host/token check failed, or a save-as path was not vended by the save dialog
404 Resource not found (config file, schema file)
500 Internal server error during execution or I/O

See Also