Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The SDK has two layers:

### FastEdge-specific layer (`assembly/fastedge/`)

- **`dictionary.ts`** — `getEnv(name)`: reads environment variables via `proxy_dictionary_get`
- **`dictionary.ts`** — `getEnv(name)`: reads environment variables via WASI `process.env` (64 KB limit). `getDictionary(name)`: reads via `proxy_dictionary_get` (no size limit, use for values > 64 KB).
- **`env.ts`** — `getEnvVar(name)`: deprecated wrapper around `process.env` (use `getEnv` instead)
- **`secrets.ts`** — `getSecret(name)`, `getSecretEffectiveAt(name, slot)`: reads secrets via `proxy_get_secret` / `proxy_get_effective_at_secret`. Also exports `getSecretVar(name)` and `getSecretVarEffectiveAt(name, slot)` as deprecated aliases.
- **`kvStore.ts`** — `KvStore` class: `open(storeName)`, `get(key)`, `scan(pattern)`, `zrangeByScore(key, min, max)`, `zscan(key, pattern)`, `bfExists(key, item)`. Also exports `ValueScoreTuple` type used by zrange/zscan results.
Expand All @@ -66,18 +66,27 @@ Host-allocated buffers are managed through `ArrayBufferReference` in `runtime.ts

## Examples

The `examples/` directory contains 8 standalone examples. Each is an independent package with its own `package.json`, `asconfig.json`, and `node_modules`. They reference the SDK via `"file:../.."` (local), not the published npm version.

| Example | Description |
| ------------- | --------------------------------------------------------------------------- |
| `body` | Request/response body read and manipulation |
| `geoBlock` | Block requests by country using a `BLACKLIST` env var |
| `geoRedirect` | Route requests to different origins by country code |
| `headers` | Add, remove, and replace HTTP headers with validation |
| `jwt` | Validate JWT Bearer tokens (requires `@gcoredev/as-jwt` dep) |
| `kvStore` | Query a KV Store — get/scan/zrange/zscan/bfExists (has `assembly/utils.ts`) |
| `logTime` | Log UTC timestamps at request and response phases |
| `properties` | Read and expose FastEdge runtime properties |
The `examples/` directory contains 17 standalone examples. Each is an independent package with its own `package.json`, `asconfig.json`, and `node_modules`. They reference the SDK via `"file:../.."` (local), not the published npm version.

| Example | Description |
| --------------------- | --------------------------------------------------------------------------- |
| `abTesting` | Cookie-based A/B traffic splitting at the CDN layer |
| `apiKey` | Validate `X-API-Key` header against a secret |
| `body` | Request/response body read and manipulation |
| `cacheControl` | Content-type-aware Cache-Control response headers |
| `cors` | CORS preflight handling and response headers |
| `customErrorPages` | Replace 4xx/5xx responses with branded HTML error pages |
| `geoBlock` | Block requests by country using a `BLACKLIST` env var |
| `geoRedirect` | Route requests to different origins by country code |
| `headers` | Add, remove, and replace HTTP headers with validation |
| `helloWorld` | Minimal CDN app skeleton — all lifecycle hooks with pass-through |
| `httpCall` | Async HTTP dispatch to an external service with callback |
| `jwt` | Validate JWT Bearer tokens (requires `@gcoredev/as-jwt` dep) |
| `kvStore` | Query a KV Store — get/scan/zrange/zscan/bfExists (has `assembly/utils.ts`) |
| `largeDictionary` | Read large env vars (> 64 KB) via `getDictionary` / dictionary API |
Comment thread
godronus marked this conversation as resolved.
| `logTime` | Log UTC timestamps at request and response phases |
| `properties` | Read and expose FastEdge runtime properties |
| `variablesAndSecrets` | Read environment variables and secrets, forward as headers |

Build output per example: `build/<name>.wasm` (release), `build/<name>-debug.wasm` (debug). The `build/` and `node_modules/` directories are gitignored per example.

Expand Down
31 changes: 28 additions & 3 deletions assembly/fastedge/dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,36 @@ import * as imports from "../imports";
import { globalArrayBufferReference, WasmResultValues } from "../runtime";

/**
* Function to get the value for the provided environment variable name.
* Reads an environment variable by name using the WASI environment interface.
*
* Use this for normal-sized environment variables (under 64 KB). For values
* that may exceed the WASI 64 KB limit, use {@link getDictionary} instead.
*
* @param {string} name - The name of the environment variable.
* @returns {string} The value of the environment variable.
* @returns {string} The value, or an empty string if not found.
*/
function getEnv(name: string): string {
const hasKey = process.env.has(name);
if (hasKey) {
return process.env.get(name);
}
return "";
}

/**
* Reads a dictionary value by name using the proxy-wasm dictionary API
* (`proxy_dictionary_get`).
*
* This bypasses the WASI 64 KB environment variable size limit and should
* be used when a value may be larger than 64 KB (e.g. large JSON configs,
* PEM certificates, policy documents).
*
* For normal-sized environment variables, prefer {@link getEnv}.
*
* @param {string} name - The dictionary key to look up.
* @returns {string} The value, or an empty string if not found.
*/
function getDictionary(name: string): string {
const buffer = String.UTF8.encode(name);
const status = imports.proxy_dictionary_get(
changetype<usize>(buffer),
Expand All @@ -24,4 +49,4 @@ function getEnv(name: string): string {
return "";
}

export { getEnv };
export { getEnv, getDictionary };
4 changes: 2 additions & 2 deletions assembly/fastedge/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function getSecret(name: string): string {
/**
* Function to get the value for the provided secret variable name from a specific slot.
* @param {string} name - The name of the secret variable.
* @param {u32} effectiveAt - The slot index of the secret. (effectiveAt >= secret_slots.slot)
* @param {u32} effectiveAt - The numeric slot index to retrieve. Slots are defined in the FastEdge UI and are always numeric (e.g. incremental integers, or Date.now()-style values to represent a point in time).
* @returns {string} The value of the secret variable.
Comment thread
godronus marked this conversation as resolved.
*/
function getSecretEffectiveAt(name: string, effectiveAt: u32): string {
Expand Down Expand Up @@ -60,7 +60,7 @@ function getSecretVar(name: string): string {
/**
* @deprecated Use {@link getSecretEffectiveAt} instead. This function will be removed in a future version.
* @param {string} name - The name of the secret variable.
* @param {u32} effectiveAt - The slot index of the secret. (effectiveAt >= secret_slots.slot)
* @param {u32} effectiveAt - The numeric slot index to retrieve. Slots are defined in the FastEdge UI and are always numeric (e.g. incremental integers, or Date.now()-style values to represent a point in time).
* @returns {string} The value of the secret variable.
Comment thread
godronus marked this conversation as resolved.
*/
function getSecretVarEffectiveAt(name: string, effectiveAt: u32): string {
Expand Down
4 changes: 2 additions & 2 deletions assembly/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ class HeaderStreamManipulator {
}

/**
* Replace a header.
* Replace a header value. No-op if the header is not present.
* @param key the header name.
* @param value the header value.
*/
Expand Down Expand Up @@ -1125,7 +1125,7 @@ export function deleteContext(context_id: u32): void {
/**
* Register a root context factory and make it available to the runtime.
* @param root_context_factory A function that creates a new root context.
* @param name Paremeter kept for backwards compatibility. It will be ignored.
* @param name Accepted for API compatibility with the proxy-wasm spec but ignored by the FastEdge runtime. The value does not need to match any configuration — pass any descriptive string.
*/
export function registerRootContext(
context_factory: (context_id: u32) => RootContext,
Expand Down
59 changes: 41 additions & 18 deletions docs/SDK_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ Returned from `onRequestHeaders` and `onResponseHeaders`.
| Value | Integer | Description |
| ------------------------------ | ------- | --------------------------------------------------------- |
| `Continue` | 0 | Pass headers downstream and continue processing. |
| `StopIteration` | 1 | Pause header processing; resume with `continueRequest()`. |
| `StopIteration` | 1 | Pause header processing. |
| `ContinueAndEndStream` | 2 | Continue processing and mark the stream as ended. |
| `StopAllIterationAndBuffer` | 3 | Stop all iteration and buffer the body. |
| `StopAllIterationAndWatermark` | 4 | Stop all iteration until the buffer watermark is reached. |
Expand Down Expand Up @@ -485,7 +485,7 @@ function set_buffer_bytes(
): WasmResultValues;
```

