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 c74421a..c70feb6 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # A/B Testing — AssemblyScript (CDN) 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 4807ec3..9a743b6 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 8495978..68adc80 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- capabilities: 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 d98a1dc..f558a45 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 41bcabf..ee2e585 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 090a82c..a514711 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # CORS — AssemblyScript (CDN) 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 bf58305..ce71e32 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -292,241 +292,3 @@ export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; - CDN app platform overview - host-services-rust reference (for Rust equivalent patterns) - best-practices reference (response body buffering, hook sequencing) - -## Source Material - -### FILE: examples/customErrorPages/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - set_buffer_bytes, - BufferTypeValues, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class ErrorPagesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new ErrorPagesContext(context_id, this); - } -} - -class ErrorPagesContext extends Context { - constructor(context_id: u32, root_context: ErrorPagesRoot) { - super(context_id, root_context); - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterHeadersStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code >= 400 && code < 600) { - stream_context.headers.response.replace("Content-Type", "text/html"); - stream_context.headers.response.remove("Content-Length"); - stream_context.headers.response.replace("Transfer-Encoding", "Chunked"); - - log(LogLevelValues.info, "Error response detected: " + code.toString()); - } - - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - // Read response status from property (no instance state between hooks) - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterDataStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code < 400 || code >= 600) { - return FilterDataStatusValues.Continue; - } - - if (!end_of_stream) { - return FilterDataStatusValues.StopIterationAndBuffer; - } - const title = this.getErrorTitle(code); - const description = this.getErrorDescription(code); - const category = code >= 500 ? "Server Error" : "Client Error"; - - const html = - '' + - '' + - "" + - code.toString() + - " — " + - title + - "" + - "
" + - "

" + - code.toString() + - "

" + - "

" + - title + - "

" + - "

" + - description + - "

" + - "

" + - category + - "

