diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md index eff6c0f..0404634 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # A/B Testing — AssemblyScript (CDN) @@ -101,6 +101,8 @@ set_property("request.url", String.UTF8.encode(newUrl)) `set_property` / `get_property` key: `"request.url"`, `"request.path"`, `"request.scheme"`, `"request.host"`, `"request.query"`. +URL rewrite is skipped entirely if `schemeBuf.byteLength === 0` or `hostBuf.byteLength === 0`. + ### Step 5 — Add upstream headers ``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md index 29a7d5b..fe6573a 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -27,8 +27,8 @@ Validates incoming requests by checking the `X-API-Key` request header against a ## Entry Point -**File:** `assembly/index.ts` -**Root context name:** `"apiKey"` +**File:** `assembly/index.ts` +**Root context name:** `"apiKey"` **Hook:** `onRequestHeaders` --- @@ -164,7 +164,7 @@ Header `X-API-Key` is set to `""` on the forwarded request (platform `.remove()` ## Build -**Package manager:** `npm` (also compatible with `pnpm`) +**Package manager:** `npm` (also compatible with `pnpm`) **SDK dependency:** `@gcoredev/proxy-wasm-sdk-as ^1.2.3` ```sh @@ -199,3 +199,168 @@ Upload `build/apiKey.wasm` to the FastEdge portal and attach it to a CDN applica - proxy-wasm-sdk-as reference (full `Context`, `RootContext`, `FilterHeadersStatusValues` API) - FastEdge secrets management (how to set and rotate application secrets) - CDN app deployment guide + +## Source Material + +### FILE: examples/apiKey/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + HeaderPair, + log, + LogLevelValues, + makeHeaderPair, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class ApiKeyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new ApiKeyContext(context_id, this); + } +} + +class ApiKeyContext extends Context { + constructor(context_id: u32, root_context: ApiKeyRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const expectedKey = getSecret("API_KEY"); + if (expectedKey === "") { + log(LogLevelValues.error, "API_KEY secret not configured"); + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const providedKey = stream_context.headers.request.get("X-API-Key"); + + if (providedKey === "") { + const authHeaders = new Array(); + authHeaders.push(makeHeaderPair("WWW-Authenticate", "API-Key")); + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Missing X-API-Key header"), + authHeaders, + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (providedKey !== expectedKey) { + log(LogLevelValues.info, "API key validation failed"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid API key"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // .remove() sets the header value to "" rather than deleting it entirely — + // the upstream will see X-API-Key: "" rather than a missing header. + stream_context.headers.request.remove("X-API-Key"); + + log(LogLevelValues.info, "API key validated successfully"); + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new ApiKeyRoot(context_id); +}, "apiKey"); +``` + + +### FILE: examples/apiKey/package.json + +```json +{ + "name": "fastedge-as-example-api-key", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: API Key — validate X-API-Key header against a secret", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/apiKey/README.md + +``` +[← Back to examples](../README.md) + +# API Key + +This application validates requests using an `X-API-Key` header checked against a stored secret. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the expected API key from the `API_KEY` secret. +2. Checks the `X-API-Key` request header. +3. Returns `401 Unauthorized` if the header is missing. +4. Returns `403 Forbidden` if the key does not match. +5. On success, clears the `X-API-Key` header before forwarding to the upstream origin (proxy-wasm `.remove()` sets the header value to an empty string rather than deleting it). + +This is a simpler alternative to JWT validation when you need basic API authentication without token expiry or claims. + +> **Production note:** The key comparison (`providedKey !== expectedKey`) is not constant-time, which opens a timing side-channel for a high-volume attacker. For production use, replace the comparison with a constant-time HMAC equality check or use the `jwt` example which includes proper cryptographic validation. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `API_KEY` | Secret | The expected API key value | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/apiKey.wasm` | Optimised release binary — upload this to FastEdge | +| `build/apiKey-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/apiKey.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `API_KEY` secret in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md index bc2390d..ca16447 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- capabilities: @@ -62,7 +62,7 @@ Reads a named secret variable configured on the FastEdge application. - **Parameter**: `name` — secret variable name (string) - **Returns**: secret value as a UTF-8 string, or `null` if not found -- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` +- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` without encoding ```typescript const secret = getSecret("SECRET"); @@ -82,6 +82,7 @@ Verifies a JWT token against an HMAC-SHA256 secret. - `secret` — HMAC signing secret (string) - **Returns**: `JwtValidation` enum value - **Package**: `@gcoredev/as-jwt` (separate dependency, not part of proxy-wasm-sdk-as) +- **Behavior**: does not throw; always check the return value against `JwtValidation.Ok` ### `JwtValidation` enum @@ -105,11 +106,11 @@ Sends an immediate HTTP response and stops the request. Body must be encoded as ### `setLogLevel(level: LogLevelValues): void` -Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. +Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. Present in source for demonstration purposes only — explicitly setting the default is optional. ### `log(level: LogLevelValues, message: string): void` -Emits a log entry. Used to record token rejection reasons. +Emits a log entry. Used to record token rejection reasons (e.g. `"Token Expired"`, `"Bad Token"`). --- @@ -150,7 +151,23 @@ All blocked responses return `FilterHeadersStatusValues.StopIteration`. |---|---|---| | `SECRET` | HMAC-SHA256 signing key | String; minimum 256 bits / 32 characters | -Configure this secret variable on the FastEdge application before deployment. For secret rotation, see `getSecretEffectiveAt` in the FastEdge secrets reference. +Configure this secret variable on the FastEdge application before deployment. For secret rotation, use `getSecretEffectiveAt` instead of `getSecret` — see the FastEdge secrets reference. + +--- + +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` --- @@ -167,6 +184,14 @@ Configure this secret variable on the FastEdge application before deployment. Fo - `@gcoredev/as-jwt` is a required peer dependency — it is NOT bundled in proxy-wasm-sdk-as. - `assemblyscript-json` is declared as a dependency but not directly used in this example. +**Dev dependencies:** +```json +{ + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" +} +``` + --- ## Build @@ -208,6 +233,7 @@ Root context name: `"auth"`. - The `Authorization` header is validated in two steps: first a null check (missing header → 401), then a `startsWith("Bearer ")` check (wrong scheme → 401). An empty-string header would fail the Bearer scheme check. - `jwtVerify` does not throw; always check the return value against `JwtValidation.Ok`. - Validation happens in `onRequestHeaders` only. There is no body or response hook in this example. +- The `setLogLevel(LogLevelValues.info)` call in `createContext` is present for demonstration only — `info` is the default level and the call is not required. - For HMAC secret rotation using slot-based secrets, use `getSecretEffectiveAt` instead of `getSecret`. See the FastEdge secrets reference. --- @@ -217,4 +243,4 @@ Root context name: `"auth"`. - proxy-wasm-sdk-as SDK reference (AssemblyScript) - FastEdge secrets reference (`getSecret`, `getSecretEffectiveAt`, rotation slots) - CDN app platform overview -- `@gcoredev/as-jwt` package (npmjs.com) +- `@gcoredev/as-jwt` package on npmjs.com diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md index 6c00fe2..e0e5128 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -337,3 +337,216 @@ When modifying body content: - host-services-rust (for equivalent Rust patterns) - platform-overview (CDN app lifecycle, hook execution order) - examples-headers-as (header-only manipulation without body buffering) + +## Source Material + +### FILE: examples/body/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BufferTypeValues, + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_buffer_bytes, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class HttpBodyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug + return new HttpBody(context_id, this); + } +} + +class HttpBody extends Context { + constructor(context_id: u32, root_context: HttpBodyRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool + ): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); + // Remove the "content-length" header + stream_context.headers.request.remove("content-length"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); + if (bodyStr.includes("Client")) { + const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; + set_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length, + String.UTF8.encode(newBody) + ); + } + } + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); + + // Remove "content-length" header as the body size will change + stream_context.headers.response.remove("content-length"); + + // Set "transfer-encoding" to "chunked" + stream_context.headers.response.replace("transfer-encoding", "Chunked"); + + const contentType = stream_context.headers.response.get("content-type"); + if (contentType.length > 0) { + set_property("response.content_type", String.UTF8.encode(contentType)); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + + // Retrieve the request URL + const urlBytes = get_property("request.url"); + const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } + + // Retrieve the response content type stored in onResponseHeaders + const contentTypeBytes = get_property("response.content_type"); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); + } + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HttpBodyRoot(context_id); +}, "httpbody"); +``` + +### FILE: examples/body/package.json + +```json +{ + "name": "fastedge-as-example-body", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Body manipulation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/body/README.md + +``` +[← Back to examples](../README.md) + +# Body + +This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. + +## What it does + +1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. + +2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. + +3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. + +4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. + +This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: + +- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). +- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ----------------------- | -------------------------------------------------- | +| `build/body.wasm` | Optimised release binary — upload this to FastEdge | +| `build/body-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md index 0010810..f135ed3 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -228,3 +228,191 @@ Build targets: - FastEdge CDN app scaffold reference - platform-overview (CDN application lifecycle and hook execution model) - best-practices (header mutation, environment variable patterns) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | +| Other | `public, max-age=600` | 10 minutes | +| Error responses (4xx/5xx) | `no-store` | — | + +Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). + +## Configuration + +All environment variables are optional — sensible defaults are applied when unset. + +| Variable | Default | Description | +|----------|---------|-------------| +| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | +| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | +| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cacheControl-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md index c1ff635..613b6e1 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CORS — AssemblyScript (CDN) @@ -193,3 +193,159 @@ pnpm run asbuild - FastEdge CDN application environment variables configuration - platform-overview (CDN app lifecycle and OPTIONS handling) - best-practices (header mutation patterns, Vary usage) + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md index 3ccdd4c..9ec6651 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -171,6 +171,24 @@ set_buffer_bytes( | other 5xx | Server Error | | other | Error | +### Descriptions + +| Code | Description | +|---|---| +| 400 | The server could not understand the request due to invalid syntax. | +| 401 | You need to authenticate to access this resource. | +| 403 | You do not have permission to access this resource. | +| 404 | The requested page could not be found. It may have been moved or deleted. | +| 405 | The request method is not supported for this resource. | +| 408 | The server timed out waiting for the request. | +| 429 | You have sent too many requests. Please try again later. | +| 500 | The server encountered an unexpected condition that prevented it from fulfilling the request. | +| 502 | The server received an invalid response from the upstream server. | +| 503 | The server is temporarily unavailable. Please try again later. | +| 504 | The server did not receive a timely response from the upstream server. | +| other 5xx | The server encountered an error processing your request. | +| other | An error occurred processing your request. | + ### Categories | Range | Category label | diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md index 0c9c162..541ac7c 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- type: example @@ -225,3 +225,140 @@ import { - examples-headers-cdn-as (request/response header manipulation patterns) - platform-overview (secret management and deployment configuration) - best-practices (logging levels and secret handling guidelines) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. + +To test locally with the visual debugger, create `fixtures/.env`: + +``` +FASTEDGE_VAR_ENV_USERNAME=my-username +FASTEDGE_VAR_SECRET_PASSWORD=my-password +``` + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| -------------------------------------- | -------------------------------------------------- | +| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | +| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/variablesAndSecrets.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md index 7535b8a..1419aa9 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md index dd5af79..a1d1383 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md index e954ccc..61f35b9 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -199,6 +199,17 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { } ``` +## Multi-Value Headers + +`new-header-03` is deliberately added twice — `add()` is called with the same name twice. This produces a multi-value header with two separate `new-header-03` entries. The validation pattern uses `Set` of `"name:value"` pairs to assert both values are present. + +## Cross-Phase Response Header Writes + +`stream_context.headers.response.add(...)` can be called during `onRequestHeaders`. Headers written in the request phase appear in the final response alongside those set in `onResponseHeaders`. The distinction: + +- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase +- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase + ## Error Response Codes Used | Code | Meaning | Trigger | @@ -239,9 +250,7 @@ onLog(): void { **`replace()` upserts on FastEdge**: `replace()` creates the header with the given value if the named header does not exist — it does not behave as a no-op on absent headers. Guard calls to `replace()` with a prior `get()` length check when you intend to update only an existing header. -**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. The distinction: -- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase -- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase +**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. ## Build diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md index 3e5ef9b..9509c21 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> ## Overview @@ -123,6 +123,8 @@ import { } from "@gcoredev/proxy-wasm-sdk-as/assembly"; import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +const INTERNAL_SERVER_ERROR: u32 = 500; + function handleHttpCallResponse( ctx: BaseContext, hdrs: u32, @@ -133,10 +135,12 @@ function handleHttpCallResponse( log(LogLevelValues.error, "HTTP call failed — no response received"); return; } + const userAgent = stream_context.headers.http_callback.get("user-agent"); if (userAgent !== "") { log(LogLevelValues.info, "User-Agent: " + userAgent); } + if (bodySize > 0) { const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpCallResponseBody, @@ -144,30 +148,43 @@ function handleHttpCallResponse( bodySize as u32, ); const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "Response body: " + bodyStr); + log( + LogLevelValues.info, + "Response body (" + bodySize.toString() + " bytes): " + bodyStr, + ); + } else { + log(LogLevelValues.info, "Response body: empty"); } } -class MyRoot extends RootContext { +class HttpCallRoot extends RootContext { createContext(context_id: u32): Context { setLogLevel(LogLevelValues.info); - return new MyContext(context_id, this); + return new HttpCallContext(context_id, this); } } -class MyContext extends Context { +class HttpCallContext extends Context { httpCallDispatched: bool = false; - constructor(context_id: u32, root_context: MyRoot) { + constructor(context_id: u32, root_context: HttpCallRoot) { super(context_id, root_context); } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Latch prevents re-dispatch on second invocation after callback fires. + // FastEdge re-invokes this hook after the httpCall response is processed. + // The latch gates re-dispatch so the second invocation returns Continue + // instead of firing another HTTP call. if (this.httpCallDispatched) { + log( + LogLevelValues.info, + "HTTP call response received, resuming request.", + ); return FilterHeadersStatusValues.Continue; } + log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); + const headers = new Array(); headers.push(makeHeaderPair(":scheme", "https")); headers.push(makeHeaderPair(":authority", "httpbin.org")); @@ -175,7 +192,8 @@ class MyContext extends Context { headers.push(makeHeaderPair(":method", "GET")); headers.push(makeHeaderPair("User-Agent", "fastedge")); - const result = (this.root_context as MyRoot).httpCall( + // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. + const result = (this.root_context as HttpCallRoot).httpCall( "httpbin.org", headers, new ArrayBuffer(0), @@ -186,8 +204,12 @@ class MyContext extends Context { ); if (result != WasmResultValues.Ok) { + log( + LogLevelValues.error, + "Failed to dispatch HTTP call: " + result.toString(), + ); send_http_response( - 500, + INTERNAL_SERVER_ERROR, "internal server error", String.UTF8.encode("Failed to dispatch HTTP call"), [], @@ -196,13 +218,15 @@ class MyContext extends Context { } this.httpCallDispatched = true; + log(LogLevelValues.info, "HTTP call dispatched, pausing request"); + return FilterHeadersStatusValues.StopIteration; } } registerRootContext((context_id: u32) => { - return new MyRoot(context_id); -}, "myApp"); + return new HttpCallRoot(context_id); +}, "httpCall"); ``` ### Callback: check failure, read headers, read body @@ -239,7 +263,7 @@ function handleHttpCallResponse( ### Error response on dispatch failure ```typescript -const result = (this.root_context as MyRoot).httpCall( +const result = (this.root_context as HttpCallRoot).httpCall( "upstream-host", headers, new ArrayBuffer(0), @@ -260,16 +284,37 @@ if (result != WasmResultValues.Ok) { } ``` +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +| File | Description | +|------|-------------| +| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | +| `build/httpCall-debug.wasm` | Debug binary with source maps | + +Dependencies (from `package.json`): + +| Package | Role | +|---------|------| +| `@gcoredev/proxy-wasm-sdk-as` | SDK — required runtime dep (`^1.2.3`) | +| `assemblyscript` | AssemblyScript compiler (`^0.28.9`) | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (`^0.1.0`) | + ## Gotchas - **No closures over mutable state.** AssemblyScript does not support closures that capture mutable variables. Capture state on the `Context` instance (instance fields) or use `set_property`/`get_property`. The `httpCallDispatched: bool` latch is an instance field for this reason. - **FastEdge resume model differs from canonical proxy-wasm.** After the callback fires, the runtime **re-invokes `onRequestHeaders` on the same `Context` instance**. Do not call `continueRequest()` — it has no effect on FastEdge. Use a latch field to distinguish the second invocation. -- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as MyRoot).httpCall(...)`. +- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as HttpCallRoot).httpCall(...)`. - **Instance state survives re-entry within the same context** but does not persist across the nginx→core-proxy hop. Do not rely on instance fields across separate request lifecycles. - **Tune `timeoutMs` per upstream.** The example uses `3000` ms to accommodate cold DNS and variable network conditions. Set a tighter value for low-latency upstreams and a looser value for slow third-party services. - **No try/catch.** AssemblyScript has no exception handling at runtime. Always check `WasmResultValues.Ok` explicitly after `httpCall()` returns. - **Pseudo-headers are required.** Include `:scheme`, `:authority`, `:path`, and `:method` in the headers array. Missing pseudo-headers will cause dispatch to fail. - **`hdrs == 0` means failure, not "no headers".** Inside the callback, `hdrs == 0` signals that the HTTP call itself failed (timeout, DNS error, invalid cluster). Do not skip this check. +- **Log the dispatch failure result.** When `result != WasmResultValues.Ok`, log `result.toString()` to surface the specific `WasmResultValues` variant for debugging. ## Related diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md index 3517ddd..dd4a9e0 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -321,6 +321,8 @@ npm run asbuild:release # release only - `onResponseBody` must buffer until `end_of_stream` is `true`; return `StopIterationAndBuffer` otherwise - `min` and `max` for `zrange` are received as strings from query params and must be parsed with `parseFloat` - `response.status` set via `set_property` in `onResponseBody` is advisory only — the origin HTTP status passes through to the client; the JSON error body is the authoritative error signal +- Empty query string is treated as an error — app responds with `545` and a JSON error body +- `validateQueryParams` returns a map containing key `"error"` on failure; the caller must check `params.has("error")` before proceeding --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md index 0961c7e..8e1ffc9 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # Large Dictionary — AssemblyScript (CDN) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md index 9aa9631..4437b87 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md index 7d4571b..f293ce8 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CDN Runtime Properties — AssemblyScript @@ -117,17 +117,26 @@ set_property("request.path", String.UTF8.encode("/new-path")); const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); - const params = queryString.split("&").map>((pair) => pair.split("=")); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map>((pair) => pair.split("=")); + for (let i = 0; i < params.length; i++) { const param = params[i]; - if (param.length !== 2) continue; + if (param.length !== 2) { + continue; // Skip invalid query parameters + } const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); set_property(REQUEST_PATH, String.UTF8.encode(value)); } } @@ -178,11 +187,75 @@ class Properties extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Error codes 551–559 identify the absent property via the HTTP response status. - // request.extension (555) and request.query (556) are optional — never trigger an error. - // Read, log, and expose all known properties as response headers. - // Return FilterHeadersStatusValues.StopIteration on any required missing property. - // Return FilterHeadersStatusValues.Continue on success. + // Error codes 551–559 identify the absent property via the HTTP response status: + // 551=uri 552=host 553=path 554=scheme 555=extension 556=query 557=x_real_ip 558=country 559=city + if (!this.handleProperty(REQUEST_URI, 551, "uri", "request-uri")) { + return FilterHeadersStatusValues.StopIteration; + } + + // host must be present for upstream routing; validated but not logged or exposed as a response header + if (!this.handleProperty(REQUEST_HOST, 552)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_PATH, 553, "path", "request-path")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_SCHEME, 554, "scheme", "request-scheme")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_EXTENSION, 555, "extension", "request-extension", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_QUERY, 556, "query", "request-query", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_X_REAL_IP, 557, "client_ip", "request-x-real-ip")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_COUNTRY, 558, "country", "request-country")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_CITY, 559, "city", "request-city")) { + return FilterHeadersStatusValues.StopIteration; + } + + // Handle query parameters — override request.url, request.host, request.path via url=, host=, path= params + const query = get_property(REQUEST_QUERY); + if (query.byteLength !== 0) { + const queryString = String.UTF8.decode(query); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map>((pair) => pair.split("=")); + + for (let i = 0; i < params.length; i++) { + const param = params[i]; + if (param.length !== 2) { + continue; + } + const key = param[0]; + const value = param[1]; + if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); + set_property(REQUEST_URI, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); + set_property(REQUEST_HOST, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); + set_property(REQUEST_PATH, String.UTF8.encode(value)); + } + } + } + + return FilterHeadersStatusValues.Continue; } onLog(): void { @@ -208,7 +281,12 @@ class Properties extends Context { } return true; } - send_http_response(errorCode, "internal server error", String.UTF8.encode("Internal server error"), []); + send_http_response( + errorCode, + "internal server error", + String.UTF8.encode("Internal server error"), + [] + ); return false; } const value = String.UTF8.decode(valueArr); @@ -323,6 +401,7 @@ Upload `build/properties.wasm` to the FastEdge portal and attach to a CDN applic - **`request.extension` and `request.query` are optional**: They may legitimately be absent (no file extension in path, no query string). Use `allowEmpty: true` — log an empty value and continue without adding a response header. - **`request.host` is validated but not exposed**: It must be non-empty for upstream routing to work correctly. It is not logged and not added as a response header. - **`set_property` on request properties takes effect immediately** for subsequent property reads and downstream filter processing within the same request. +- **Query parameter override logs change**: When a query parameter overrides a property, the change is logged with a message of the form `` `change url to: ${value}` ``, `` `change host to: ${value}` ``, or `` `change path to: ${value}` ``. - **Lifecycle constraint**: Request properties (`request.*`) are only meaningful during request processing phases. Attempting to read them outside `onRequestHeaders` or `onRequestBody` may yield empty buffers. - **Query parameter parsing is manual**: The SDK provides no built-in query string parser. Split on `&`, then on `=`, and validate `param.length === 2` before accessing indices. - **`setLogLevel` must be called in `createContext`**: Log level is set to `LogLevelValues.info` inside `PropertiesRoot.createContext`, not in the constructor of the `Properties` context class. diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md index e408331..05333df 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # Quickstart: AssemblyScript CDN Apps on FastEdge @@ -118,7 +118,7 @@ registerRootContext((context_id: u32) => { |------|---------| | `export * from ".../assembly/proxy"` | Exposes wasm entry points the host runtime calls into. Required in every app. Must be the first line. | | `RootContext` subclass | Created once per worker. `createContext` produces a `Context` instance for each hook invocation. | -| `Context` subclass | Handles a single lifecycle hook phase. Instance fields do not persist across hooks (hook state isolation). | +| `Context` subclass | Handles a single lifecycle hook phase. A fresh instance is created for each hook phase — instance fields do not persist across hooks (hook state isolation). | | `registerRootContext` | Registers the root context factory with the proxy runtime. | ### Lifecycle hooks diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md index 8b77309..8d3c4f2 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # AssemblyScript Proxy-Wasm SDK Reference @@ -65,6 +65,8 @@ registerRootContext( ); ``` +**No default parameters on nested functions**: AssemblyScript compiles functions defined inside a method body to `call_indirect` entries in the WebAssembly element table. Default parameter values are applied at direct call sites only — for indirect calls, unspecified argument slots receive `0` (a null pointer). If those slots are used as strings or objects, the wasm traps with a memory access out-of-bounds error at runtime. Fix: promote the helper to a `private` class method (dispatched as a direct call; defaults apply correctly), or pass all arguments explicitly at every call site. + --- ## Build Configuration @@ -873,14 +875,22 @@ function getEnv(name: string): string; Returns the value of the environment variable, or an empty string if the variable is not set. +**Empty string fallback**: AssemblyScript's `||` operator performs a pointer-non-null check, not a value-falsy check. `getEnv("X") || "default"` returns `""` rather than `"default"` when the variable is unset, because the empty-string object has a non-zero pointer. Use an explicit check instead: + +```typescript +const raw = getEnv("X"); +const val = raw === "" ? "default" : raw; +``` + +The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getEnv } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; import { log, LogLevelValues } from "@gcoredev/proxy-wasm-sdk-as/assembly"; -const blocklist = getEnv("BLOCKLIST"); -if (blocklist.length == 0) { - log(LogLevelValues.warn, "BLOCKLIST env var is not set"); -} +const raw = getEnv("BLOCKLIST"); +const blocklist = raw === "" ? "none" : raw; +log(LogLevelValues.info, "Blocklist: " + blocklist); ``` ### Dictionary (getDictionary) @@ -919,6 +929,8 @@ function getSecretEffectiveAt(name: string, effectiveAt: u32): string; `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`. +**Empty string fallback**: AssemblyScript's `||` operator does not fall back on empty strings. `getSecret("KEY") || "default"` returns `""` rather than `"default"` when the secret is unset. Use an explicit check: `const raw = getSecret("KEY"); const key = raw === "" ? "default" : raw;`. The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getSecret, @@ -959,7 +971,7 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the | `scan(pattern: string): string[]` | `string[]` | Glob-style key scan. Pattern must include a wildcard (e.g., `"foo*"`). | | `zrangeByScore(key: string, min: f64, max: f64): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set range query: returns entries where `min <= score <= max`. | | `zscan(key: string, pattern: string): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set pattern scan: matches values against the glob pattern. | -| `bfExists(key: string, item: string): bool` | `bool` | Bloom filter membership check. Returns `true` if item may exist. | +| `bfExists(key: string, item: string): boolean` | `boolean` | Bloom filter membership check. Returns `true` if item may exist. | `get` returns `ArrayBuffer | null` — the caller must decode the buffer. @@ -970,9 +982,9 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the ```typescript class ValueScoreTuple { value: ArrayBuffer; // The stored value bytes - score: f64; // The associated score + score: number; // The associated score (f64) - constructor(value: ArrayBuffer, score: f64); + constructor(value: ArrayBuffer, score: number); } ``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-as.md index 52ac44f..d2b1159 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -222,3 +222,233 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide + +## Source Material + +### FILE: examples/abTesting/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; + +class AbTestingRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new AbTestingContext(context_id, this); + } +} + +class AbTestingContext extends Context { + constructor(context_id: u32, root_context: AbTestingRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const experimentName = getEnv("EXPERIMENT_NAME"); + if (experimentName === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const variantAPath = getEnv("VARIANT_A_PATH"); + const variantBPath = getEnv("VARIANT_B_PATH"); + if (variantAPath === "" || variantBPath === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode( + "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", + ), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Check for existing experiment cookie + const cookieName = "fe_exp_" + experimentName; + const cookieHeader = stream_context.headers.request.get("Cookie"); + let assignedVariant = this.getCookieValue(cookieHeader, cookieName); + + // Assign variant if not already set + if (assignedVariant !== "A" && assignedVariant !== "B") { + // Use current time as a simple entropy source for 50/50 split + const now = getCurrentTime(); + assignedVariant = now % 2 == 0 ? "A" : "B"; + } + + // Rewrite the request path to the variant path + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + return FilterHeadersStatusValues.Continue; + } + const originalPath = String.UTF8.decode(pathArrBuf); + const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; + const newPath = variantPath + originalPath; + + // Reconstruct request.url from its decomposed parts rather than splicing + // the path out of the full URL — splicing breaks when the path happens to + // appear inside the host, and it can silently lose the query string. + const schemeBuf = get_property("request.scheme"); + const hostBuf = get_property("request.host"); + if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { + const scheme = String.UTF8.decode(schemeBuf); + const host = String.UTF8.decode(hostBuf); + const queryBuf = get_property("request.query"); + const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; + const newUrl = + scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); + log(LogLevelValues.info, `A/B routing: ${newUrl}`); + set_property("request.url", String.UTF8.encode(newUrl)); + } + + // Add variant header for upstream visibility + stream_context.headers.request.add("X-Experiment", experimentName); + stream_context.headers.request.add("X-Variant", assignedVariant); + + log( + LogLevelValues.info, + `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, + ); + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Recover the assigned variant from the request header set in onRequestHeaders. + // Instance state (this.variant) does not survive the nginx -> core-proxy hop. + const variant = stream_context.headers.request.get("X-Variant"); + if (variant === "") { + return FilterHeadersStatusValues.Continue; + } + + const experimentName = getEnv("EXPERIMENT_NAME"); + const cookieName = "fe_exp_" + experimentName; + + // Set the experiment cookie so subsequent requests stick to the same variant + stream_context.headers.response.add( + "Set-Cookie", + cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", + ); + + // Add variant as response header for observability + stream_context.headers.response.add("X-Variant", variant); + + return FilterHeadersStatusValues.Continue; + } + + private getCookieValue(cookieHeader: string, name: string): string { + if (cookieHeader === "") return ""; + const pairs = cookieHeader.split(";"); + const prefix = name + "="; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].trim(); + if (pair.startsWith(prefix)) { + return pair.substring(prefix.length); + } + } + return ""; + } +} + +registerRootContext((context_id: u32) => { + return new AbTestingRoot(context_id); +}, "abTesting"); +``` + + +### FILE: examples/abTesting/package.json + +```json +{ + "name": "fastedge-as-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/abTesting/README.md + +``` +[← Back to examples](../README.md) + +# A/B Testing + +This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. + +## What it does + +In `onRequestHeaders`, the app: + +1. Checks for an existing experiment cookie (`fe_exp_`). +2. If no cookie is found, assigns the user to variant **A** or **B** (50/50 split). +3. Rewrites the request path by prepending the variant-specific path prefix (e.g. `/variant-a/original/path`). +4. Adds `X-Experiment` and `X-Variant` request headers for upstream visibility. + +In `onResponseHeaders`, the app sets a `Set-Cookie` header to persist the variant assignment for subsequent requests (24-hour TTL). + +> **Note on variant assignment entropy:** New-visitor assignment uses `getCurrentTime() % 2` as a simple 50/50 source. This is illustrative — it is not sticky across two requests that arrive in the same millisecond and is not reproducible in tests. Production A/B implementations typically hash a stable visitor identifier (e.g. client IP or session token) for deterministic, sticky pre-cookie assignment. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `EXPERIMENT_NAME` | `homepage-redesign` | Name of the experiment (required) | +| `VARIANT_A_PATH` | `/variant-a` | Path prefix for variant A (required) | +| `VARIANT_B_PATH` | `/variant-b` | Path prefix for variant B (required) | + +Your origin server should serve different content at each variant path prefix. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/abTesting.wasm` | Optimised release binary — upload this to FastEdge | +| `build/abTesting-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/abTesting.wasm` to the FastEdge portal and attach it to your CDN application. Configure the experiment environment variables in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-as.md index 218310a..436b145 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -122,6 +122,8 @@ class ApiKeyContext extends Context { } // 3. Strip key before forwarding to upstream + // .remove() sets the header value to "" rather than deleting it entirely — + // the upstream will see X-API-Key: "" rather than a missing header. stream_context.headers.request.remove("X-API-Key"); log(LogLevelValues.info, "API key validated successfully"); diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-as.md index 6b54958..ff3c689 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -234,6 +234,20 @@ registerRootContext((context_id: u32) => { | Token invalid (any other error) | 403 | `Invalid token` | | Token valid | — | Pass-through (`Continue`) | +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + ## Build Output | File | Description | @@ -253,3 +267,206 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint + +## Source Material + +### FILE: examples/jwt/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class AuthRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes + return new Auth(context_id, this); + } +} + +class Auth extends Context { + constructor(context_id: u32, root_context: AuthRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const secret = getSecret("SECRET"); + if (!secret) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const authHeader = stream_context.headers.request.get("Authorization"); + if (!authHeader) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("No Authorization header"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (!authHeader.startsWith("Bearer ")) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Authorization header must use Bearer scheme"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const token = authHeader.slice(7); // strip "Bearer " prefix + if (!token) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Token not found"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Decode the JWT token + const jwtResult = jwtVerify(token, secret); + if (jwtResult !== JwtValidation.Ok) { + if (jwtResult === JwtValidation.Expired) { + log(LogLevelValues.info, "Token Expired"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Expired token"), + [], + ); + } else { + log(LogLevelValues.info, "Bad Token"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid token"), + [], + ); + } + return FilterHeadersStatusValues.StopIteration; + } + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new AuthRoot(context_id); +}, "auth"); +``` + + +### FILE: examples/jwt/package.json + +```json +{ + "name": "fastedge-as-example-jwt", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: JWT validation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/as-jwt": "^1.0.3", + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", + "assemblyscript-json": "^1.1.0" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/jwt/README.md + +``` +[← Back to examples](../README.md) + +# JWT Validation + +This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. +2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. +3. Verifies the token signature and expiry using `jwtVerify()`. +4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. + +## Configuration + +Set the following secret variable on your FastEdge application: + +| Secret | Description | +| -------- | ------------------------------------------------------------------ | +| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | + +## Testing tokens + +**Expired token** (will return `403 Forbidden`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, will return `200 OK`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ---------------------- | -------------------------------------------------- | +| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | +| `build/jwt-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/jwt.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. + +For more on secrets and secret rotation slots, see the [FastEdge secrets documentation](https://gcore.com/docs/fastedge/secrets-manager/slots). +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md index 0138cee..8b560bd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -14,8 +14,8 @@ languages: [assemblyscript] template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as -source_ref: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 -updated: 2026-05-20 +source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 +updated: 2026-08-17 --- # Base Skeleton: CDN AssemblyScript @@ -263,112 +263,3 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) - -## Source Material - -### FILE: examples/helloWorld/assembly/index.ts - -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; - -class HelloWorldRoot extends RootContext { - createContext(context_id: u32): Context { - return new HelloWorld(context_id, this); - } -} - -class HelloWorld extends Context { - constructor(context_id: u32, root_context: HelloWorldRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool, - ): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onRequestBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onResponseBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HelloWorldRoot(context_id); -}, "helloWorld"); - - -### FILE: examples/helloWorld/package.json - -{ - "name": "fastedge-as-example-hello-world", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} - - -### FILE: examples/helloWorld/asconfig.json - -{ - "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", - "targets": { - "debug": { - "outFile": "build/helloWorld-debug.wasm", - "textFile": "build/helloWorld-debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/helloWorld.wasm", - "textFile": "build/helloWorld.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "use": "abort=abort_proc_exit" - } -} diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md index da93373..e3da1c6 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -70,7 +70,7 @@ registerRootContext() → registers the root context factory with a plugin name ```typescript class HttpBodyRoot extends RootContext { createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); + setLogLevel(LogLevelValues.info); // reduce to LogLevelValues.debug for more logging return new HttpBody(context_id, this); } } @@ -97,6 +97,7 @@ Called when request headers arrive. Must remove `content-length` before the body ```typescript onRequestHeaders(headers: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); stream_context.headers.request.remove("content-length"); return FilterHeadersStatusValues.Continue; } @@ -112,6 +113,7 @@ Called (possibly multiple times) as request body chunks arrive. Buffer until `en ```typescript onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } @@ -124,6 +126,7 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); if (bodyStr.includes("Client")) { const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; set_buffer_bytes( @@ -154,10 +157,11 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV ### `onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues` -Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. +Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. Captures the `content-type` header into a runtime property for use in `onResponseBody`. ```typescript onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); stream_context.headers.response.remove("content-length"); stream_context.headers.response.replace("transfer-encoding", "Chunked"); @@ -185,15 +189,31 @@ Called (possibly multiple times) as response body chunks arrive. Buffer until `e ```typescript onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + const urlBytes = get_property("request.url"); const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } const contentTypeBytes = get_property("response.content_type"); - const contentType = contentTypeBytes.byteLength === 0 ? "" : String.UTF8.decode(contentTypeBytes); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpResponseBody, @@ -203,7 +223,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "onHttpResponseBody >> bodyStr: " + bodyStr); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); } return FilterDataStatusValues.Continue; } @@ -217,13 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Used for final audit logging. - -```typescript -onLog(): void { - log(LogLevelValues.info, "onLog >> completed (contextId): " + this.context_id.toString()); -} -``` +Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. ## Registration @@ -334,7 +348,7 @@ pnpm run asbuild Build scripts defined in `package.json`: - `asbuild:debug` — `asc assembly/index.ts --target debug` - `asbuild:release` — `asc assembly/index.ts --target release` -- `asbuild` — runs both +- `asbuild` — runs both (`npm run asbuild:debug && npm run asbuild:release`) ## Deploy diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-as.md index eff8100..2987a96 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -187,3 +187,191 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | +| Other | `public, max-age=600` | 10 minutes | +| Error responses (4xx/5xx) | `no-store` | — | + +Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). + +## Configuration + +All environment variables are optional — sensible defaults are applied when unset. + +| Variable | Default | Description | +|----------|---------|-------------| +| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | +| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | +| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cacheControl-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md index dd83b67..8c95ff2 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -146,3 +146,157 @@ Build scripts: - proxy-wasm-sdk-as SDK reference - FastEdge CDN application environment variable configuration - FastEdge portal deployment guide + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-as.md index 0bdc459..ea845c5 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-as.md index e2d8383..912df27 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -148,9 +148,9 @@ Configure both on the FastEdge application before deployment. The test runner maps environment variable names as follows: -| Env key | Maps to | -| ------------------------------ | ------------------ | -| `FASTEDGE_VAR_ENV_` | `getEnv("NAME")` | +| Env key | Maps to | +| ------------------------------ | ------------------- | +| `FASTEDGE_VAR_ENV_` | `getEnv("NAME")` | | `FASTEDGE_VAR_SECRET_` | `getSecret("NAME")` | Use `"dotenv": {"enabled": true}` in the test fixture to load values from a `.env` file (e.g. `fixtures/.env`): @@ -179,3 +179,138 @@ Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to - cdn-base skeleton (base request/response handling structure) - fastedge-test reference (local WASM test runner and fixture format) - platform-overview reference (FastEdge application configuration and secret management) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. + +To test locally with the visual debugger, create `fixtures/.env`: + +``` +FASTEDGE_VAR_ENV_USERNAME=my-username +FASTEDGE_VAR_SECRET_PASSWORD=my-password +``` + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| -------------------------------------- | -------------------------------------------------- | +| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | +| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-as.md index ea43437..ae208d8 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -132,7 +132,20 @@ const countrySpecificOrigin = getEnv(countryCode); - Returns `""` (empty string) when no matching env var is set. - Do **not** use a null/undefined check; use an empty-string check. -### Step 4 — Read request path +### Step 4 — Read request host (optional) + +```typescript +const hostArrBuf = get_property("request.host"); +if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); +} +``` + +- `get_property("request.host")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. +- Reading the host is optional; absence does not stop iteration. + +### Step 5 — Read request path ```typescript const pathArrBuf = get_property("request.path"); @@ -151,7 +164,7 @@ const path = String.UTF8.decode(pathArrBuf); - `get_property("request.path")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. - Missing path → 500 response, stop iteration. -### Step 5 — Build and set target URL +### Step 6 — Build and set target URL ```typescript const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; @@ -212,7 +225,8 @@ registerRootContext((context_id: u32) => { ## Logging ```typescript -log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${matchedOrigin}`); +log(LogLevelValues.info, "onRequestHeaders >> "); +log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin}`); log(LogLevelValues.info, `Provided Host: ${host}`); log(LogLevelValues.info, `request-url: ${requestUrl}`); ``` @@ -252,3 +266,185 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) + +## Source Material + +### FILE: examples/geoRedirect/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const BAD_GATEWAY: u32 = 502; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class GeoRedirectRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new GeoRedirect(context_id, this); + } +} + +class GeoRedirect extends Context { + constructor(context_id: u32, root_context: GeoRedirectRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> "); + + const defaultOrigin = getEnv("DEFAULT"); + + if (!defaultOrigin) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured - DEFAULT must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const countryArrBuf = get_property("request.country"); + if (countryArrBuf.byteLength === 0) { + send_http_response( + BAD_GATEWAY, + "bad gateway", + String.UTF8.encode("Missing country information"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + const countryCode = String.UTF8.decode(countryArrBuf); + const countrySpecificOrigin = getEnv(countryCode); + + log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, + ); + + const hostArrBuf = get_property("request.host"); + if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); + } + + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Internal server error - no request path"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const path = String.UTF8.decode(pathArrBuf); + const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; + // remove trailing slashes from the origin + const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; + + const requestUrl = `${cleanedOrigin}${path}`; + + log(LogLevelValues.info, `request-url: ${requestUrl}`); + + set_property("request.url", String.UTF8.encode(requestUrl)); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new GeoRedirectRoot(context_id); +}, "geoRedirect"); +``` + + +### FILE: examples/geoRedirect/package.json + +```json +{ + "name": "fastedge-as-example-georedirect", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Geo Redirect", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/geoRedirect/README.md + +``` +[← Back to examples](../README.md) + +# Geo Redirect + +This application redirects requests to different origin URLs based on the client's country code. + +## What it does + +In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. + +- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. +- Otherwise it falls back to the `DEFAULT` origin. +- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. + +> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. + +> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | +| `` | `DE=https://de.example.com` | Per-country origin URL (optional, one per country) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/geoRedirect.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `DEFAULT` environment variable and any per-country overrides in the application settings. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md index 54555ac..c0544ac 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -158,8 +158,8 @@ registerRootContext((context_id: u32) => { Both blocked and allowed requests are logged at `INFO` level, providing an audit trail for all traffic decisions. -| Event | Log message | -| ---------------- | ---------------------------------------------- | +| Event | Log message | +| ---------------- | ------------------------------------------------ | | Request blocked | `"geoBlock: blocked request from " + countryStr` | | Request allowed | `"geoBlock: allowed request from " + countryStr` | @@ -169,11 +169,11 @@ Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. ## Status Codes Used -| Constant | Value | Condition | -| ----------------------- | ----- | ------------------------------------------------------- | -| `FORBIDDEN` | 403 | Request's country code is in the blacklist | -| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | -| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | +| Constant | Value | Condition | +| ----------------------- | ----- | ------------------------------------------------------ | +| `FORBIDDEN` | 403 | Request's country code is in the blacklist | +| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | +| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | --- @@ -256,23 +256,23 @@ Build scripts defined in `package.json`: ## Dependencies -| Package | Role | -| -------------------------------- | ------------------------------------------- | -| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | -| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | -| `assemblyscript` | AssemblyScript compiler (dev) | +| Package | Role | +| ----------------------------- | -------------------------------------- | +| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | +| `assemblyscript` | AssemblyScript compiler (dev) | --- ## Error Conditions -| Condition | Response | Status | -| --------------------------------------------- | ------------------------------------------------ | ------ | -| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | -| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | -| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | -| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | -| Country code not in blacklist | `Continue` | — | +| Condition | Response | Status | +| -------------------------------------------- | ---------------------------------------------------- | ------ | +| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | +| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | +| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | +| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | +| Country code not in blacklist | `Continue` | — | --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md index bf8dc73..bae879f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-as.md index 2d323d6..590549a 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -91,12 +91,12 @@ function handleHttpCallResponse( **Parameters:** -| Parameter | Type | Description | -|------------|-------------|-------------| +| Parameter | Type | Description | +|------------|---------------|-------------| | `ctx` | `BaseContext` | Originating context | -| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | -| `bodySize` | `usize` | Size of the response body in bytes | -| `trls` | `u32` | Number of response trailers | +| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | +| `bodySize` | `usize` | Size of the response body in bytes | +| `trls` | `u32` | Number of response trailers | --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-as.md index 4377eda..717fbc3 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -205,6 +205,14 @@ The `validateQueryParams` function: - Validates `action` against `ALL_ACTIONS = ["get", "scan", "zscan", "zrange", "bfExists"]` - Checks all required parameters for the resolved action are present and non-empty +**Required parameters by action** (as defined in `validateQueryParams`): +- `store` — required for all actions +- `key` — required for `get`, `zrange`, `zscan`, `bfExists` +- `match` — required for `scan`, `zscan` +- `min` — required for `zrange` +- `max` — required for `zrange` +- `item` — required for `bfExists` + --- ## Error Handling @@ -248,6 +256,18 @@ set_buffer_bytes( The response body is a JSON object built from a `Map` using `stringifyMap`. It includes fields such as `Store`, `Action`, `Key`, `Response`, and action-specific fields (`Match`, `Min`, `Max`, `Item`). +**`stringifyMap` format:** +```typescript +// Output: {"key1": "value1", "key2": "value2"} +function stringifyMap(map: Map): string +``` + +**`stringifyValueScoreTuples` format:** +```typescript +// Output: "{ value: , score: }, ..." +function stringifyValueScoreTuples(arr: Array): string +``` + --- ## Hook: `onResponseBody` @@ -295,6 +315,11 @@ registerRootContext → KvStoreRoot.createContext (sets log level to info) The KV Store must be configured and linked to the FastEdge application before use. `KvStore.open` will return `null` if the named store is not attached. The `store` query parameter at runtime must exactly match the binding name configured on the application. +KV Store setup steps: +1. Create a KV Store in the FastEdge portal under the Key-Value storage section. +2. Populate it with the keys and values you want to query. +3. Link the store to the app — when configuring the FastEdge application, add the store under the app's KV store bindings. The name given to the binding is what the `store` query parameter must match at runtime. + --- ## See Also diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-as.md index d9a6ea5..8d280e2 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -76,14 +76,10 @@ class LargeDictionaryContext extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. const config = getDictionary("LARGE_CONFIG"); - // getDictionary returns "" (never null) when variable is not set - if (config.length === 0) { - log(LogLevelValues.info, "LARGE_CONFIG is not set"); - return FilterHeadersStatusValues.Continue; - } - const size = config.length; log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md index 14d1bd8..f6472f8 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -130,7 +130,7 @@ set_property("request.path", String.UTF8.encode(newPath)); Parse the raw query string and rewrite properties based on matching parameter keys. ```typescript -const query = get_property("request.query"); +const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); const params = queryString @@ -145,11 +145,11 @@ if (query.byteLength !== 0) { const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { - set_property("request.url", String.UTF8.encode(value)); + set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { - set_property("request.host", String.UTF8.encode(value)); + set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { - set_property("request.path", String.UTF8.encode(value)); + set_property(REQUEST_PATH, String.UTF8.encode(value)); } } }