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 0404634..c74421a 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 fe6573a..4807ec3 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -199,168 +199,3 @@ 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 ca16447..8495978 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 e0e5128..d98a1dc 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -337,216 +337,3 @@ 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 f135ed3..41bcabf 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 613b6e1..090a82c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # CORS — AssemblyScript (CDN) @@ -283,7 +283,6 @@ registerRootContext((context_id: u32) => { }, "cors"); ``` - ### FILE: examples/cors/package.json ```json @@ -306,7 +305,6 @@ registerRootContext((context_id: u32) => { } ``` - ### FILE: examples/cors/README.md ``` 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 9ec6651..bf58305 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -292,3 +292,241 @@ 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 541ac7c..2039ea1 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- type: example @@ -225,140 +225,3 @@ 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 1419aa9..03f0c0b 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -114,6 +114,8 @@ onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues All properties return `ArrayBuffer`. Decode with `String.UTF8.decode(buf)` before use. Check `buf.byteLength > 0` before decoding to detect missing values. +Routing to the selected origin is achieved by writing `request.url` — this is not an HTTP redirect. No `Location` header is set and no 3xx response is sent to the client. The upstream fetch target is rewritten transparently. + --- ## API Calls @@ -196,6 +198,13 @@ Build scripts (from `package.json`): | `asbuild:release` | `asc assembly/index.ts --target release` | | `asbuild` | Runs both debug and release | +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + --- ## Key Patterns @@ -225,6 +234,16 @@ const defaultOrigin = getEnv("DEFAULT"); if (!defaultOrigin) { /* handles both null and empty string */ } ``` +**Logging country code and matched origin for observability:** +```typescript +log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, +); +``` + --- ## Constraints and Gotchas @@ -236,6 +255,7 @@ if (!defaultOrigin) { /* handles both null and empty string */ } - Country code matching is case-sensitive and depends on the exact casing provided by `request.country` at runtime. - `getEnv` returns `""` (empty string) when a country-code variable is not set; the `=== ""` check must be used for country-specific origin fallback (not a null check). - The `DEFAULT` check uses `!defaultOrigin` (falsy), catching both null and empty string returns. +- The app logs at INFO level: country code with matched origin, host value (if present), and final request URL. These are visible in FastEdge application logs. --- 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 a1d1383..462ec1f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 61f35b9..981cd2f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -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 @@ -120,11 +120,11 @@ Operations performed in order: Expected post-mutation request headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Response Phase — `onResponseHeaders` @@ -144,11 +144,11 @@ Operations performed in order: Expected post-mutation response headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Header Validation Pattern @@ -212,11 +212,11 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { ## Error Response Codes Used -| Code | Meaning | Trigger | -| ---- | --------------------------------- | ----------------------------------------------------------- | -| 550 | No headers present | `get_headers()` returns empty collection | -| 551 | Host header present but empty | `get("host")` returns `""` | -| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | +| Code | Meaning | Trigger | +| ---- | ------------------------------ | ---------------------------------------------------------- | +| 550 | No headers present | `get_headers()` returns empty collection | +| 551 | Host header present but empty | `get("host")` returns `""` | +| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | All errors use `send_http_response(code, "internal server error", body, [])`. @@ -259,18 +259,18 @@ pnpm install pnpm run asbuild ``` -| Output file | Description | -| -------------------------- | ---------------------------------------------- | -| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | -| `build/headers-debug.wasm` | Debug binary with source maps | +| Output file | Description | +| -------------------------- | --------------------------------------------- | +| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | +| `build/headers-debug.wasm` | Debug binary with source maps | Build scripts defined in `package.json`: -| Script | Command | -| ------------------- | ------------------------------------------ | -| `asbuild:debug` | `asc assembly/index.ts --target debug` | -| `asbuild:release` | `asc assembly/index.ts --target release` | -| `asbuild` | Runs both debug and release builds | +| Script | Command | +| ----------------- | ---------------------------------------- | +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | Runs both debug and release builds | ## Deployment 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 9509c21..bc289c7 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> ## Overview @@ -174,7 +174,7 @@ class HttpCallContext extends 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. + // instead of firing another HTTP call. See SDK docs → Outbound HTTP. if (this.httpCallDispatched) { log( LogLevelValues.info, @@ -321,3 +321,202 @@ 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 dd4a9e0..0a8fd7b 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -165,12 +165,14 @@ if (!end_of_stream) { **Execution flow**: 1. Read query string: `get_property("request.query")` → `ArrayBuffer`, decode to string -2. Validate query params via `validateQueryParams(query)` → `Map` -3. Open store: `KvStore.open(store)` — send error response if `null` -4. Dispatch on `action` parameter -5. Serialize result map with `stringifyMap` → JSON string -6. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` -7. Returns `FilterDataStatusValues.Continue` +2. If query string is empty, send error response immediately +3. Validate query params via `validateQueryParams(query)` → `Map` +4. Check `params.has("error")` — send error response if validation failed +5. Open store: `KvStore.open(store)` — send error response if `null` +6. Dispatch on `action` parameter +7. Serialize result map with `stringifyMap` → JSON string +8. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` +9. Returns `FilterDataStatusValues.Continue` **Error path**: calls `sendErrorResponse(msg, body_buffer_length)` which: - Sets `response.status` to `545` via `set_property("response.status", ...)` @@ -208,11 +210,13 @@ switch (action) { const storeArrBuff = myStore.get(key); // null → responseBodyMap.set("Response", "null (Not found)") // non-null → responseBodyMap.set("Response", String.UTF8.decode(storeArrBuff)) + break; } case "scan": { const match = params.get("match"); const keys = myStore.scan(match); // returns string[] — responseBodyMap.set("Response", keys.join(", ")) + break; } case "zrange": { const key = params.get("key"); @@ -220,18 +224,21 @@ switch (action) { const max = params.get("max"); const tuples = myStore.zrangeByScore(key, parseFloat(min), parseFloat(max)); // returns ValueScoreTuple[] + break; } case "zscan": { const key = params.get("key"); const match = params.get("match"); const tuples = myStore.zscan(key, match); // returns ValueScoreTuple[] + break; } case "bfExists": { const key = params.get("key"); const item = params.get("item"); const exists = myStore.bfExists(key, item); // returns bool → responseBodyMap.set("Response", exists ? "true" : "false") + break; } } ``` @@ -252,7 +259,7 @@ Error response: { "error": "" } ``` -Error HTTP status: `545` (set via `response.status` property). +Error HTTP status: `545` (set via `response.status` property — advisory only). --- @@ -274,6 +281,14 @@ Internal helper (not exported). Splits query string on `&`, handles `key=value` Decodes `%xx` hex sequences and converts `+` to space. If `%xx` is not a valid hex sequence, passes through literally. +### `stringifyMap(map: Map): string` + +Exported helper. Serializes a `Map` to a JSON object string. Iterates keys in insertion order, produces `{ "key": "value", ... }` format. + +### `stringifyValueScoreTuples(arr: Array): string` + +Exported helper. Serializes an array of `ValueScoreTuple` to a comma-separated string of `{ value: , score: }` entries. Decodes each `tuple.value` (`ArrayBuffer`) with `String.UTF8.decode`. + --- ## Logging @@ -323,6 +338,7 @@ npm run asbuild:release # release only - `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 +- `ArrayBuffer` decoding: both `KvStore.get` return values and `ValueScoreTuple.value` fields must be decoded with `String.UTF8.decode` before use as strings --- 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 8e1ffc9..d1779cc 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 4437b87..c6fc860 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 f293ce8..137fe6c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 05333df..9078785 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 8d3c4f2..cad60ae 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 d2b1159..695920d 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -222,233 +222,3 @@ 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 436b145..68c55ab 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 ff3c689..07cdb77 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -267,206 +267,3 @@ 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 8b560bd..3f82231 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -15,7 +15,7 @@ template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN AssemblyScript @@ -263,3 +263,118 @@ 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 e3da1c6..837648f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -237,7 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. +Called at the end of the request lifecycle. Inherited from `Context`. Used for final audit logging. Not implemented in this example's source. ## Registration 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 2987a96..f49e70c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -187,191 +187,3 @@ 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 8c95ff2..0d9ab54 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -140,13 +140,6 @@ Build scripts: - `Vary: Origin` is always added alongside `Access-Control-Allow-Origin` to prevent cache poisoning - OPTIONS preflights are answered by the FastEdge edge layer before this hook fires — configure preflight behaviour (allowed methods, max-age) in CDN application settings, not in WASM -## See Also - -- cdn-base skeleton reference -- proxy-wasm-sdk-as SDK reference -- FastEdge CDN application environment variable configuration -- FastEdge portal deployment guide - ## Source Material ### FILE: examples/cors/assembly/index.ts @@ -300,3 +293,10 @@ Build output: 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. ``` + +## See Also + +- cdn-base skeleton reference +- proxy-wasm-sdk-as SDK reference +- FastEdge CDN application environment variable configuration +- FastEdge portal deployment guide 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 ea845c5..bb0b1ac 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -118,13 +118,34 @@ onResponseBody( const description = this.getErrorDescription(code); const category = code >= 500 ? "Server Error" : "Client Error"; - const html = /* ... build HTML string ... */; + 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, // use original length, NOT body.byteLength + body_buffer_length as u32, body, ); @@ -158,18 +179,30 @@ private getErrorTitle(code: u32): string { } 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."; + 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."; } ``` @@ -259,6 +292,14 @@ pnpm run asbuild | `build/customErrorPages.wasm` | Optimised release binary — upload to FastEdge | | `build/customErrorPages-debug.wasm` | Debug binary with source maps | +Build scripts defined in `package.json`: + +| Script | Command | +|---|---| +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | runs both debug and release | + --- ## Deploy @@ -273,6 +314,7 @@ Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to you - **Buffer entire body before replacing**: Return `FilterDataStatusValues.StopIterationAndBuffer` until `end_of_stream` is true, then replace. - **Replace length must be `body_buffer_length`**: Using the new body's byte length as the replacement length will leave original response bytes at the tail. - **Status buffer is big-endian 2 bytes**: Always check `byteLength >= 2` before decoding. +- **Status lookup methods must be private class methods**: No closures, no default args on nested functions. --- 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 912df27..0a75003 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -179,138 +179,3 @@ 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 ae208d8..c505afe 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -266,185 +266,3 @@ 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 c0544ac..2593263 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 bae879f..0c7d16c 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -280,6 +280,32 @@ Resulting effective request headers after mutation: The same add/remove/replace sequence is repeated identically in `onResponseHeaders` for response headers. +## Complete Mutation Sequence (Response Phase) + +```typescript +// Add headers +stream_context.headers.response.add("new-header-01", "value-01"); +stream_context.headers.response.add("new-header-02", "value-02"); +stream_context.headers.response.add("new-header-03", "value-03"); + +// Remove (sets to empty string in nginx — does not delete) +stream_context.headers.response.remove("new-header-01"); + +// Replace value +stream_context.headers.response.replace("new-header-02", "new-value-02"); + +// Add a second value for the same header name +stream_context.headers.response.add("new-header-03", "value-03-a"); +``` + +Resulting effective response headers after mutation: + +| Header | Value | +|---|---| +| `new-header-01` | `""` (empty — nginx cannot delete) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03` and `value-03-a` (multi-value) | + ## Class Structure ```typescript 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 590549a..786000c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 717fbc3..427c7fe 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 8d280e2..aaa3722 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -138,3 +138,129 @@ 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 f6472f8..3761f34 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-as.md @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> ---