`get_buffer_bytes` reads a slice of the body or buffer starting at `start` for `length` bytes. Returns an empty `ArrayBuffer` (byteLength 0) on failure or when the buffer is empty.
`get_buffer_bytes` reads a slice of the body or buffer starting at `start` for `length` bytes. Returns an empty `ArrayBuffer` (`byteLength == 0`) on failure or when the buffer is empty.

`set_buffer_bytes` replaces the range `[start, start+length)` in the body or buffer with `value`. To replace the entire body, pass `start=0` and `length=body_buffer_length`.

Expand All @@ -510,7 +510,7 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV
const body = get_buffer_bytes(
BufferTypeValues.HttpRequestBody,
0,
body_buffer_length as u32
body_buffer_length as u32,
);
const text = String.UTF8.decode(body);
log(LogLevelValues.info, "Body: " + text);
Expand All @@ -520,7 +520,7 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV
BufferTypeValues.HttpRequestBody,
0,
body_buffer_length as u32,
modified
modified,
);
return FilterDataStatusValues.Continue;
}
Expand All @@ -547,7 +547,7 @@ function get_property(path: string): ArrayBuffer;
function set_property(path: string, data: ArrayBuffer): WasmResultValues;
```

`get_property` returns an empty `ArrayBuffer` (byteLength 0) when the property is not found. Check `byteLength > 0` before decoding. All properties listed below are UTF-8 encoded strings decoded with `String.UTF8.decode()`, except `response.status` which is a 2-byte big-endian unsigned 16-bit integer.
`get_property` returns an empty `ArrayBuffer` (`byteLength == 0`) when the property is not found. Check `byteLength > 0` before decoding. All properties listed below are UTF-8 encoded strings decoded with `String.UTF8.decode()`, except `response.status` which is a 2-byte big-endian unsigned 16-bit integer.

`set_property` stores a custom property value accessible in later hooks via `get_property`. It does not modify the built-in properties in the table below.

Expand Down Expand Up @@ -651,7 +651,7 @@ onRequestHeaders(headers: u32, end_of_stream: bool): FilterHeadersStatusValues {
[
makeHeaderPair("content-type", "text/plain"),
makeHeaderPair("www-authenticate", "Bearer"),
]
],
);
return FilterHeadersStatusValues.StopIteration;
}
Expand Down Expand Up @@ -688,15 +688,15 @@ class RootContext {

**Parameters:**

| Parameter | Type | Description |
| ---------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `cluster` | `string` | The upstream host to call. Must be a public host. |
| `headers` | `Headers` | Request headers. Certain headers are automatically filtered by the host (`host`, `content-length`, `transfer-encoding`). |
| `body` | `ArrayBuffer` | Request body. Pass `new ArrayBuffer(0)` for no body. |
| `trailers` | `Headers` | Request trailers. Pass `[]` if none. |
| `timeout_milliseconds` | `u32` | Request timeout in milliseconds. |
| `origin_context` | `BaseContext` | The context to pass back to the callback. Pass `this` from within a `Context`. |
| `cb` | `(origin_context: BaseContext, headers: u32, body_size: usize, trailers: u32) => void` | Callback invoked when the response is received. |
| Parameter | Type | Description |
| ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `cluster` | `string` | The upstream host to call. Must be a public host. |
| `headers` | `Headers` | Request headers. Certain headers are automatically filtered by the host (`host`, `content-length`, `transfer-encoding`). |
| `body` | `ArrayBuffer` | Request body. Pass `new ArrayBuffer(0)` for no body. |
| `trailers` | `Headers` | Request trailers. Pass `[]` if none. |
| `timeout_milliseconds` | `u32` | Request timeout in milliseconds. |
| `origin_context` | `BaseContext` | The context to pass back to the callback. Pass `this` from within a `Context`. |
| `cb` | `(origin_context: BaseContext, headers: u32, body_size: usize, trailers: u32) => void` | Callback invoked when the response is received. |

In the callback, read the response headers via `stream_context.headers.http_callback` and the body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, 0, body_size as u32)`.