" + - "
"; - - const body = String.UTF8.encode(html); - // Replace the entire original body — pass body_buffer_length as the length - // to replace, not body.byteLength. Otherwise any original bytes beyond the - // new body's length survive at the tail of the response. - set_buffer_bytes( - BufferTypeValues.HttpResponseBody, - 0, - body_buffer_length as u32, - body, - ); - - return FilterDataStatusValues.Continue; - } - - private getErrorTitle(code: u32): string { - if (code == 400) return "Bad Request"; - if (code == 401) return "Unauthorized"; - if (code == 403) return "Forbidden"; - if (code == 404) return "Not Found"; - if (code == 405) return "Method Not Allowed"; - if (code == 408) return "Request Timeout"; - if (code == 429) return "Too Many Requests"; - if (code == 500) return "Internal Server Error"; - if (code == 502) return "Bad Gateway"; - if (code == 503) return "Service Unavailable"; - if (code == 504) return "Gateway Timeout"; - if (code >= 500) return "Server Error"; - return "Error"; - } - - private getErrorDescription(code: u32): string { - if (code == 400) - return "The server could not understand the request due to invalid syntax."; - if (code == 401) - return "You need to authenticate to access this resource."; - if (code == 403) - return "You do not have permission to access this resource."; - if (code == 404) - return "The requested page could not be found. It may have been moved or deleted."; - if (code == 405) - return "The request method is not supported for this resource."; - if (code == 408) - return "The server timed out waiting for the request."; - if (code == 429) - return "You have sent too many requests. Please try again later."; - if (code == 500) - return "The server encountered an unexpected condition that prevented it from fulfilling the request."; - if (code == 502) - return "The server received an invalid response from the upstream server."; - if (code == 503) - return "The server is temporarily unavailable. Please try again later."; - if (code == 504) - return "The server did not receive a timely response from the upstream server."; - if (code >= 500) - return "The server encountered an error processing your request."; - return "An error occurred processing your request."; - } -} - -registerRootContext((context_id: u32) => { - return new ErrorPagesRoot(context_id); -}, "customErrorPages"); -``` - - -### FILE: examples/customErrorPages/package.json - -```json -{ - "name": "fastedge-as-example-custom-error-pages", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Custom Error Pages — replace 4xx/5xx responses with branded HTML", - "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/customErrorPages/README.md - -``` -[← Back to examples](../README.md) - -# Custom Error Pages - -This application intercepts 4xx and 5xx error responses and replaces them with clean, branded HTML error pages. - -## What it does - -In `onResponseHeaders`, the app reads the `response.status` property. If it is in the 400-599 range, it sets the `Content-Type` to `text/html` and prepares for body replacement. - -In `onResponseBody`, the app buffers the full response, then replaces the body with a styled HTML page containing: - -- The numeric status code -- A human-readable error title (e.g. "Not Found", "Bad Gateway") -- A description explaining what went wrong -- An error category label ("Client Error" or "Server Error") - -Covers all common HTTP error codes (400, 401, 403, 404, 405, 408, 429, 500, 502, 503, 504) with specific messages and falls back to generic messages for other codes. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/customErrorPages.wasm` | Optimised release binary — upload this to FastEdge | -| `build/customErrorPages-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` 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 2039ea1..cee8f25 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- type: example 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 03f0c0b..849560f 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 462ec1f..0a9923a 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 981cd2f..7a52ea5 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -66,14 +66,14 @@ registerRootContext((context_id: u32) => { All header operations are accessed via `stream_context.headers.request` or `stream_context.headers.response`. -| Method | Signature | Description | -| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | -| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | -| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | -| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | -| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | -| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | +| Method | Signature | Description | +| -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | +| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | +| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | +| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | +| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | +| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | ### `Headers` / `HeaderPair` type 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 bc289c7..95ba36c 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> ## Overview @@ -321,202 +321,3 @@ Dependencies (from `package.json`): - SDK API reference — full lifecycle hook signatures, `RootContext` and `Context` base class APIs, all enum values (`WasmResultValues`, `FilterHeadersStatusValues`, `BufferTypeValues`, `LogLevelValues`) - Platform overview — FastEdge CDN execution model, context lifecycle, how the runtime re-enters hooks after async callbacks - Best practices — timeout tuning, upstream cluster naming, error response patterns - -## Source Material - -### FILE: examples/httpCall/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 { - BaseContext, - BufferTypeValues, - Context, - FilterHeadersStatusValues, - get_buffer_bytes, - HeaderPair, - log, - LogLevelValues, - makeHeaderPair, - registerRootContext, - RootContext, - send_http_response, - stream_context, - WasmResultValues, -} 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, - bodySize: usize, - trls: u32, -): void { - if (hdrs == 0) { - 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, - 0, - bodySize as u32, - ); - const bodyStr = String.UTF8.decode(bodyBytes); - log( - LogLevelValues.info, - "Response body (" + bodySize.toString() + " bytes): " + bodyStr, - ); - } else { - log(LogLevelValues.info, "Response body: empty"); - } -} - -class HttpCallRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new HttpCallContext(context_id, this); - } -} - -class HttpCallContext extends Context { - httpCallDispatched: bool = false; - - constructor(context_id: u32, root_context: HttpCallRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // 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. See SDK docs → Outbound HTTP. - 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")); - headers.push(makeHeaderPair(":path", "/ip")); - headers.push(makeHeaderPair(":method", "GET")); - headers.push(makeHeaderPair("User-Agent", "fastedge")); - - // 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), - new Array(), - 3000, - this, - handleHttpCallResponse, - ); - - if (result != WasmResultValues.Ok) { - log( - LogLevelValues.error, - "Failed to dispatch HTTP call: " + result.toString(), - ); - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("Failed to dispatch HTTP call"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - this.httpCallDispatched = true; - log(LogLevelValues.info, "HTTP call dispatched, pausing request"); - - return FilterHeadersStatusValues.StopIteration; - } -} - -registerRootContext((context_id: u32) => { - return new HttpCallRoot(context_id); -}, "httpCall"); -``` - -### FILE: examples/httpCall/package.json - -```json -{ - "name": "fastedge-as-example-http-call", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: HTTP Call — async HTTP dispatch with callback", - "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/httpCall/README.md - -``` -[← Back to examples](../README.md) - -# HTTP Call - -This application makes an asynchronous HTTP call to an external service using the proxy-wasm HTTP dispatch API. - -## What it does - -In `onRequestHeaders`, the app dispatches an outbound HTTP GET request to `httpbin.org/ip` and pauses the hook (`StopIteration`) until the response arrives. Dispatch is latched on an instance field so the second invocation of the hook (after the response is processed) returns `Continue` instead of dispatching again. - -Inside the response callback passed to `httpCall`: - -1. Checks whether the call succeeded (a `headers` value of `0` indicates failure — timeout, DNS error, etc.). -2. Reads and logs the `User-Agent` response header via `stream_context.headers.http_callback`. -3. Reads and logs the response body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`. - -If the dispatch itself fails (e.g. invalid arguments), the app returns a `500` error to the client. - -## Key concepts - -- **`httpCall()`** on `RootContext` dispatches an async HTTP request. It accepts a cluster (host), headers, body, trailers, a timeout in milliseconds, the originating context, and a callback. -- **FastEdge resume model.** Returning `FilterHeadersStatusValues.StopIteration` pauses the hook. The runtime processes the HTTP response, invokes the callback, then **re-invokes `onRequestHeaders` on the same `Context` instance**. The `httpCallDispatched` latch gates re-dispatch so the hook returns `Continue` the second time. This differs from canonical proxy-wasm — calling `continueRequest()` is not required (and has no effect on FastEdge). -- **`stream_context.headers.http_callback`** provides access to the HTTP call response headers inside the callback. The SDK sets the effective context before the callback fires, so these lookups resolve against the originating request. -- **`get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`** reads the response body inside the callback. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | -| `build/httpCall-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/httpCall.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` 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 0a8fd7b..8b35bae 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 d1779cc..fe756b2 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # Large Dictionary — AssemblyScript (CDN) @@ -136,3 +136,129 @@ Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your - `getEnv` — standard WASI environment variable access (under 64 KB) - proxy-wasm-sdk-as SDK reference — full list of imports from `@gcoredev/proxy-wasm-sdk-as/assembly` and `@gcoredev/proxy-wasm-sdk-as/assembly/fastedge` - FastEdge CDN app examples (other concepts) — additional CDN app patterns in the fastedge-docs reference + +## Source Material + +### FILE: examples/largeDictionary/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 { + getDictionary, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class LargeDictionaryRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new LargeDictionaryContext(context_id, this); + } +} + +class LargeDictionaryContext extends Context { + constructor(context_id: u32, root_context: LargeDictionaryRoot) { + super(context_id, root_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"); + + const size = config.length; + log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); + + stream_context.headers.request.add("x-config-size", size.toString()); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new LargeDictionaryRoot(context_id); +}, "largeDictionary"); +``` + + +### FILE: examples/largeDictionary/package.json + +```json +{ + "name": "fastedge-as-example-large-dictionary", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", + "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/largeDictionary/README.md + +``` +[← Back to examples](../README.md) + +# Large Dictionary + +This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. + +## When to use `getDictionary` vs `getEnv` + +| Function | Use when | +|----------|----------| +| `getEnv(name)` | Variable value is under 64 KB (most cases) | +| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | + +The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. + +For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. + +## What it does + +In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | +| `build/largeDictionary-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. +``` 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 c6fc860..1338661 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 137fe6c..94c40a9 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # CDN Runtime Properties — AssemblyScript 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 9078785..fcee56a 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # Quickstart: AssemblyScript CDN Apps on FastEdge 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 cad60ae..3a8d32e 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # AssemblyScript Proxy-Wasm SDK Reference 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 695920d..2393c2e 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -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 68c55ab..8a3a3e4 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 07cdb77..9da636d 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -267,3 +267,204 @@ 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 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. +``` 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 3f82231..6a43237 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -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: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-20 +source_ref: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 +updated: 2026-09-22 --- # Base Skeleton: CDN AssemblyScript @@ -263,118 +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 - -```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 - -```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 - -```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 837648f..89784fd 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -360,3 +360,218 @@ Upload `build/body.wasm` via the FastEdge portal and attach it to your CDN appli - cdn-base skeleton - FastEdge portal deployment guide - fastedge-test reference (for testing body manipulation logic) + +## 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/scaffold/reference/cdn/cache-control-as.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-as.md index f49e70c..63a5c49 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -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 0d9ab54..18a4276 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 bb0b1ac..a83ce60 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 0a75003..af56b70 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 c505afe..35f839a 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -266,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 2593263..2b9a311 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -158,10 +158,10 @@ 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 | -| ---------------- | ------------------------------------------------ | -| Request blocked | `"geoBlock: blocked request from " + countryStr` | -| Request allowed | `"geoBlock: allowed request from " + countryStr` | +| Event | Log message | +| --------------- | ------------------------------------------------ | +| Request blocked | `"geoBlock: blocked request from " + countryStr` | +| Request allowed | `"geoBlock: allowed request from " + countryStr` | Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. 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 0c7d16c..c68f1a6 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 786000c..410be81 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 427c7fe..ecbd5ed 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- 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 aaa3722..bb51254 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -138,129 +138,3 @@ No additional dependencies beyond the cdn-base skeleton. - cdn-base skeleton reference - proxy-wasm-sdk-as SDK reference (AssemblyScript) - platform-overview reference for environment variable configuration - -## Source Material - -### FILE: examples/largeDictionary/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 { - getDictionary, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class LargeDictionaryRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new LargeDictionaryContext(context_id, this); - } -} - -class LargeDictionaryContext extends Context { - constructor(context_id: u32, root_context: LargeDictionaryRoot) { - super(context_id, root_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"); - - const size = config.length; - log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); - - stream_context.headers.request.add("x-config-size", size.toString()); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new LargeDictionaryRoot(context_id); -}, "largeDictionary"); -``` - - -### FILE: examples/largeDictionary/package.json - -```json -{ - "name": "fastedge-as-example-large-dictionary", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", - "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/largeDictionary/README.md - -``` -[← Back to examples](../README.md) - -# Large Dictionary - -This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. - -## When to use `getDictionary` vs `getEnv` - -| Function | Use when | -|----------|----------| -| `getEnv(name)` | Variable value is under 64 KB (most cases) | -| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | - -The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. - -For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. - -## What it does - -In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -|------|------|-------------| -| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | -| `build/largeDictionary-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. -``` 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 3761f34..416db62 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: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -302,9 +302,12 @@ 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: + // 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; }