Expand Down Expand Up @@ -799,7 +799,7 @@ function registerRootContext(
): void;
```

The `name` parameter is accepted for backwards compatibility but is ignored by the runtime.
The `name` parameter is accepted for API compatibility with the proxy-wasm spec but is ignored by the FastEdge runtime. The value does not need to match any configuration — pass any descriptive string.

```typescript
registerRootContext(
Expand All @@ -818,7 +818,7 @@ These APIs are specific to the FastEdge platform and are not part of the core pr

### Environment Variables (getEnv)

Read environment variables configured at deployment time via the FastEdge platform.
Read environment variables configured at deployment time via the FastEdge platform. Uses the standard WASI environment interface (subject to the 64 KB per-variable size limit).

```typescript
function getEnv(name: string): string;
Expand All @@ -836,6 +836,29 @@ if (blocklist.length == 0) {
}
```

### Dictionary (getDictionary)

Read dictionary values using the proxy-wasm `proxy_dictionary_get` host call. This bypasses the 64 KB WASI environment variable size limit and should be used when a value may be larger than 64 KB (e.g. large JSON configs, PEM certificates, policy documents).

```typescript
function getDictionary(name: string): string;
```

Returns the value, or an empty string if not found.

| Function | Use when |
| --------------------- | ------------------------------------------ |
| `getEnv(name)` | Variable value is under 64 KB (most cases) |
| `getDictionary(name)` | Variable value may exceed 64 KB |

```typescript
import { getDictionary } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge";
import { log, LogLevelValues } from "@gcoredev/proxy-wasm-sdk-as/assembly";

const config = getDictionary("LARGE_CONFIG");
log(LogLevelValues.info, "Config size: " + config.length.toString() + " bytes");
```

### Secrets (getSecret, getSecretEffectiveAt)

Read secret values stored in the FastEdge secrets store. Secrets are configured via the platform and are not visible in logs or configuration files.
Expand All @@ -847,7 +870,7 @@ function getSecretEffectiveAt(name: string, effectiveAt: u32): string;

`getSecret` returns the current value of the named secret, or an empty string if not found.

`getSecretEffectiveAt` reads a secret from a specific rotation slot. Use this for secret rotation: pass the current Unix timestamp in seconds as `effectiveAt`. The host selects the slot where `effectiveAt >= secret_slots.slot`.
`getSecretEffectiveAt` reads a secret from a specific rotation slot. Slots are defined in the FastEdge UI and are always numeric (e.g., incremental integers, or timestamp-style values representing a point in time). Use this for secret rotation: pass the current Unix timestamp in seconds as `effectiveAt`. The host selects the slot where `effectiveAt >= secret_slots.slot`.

```typescript
import {
Expand Down
Loading
Loading