From c0eae99d53c61c4a07f3d8c679d2aa693f226660 Mon Sep 17 00:00:00 2001 From: "fastedge-plugin-sync[bot]" Date: Tue, 18 Aug 2026 07:33:55 +0000 Subject: [PATCH 1/2] =?UTF-8?q?release:=20v0.2.6=20=E2=80=94=20update=20do?= =?UTF-8?q?cs=20index=20and=20bump=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/marketplace.json | 2 +- .../.codex-plugin/plugin.json | 2 +- plugins/gcore-fastedge-codex/docs-index.json | 5482 +++++++++++++--- .../reference/cdn/examples-ab-testing-as.md | 6 +- .../reference/cdn/examples-ab-testing-rust.md | 190 +- .../reference/cdn/examples-api-key-as.md | 175 +- .../reference/cdn/examples-auth-jwt-as.md | 40 +- .../reference/cdn/examples-body-as.md | 217 +- .../cdn/examples-cache-control-as.md | 192 +- .../cdn/examples-convert-image-rust.md | 299 +- .../reference/cdn/examples-cors-as.md | 160 +- .../cdn/examples-custom-error-pages-as.md | 22 +- .../cdn/examples-custom-error-pages-rust.md | 71 +- .../reference/cdn/examples-custom-rust.md | 122 +- .../reference/cdn/examples-env-secrets-as.md | 141 +- .../reference/cdn/examples-headers-as.md | 19 +- .../reference/cdn/examples-headers-rust.md | 355 +- .../reference/cdn/examples-http-call-as.md | 73 +- .../reference/cdn/examples-http-call-rust.md | 65 +- .../reference/cdn/examples-kv-store-as.md | 6 +- .../reference/cdn/examples-md2html-rust.md | 4 +- .../reference/cdn/examples-properties-as.md | 99 +- .../reference/http/examples-ab-testing-js.md | 120 +- .../http/examples-backend-basic-rust.md | 113 +- ...xamples-bloom-filter-denylist-wasi-rust.md | 161 +- .../http/examples-cache-wasi-rust.md | 196 +- .../examples-diagnostic-logging-wasi-rust.md | 151 +- .../http/examples-geo-redirect-wasi-rust.md | 116 +- .../http/examples-hello-world-wasi-rust.md | 76 +- .../reference/http/examples-kv-store-js.md | 32 +- .../http/examples-kv-store-wasi-rust.md | 276 +- .../examples-markdown-render-basic-rust.md | 13 +- .../examples-outbound-fetch-basic-rust.md | 119 +- ...ples-outbound-modify-response-wasi-rust.md | 101 +- .../http/examples-s3upload-basic-rust.md | 3 +- .../http/examples-secret-basic-rust.md | 184 +- .../http/examples-simple-fetch-wasi-rust.md | 122 +- .../http/examples-streaming-wasi-rust.md | 104 +- .../fastedge-docs/reference/quickstart-as.md | 6 +- .../reference/sdk-reference-as.md | 30 +- .../reference/templates/catalog.md | 94 + .../templates/edge-sso-integration.md | 214 + .../templates/edge-totp-integration.md | 120 + .../scaffold/reference/cdn/ab-testing-as.md | 234 +- .../scaffold/reference/cdn/api-key-as.md | 6 +- .../scaffold/reference/cdn/api-key-rust.md | 121 +- .../scaffold/reference/cdn/auth-jwt-as.md | 221 +- .../scaffold/reference/cdn/auth-jwt-rust.md | 141 +- .../skills/scaffold/reference/cdn/base-as.md | 117 +- .../scaffold/reference/cdn/base-rust.md | 108 +- .../skills/scaffold/reference/cdn/body-as.md | 42 +- .../scaffold/reference/cdn/body-rust.md | 124 +- .../reference/cdn/cache-control-as.md | 192 +- .../reference/cdn/cache-control-rust.md | 143 +- .../reference/cdn/convert-image-rust.md | 301 +- .../skills/scaffold/reference/cdn/cors-as.md | 158 +- .../reference/cdn/custom-error-pages-rust.md | 206 +- .../scaffold/reference/cdn/custom-rust.md | 122 +- .../scaffold/reference/cdn/env-secrets-as.md | 145 +- .../reference/cdn/env-secrets-rust.md | 103 +- .../scaffold/reference/cdn/geo-redirect-as.md | 206 +- .../reference/cdn/geo-redirect-rust.md | 15 +- .../scaffold/reference/cdn/geoblock-as.md | 42 +- .../scaffold/reference/cdn/geoblock-rust.md | 113 +- .../scaffold/reference/cdn/headers-rust.md | 20 +- .../scaffold/reference/cdn/http-call-as.md | 14 +- .../scaffold/reference/cdn/kv-store-as.md | 29 +- .../reference/cdn/large-dictionary-as.md | 12 +- .../reference/cdn/large-dictionary-rust.md | 110 +- .../scaffold/reference/cdn/properties-as.md | 12 +- .../scaffold/reference/http/ab-testing-ts.md | 120 +- .../reference/http/ab-testing-wasi-rust.md | 213 +- .../scaffold/reference/http/base-rust.md | 46 +- .../http/bloom-filter-denylist-ts.md | 66 +- .../http/bloom-filter-denylist-wasi-rust.md | 116 +- .../scaffold/reference/http/cache-basic-ts.md | 116 +- .../scaffold/reference/http/cache-ts.md | 280 +- .../reference/http/cache-wasi-rust.md | 200 +- .../reference/http/crypto-hmac-jwt-ts.md | 4 +- .../http/diagnostic-logging-wasi-rust.md | 102 +- .../reference/http/geo-redirect-wasi-rust.md | 118 +- .../scaffold/reference/http/headers-ts.md | 3 +- .../reference/http/headers-wasi-rust.md | 4 +- .../scaffold/reference/http/kv-store-ts.md | 185 +- .../scaffold/reference/http/mcp-server-ts.md | 281 +- .../http/outbound-fetch-wasi-rust.md | 89 +- .../http/outbound-modify-response-ts.md | 48 +- .../outbound-modify-response-wasi-rust.md | 81 +- .../http/secret-rollover-wasi-rust.md | 152 +- .../scaffold/reference/http/streaming-ts.md | 51 +- .../reference/http/streaming-wasi-rust.md | 66 +- .../reference/http/template-invoice-ts.md | 86 +- .../http/variables-and-secrets-wasi-rust.md | 88 +- .../skills/test/reference/dotenv.md | 4 +- .../skills/test/reference/runner-internals.md | 4 +- .../skills/test/reference/server-api.md | 4 +- .../skills/test/reference/test-config.md | 4 +- .../skills/test/reference/test-framework.md | 4 +- .../skills/test/reference/vscode-debugger.md | 4 +- .../.cursor-plugin/plugin.json | 2 +- plugins/gcore-fastedge-cursor/docs-index.json | 5482 +++++++++++++--- .../rules/fastedge-knowledge.mdc | 5 + .../skills/fastedge-docs/SKILL.md | 13 + .../reference/cdn/examples-ab-testing-as.md | 6 +- .../reference/cdn/examples-ab-testing-rust.md | 190 +- .../reference/cdn/examples-api-key-as.md | 175 +- .../reference/cdn/examples-auth-jwt-as.md | 40 +- .../reference/cdn/examples-body-as.md | 217 +- .../cdn/examples-cache-control-as.md | 192 +- .../cdn/examples-convert-image-rust.md | 299 +- .../reference/cdn/examples-cors-as.md | 160 +- .../cdn/examples-custom-error-pages-as.md | 22 +- .../cdn/examples-custom-error-pages-rust.md | 71 +- .../reference/cdn/examples-custom-rust.md | 122 +- .../reference/cdn/examples-env-secrets-as.md | 141 +- .../reference/cdn/examples-headers-as.md | 19 +- .../reference/cdn/examples-headers-rust.md | 355 +- .../reference/cdn/examples-http-call-as.md | 73 +- .../reference/cdn/examples-http-call-rust.md | 65 +- .../reference/cdn/examples-kv-store-as.md | 6 +- .../reference/cdn/examples-md2html-rust.md | 4 +- .../reference/cdn/examples-properties-as.md | 99 +- .../reference/http/examples-ab-testing-js.md | 120 +- .../http/examples-backend-basic-rust.md | 113 +- ...xamples-bloom-filter-denylist-wasi-rust.md | 161 +- .../http/examples-cache-wasi-rust.md | 196 +- .../examples-diagnostic-logging-wasi-rust.md | 151 +- .../http/examples-geo-redirect-wasi-rust.md | 116 +- .../http/examples-hello-world-wasi-rust.md | 76 +- .../reference/http/examples-kv-store-js.md | 32 +- .../http/examples-kv-store-wasi-rust.md | 276 +- .../examples-markdown-render-basic-rust.md | 13 +- .../examples-outbound-fetch-basic-rust.md | 119 +- ...ples-outbound-modify-response-wasi-rust.md | 101 +- .../http/examples-s3upload-basic-rust.md | 3 +- .../http/examples-secret-basic-rust.md | 184 +- .../http/examples-simple-fetch-wasi-rust.md | 122 +- .../http/examples-streaming-wasi-rust.md | 104 +- .../fastedge-docs/reference/quickstart-as.md | 6 +- .../reference/sdk-reference-as.md | 30 +- .../reference/templates/catalog.md | 94 + .../templates/edge-sso-integration.md | 214 + .../templates/edge-totp-integration.md | 120 + .../scaffold/reference/cdn/ab-testing-as.md | 234 +- .../scaffold/reference/cdn/api-key-as.md | 6 +- .../scaffold/reference/cdn/api-key-rust.md | 121 +- .../scaffold/reference/cdn/auth-jwt-as.md | 221 +- .../scaffold/reference/cdn/auth-jwt-rust.md | 141 +- .../skills/scaffold/reference/cdn/base-as.md | 117 +- .../scaffold/reference/cdn/base-rust.md | 108 +- .../skills/scaffold/reference/cdn/body-as.md | 42 +- .../scaffold/reference/cdn/body-rust.md | 124 +- .../reference/cdn/cache-control-as.md | 192 +- .../reference/cdn/cache-control-rust.md | 143 +- .../reference/cdn/convert-image-rust.md | 301 +- .../skills/scaffold/reference/cdn/cors-as.md | 158 +- .../reference/cdn/custom-error-pages-rust.md | 206 +- .../scaffold/reference/cdn/custom-rust.md | 122 +- .../scaffold/reference/cdn/env-secrets-as.md | 145 +- .../reference/cdn/env-secrets-rust.md | 103 +- .../scaffold/reference/cdn/geo-redirect-as.md | 206 +- .../reference/cdn/geo-redirect-rust.md | 15 +- .../scaffold/reference/cdn/geoblock-as.md | 42 +- .../scaffold/reference/cdn/geoblock-rust.md | 113 +- .../scaffold/reference/cdn/headers-rust.md | 20 +- .../scaffold/reference/cdn/http-call-as.md | 14 +- .../scaffold/reference/cdn/kv-store-as.md | 29 +- .../reference/cdn/large-dictionary-as.md | 12 +- .../reference/cdn/large-dictionary-rust.md | 110 +- .../scaffold/reference/cdn/properties-as.md | 12 +- .../scaffold/reference/http/ab-testing-ts.md | 120 +- .../reference/http/ab-testing-wasi-rust.md | 213 +- .../scaffold/reference/http/base-rust.md | 46 +- .../http/bloom-filter-denylist-ts.md | 66 +- .../http/bloom-filter-denylist-wasi-rust.md | 116 +- .../scaffold/reference/http/cache-basic-ts.md | 116 +- .../scaffold/reference/http/cache-ts.md | 280 +- .../reference/http/cache-wasi-rust.md | 200 +- .../reference/http/crypto-hmac-jwt-ts.md | 4 +- .../http/diagnostic-logging-wasi-rust.md | 102 +- .../reference/http/geo-redirect-wasi-rust.md | 118 +- .../scaffold/reference/http/headers-ts.md | 3 +- .../reference/http/headers-wasi-rust.md | 4 +- .../scaffold/reference/http/kv-store-ts.md | 185 +- .../scaffold/reference/http/mcp-server-ts.md | 281 +- .../http/outbound-fetch-wasi-rust.md | 89 +- .../http/outbound-modify-response-ts.md | 48 +- .../outbound-modify-response-wasi-rust.md | 81 +- .../http/secret-rollover-wasi-rust.md | 152 +- .../scaffold/reference/http/streaming-ts.md | 51 +- .../reference/http/streaming-wasi-rust.md | 66 +- .../reference/http/template-invoice-ts.md | 86 +- .../http/variables-and-secrets-wasi-rust.md | 88 +- .../skills/test/reference/dotenv.md | 4 +- .../skills/test/reference/runner-internals.md | 4 +- .../skills/test/reference/server-api.md | 4 +- .../skills/test/reference/test-config.md | 4 +- .../skills/test/reference/test-framework.md | 4 +- .../skills/test/reference/vscode-debugger.md | 4 +- .../gcore-fastedge/.claude-plugin/plugin.json | 2 +- plugins/gcore-fastedge/docs-index.json | 5634 ++++++++++++++--- 201 files changed, 29365 insertions(+), 7665 deletions(-) create mode 100644 plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md create mode 100644 plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-sso-integration.md create mode 100644 plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md create mode 100644 plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md create mode 100644 plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-sso-integration.md create mode 100644 plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 63d1481..2c9c3f3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "gcore-fastedge", "source": "./plugins/gcore-fastedge", "description": "Build, deploy, and manage serverless edge applications on Gcore FastEdge", - "version": "0.2.5" + "version": "0.2.6" } ] } diff --git a/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json b/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json index 2609137..3e0afd3 100644 --- a/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json +++ b/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "gcore-fastedge", - "version": "0.2.5", + "version": "0.2.6", "description": "Codex plugin for Gcore FastEdge with local indexed docs and MCP-backed build/deploy workflows.", "author": { "name": "Gcore", diff --git a/plugins/gcore-fastedge-codex/docs-index.json b/plugins/gcore-fastedge-codex/docs-index.json index c553c5c..95cea94 100644 --- a/plugins/gcore-fastedge-codex/docs-index.json +++ b/plugins/gcore-fastedge-codex/docs-index.json @@ -1,9 +1,9 @@ { "schema_version": "1.0.0", - "generated_at": "2026-07-24T07:50:35.224Z", + "generated_at": "2026-08-18T07:33:55.044Z", "source": { "repo": "fastedge-plugin", - "commit": "05c2493", + "commit": "067d44e", "pipeline": "sync-reference-docs" }, "topics": [ @@ -532,7 +532,7 @@ { "id": "cdn-examples-ab-testing-as", "title": "A/B Testing — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md", "skill": "fastedge-docs", "category": "examples", @@ -554,9 +554,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "cookie-based", "traffic", @@ -566,7 +566,7 @@ "requests", "different" ], - "content_sha256": "419863edee5fea328eaa0d29bec26e3e2baab36e05c21d29fe5d8837fb0a4c9b", + "content_sha256": "9546c33b6d3c8657538a75f442e7738541cdad753fc7b88a1cd759cc036629f0", "sections": [ { "id": "cdn-examples-ab-testing-as#introduction", @@ -583,9 +583,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "assemblyscript", "cdn", @@ -694,7 +694,7 @@ "level": 2, "anchor": "request-flow-—-onrequestheaders", "line_start": 44, - "line_end": 118, + "line_end": 120, "keywords": [ "request", "flow", @@ -729,8 +729,8 @@ "heading": "Response Flow — `onResponseHeaders`", "level": 2, "anchor": "response-flow-—-onresponseheaders", - "line_start": 119, - "line_end": 151, + "line_start": 121, + "line_end": 153, "keywords": [ "response", "flow", @@ -773,8 +773,8 @@ "heading": "API Surface", "level": 2, "anchor": "api-surface", - "line_start": 152, - "line_end": 169, + "line_start": 154, + "line_end": 171, "keywords": [ "api", "surface", @@ -822,8 +822,8 @@ "heading": "Cookie Convention", "level": 2, "anchor": "cookie-convention", - "line_start": 170, - "line_end": 181, + "line_start": 172, + "line_end": 183, "keywords": [ "cookie", "convention", @@ -846,8 +846,8 @@ "heading": "Error Conditions", "level": 2, "anchor": "error-conditions", - "line_start": 182, - "line_end": 192, + "line_start": 184, + "line_end": 194, "keywords": [ "error", "conditions", @@ -884,8 +884,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 193, - "line_end": 208, + "line_start": 195, + "line_end": 210, "keywords": [ "build", "pnpm", @@ -917,8 +917,8 @@ "heading": "AssemblyScript-Specific Constraints", "level": 2, "anchor": "assemblyscript-specific-constraints", - "line_start": 209, - "line_end": 218, + "line_start": 211, + "line_end": 220, "keywords": [ "assemblyscript-specific", "constraints", @@ -967,8 +967,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 219, - "line_end": 225, + "line_start": 221, + "line_end": 227, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -1000,7 +1000,7 @@ { "id": "cdn-examples-ab-testing-rust", "title": "A/B Testing — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1024,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1034,7 +1034,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "e0967ff8a5a76ceb6efaaf12499f28d2197799098608bbdbe5e3637b1b9e8495", + "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1053,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1650,13 +1650,63 @@ "structure", "examples-a" ] + }, + { + "id": "cdn-examples-ab-testing-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 252, + "line_end": 439, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "ab_testing", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "traffic", + "splitting", + "layer.", + "uses", + "cookie", + "assign", + "users", + "variant", + "rewrites", + "request", + "path", + "route", + "different", + "parts", + "origin", + "server.", + "required", + "configuration", + "environment", + "variable", + "experiment_name", + "variant_a_path", + "prefix" + ] } ] }, { "id": "cdn-examples-api-key-as", "title": "CDN Example: API Key Authentication (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1678,9 +1728,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -1690,7 +1740,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "144feff24d4893ee721a26628b201437fd48d6c34b14d465c821cab5cff05f07", + "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1707,9 +1757,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -2064,6 +2114,176 @@ "deployment", "guide" ] + }, + { + "id": "cdn-examples-api-key-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 203, + "line_end": 326, + "keywords": [ + "source", + "material", + "file", + "examples", + "apikey", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "getsecret", + "setloglevel", + "fastedge", + "const", + "unauthorized", + "u32", + "401", + "forbidd" + ] + }, + { + "id": "cdn-examples-api-key-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 327, + "line_end": 340, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "expected", + "api", + "key", + "api_key", + "secret.", + "checks", + "x-api-key", + "request", + "header.", + "returns", + "401", + "unauthorized", + "header", + "missing.", + "403", + "forbidden", + "match.", + "success", + "clears", + "forwarding", + "upstream", + "origin", + "proxy-wasm", + ".remove", + "sets", + "value", + "empty", + "string", + "rather", + "deleting", + "simpler", + "alternative", + "jwt", + "validation", + "you" + ] + }, + { + "id": "cdn-examples-api-key-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 341, + "line_end": 348, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "api_key", + "secret", + "expected", + "api", + "key", + "value" + ] + }, + { + "id": "cdn-examples-api-key-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 349, + "line_end": 362, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "apikey.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "apikey-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-api-key-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 363, + "line_end": 367, + "keywords": [ + "deploy", + "upload", + "build", + "apikey.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "api_key", + "secret", + "application", + "settings." + ] } ] }, @@ -2374,7 +2594,7 @@ { "id": "cdn-examples-auth-jwt-as", "title": "JWT Validation — CDN App (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md", "skill": "fastedge-docs", "category": "examples", @@ -2398,9 +2618,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2411,7 +2631,7 @@ "example", "app_type" ], - "content_sha256": "6facd62118f8f4e03232cb0574acfa59cfdf3cd113a070d7d46847c6e62084dd", + "content_sha256": "c9792a91f6b66ee092a92fc63a8c0a961358dc9ee9cd2b42f8e0d5a9791869d5", "sections": [ { "id": "cdn-examples-auth-jwt-as#introduction", @@ -2428,9 +2648,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2520,7 +2740,7 @@ "level": 2, "anchor": "key-apis", "line_start": 57, - "line_end": 115, + "line_end": 116, "keywords": [ "key", "apis", @@ -2546,6 +2766,8 @@ "pass", "directly", "jwtverify", + "without", + "encoding", "typescript", "const", "send_http_response", @@ -2553,7 +2775,7 @@ "internal", "server", "error", - "string.utf8.encode" + "strin" ] }, { @@ -2561,8 +2783,8 @@ "heading": "Validation Flow", "level": 2, "anchor": "validation-flow", - "line_start": 116, - "line_end": 131, + "line_start": 117, + "line_end": 132, "keywords": [ "validation", "flow", @@ -2602,8 +2824,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 132, - "line_end": 146, + "line_start": 133, + "line_end": 147, "keywords": [ "error", "responses", @@ -2640,8 +2862,8 @@ "heading": "Required Secret", "level": 2, "anchor": "required-secret", - "line_start": 147, - "line_end": 156, + "line_start": 148, + "line_end": 157, "keywords": [ "required", "secret", @@ -2663,19 +2885,48 @@ "application", "deployment.", "rotation", - "see", + "use", "getsecreteffectiveat", + "instead", + "getsecret", + "see", "secrets", "reference." ] }, + { + "id": "cdn-examples-auth-jwt-as#testing-tokens", + "heading": "Testing Tokens", + "level": 2, + "anchor": "testing-tokens", + "line_start": 158, + "line_end": 173, + "keywords": [ + "testing", + "tokens", + "use", + "secret", + "a-string-secret-at-least-256-bits-long-thats-hard-to-break", + "expired", + "token", + "returns", + "403", + "forbidden", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiylcjlehaiojk3odmxmdg2mx0.egssdoddahz8kqee7be9n168cdewoioej96idm2c1yq", + "valid", + "expiry", + "2035-01-01", + "200", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte" + ] + }, { "id": "cdn-examples-auth-jwt-as#dependencies", "heading": "Dependencies", "level": 2, "anchor": "dependencies", - "line_start": 157, - "line_end": 171, + "line_start": 174, + "line_end": 196, "keywords": [ "dependencies", "json", @@ -2695,6 +2946,11 @@ "directly", "used", "example.", + "dev", + "assemblyscript", + "wasi-shim", + "0.1.0", + "0.28.9", "---" ] }, @@ -2703,8 +2959,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 172, - "line_end": 190, + "line_start": 197, + "line_end": 215, "keywords": [ "build", "pnpm", @@ -2738,8 +2994,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 191, - "line_end": 202, + "line_start": 216, + "line_end": 227, "keywords": [ "registration", "typescript", @@ -2761,8 +3017,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 203, - "line_end": 214, + "line_start": 228, + "line_end": 240, "keywords": [ "gotchas", "secret", @@ -2811,8 +3067,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 215, - "line_end": 221, + "line_start": 241, + "line_end": 247, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -3396,7 +3652,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3418,9 +3674,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3430,7 +3686,7 @@ "app_type", "languages" ], - "content_sha256": "131d3cb75bfb089b33860c1da8965a6ff2dbf7ba478f8e12cdb37384d5412f3e", + "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3447,9 +3703,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3876,6 +4132,139 @@ "body", "buffering" ] + }, + { + "id": "cdn-examples-body-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 341, + "line_end": 519, + "keywords": [ + "source", + "material", + "file", + "examples", + "body", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "buffertypevalues", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_buffer_bytes", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "set_property", + "stream_context" + ] + }, + { + "id": "cdn-examples-body-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 520, + "line_end": 534, + "keywords": [ + "onrequestheaders", + "removes", + "content-length", + "header", + "required", + "because", + "body", + "content", + "altered.", + "onrequestbody", + "buffers", + "full", + "request", + "checks", + "whether", + "contains", + "word", + "client", + "found", + "replaced", + "redaction", + "notice.", + "onresponseheaders", + "sets", + "transfer-encoding", + "chunked", + "captures", + "content-type", + "runtime", + "property.", + "onresponsebody", + "logs", + "url" + ] + }, + { + "id": "cdn-examples-body-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 535, + "line_end": 548, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "-----------------------", + "--------------------------------------------------", + "body.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "body-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-body-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 549, + "line_end": 553, + "keywords": [ + "deploy", + "upload", + "build", + "body.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application." + ] } ] }, @@ -4338,7 +4727,7 @@ { "id": "cdn-examples-cache-control-as", "title": "Cache Control — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md", "skill": "fastedge-docs", "category": "examples", @@ -4362,9 +4751,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4374,7 +4763,7 @@ "cache-control", "response-headers" ], - "content_sha256": "a088ac66592e155e209debe7d4c503dbc1cd638a3570ff06890f35c6258c1fa8", + "content_sha256": "1addef442e8b046f2689030c6ded25b7bd7ce8c303a7d46d5249db12d9955e1f", "sections": [ { "id": "cdn-examples-cache-control-as#introduction", @@ -4391,9 +4780,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4784,6 +5173,184 @@ "variable", "patterns" ] + }, + { + "id": "cdn-examples-cache-control-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 232, + "line_end": 376, + "keywords": [ + "source", + "material", + "file", + "examples", + "cachecontrol", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "cachecontrolroot", + "extends", + "createcontext", + "context_id", + "u32" + ] + }, + { + "id": "cdn-examples-cache-control-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 377, + "line_end": 390, + "keywords": [ + "onresponseheaders", + "app", + "inspects", + "content-type", + "response.status", + "apply", + "appropriate", + "caching", + "policy", + "content", + "type", + "cache", + "default", + "max-age", + "---", + "images", + "fonts", + "css", + "wasm", + "public", + "static_max_age", + "immutable", + "year", + "31536000s", + "text", + "html", + "html_max_age", + "must-revalidate", + "hour", + "3600s", + "application", + "json", + "xml", + "private", + "api_max_age", + "no-cache", + "no-" + ] + }, + { + "id": "cdn-examples-cache-control-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 391, + "line_end": 400, + "keywords": [ + "configuration", + "environment", + "variables", + "optional", + "sensible", + "defaults", + "applied", + "unset.", + "variable", + "default", + "description", + "----------", + "---------", + "-------------", + "static_max_age", + "31536000", + "max-age", + "static", + "assets", + "seconds", + "html_max_age", + "3600", + "html", + "responses", + "api_max_age", + "api", + "no-cache" + ] + }, + { + "id": "cdn-examples-cache-control-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 401, + "line_end": 414, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cachecontrol.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cachecontrol-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cache-control-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 415, + "line_end": 419, + "keywords": [ + "deploy", + "upload", + "build", + "cachecontrol.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "optionally", + "configure", + "max-age", + "environment", + "variables." + ] } ] }, @@ -5376,7 +5943,7 @@ { "id": "cdn-examples-convert-image-rust", "title": "CDN Example: Convert Image (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5400,7 +5967,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "example", "convert", "image", @@ -5410,7 +5977,7 @@ "avif", "format" ], - "content_sha256": "b2e6e63bcb77be6336941490ac3a834292ab5f939ac981b1d4d2e13942ece297", + "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5429,7 +5996,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "cdn", "example", "convert", @@ -5894,129 +6461,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-convert-image-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 206, - "line_end": 488, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "convert_image", - "src", - "lib.rs", - "rust", - "use", - "image", - "proxy_wasm", - "traits", - "types", - "std", - "env", - "varerror", - "cursor", - "str", - "from_utf8", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "convertimageroot", - "struct", - "impl", - "context", - "get_type", - "self" - ] - }, - { - "id": "cdn-examples-convert-image-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 489, - "line_end": 495, - "keywords": [ - "configuration", - "environment", - "variable", - "formats_to_transform", - "comma-separated", - "list", - "file", - "extensions", - "convert", - "e.g.", - "jpg", - "jpeg", - "png", - "ignored_ua_list", - "optional", - "user-agent", - "substrings", - "skip", - "avif_speed", - "avif", - "encoding", - "speed", - "1-10", - "default", - "avif_quality", - "quality", - "1-100" - ] - }, - { - "id": "cdn-examples-convert-image-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 496, - "line_end": 502, - "keywords": [ - "works", - "on_request_headers", - "checks", - "file", - "extension", - "user-agent", - "sets", - "image-format", - "header", - "cache", - "variation", - "on_response_headers", - "response", - "headers", - "avif", - "content", - "type", - "200", - "responses", - "on_response_body", - "decodes", - "original", - "image", - "re-encodes" - ] } ] }, { "id": "cdn-examples-cors-as", "title": "CORS — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6038,9 +6489,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "response-header", "injection", @@ -6050,7 +6501,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "9d28e66e98ae483455490d57761e0994771a24b3e869097c66a3190591c02e2e", + "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6067,9 +6518,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "assemblyscript", "cdn", @@ -6449,6 +6900,189 @@ "vary", "usage" ] + }, + { + "id": "cdn-examples-cors-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 197, + "line_end": 318, + "keywords": [ + "source", + "material", + "file", + "examples", + "cors", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "corsroot", + "extends", + "createcontext", + "context_id", + "u32", + "loglev" + ] + }, + { + "id": "cdn-examples-cors-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 319, + "line_end": 324, + "keywords": [ + "onresponseheaders", + "requests", + "allowed", + "origins", + "app", + "adds", + "access-control-allow-origin", + "vary", + "origin", + "response", + "headers.", + "optionally", + "exposes", + "additional", + "headers", + "via", + "access-control-expose-headers", + "disallowed", + "pass", + "unchanged", + "cors", + "added", + "note", + "options", + "preflights", + "fastedge", + "edge", + "layer", + "answers", + "preflight", + "directly", + "proxy-wasm", + "hooks", + "fire", + "options.", + "configure", + "behaviour", + "meth" + ] + }, + { + "id": "cdn-examples-cors-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 325, + "line_end": 333, + "keywords": [ + "configuration", + "set", + "following", + "environment", + "variables", + "your", + "fastedge", + "application", + "variable", + "example", + "description", + "----------", + "---------", + "-------------", + "allowed_origins", + "https", + "example.com", + "app.example.com", + "comma-separated", + "allowed", + "origins", + "required", + "expose_headers", + "x-request-id", + "x-trace-id", + "response", + "headers", + "expose", + "browser", + "optional" + ] + }, + { + "id": "cdn-examples-cors-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 334, + "line_end": 347, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cors.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cors-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cors-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 348, + "line_end": 352, + "keywords": [ + "deploy", + "upload", + "build", + "cors.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "allowed_origins", + "environment", + "variable", + "application", + "settings." + ] } ] }, @@ -7009,7 +7643,7 @@ { "id": "cdn-examples-custom-error-pages-as", "title": "Custom Error Pages — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7031,9 +7665,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7043,7 +7677,7 @@ "response-headers", "response-body" ], - "content_sha256": "bc71a12495f399f47434a5c8529feafb96e975dc762a1cd99a49ea8b8df2465f", + "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7060,9 +7694,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7248,7 +7882,7 @@ "level": 2, "anchor": "error-code-coverage", "line_start": 154, - "line_end": 182, + "line_end": 200, "keywords": [ "error", "code", @@ -7283,13 +7917,10 @@ "unavailable", "504", "5xx", - "categories", - "range", - "category", - "label", - "499", - "client", - "599" + "descriptions", + "description", + "understand", + "due" ] }, { @@ -7297,8 +7928,8 @@ "heading": "HTML Output Structure", "level": 2, "anchor": "html-output-structure", - "line_start": 183, - "line_end": 210, + "line_start": 201, + "line_end": 228, "keywords": [ "html", "output", @@ -7339,8 +7970,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 211, - "line_end": 229, + "line_start": 229, + "line_end": 247, "keywords": [ "build", "pnpm", @@ -7373,8 +8004,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 230, - "line_end": 235, + "line_start": 248, + "line_end": 253, "keywords": [ "deploy", "upload", @@ -7397,8 +8028,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 236, - "line_end": 248, + "line_start": 254, + "line_end": 266, "keywords": [ "constraints", "gotchas", @@ -7442,8 +8073,8 @@ "heading": "Imports", "level": 2, "anchor": "imports", - "line_start": 249, - "line_end": 270, + "line_start": 267, + "line_end": 288, "keywords": [ "imports", "import", @@ -7473,8 +8104,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 271, - "line_end": 277, + "line_start": 289, + "line_end": 295, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -7501,7 +8132,7 @@ { "id": "cdn-examples-custom-error-pages-rust", "title": "cdn-examples-custom-error-pages-rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7525,7 +8156,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "custom", "error", "pages", @@ -7535,7 +8166,7 @@ "5xx", "http" ], - "content_sha256": "e2e516ccf39ab64b58e9c2111f315294d4ae02c35a28c5ae42705b56fbce70e1", + "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", "sections": [ { "id": "cdn-examples-custom-error-pages-rust#introduction", @@ -7554,7 +8185,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -7563,7 +8194,7 @@ "level": 2, "anchor": "custom-error-pages-—-cdn-rust", "line_start": 10, - "line_end": 242, + "line_end": 297, "keywords": [ "custom", "error", @@ -7595,16 +8226,16 @@ "regardless", "origin", "returns.", - "---", - "api", - "patterns", - "logic", + "build", + "script", + "build.rs", "runs", - "inside", - "proxy_wasm", - "traits", - "httpcontext", - "implementation." + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] @@ -7612,7 +8243,7 @@ { "id": "cdn-examples-custom-rust", "title": "CDN Example: Custom — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7636,7 +8267,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -7646,7 +8277,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "45a2197a3910b5dbb1f0873d4726f360fe12ced198feef50a806e0d3c08505d9", + "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -7665,7 +8296,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -8047,13 +8678,59 @@ "overview", "platform-overview" ] + }, + { + "id": "cdn-examples-custom-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 373, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "custom", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "const", + "bad_request", + "u32", + "400", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id" + ] } ] }, { "id": "cdn-examples-env-secrets-as", "title": "Environment Variables and Secrets — CDN (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md", "skill": "fastedge-docs", "category": "examples", @@ -8075,9 +8752,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8088,7 +8765,7 @@ "secrets", "environment" ], - "content_sha256": "34d6cb6344af07831efe048bd621e270ce2ad9377a1645d26b883ede493e6cb5", + "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8105,9 +8782,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8518,6 +9195,221 @@ "handling", "guidelines" ] + }, + { + "id": "cdn-examples-env-secrets-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 229, + "line_end": 313, + "keywords": [ + "source", + "material", + "file", + "examples", + "variablesandsecrets", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "getsecret", + "setloglevel", + "fastedge" + ] + }, + { + "id": "cdn-examples-env-secrets-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 314, + "line_end": 326, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "username", + "environment", + "variable", + "using", + "getenv", + "password", + "secret", + "getsecret", + "logs", + "values", + "retrieved", + "without", + "logging", + "value", + "itself", + "injects", + "them", + "x-env-username", + "x-env-password", + "request", + "headers", + "upstream", + "receives", + "them.", + "useful", + "reference", + "understanding", + "access", + "variables", + "secrets", + "within", + "fastedge", + "plugin.", + "security", + "warning", + "neve" + ] + }, + { + "id": "cdn-examples-env-secrets-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 327, + "line_end": 335, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "----------", + "--------------------", + "--------------------------------------", + "username", + "environment", + "variable", + "value", + "forward", + "upstream", + "password", + "secret" + ] + }, + { + "id": "cdn-examples-env-secrets-as#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 336, + "line_end": 346, + "keywords": [ + "local", + "testing", + "fixture", + "fixtures", + "happy-path.test.json", + "uses", + "dotenv", + "enabled", + "true", + "load", + "values", + ".env", + "runner", + "maps", + "fastedge_var_env_", + "name", + "getenv", + "fastedge_var_secret_", + "getsecret", + "test", + "locally", + "visual", + "debugger", + "create", + "fastedge_var_env_username", + "my-username", + "fastedge_var_secret_password", + "my-password" + ] + }, + { + "id": "cdn-examples-env-secrets-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 347, + "line_end": 360, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "--------------------------------------", + "--------------------------------------------------", + "variablesandsecrets.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "variablesandsecrets-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-env-secrets-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 361, + "line_end": 365, + "keywords": [ + "deploy", + "upload", + "build", + "variablesandsecrets.wasm", + "fastedge", + "portal", + "https", + "portal.gcore.com", + "attach", + "your", + "cdn", + "application.", + "configure", + "username", + "environment", + "variable", + "password", + "secret", + "application", + "settings." + ] } ] }, @@ -11210,7 +12102,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11234,9 +12126,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11246,7 +12138,7 @@ "headers", "request-headers" ], - "content_sha256": "ce0dbc8a8e9aa6790dfde6e27b5a069105063a9725c0593334e625084387b9f7", + "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11263,9 +12155,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11572,13 +12464,82 @@ "hea" ] }, + { + "id": "cdn-examples-headers-as#multi-value-headers", + "heading": "Multi-Value Headers", + "level": 2, + "anchor": "multi-value-headers", + "line_start": 202, + "line_end": 205, + "keywords": [ + "multi-value", + "headers", + "new-header-03", + "deliberately", + "added", + "twice", + "add", + "called", + "same", + "name", + "twice.", + "produces", + "header", + "two", + "separate", + "entries.", + "validation", + "pattern", + "uses", + "set", + "string", + "value", + "pairs", + "assert", + "values", + "present." + ] + }, + { + "id": "cdn-examples-headers-as#cross-phase-response-header-writes", + "heading": "Cross-Phase Response Header Writes", + "level": 2, + "anchor": "cross-phase-response-header-writes", + "line_start": 206, + "line_end": 212, + "keywords": [ + "cross-phase", + "response", + "header", + "writes", + "stream_context.headers.response.add", + "...", + "called", + "onrequestheaders", + "headers", + "written", + "request", + "phase", + "appear", + "final", + "alongside", + "set", + "onresponseheaders", + "distinction", + "new-response-header", + "value-01", + "safe", + "stream_context.headers.response.get", + "panics" + ] + }, { "id": "cdn-examples-headers-as#error-response-codes-used", "heading": "Error Response Codes Used", "level": 2, "anchor": "error-response-codes-used", - "line_start": 202, - "line_end": 211, + "line_start": 213, + "line_end": 222, "keywords": [ "error", "response", @@ -11611,8 +12572,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 212, - "line_end": 235, + "line_start": 223, + "line_end": 246, "keywords": [ "logging", "log", @@ -11651,8 +12612,8 @@ "heading": "Known Issues", "level": 2, "anchor": "known-issues", - "line_start": 236, - "line_end": 245, + "line_start": 247, + "line_end": 254, "keywords": [ "known", "issues", @@ -11694,8 +12655,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 246, - "line_end": 265, + "line_start": 255, + "line_end": 274, "keywords": [ "build", "pnpm", @@ -11730,8 +12691,8 @@ "heading": "Deployment", "level": 2, "anchor": "deployment", - "line_start": 266, - "line_end": 269, + "line_start": 275, + "line_end": 278, "keywords": [ "deployment", "upload", @@ -11752,8 +12713,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 270, - "line_end": 276, + "line_start": 279, + "line_end": 285, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -11774,7 +12735,7 @@ { "id": "cdn-examples-headers-rust", "title": "Headers — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -11798,7 +12759,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -11808,7 +12769,7 @@ "headers", "request-headers" ], - "content_sha256": "9b8fa82906d3a7e8498f68496bb247ca4f942000fddfef9a2e4bf8b65faf16cc", + "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -11827,7 +12788,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -12167,13 +13128,58 @@ "abi", "surface" ] + }, + { + "id": "cdn-examples-headers-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 234, + "line_end": 586, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "headers", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "collections", + "hashset", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "context_id", + "u32", + "option", + "httpcont" + ] } ] }, { "id": "cdn-examples-http-call-as", "title": "cdn-examples-http-call-as", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12197,9 +13203,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "overview", "httpcall", "dispatches", @@ -12210,7 +13216,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "11691976691add5d7914e80eff68a71b36c33bc8d12beac13c0bbacdb1576e55", + "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12227,9 +13233,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20" + "2026-08-17" ] }, { @@ -12328,7 +13334,7 @@ "level": 2, "anchor": "common-patterns", "line_start": 102, - "line_end": 262, + "line_end": 286, "keywords": [ "common", "patterns", @@ -12360,13 +13366,60 @@ "proxy-wasm-sdk" ] }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 287, + "line_end": 306, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps", + "dependencies", + "package.json", + "package", + "role", + "---------", + "gcoredev", + "proxy-wasm-sdk-as", + "sdk", + "required", + "runtime", + "dep", + "1.2.3", + "assemblyscript", + "compiler", + "0.28.9", + "wasi-shim", + "wasi", + "compatibility" + ] + }, { "id": "cdn-examples-http-call-as#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 263, - "line_end": 273, + "line_start": 307, + "line_end": 318, "keywords": [ "gotchas", "closures", @@ -12410,8 +13463,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 274, - "line_end": 279, + "line_start": 319, + "line_end": 324, "keywords": [ "related", "sdk", @@ -12460,7 +13513,7 @@ { "id": "cdn-examples-http-call-rust", "title": "HTTP Call — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12484,7 +13537,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "makes", @@ -12494,7 +13547,7 @@ "services", "timeout" ], - "content_sha256": "a877fd720fb921fd1b8c38d69e81717621c2c7ffa28ee41f98bbded108fea44c", + "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -12513,7 +13566,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "cdn", @@ -12909,13 +13962,54 @@ "resume_http_request" ] }, + { + "id": "cdn-examples-http-call-rust#complete-example", + "heading": "Complete Example", + "level": 2, + "anchor": "complete-example", + "line_start": 247, + "line_end": 359, + "keywords": [ + "complete", + "example", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id", + "u32", + "option", + "httpcontext", + "httpheaders", + "sta" + ] + }, { "id": "cdn-examples-http-call-rust#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 247, - "line_end": 257, + "line_start": 360, + "line_end": 370, "keywords": [ "gotchas", "action", @@ -12964,8 +14058,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 258, - "line_end": 264, + "line_start": 371, + "line_end": 377, "keywords": [ "see", "proxy-wasm", @@ -12986,58 +14080,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-http-call-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 265, - "line_end": 408, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "http_call", - "src", - "lib.rs", - "rust", - "use", - "proxy_wasm", - "traits", - "types", - "std", - "time", - "duration", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "httpheadersroot", - "struct", - "impl", - "context", - "create_http_context", - "self", - "_context_id", - "u32", - "option", - "httpcontext" - ] } ] }, { "id": "cdn-examples-kv-store-as", "title": "KV Store — AssemblyScript CDN Example", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13061,9 +14110,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13073,7 +14122,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "5818392d7f01341518fb38b62a6267eece48af77d2334793bc92cda935fb596a", + "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -13090,9 +14139,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13631,7 +14680,7 @@ "level": 2, "anchor": "gotchas", "line_start": 316, - "line_end": 326, + "line_end": 328, "keywords": [ "gotchas", "kvstore.get", @@ -13675,8 +14724,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 327, - "line_end": 332, + "line_start": 329, + "line_end": 334, "keywords": [ "see", "kvstore", @@ -15332,7 +16381,7 @@ { "id": "cdn-examples-md2html-rust", "title": "CDN Example: Markdown to HTML (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15356,7 +16405,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15366,7 +16415,7 @@ "response-body-transform", "request-path-rewrite" ], - "content_sha256": "e22ac7622cc26345ca2016244b7a82727792c8c2294d27f9cfa4c87008fafd64", + "content_sha256": "796fc091efb4397c28b107d9b27eda72793c58776c4ecb1f23300362f6b68f86", "sections": [ { "id": "cdn-examples-md2html-rust#introduction", @@ -15385,7 +16434,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15814,7 +16863,7 @@ { "id": "cdn-examples-properties-as", "title": "CDN Runtime Properties — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-properties-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15836,9 +16885,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "runtime", "properties", "language", @@ -15848,7 +16897,7 @@ "type", "overview" ], - "content_sha256": "92e45ac0e88b8037fef119fc2a3a3010b2409de712b9b40a8a1db75a1b75b5a4", + "content_sha256": "487273a2f72dca3787758f1b60af9110a2c173879ba02842d23c21af59930534", "sections": [ { "id": "cdn-examples-properties-as#introduction", @@ -15865,9 +16914,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cdn", "runtime", "properties", @@ -16051,7 +17100,7 @@ "level": 2, "anchor": "usage-patterns", "line_start": 86, - "line_end": 138, + "line_end": 147, "keywords": [ "usage", "patterns", @@ -16088,8 +17137,8 @@ "heading": "Complete Example Structure", "level": 2, "anchor": "complete-example-structure", - "line_start": 139, - "line_end": 236, + "line_start": 148, + "line_end": 314, "keywords": [ "complete", "example", @@ -16127,8 +17176,8 @@ "heading": "Error Handling Pattern", "level": 2, "anchor": "error-handling-pattern", - "line_start": 237, - "line_end": 268, + "line_start": 315, + "line_end": 346, "keywords": [ "error", "handling", @@ -16173,8 +17222,8 @@ "heading": "Expected Output", "level": 2, "anchor": "expected-output", - "line_start": 269, - "line_end": 288, + "line_start": 347, + "line_end": 366, "keywords": [ "expected", "output", @@ -16211,8 +17260,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 312, + "line_start": 367, + "line_end": 390, "keywords": [ "build", "pnpm", @@ -16254,8 +17303,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 313, - "line_end": 318, + "line_start": 391, + "line_end": 396, "keywords": [ "deploy", "upload", @@ -16277,8 +17326,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 319, - "line_end": 331, + "line_start": 397, + "line_end": 410, "keywords": [ "gotchas", "get_property", @@ -16327,8 +17376,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 332, - "line_end": 338, + "line_start": 411, + "line_end": 417, "keywords": [ "see", "cdn", @@ -17091,7 +18140,7 @@ { "id": "dotenv", "title": "Dotenv — Runtime Secrets and Environment Variables", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/dotenv.md", "skill": "test", "category": "testing", @@ -17108,11 +18157,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17123,7 +18172,7 @@ "debugger", "inject" ], - "content_sha256": "cae196f2afaa09f7a84db329df3c77208afb1e4e206ad0e023cd22fef12f6491", + "content_sha256": "c7c39cf06d60e6c2cfb502b636db8a85f3ad05d79ed5acbcc9ded2e54390e567", "sections": [ { "id": "dotenv#introduction", @@ -17138,11 +18187,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17708,7 +18757,7 @@ { "id": "http-examples-ab-testing-js", "title": "A/B Testing — FastEdge Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -17732,7 +18781,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example", @@ -17743,7 +18792,7 @@ "visitors", "test" ], - "content_sha256": "e840de6f715610ad3709f719b391b7dab00230196769178c24e05dd501f87a85", + "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -17762,7 +18811,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example" @@ -18227,6 +19276,47 @@ "keys", "vary" ] + }, + { + "id": "http-examples-ab-testing-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 211, + "line_end": 328, + "keywords": [ + "source", + "material", + "file", + "examples", + "ab-testing", + "src", + "index.js", + "import", + "getenv", + "fastedge", + "env", + "const", + "testconfig", + "logo", + "variant", + "hops", + "weight", + "bottle", + "font", + "exo2", + "gloria", + "standard", + "async", + "function", + "eventhandler", + "request", + "xid", + "slicedheaders", + "sliceabtestidfromcookie", + "headers", + "createabtesthe" + ] } ] }, @@ -19695,7 +20785,7 @@ { "id": "http-examples-backend-basic-rust", "title": "Example: Backend (URL Proxy) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19719,7 +20809,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -19729,7 +20819,7 @@ "outbound-http", "query-params" ], - "content_sha256": "a2b700d31b619ab571bf016b276a2063db60d54b0a63308088097e1a1b5f4446", + "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -19748,7 +20838,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20170,207 +21260,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20394,7 +21290,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20404,7 +21300,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "19e3d3e017e89bd5e2f2d3cc57067a1b5b2d83502c762e0ada31536984710a19", + "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -20423,7 +21319,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20876,6 +21772,188 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 281, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "bloom_filter_denylist", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "bloom-filter", + "denylist", + "example.", + "checks", + "client", + "x-real-ip", + "request", + "header", + "falling", + "back", + "x-forwarded-for", + "against", + "bloom", + "filter", + "stored", + "fastedge", + "kv.", + "returns", + "403", + "hit", + "200", + "otherwise.", + "required", + "configuration", + "environment" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 282, + "line_end": 287, + "keywords": [ + "configuration", + "environment", + "variable", + "denylist_store", + "name", + "store", + "holds", + "bloom", + "filter.", + "bloom-filter", + "key", + "hardcoded", + "blocked-ips", + "change", + "bloom_key", + "src", + "lib.rs", + "your", + "different." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#behaviour", + "heading": "Behaviour", + "level": 2, + "anchor": "behaviour", + "line_start": 288, + "line_end": 294, + "keywords": [ + "behaviour", + "bf_exists", + "blocked-ips", + "response", + "---", + "true", + "403", + "allowed", + "false", + "...", + "200" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#tradeoff-false-positives", + "heading": "Tradeoff: false positives", + "level": 2, + "anchor": "tradeoff-false-positives", + "line_start": 295, + "line_end": 302, + "keywords": [ + "tradeoff", + "false", + "positives", + "bloom", + "filters", + "answer", + "definitely", + "set", + "maybe", + "bf_exists", + "returns", + "true", + "probably", + "added", + "small", + "fraction", + "hits", + "meaning", + "legitimate", + "ips", + "over-blocked.", + "acceptable", + "denylist", + "allowlists", + "anything", + "requiring", + "exact", + "membership", + "use", + "store.get", + "against", + "regular", + "key", + "instead." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#populating-the-filter", + "heading": "Populating the filter", + "level": 2, + "anchor": "populating-the-filter", + "line_start": 303, + "line_end": 307, + "keywords": [ + "populating", + "filter", + "edge", + "handler", + "read-only.", + "populate", + "blocked-ips", + "out", + "band", + "example", + "via", + "fastedge", + "api" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 308, + "line_end": 312, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "bloom-filter-denylist" + ] } ] }, @@ -21724,7 +22802,7 @@ { "id": "http-examples-cache-wasi-rust", "title": "Cache (WASI) — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21748,7 +22826,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -21758,7 +22836,7 @@ "cache", "outbound-http" ], - "content_sha256": "e0929deee35dbe662b0a26183b2655f95b7796526be190276c8e4ca0bce0eddf", + "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -21777,7 +22855,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22126,13 +23204,201 @@ "workflow", "skill" ] + }, + { + "id": "http-examples-cache-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 192, + "line_end": 359, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "response", + "caching", + "purging", + "via", + "interface.", + "reads", + "origin_host", + "environment", + "forwards", + "incoming", + "request", + "origin", + "caches", + "body", + "keyed", + "path.", + "subsequent", + "requests", + "same", + "path", + "cached", + "returned" + ] + }, + { + "id": "http-examples-cache-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 360, + "line_end": 366, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "origin_host", + "yes", + "base", + "url", + "upstream", + "origin", + "e.g.", + "https", + "api.example.com", + "returns", + "500", + "unset.", + "cache_ttl_ms", + "long", + "cache", + "responses", + "milliseconds.", + "default", + "60000" + ] + }, + { + "id": "http-examples-cache-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 367, + "line_end": 373, + "keywords": [ + "works", + "get", + "data", + "cache", + "miss", + "forward", + "origin_host", + "2xx", + "body", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "path", + "query", + "only", + "responses", + "origin", + "error", + "pass", + "without", + "stored.", + "response", + "headers", + "replayed", + "cache-hit", + "use", + "content-type", + "application", + "octet-stream", + "sinc" + ] + }, + { + "id": "http-examples-cache-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 374, + "line_end": 378, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "cache_wasi.wasm" + ] + }, + { + "id": "http-examples-cache-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 379, + "line_end": 385, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry", + "wstd", + "http", + "client", + "new", + ".send", + "req", + ".await", + "async", + "outbound", + "request", + "origin" + ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22156,7 +23422,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22166,7 +23432,7 @@ "languages", "diagnostic" ], - "content_sha256": "07465c714cb1fa963a3f6a8ff6759449d473393481ff4aac12103dbef85e899f", + "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -22185,7 +23451,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22686,125 +23952,342 @@ "store", "usage" ] - } - ] - }, - { - "id": "http-examples-fetch-js", - "title": "http-examples-fetch-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-fetch-js.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "http" - ], - "languages": [ - "javascript" - ], - "tags": [ - "examples", - "fastedge-docs", - "http", - "javascript", - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23", - "fetch", - "outbound", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url" - ], - "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", - "sections": [ + }, { - "id": "http-examples-fetch-js#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, + "id": "http-examples-diagnostic-logging-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 194, + "line_end": 307, "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23" + "source", + "material", + "file", + "examples", + "http", + "wasi", + "diagnostic_logging", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "diagnostic", + "logging", + "example.", + "tiny", + "pass-through", + "proxy", + "writes", + "single", + "set_user_diag", + "tag", + "per", + "request", + "summarising", + "outcome", + "config_error", + "origin_unreachable", + "proxied", + "appears", + "fastedge", + "platform", + "per-request", + "log", + "viewer", + "distinct", + "stdout" ] }, { - "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", - "heading": "fetch — Outbound HTTP Requests", + "id": "http-examples-diagnostic-logging-wasi-rust#configuration", + "heading": "Configuration", "level": 2, - "anchor": "fetch-—-outbound-http-requests", - "line_start": 10, - "line_end": 93, + "anchor": "configuration", + "line_start": 308, + "line_end": 311, "keywords": [ - "fetch", - "outbound", - "http", + "configuration", + "origin_url", + "environment", + "variable", + "origin", "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url", - "options", - "api.", - "runtime", - "exposes", - "subset", - "browser", - "api", - "sufficient", - "downstream", - "service", - "calls.", + "proxied", + "to." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", + "heading": "Outcomes", + "level": 2, + "anchor": "outcomes", + "line_start": 312, + "line_end": 321, + "keywords": [ + "outcomes", + "request", + "writes", + "exactly", + "one", + "condition", + "tag", "---", - "signature", - "string", - "requestinit", - "promise", - "response", - "parameters", - "parameter", - "type", - "required", - "description", - "-----------", - "------", - "----------", - "-------------", - "yes", - "absolute", - "resou" + "origin_url", + "missing", + "outcome", + "config_error", + "reason", + "origin_missing", + "origin", + "unreachable", + "origin_unreachable", + "method", + "path", + "err", + "proxied", + "status" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", + "heading": "`set_user_diag` vs `println!`", + "level": 2, + "anchor": "set_user_diag-vs-println", + "line_start": 322, + "line_end": 330, + "keywords": [ + "set_user_diag", + "println", + "---", + "channel", + "stdout", + "general", + "application", + "logs", + "per-request", + "structured", + "tag", + "platform", + "log", + "viewer", + "cardinality", + "many", + "per", + "request", + "one", + "multiple", + "calls", + "leave", + "only", + "last", + "concatenated", + "undefined", + "best", + "verbose", + "traces", + "debug", + "details", + "single", + "filterable", + "outcome", + "label", + "forbidden", + "secrets", + "pii", + "tags", + "appear" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#convention", + "heading": "Convention", + "level": 2, + "anchor": "convention", + "line_start": 331, + "line_end": 336, + "keywords": [ + "convention", + "call", + "set_user_diag", + "once", + "branch", + "late", + "enough", + "handler", + "know", + "outcome.", + "logfmt", + "-ish", + "format", + "outcome", + "verb", + "key", + "value", + "easy", + "slice", + "log", + "search", + "tooling", + "keep", + "keys", + "short", + "stable", + "they", + "make", + "good", + "filter", + "terms." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 337, + "line_end": 342, + "keywords": [ + "related", + "cdn", + "proxy-wasm", + "variant", + "fastedge", + "proxywasm", + "utils", + "set_user_diag", + "same", + "semantics", + "different", + "module", + "path.", + "see", + "docs", + "cdn_apps.md" ] } ] }, { - "id": "http-examples-geo-redirect-js", - "title": "http-examples-geo-redirect-js", + "id": "http-examples-fetch-js", + "title": "http-examples-fetch-js", "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-fetch-js.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "http" + ], + "languages": [ + "javascript" + ], + "tags": [ + "examples", + "fastedge-docs", + "http", + "javascript", + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23", + "fetch", + "outbound", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url" + ], + "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", + "sections": [ + { + "id": "http-examples-fetch-js#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23" + ] + }, + { + "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", + "heading": "fetch — Outbound HTTP Requests", + "level": 2, + "anchor": "fetch-—-outbound-http-requests", + "line_start": 10, + "line_end": 93, + "keywords": [ + "fetch", + "outbound", + "http", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url", + "options", + "api.", + "runtime", + "exposes", + "subset", + "browser", + "api", + "sufficient", + "downstream", + "service", + "calls.", + "---", + "signature", + "string", + "requestinit", + "promise", + "response", + "parameters", + "parameter", + "type", + "required", + "description", + "-----------", + "------", + "----------", + "-------------", + "yes", + "absolute", + "resou" + ] + } + ] + }, + { + "id": "http-examples-geo-redirect-js", + "title": "http-examples-geo-redirect-js", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -22916,7 +24399,7 @@ { "id": "http-examples-geo-redirect-wasi-rust", "title": "Geo Redirect — WASI HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22940,7 +24423,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22950,7 +24433,7 @@ "geo-redirect", "headers" ], - "content_sha256": "77ed57c49c605d2b3301511ccb2591fd39dc6726111761bc541c9229ec53ca23", + "content_sha256": "5c4595ed117a31ae93362494563368610e2f957a6ae0f6fcdd2e9d9ca9570e2a", "sections": [ { "id": "http-examples-geo-redirect-wasi-rust#introduction", @@ -22969,7 +24452,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -23375,6 +24858,167 @@ "examples-env-vars-wasi-rust", "access" ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 149, + "line_end": 232, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "geo_redirect", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "wasi-http", + "app", + "demonstrating", + "geo-based", + "redirects.", + "reads", + "country", + "code", + "geoip-country-code", + "request", + "header", + "redirects", + "country-specific", + "origin", + "url.", + "falls", + "back", + "base_origin", + "mapping", + "configured.", + "required", + "configuration", + "environment", + "variable" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 233, + "line_end": 239, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "base_origin", + "yes", + "fallback", + "redirect", + "url", + "e.g.", + "https", + "example.com", + "returns", + "500", + "unset.", + "country_code", + "per-country", + "keyed", + "2-letter", + "country", + "code", + "falls", + "back", + "set." + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 240, + "line_end": 248, + "keywords": [ + "works", + "geoip-country-code", + "env", + "var", + "set", + "302", + "value", + "base_origin", + "header", + "500" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 249, + "line_end": 255, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "geo_redirect_wasi.wasm" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 256, + "line_end": 262, + "keywords": [ + "apis", + "used", + "request.headers", + ".get", + "geoip-country-code", + "read", + "geo", + "header", + "injected", + "fastedge", + "edge", + "std", + "env", + "var", + "country_code", + "dynamic", + "lookup", + "country", + "code", + "response", + "builder", + ".status", + "302", + ".header", + "location", + "url", + "redirect" + ] } ] }, @@ -24569,7 +26213,7 @@ { "id": "http-examples-hello-world-wasi-rust", "title": "Example: Hello World (WASI) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24593,7 +26237,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24603,7 +26247,7 @@ "hello-world", "wasi" ], - "content_sha256": "1777edec022e50301971e706c675717cea56db7b51e3e487b9379ceb00d95d08", + "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -24622,7 +26266,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -25015,6 +26659,127 @@ "base", "rust" ] + }, + { + "id": "http-examples-hello-world-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 186, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "hello_world", + "src", + "lib.rs", + "rust", + "use", + "wstd", + "body", + "request", + "response", + "http_server", + "async", + "main", + "anyhow", + "result", + "let", + "url", + "request.uri", + ".to_string", + "builder", + ".status", + "200", + ".header", + "content-type", + "text", + "plain", + "charset", + "utf-8", + ".body", + "format", + "hello", + "you", + "made" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 187, + "line_end": 195, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "plain", + "charset", + "utf-8", + "hello", + "you", + "made", + "wasi", + "request", + "host", + "path", + "query" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 196, + "line_end": 202, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "hello_world.wasm" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 203, + "line_end": 210, + "keywords": [ + "apis", + "used", + "wstd", + "http_server", + "wasi", + "http", + "entry-point", + "macro", + "request", + "response", + "types", + "body", + "construction", + "request.uri", + ".to_string", + "full", + "absolute", + "uri" + ] } ] }, @@ -25302,7 +27067,7 @@ { "id": "http-examples-kv-store-js", "title": "http-examples-kv-store-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-js.md", "skill": "fastedge-docs", "category": "examples", @@ -25326,7 +27091,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "store", "example", "reference", @@ -25338,7 +27103,7 @@ "fastedge", "edge" ], - "content_sha256": "1ffcb44a8617f30c59fe878b41487d54f63062a0862ae4578a2ad41f1016b76d", + "content_sha256": "d3c4dbab0c624edfd7e3590c6413091cc12881c8a06e43b9a74fe7008ce7e515", "sections": [ { "id": "http-examples-kv-store-js#introduction", @@ -25357,7 +27122,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -25366,7 +27131,7 @@ "level": 2, "anchor": "kv-store-—-example-reference", "line_start": 10, - "line_end": 302, + "line_end": 332, "keywords": [ "store", "example", @@ -25413,7 +27178,7 @@ { "id": "http-examples-kv-store-wasi-rust", "title": "KV Store — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25437,7 +27202,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25447,7 +27212,7 @@ "languages", "store" ], - "content_sha256": "b6fb89540d2ef2384d671a94a18bf815537190adacbc1f2e03b861eae1e5ff83", + "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -25466,7 +27231,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25893,6 +27658,222 @@ "javascript", "equivalent" ] + }, + { + "id": "http-examples-kv-store-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 382, + "line_end": 605, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "key_value", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "store", + "operations", + "via", + "wasi-http", + "interface.", + "supports", + "query", + "parameters", + "name", + "action", + "get", + "key", + "scan", + "match", + "pattern", + "zrange", + "min", + "f64", + "max", + "zscan", + "bfexists" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 606, + "line_end": 619, + "keywords": [ + "usage", + "operations", + "require", + "store", + "name", + "action", + "query", + "parameters", + "additional", + "params", + "description", + "---", + "get", + "key", + "fetch", + "single", + "value", + "scan", + "match", + "pattern", + "list", + "keys", + "matching", + "glob", + "zrange", + "min", + "f64", + "max", + "sorted-set", + "members", + "score", + "range", + "zscan", + "bfexists", + "item", + "check", + "bloom", + "filt" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 620, + "line_end": 629, + "keywords": [ + "example", + "get", + "store", + "my-store", + "action", + "key", + "hello", + "200", + "response", + "world", + "scan", + "match", + "user" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#error-responses", + "heading": "Error responses", + "level": 2, + "anchor": "error-responses", + "line_start": 630, + "line_end": 638, + "keywords": [ + "error", + "responses", + "condition", + "status", + "body", + "---", + "store", + "found", + "access", + "denied", + "403", + "missing", + "required", + "params", + "530", + "runtime", + "open", + "500", + "...", + "invalid", + "action", + "400", + "supported" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 639, + "line_end": 645, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "key_value_wasi.wasm" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 646, + "line_end": 655, + "keywords": [ + "apis", + "used", + "fastedge", + "key_value", + "store", + "open", + "name", + "named", + "store.get", + "key", + "fetch", + "value", + "returns", + "option", + "vec", + "store.scan", + "pattern", + "list", + "keys", + "glob", + "store.zrange_by_score", + "min", + "max", + "range", + "query", + "sorted", + "set", + "store.zscan", + "scan", + "store.bf_exists", + "item", + "bloom", + "filter", + "membership", + "test" + ] } ] }, @@ -26288,7 +28269,7 @@ { "id": "http-examples-markdown-render-basic-rust", "title": "HTTP Example: Markdown Render (Rust, Basic)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26312,7 +28293,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26322,7 +28303,7 @@ "outbound-fetch", "env-vars" ], - "content_sha256": "f72f4be89063520134023c34d38ba6d901f66977bbeab889a4f8640e36fa288b", + "content_sha256": "2c31f5e301b8fd9c5eede29a285d26452eeb1606d1d009ebb4bbe5d5bc1fafc9", "sections": [ { "id": "http-examples-markdown-render-basic-rust#introduction", @@ -26341,7 +28322,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26472,6 +28453,8 @@ "var", "strip", "trailing", + "via", + "base.trim_end_matches", "validate", "request", "path", @@ -26483,27 +28466,25 @@ "fastedge", "call", "send_request", - "via", "internal", "helper.", "follow", "redirects", "max_redirects", - "hops.", + "hops", + "request_inner", + "req", + "depth", "decode", "response", "body", "utf-8", + "string", + "from_utf8", + "rsp.body", + ".to_vec", "failure", - "500", - "parse", - "markdown", - "parser", - "new_ext", - "options", - "enable_tables", - "enable_footnotes", - "render" + "500" ] }, { @@ -26728,7 +28709,7 @@ "level": 2, "anchor": "gotchas", "line_start": 189, - "line_end": 198, + "line_end": 199, "keywords": [ "gotchas", "base.trim_end_matches", @@ -26777,8 +28758,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 199, - "line_end": 206, + "line_start": 200, + "line_end": 207, "keywords": [ "see", "fastedge-sdk-rust", @@ -26814,7 +28795,7 @@ { "id": "http-examples-outbound-fetch-basic-rust", "title": "Example: Outbound Fetch — Basic HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26838,7 +28819,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26848,7 +28829,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "700a80efb4a443683f2d328b4b720fcbb8bdcfacaa14c271fd4afa30db17ae2d", + "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -26867,7 +28848,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27235,6 +29216,171 @@ "target", "context" ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 190, + "line_end": 262, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "outbound_fetch", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "fastedge", + "body", + "request", + "response", + "statuscode", + "serde_json", + "json", + "value", + "main", + "_req", + "let", + "upstream_req", + "builder", + ".uri", + "jsonplaceholder.typicode.com", + "users", + ".body", + "empty", + "upstream_resp", + "send_request", + ".map_err" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 263, + "line_end": 282, + "keywords": [ + "incoming", + "request", + "makes", + "get", + "http", + "jsonplaceholder.typicode.com", + "users", + "using", + "fastedge", + "send_request", + "parses", + "json", + "response", + "body.", + "takes", + "first", + "array.", + "returns", + "envelope", + "pagination", + "metadata.", + "example", + "body", + "name", + "leanne", + "graham", + "...", + "total", + "skip", + "limit" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 283, + "line_end": 292, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "send_request", + "outbound", + "request", + "upstream", + "response", + "statuscode", + "types", + "body", + "bodies", + "serde_json", + "json", + "parsing", + "serialisation" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 293, + "line_end": 299, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "outbound_fetch.wasm" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 300, + "line_end": 306, + "keywords": [ + "expected", + "behavior", + "request", + "response", + "status", + "content-type", + "body", + "---", + "get", + "200", + "application", + "json", + "object", + "users", + "array", + "total", + "skip", + "limit" + ] } ] }, @@ -27691,7 +29837,7 @@ { "id": "http-examples-outbound-modify-response-wasi-rust", "title": "Example: Outbound Modify Response (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27715,7 +29861,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27725,7 +29871,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "17e973e59daaef6de41a976cc9c3cbb1c037242bd84aee527c2d6a840100df18", + "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -27744,7 +29890,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -28060,6 +30206,78 @@ "serde_json", "crate" ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 277, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "outbound_modify_response", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "outbound", + "fetch", + "response", + "transformation.", + "fetches", + "json", + "upstream", + "origin", + "reads", + "parses", + "body", + "reshapes", + "new", + "object", + "first", + "users", + "pagination", + "metadata", + "returns", + "fresh", + "content-type", + "application", + "header.", + "stepping-stone", + "beyond" + ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 278, + "line_end": 284, + "keywords": [ + "related", + "outbound_fetch", + "simpler", + "variant", + "passes", + "upstream", + "response", + "unchanged.", + "mirror", + "fastedge-sdk-js", + "examples", + "outbound-modify-response" + ] } ] }, @@ -29212,7 +31430,7 @@ { "id": "http-examples-s3upload-basic-rust", "title": "S3 Upload — HTTP Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29236,7 +31454,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29246,7 +31464,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "16c81799dc962bed122dbb754b1370cc70719e1fb425adbb9b343a066ddea4fc", + "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -29265,7 +31483,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29630,7 +31848,7 @@ "level": 2, "anchor": "key-constraints-and-gotchas", "line_start": 166, - "line_end": 176, + "line_end": 177, "keywords": [ "key", "constraints", @@ -29679,8 +31897,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 177, - "line_end": 184, + "line_start": 178, + "line_end": 185, "keywords": [ "see", "fastedge-docs", @@ -29710,7 +31928,7 @@ { "id": "http-examples-secret-basic-rust", "title": "Secret Access — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29734,7 +31952,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "example", @@ -29744,7 +31962,7 @@ "fastedge", "platform" ], - "content_sha256": "3036554dd9530db71002d821ac1c653a53831fbcaf9598cd6ac93742ef7edba8", + "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -29763,7 +31981,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "rust", @@ -30153,6 +32371,277 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 370, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "secret", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "std", + "time", + "systemtime", + "fastedge", + "body", + "request", + "response", + "statuscode", + "allow", + "dead_code", + "main", + "_req", + "let", + "value", + "match", + "get", + "err", + "accessdenied", + "return", + "builder" + ] + }, + { + "id": "http-examples-secret-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 371, + "line_end": 380, + "keywords": [ + "request", + "handler", + "calls", + "secret", + "get", + "retrieves", + "current", + "value", + "named", + "missing", + "returned", + "none", + "returns", + "404", + "get_effective_at", + "unix_ts", + "effective", + "unix", + "timestamp", + "demonstrating", + "rotation", + "versioning", + "api.", + "success", + "200", + "values", + "body", + "debug" + ] + }, + { + "id": "http-examples-secret-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 381, + "line_end": 391, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "secret", + "get", + "key", + "retrieve", + "current", + "value", + "named", + "get_effective_at", + "timestamp", + "effective", + "specific", + "unix", + "error", + "variants", + "accessdenied", + "msg", + "decrypterror", + "request", + "response", + "statuscode", + "types", + "body", + "respo" + ] + }, + { + "id": "http-examples-secret-basic-rust#secret-error-variants", + "heading": "Secret error variants", + "level": 2, + "anchor": "secret-error-variants", + "line_start": 392, + "line_end": 401, + "keywords": [ + "secret", + "error", + "variants", + "variant", + "http", + "response", + "meaning", + "---", + "value", + "200", + "body", + "found", + "none", + "404", + "empty", + "name", + "valid", + "set", + "err", + "accessdenied", + "403", + "app", + "permitted", + "read", + "msg", + "denial", + "human-readable", + "message", + "decrypterror", + "500", + "exists", + "decrypted" + ] + }, + { + "id": "http-examples-secret-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 402, + "line_end": 408, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] + }, + { + "id": "http-examples-secret-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 409, + "line_end": 418, + "keywords": [ + "expected", + "behavior", + "scenario", + "secret", + "response", + "status", + "body", + "---", + "happy", + "path", + "my-value", + "200", + "get", + "nget_efective_at", + "set", + "absent", + "404", + "empty", + "access", + "denied", + "403", + "note", + "contains", + "typo", + "field", + "name", + "get_efective_at", + "instead", + "get_effective_at", + "known", + "cosmetic", + "issue", + "source." + ] + }, + { + "id": "http-examples-secret-basic-rust#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 419, + "line_end": 436, + "keywords": [ + "local", + "testing", + "inject", + "secret", + "via", + ".env", + "file", + "your", + "fixtures", + "directory", + "using", + "fastedge_var_secret_", + "name", + "prefix", + "fastedge_var_secret_secret", + "my-test-value", + "run", + "fixture", + "validator", + "node", + "tools", + "fixture-validator", + "index.mjs", + "fastedge-sdk-rust", + "examples", + "http", + "basic", + "--wasm", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] } ] }, @@ -30616,7 +33105,7 @@ { "id": "http-examples-simple-fetch-wasi-rust", "title": "Example: Simple Fetch (WASI HTTP, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -30640,7 +33129,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -30650,7 +33139,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "59940cbcac813462e867ba44395edd99653c63a1b5dad9fd847815d05a4a3ce8", + "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -30669,7 +33158,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31034,6 +33523,186 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 151, + "line_end": 235, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "simple_fetch", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "wasi-http", + "interface", + "via", + "wstd", + "crate.", + "receives", + "incoming", + "request", + "makes", + "outbound", + "url", + "specified", + "x-fetch-url", + "header", + "defaults", + "https", + "httpbin.org", + "get", + "build", + "cargo-component", + "cargo", + "component" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 236, + "line_end": 241, + "keywords": [ + "works", + "app", + "receives", + "incoming", + "request", + "reads", + "target", + "url", + "x-fetch-url", + "header", + "makes", + "outbound", + "get", + "streams", + "response", + "back", + "caller.", + "absent", + "defaults", + "https", + "httpbin.org" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#request-headers", + "heading": "Request headers", + "level": 2, + "anchor": "request-headers", + "line_start": 242, + "line_end": 247, + "keywords": [ + "request", + "headers", + "header", + "required", + "description", + "--------", + "----------", + "-------------", + "x-fetch-url", + "url", + "fetch.", + "defaults", + "https", + "httpbin.org", + "get" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 248, + "line_end": 253, + "keywords": [ + "example", + "bash", + "curl", + "x-fetch-url", + "https", + "httpbin.org", + "uuid", + "your-app-domain" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 254, + "line_end": 260, + "keywords": [ + "build", + "bash", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "simple_fetch.wasm" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#key-differences-from-basic-http-examples", + "heading": "Key differences from basic HTTP examples", + "level": 2, + "anchor": "key-differences-from-basic-http-examples", + "line_start": 261, + "line_end": 270, + "keywords": [ + "key", + "differences", + "basic", + "http", + "examples", + "fastedge", + "crate", + "wasi", + "wstd", + "---", + "handler", + "main", + "req", + "sync", + "async", + "macro", + "http_server", + "outbound", + "send_request", + "client", + "new", + ".send", + ".await", + "build", + "target", + "wasm32-wasip1", + "wasm32-wasip2" + ] } ] }, @@ -32109,7 +34778,7 @@ { "id": "http-examples-streaming-wasi-rust", "title": "Streaming Response — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32133,7 +34802,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32143,7 +34812,7 @@ "streaming", "async" ], - "content_sha256": "d43182bd8af1e158b7f960a742038ab49a7da9f93a10b9cd8a9163b91e79cb1d", + "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -32162,7 +34831,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32491,6 +35160,144 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, @@ -36775,7 +39582,7 @@ { "id": "quickstart-as", "title": "Quickstart: AssemblyScript CDN Apps on FastEdge", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/quickstart-as.md", "skill": "fastedge-docs", "category": "reference", @@ -36796,9 +39603,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "cdn", "apps", @@ -36809,7 +39616,7 @@ "compile", "webassembly" ], - "content_sha256": "5fc2440dbcf9811f874674b3390cc4c3227b2c2d095903c73343095e2f218549", + "content_sha256": "41ad91b4703ac00f166601df66080f0e3bd82de89c6e9fd8abfe5f3cf36abc80", "sections": [ { "id": "quickstart-as#introduction", @@ -36826,9 +39633,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "assemblyscript", "cdn", @@ -37900,7 +40707,7 @@ { "id": "runner-internals", "title": "Runner Internals — Low-Level Runner API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/runner-internals.md", "skill": "test", "category": "testing", @@ -37914,11 +40721,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -37930,7 +40737,7 @@ "use", "you" ], - "content_sha256": "2281ea5aaf755391320fbb293d097ebbb9ca95c272b510df303c43541b5c4806", + "content_sha256": "3ece5464164e143468e6f52883fffde960a434dddb1352dc3f9b65f188f0a4f8", "sections": [ { "id": "runner-internals#introduction", @@ -37945,11 +40752,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -38295,7 +41102,7 @@ { "id": "sdk-reference-as", "title": "AssemblyScript Proxy-Wasm SDK Reference", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/sdk-reference-as.md", "skill": "fastedge-docs", "category": "sdk", @@ -38316,9 +41123,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "proxy-wasm", "reference", "gcoredev", @@ -38328,7 +41135,7 @@ "writing", "cdn" ], - "content_sha256": "41b8129100103bce01866f20179662cdbe02e850593df1728abec65ffa86b79a", + "content_sha256": "353effa70cde17d46a928071309fca138aa211b6c6768542059770adcf6cc021", "sections": [ { "id": "sdk-reference-as#introduction", @@ -38345,9 +41152,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "assemblyscript", "proxy-wasm", "sdk", @@ -38383,7 +41190,7 @@ "level": 2, "anchor": "entry-point-pattern", "line_start": 20, - "line_end": 69, + "line_end": 71, "keywords": [ "entry", "point", @@ -38429,8 +41236,8 @@ "heading": "Build Configuration", "level": 2, "anchor": "build-configuration", - "line_start": 70, - "line_end": 111, + "line_start": 72, + "line_end": 113, "keywords": [ "build", "configuration", @@ -38471,8 +41278,8 @@ "heading": "Proxy-Wasm Lifecycle", "level": 2, "anchor": "proxy-wasm-lifecycle", - "line_start": 112, - "line_end": 317, + "line_start": 114, + "line_end": 319, "keywords": [ "proxy-wasm", "lifecycle", @@ -38518,8 +41325,8 @@ "heading": "Return Value Enums", "level": 2, "anchor": "return-value-enums", - "line_start": 318, - "line_end": 397, + "line_start": 320, + "line_end": 399, "keywords": [ "return", "value", @@ -38551,8 +41358,8 @@ "heading": "Request and Response Manipulation", "level": 2, "anchor": "request-and-response-manipulation", - "line_start": 398, - "line_end": 668, + "line_start": 400, + "line_end": 670, "keywords": [ "request", "response", @@ -38587,8 +41394,8 @@ "heading": "Outbound HTTP (httpCall)", "level": 2, "anchor": "outbound-http-httpcall", - "line_start": 669, - "line_end": 814, + "line_start": 671, + "line_end": 816, "keywords": [ "outbound", "http", @@ -38632,8 +41439,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 815, - "line_end": 837, + "line_start": 817, + "line_end": 839, "keywords": [ "logging", "import", @@ -38675,8 +41482,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 838, - "line_end": 859, + "line_start": 840, + "line_end": 861, "keywords": [ "registration", "import", @@ -38723,8 +41530,8 @@ "heading": "FastEdge Host APIs", "level": 2, "anchor": "fastedge-host-apis", - "line_start": 860, - "line_end": 1067, + "line_start": 862, + "line_end": 1079, "keywords": [ "fastedge", "host", @@ -38771,8 +41578,8 @@ "heading": "Deprecated Functions", "level": 2, "anchor": "deprecated-functions", - "line_start": 1068, - "line_end": 1081, + "line_start": 1080, + "line_end": 1093, "keywords": [ "deprecated", "functions", @@ -38804,8 +41611,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 1082, - "line_end": 1088, + "line_start": 1094, + "line_end": 1100, "keywords": [ "see", "fastedge", @@ -39844,7 +42651,7 @@ { "id": "server-api", "title": "Server API — REST and WebSocket Endpoints", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/server-api.md", "skill": "test", "category": "testing", @@ -39858,11 +42665,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -39874,7 +42681,7 @@ "interfaces", "loading" ], - "content_sha256": "d03fdff0b2a4179f0a7e45bbd2519f4edb1ac604a36369aba6b661c45a82d201", + "content_sha256": "96fce5e89649293442cc67e66685fffc2282f052c1d92ac935c72b815ae71822", "sections": [ { "id": "server-api#introduction", @@ -39889,11 +42696,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -40096,10 +42903,1055 @@ } ] }, + { + "id": "templates-catalog", + "title": "FastEdge Bolt-On Templates", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check" + ], + "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "sections": [ + { + "id": "templates-catalog#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 13, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check", + "catalog.", + "entry", + "maintained", + "tested", + "application", + "handles", + "security-sensitive", + "parts", + "hand-rolled", + "implementation", + "get", + "right", + "independently.", + "use", + "adapt", + "templat" + ] + }, + { + "id": "templates-catalog#html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "heading": "html2md — HTML-to-Markdown conversion / content transformation / Accept header negotiation", + "level": 2, + "anchor": "html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "line_start": 14, + "line_end": 31, + "keywords": [ + "html2md", + "html-to-markdown", + "conversion", + "content", + "transformation", + "accept", + "header", + "negotiation", + "solves", + "cdn", + "filter", + "transparently", + "converts", + "html", + "origin", + "responses", + "markdown", + "client", + "sends", + "text", + "handles", + "encoding", + "caching", + "concerns", + "hand-rolled", + "get", + "right", + "independently.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "110", + "changes" + ] + }, + { + "id": "templates-catalog#harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "heading": "harden-cookies — Cookie security hardening / Secure HttpOnly SameSite attributes / Set-Cookie rewriting", + "level": 2, + "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "line_start": 32, + "line_end": 47, + "keywords": [ + "harden-cookies", + "cookie", + "security", + "hardening", + "secure", + "httponly", + "samesite", + "attributes", + "set-cookie", + "rewriting", + "solves", + "cdn", + "filter", + "adds", + "strict", + "targeted", + "response", + "headers.", + "eliminates", + "modify", + "backend", + "code", + "enforce", + "policy", + "edge.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "184", + "origin", + "changes", + "required.", + "runtime" + ] + }, + { + "id": "templates-catalog#edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "heading": "edge-sso — SSO / login / identity federation / Identity-Aware Proxy (Google, GitHub, Microsoft, Facebook, SAML)", + "level": 2, + "anchor": "edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "line_start": 48, + "line_end": 71, + "keywords": [ + "edge-sso", + "sso", + "login", + "identity", + "federation", + "identity-aware", + "proxy", + "google", + "github", + "microsoft", + "facebook", + "saml", + "solves", + "bolt-on", + "adds", + "multi-provider", + "2.0", + "existing", + "site", + "without", + "changing", + "backend.", + "handles", + "oauth", + "oidc", + "flows", + "session", + "token", + "issuance", + "per-request", + "enforcement.", + "building", + "scratch", + "requires", + "correctly", + "implementing", + "multiple", + "signing", + "edge", + "enforcem" + ] + }, + { + "id": "templates-catalog#edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "heading": "edge-totp — TOTP MFA / two-factor authentication / OTP challenge / RFC 6238", + "level": 2, + "anchor": "edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "line_start": 72, + "line_end": 95, + "keywords": [ + "edge-totp", + "totp", + "mfa", + "two-factor", + "authentication", + "otp", + "challenge", + "rfc", + "6238", + "solves", + "adds", + "step", + "front", + "existing", + "site", + "login", + "without", + "modifying", + "backend.", + "customer", + "origin", + "handles", + "password", + "validation", + "app", + "hosts", + "6-digit", + "verifies", + "code", + "replay", + "brute-force", + "protection", + "issues", + "signed", + "session", + "assertion", + "trusts.", + "building", + "scratch", + "requires" + ] + } + ] + }, + { + "id": "templates-edge-sso-integration", + "title": "edge-sso — Origin Integration Reference", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-sso-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app" + ], + "content_sha256": "7301aea1c160744e3ee14e1d9dbe28aa1fb33e22fd28c5a4bc99be69b5bbc361", + "sections": [ + { + "id": "templates-edge-sso-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "reference", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app", + "cdn-filter", + "covers", + "contract", + "relies", + "identity", + "delivery", + "auth", + "routes", + "login", + "page", + "customization", + "redirect", + "validation.", + "---" + ] + }, + { + "id": "templates-edge-sso-integration#1-what-sso_variant-means-for-the-origin", + "heading": "1. What `SSO_VARIANT` Means for the Origin", + "level": 2, + "anchor": "1-what-sso_variant-means-for-the-origin", + "line_start": 16, + "line_end": 57, + "keywords": [ + "sso_variant", + "means", + "origin", + "set", + "identically", + "auth-app", + "cdn-filter.", + "controls", + "edge", + "delivers", + "identity", + "responsible", + "verifying.", + "gate-only", + "receives", + "nothing", + "filter", + "enforces", + "allow", + "deny", + "only", + "authenticated", + "requests", + "reach", + "origin.", + "responsibility", + "none.", + "receive", + "token", + "headers", + "sees", + "pas" + ] + }, + { + "id": "templates-edge-sso-integration#2-auth-app-routes", + "heading": "2. Auth-App Routes", + "level": 2, + "anchor": "2-auth-app-routes", + "line_start": 58, + "line_end": 80, + "keywords": [ + "auth-app", + "routes", + "served", + "under", + "auth_prefix", + "default", + "auth", + "single-domain", + "routing", + "cdn", + "origin", + "customer", + "own", + "domain.", + "cdn-filter", + "bypasses", + "gate", + "routes.", + "route", + "method", + "caller", + "purpose", + "---", + "get", + "browser", + "hosted", + "login", + "page", + "server-rendered", + "branded", + "provider", + "buttons.", + "honours", + "redirect", + "providers" + ] + }, + { + "id": "templates-edge-sso-integration#3-login-page-customization-tiers", + "heading": "3. Login Page Customization Tiers", + "level": 2, + "anchor": "3-login-page-customization-tiers", + "line_start": 81, + "line_end": 134, + "keywords": [ + "login", + "page", + "customization", + "tiers", + "tier", + "env", + "var", + "branding", + "recommended", + "default", + "built-in", + "hosted", + "reads", + "vars", + "per-request.", + "code", + "changes", + "required.", + "effect", + "---", + "login_page_title", + "sign", + "title", + "login_page_subtitle", + "choose", + "sign-in", + "method", + "subheading", + "login_page_logo_url", + "logo", + "image", + "login_page_favicon_url", + "tab", + "favicon", + "login_page_accent_" + ] + }, + { + "id": "templates-edge-sso-integration#4-json-contracts", + "heading": "4. JSON Contracts", + "level": 2, + "anchor": "4-json-contracts", + "line_start": 135, + "line_end": 173, + "keywords": [ + "json", + "contracts", + "get", + "auth", + "providers", + "jsonc", + "redirect", + "cart", + "google", + "label", + "loginurl", + "login", + "2fcart", + "github", + "saml", + "sso", + "stable", + "identifier", + "matches", + "value", + "used", + "sso_providers", + "human" + ] + }, + { + "id": "templates-edge-sso-integration#5-redirect-validation", + "heading": "5. Redirect Validation", + "level": 2, + "anchor": "5-redirect-validation", + "line_start": 174, + "line_end": 187, + "keywords": [ + "redirect", + "validation", + "parameter", + "validated", + "against", + "sso_allowed_origins", + "route", + "honours", + "read", + "saml_relay", + "cookie.", + "rules", + "relative", + "urls", + "starting", + "always", + "permitted.", + "off-origin", + "absolute", + "silently", + "dropped", + "post-login", + "falls", + "back", + "protocol-relative", + "backslash", + "bypasses", + "e.g.", + "evil.com", + "rejected.", + "permit", + "redirects", + "specific", + "origins", + "set" + ] + }, + { + "id": "templates-edge-sso-integration#6-shared-config-the-origin-needs-to-know-about", + "heading": "6. Shared Config the Origin Needs to Know About", + "level": 2, + "anchor": "6-shared-config-the-origin-needs-to-know-about", + "line_start": 188, + "line_end": 207, + "keywords": [ + "shared", + "config", + "origin", + "needs", + "know", + "about", + "env", + "vars", + "affect", + "contract", + "operates", + "under.", + "they", + "set", + "fastedge", + "apps", + "auth-app", + "cdn-filter", + "integration", + "depends", + "their", + "values.", + "var", + "concern", + "---", + "sso_variant", + "match", + "determines", + "receives", + "nothing", + "gate-only", + "jwt", + "cookie", + "verify", + "identity", + "headers", + "header", + "sso_audience" + ] + }, + { + "id": "templates-edge-sso-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 208, + "line_end": 215, + "keywords": [ + "see", + "edge-sso", + "template", + "readme", + "deployment", + "configuration", + "auth-app", + "cdn-filter", + ".env.example", + "authoritative", + "exhaustive", + "env", + "var", + "lists", + "inline", + "guidance", + "architecture", + "auth-modes", + "sso_variant", + "axis", + "detail", + "security", + "token", + "trust", + "model", + "known", + "limitations", + "including", + "revocation", + "idp", + "single", + "logout", + "overview", + "two-app", + "signing", + "strategy", + "canonical_host" + ] + } + ] + }, + { + "id": "templates-edge-totp-integration", + "title": "edge-totp — Origin Integration", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password" + ], + "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "sections": [ + { + "id": "templates-edge-totp-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "reference", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password", + "login.", + "template", + "otp-app", + "otp-filter", + "already", + "deployed", + "gcore", + "portal", + "document", + "covers", + "three", + "origin-side", + "code", + "changes", + "required", + "connect", + "it.", + "---" + ] + }, + { + "id": "templates-edge-totp-integration#the-three-origin-side-changes", + "heading": "The Three Origin-Side Changes", + "level": 2, + "anchor": "the-three-origin-side-changes", + "line_start": 16, + "line_end": 58, + "keywords": [ + "three", + "origin-side", + "changes", + "only", + "origin", + "auth", + "module", + "changes.", + "rest", + "site", + "untouched.", + "split", + "login", + "password-then-otp", + "password", + "check", + "succeeds", + "instead", + "immediately", + "minting", + "full", + "session", + "sign", + "handoff", + "ticket", + "hmac", + "handoff_key", + "over", + "sub", + "userid", + "next", + "exp", + "now", + "ticket_ttl", + "set", + "short-lived", + "pre_mfa", + "cookie", + "marker", + "remembers" + ] + }, + { + "id": "templates-edge-totp-integration#profile-a-vs-profile-b-—-decision-guide", + "heading": "Profile A vs Profile B — Decision Guide", + "level": 2, + "anchor": "profile-a-vs-profile-b-—-decision-guide", + "line_start": 59, + "line_end": 77, + "keywords": [ + "profile", + "decision", + "guide", + "security-posture", + "decision.", + "wrong", + "choice", + "here", + "real", + "vulnerability", + "style", + "issue.", + "criterion", + "---", + "origin", + "crypto", + "required", + "none", + "handoff_key", + "only", + "es256", + "proof", + "verification", + "via", + "jwks", + "knows", + "user", + "passed", + "mfa", + "filter", + "verifies", + "valid", + "mfa_session", + "exists", + "yes", + "carries", + "sub", + "binds", + "pre_mfa", + "session" + ] + }, + { + "id": "templates-edge-totp-integration#shared-configuration", + "heading": "Shared Configuration", + "level": 2, + "anchor": "shared-configuration", + "line_start": 78, + "line_end": 93, + "keywords": [ + "shared", + "configuration", + "keys", + "endpoints", + "origin", + "edge.", + "components", + "agree", + "values.", + "key", + "endpoint", + "edge", + "---", + "handoff_key", + "hs256", + "signs", + "handoff", + "ticket", + "password", + "success", + "verifies", + "auth_prefix", + "challenge", + "verify", + "jwks", + "profile", + "only", + "fetches", + ".well-known", + "jwks.json", + "via", + "createremotejwkset", + "public", + "cannot", + "forge", + "proofs", + "one" + ] + }, + { + "id": "templates-edge-totp-integration#required-deployment-precondition", + "heading": "Required Deployment Precondition", + "level": 2, + "anchor": "required-deployment-precondition", + "line_start": 94, + "line_end": 103, + "keywords": [ + "required", + "deployment", + "precondition", + "lock", + "origin", + "edge-only", + "traffic.", + "hard", + "requirement", + "profiles", + "suggestion.", + "edge", + "gate", + "profile", + "meaningless", + "directly", + "reachable.", + "hostname", + "accessible", + "without", + "going", + "gcore", + "cdn", + "attacker", + "simply", + "skips", + "entirely", + "mfa_session", + "rust", + "filter", + "signed", + "proof", + "never", + "run.", + "restrict", + "ingress", + "using", + "allowlist", + "authenticat" + ] + }, + { + "id": "templates-edge-totp-integration#recovery-and-self-service-enrollment-caveat", + "heading": "Recovery and Self-Service Enrollment Caveat", + "level": 2, + "anchor": "recovery-and-self-service-enrollment-caveat", + "line_start": 104, + "line_end": 113, + "keywords": [ + "recovery", + "self-service", + "enrollment", + "caveat", + "auth_prefix", + "activate", + "inherits", + "trust", + "level", + "password", + "step", + "user", + "pass", + "check", + "self-enroll", + "new", + "authenticator.", + "correct", + "default", + "deployments", + "sensitive", + "accounts", + "means", + "compromised", + "compromises", + "totp", + "second", + "factor.", + "decision", + "point", + "your", + "threat", + "model", + "requires", + "factor", + "remain", + "independent" + ] + }, + { + "id": "templates-edge-totp-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 114, + "line_end": 121, + "keywords": [ + "see", + "edge-totp", + "storage", + "secrets", + "reference", + "storage-and-secrets.md", + "full", + "env", + "var", + "secret", + "list", + "otp-app", + "otp-filter", + "threat", + "model", + "threat-model.md", + "trust", + "boundaries", + "residual", + "risks", + "risk", + "register", + "including", + "token", + "scope", + "self-enrollment", + "level", + "architecture", + "flow", + "flow.md", + "seed", + "fetched", + "verify", + "time", + "pop", + "replication", + "behavior", + "fastedge", + "store", + "sdk" + ] + } + ] + }, { "id": "test-config", "title": "test-config Reference", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/test-config.md", "skill": "test", "category": "testing", @@ -40113,11 +43965,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40129,7 +43981,7 @@ "request", "cdn" ], - "content_sha256": "009f7caa6b308ade8af63bdd256f2e553729893224f02cf420a8b5275e280424", + "content_sha256": "cf3e921e482468b97114a9494e3123f97e275060da3afffe2206862b7ffe0469", "sections": [ { "id": "test-config#introduction", @@ -40144,11 +43996,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40425,7 +44277,7 @@ { "id": "test-framework", "title": "FastEdge Test Framework API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/test-framework.md", "skill": "test", "category": "testing", @@ -40442,11 +44294,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "framework", "api", @@ -40457,7 +44309,7 @@ "runner", "apps." ], - "content_sha256": "538ad571eed95fce3e11226e6ac4355e725c8370b05c21a4bf3854ba991b527b", + "content_sha256": "e125a779b09525ed8048d05e09b9979c2ceea4c1c90bbc4a988c1ce98a9fcda1", "sections": [ { "id": "test-framework#introduction", @@ -40472,11 +44324,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "test", "framework", @@ -40841,7 +44693,7 @@ { "id": "vscode-debugger", "title": "FastEdge Visual Debugger", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-codex/skills/test/reference/vscode-debugger.md", "skill": "test", "category": "testing", @@ -40855,11 +44707,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", @@ -40871,7 +44723,7 @@ "gcoredev", "same" ], - "content_sha256": "dca66acac8714a6340718db344e69a3189c65884f062ac5b3a725420213a3e85", + "content_sha256": "627d9b7700750420febf279bfb44b3cea3ea212faa41800f660fa1f74a5965e5", "sections": [ { "id": "vscode-debugger#introduction", @@ -40886,11 +44738,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md index eff6c0f..0404634 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # A/B Testing — AssemblyScript (CDN) @@ -101,6 +101,8 @@ set_property("request.url", String.UTF8.encode(newUrl)) `set_property` / `get_property` key: `"request.url"`, `"request.path"`, `"request.scheme"`, `"request.host"`, `"request.query"`. +URL rewrite is skipped entirely if `schemeBuf.byteLength === 0` or `hostBuf.byteLength === 0`. + ### Step 5 — Add upstream headers ``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md index 60d90b9..0205b81 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -248,3 +248,191 @@ No additional dependencies. Uses `std::env` and `std::time::UNIX_EPOCH` from the - FastEdge environment variable configuration (setting `EXPERIMENT_NAME`, `VARIANT_A_PATH`, `VARIANT_B_PATH` at deploy time) - examples-geoblock-rust reference (similar CDN proxy-wasm filter structure) - examples-auth-jwt-rust reference (CDN Rust example with cross-hook state pattern) + +## Source Material + +### FILE: examples/cdn/ab_testing/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating A/B traffic splitting at the CDN layer. + +Uses a cookie to assign users to variant A or B, then rewrites the +request path to route to different parts of the origin server. + +Required configuration: + - Environment variable: EXPERIMENT_NAME + - Environment variable: VARIANT_A_PATH (path prefix for variant A) + - Environment variable: VARIANT_B_PATH (path prefix for variant B) +*/ + +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::env; +use std::time::UNIX_EPOCH; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(AbTestingRoot) }); +}} + +struct AbTestingRoot; + +impl Context for AbTestingRoot {} + +impl RootContext for AbTestingRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(AbTestingContext)) + } +} + +struct AbTestingContext; + +impl Context for AbTestingContext {} + +impl HttpContext for AbTestingContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(experiment_name) = env::var("EXPERIMENT_NAME") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - EXPERIMENT_NAME must be set"), + ); + return Action::Pause; + }; + + let Ok(variant_a_path) = env::var("VARIANT_A_PATH") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - VARIANT_A_PATH must be set"), + ); + return Action::Pause; + }; + + let Ok(variant_b_path) = env::var("VARIANT_B_PATH") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - VARIANT_B_PATH must be set"), + ); + return Action::Pause; + }; + + let cookie_name = format!("fe_exp_{}", experiment_name); + + // Check for existing experiment cookie + let cookie_header = self + .get_http_request_header("Cookie") + .unwrap_or_default(); + let mut assigned = get_cookie_value(&cookie_header, &cookie_name); + + // Assign variant if not already set + if assigned != "A" && assigned != "B" { + let now = self + .get_current_time() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + assigned = if now % 2 == 0 { "A" } else { "B" }.to_string(); + } + + // Rewrite request path + let path = self + .get_property(vec!["request.path"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| "/".to_string()); + + let variant_path = if assigned == "A" { + &variant_a_path + } else { + &variant_b_path + }; + let new_path = format!("{}{}", variant_path, path); + + // Update the request path directly to avoid ambiguous URL rewriting. + self.set_property(vec!["request.path"], Some(new_path.as_bytes())); + + // Add variant headers for upstream visibility + self.add_http_request_header("X-Experiment", &experiment_name); + self.add_http_request_header("X-Variant", &assigned); + + println!( + "A/B test \"{}\": variant {}, path {}", + experiment_name, assigned, new_path + ); + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // Recover the assigned variant and experiment name from the request headers set in + // on_http_request_headers. Instance state does not survive the nginx -> core-proxy hop. + let Some(variant) = self.get_http_request_header("X-Variant") else { + return Action::Continue; + }; + let Some(experiment_name) = self.get_http_request_header("X-Experiment") else { + return Action::Continue; + }; + + let cookie = format!( + "fe_exp_{}={}; Path=/; Max-Age=86400; SameSite=Lax", + experiment_name, variant + ); + self.add_http_response_header("Set-Cookie", &cookie); + self.add_http_response_header("X-Variant", &variant); + + Action::Continue + } +} + +fn get_cookie_value(cookie_header: &str, name: &str) -> String { + if cookie_header.is_empty() { + return String::new(); + } + let prefix = format!("{}=", name); + for pair in cookie_header.split(';') { + let pair = pair.trim(); + if let Some(value) = pair.strip_prefix(&prefix) { + return value.to_string(); + } + } + String::new() +} +``` + + +### FILE: examples/cdn/ab_testing/Cargo.toml + +```toml +[workspace] + +[package] +name = "ab_testing" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + + +### FILE: examples/cdn/ab_testing/README.md + +``` +[← Back to examples](../../README.md) + +# A/B Testing (CDN) + +Cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md index 29a7d5b..fe6573a 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -27,8 +27,8 @@ Validates incoming requests by checking the `X-API-Key` request header against a ## Entry Point -**File:** `assembly/index.ts` -**Root context name:** `"apiKey"` +**File:** `assembly/index.ts` +**Root context name:** `"apiKey"` **Hook:** `onRequestHeaders` --- @@ -164,7 +164,7 @@ Header `X-API-Key` is set to `""` on the forwarded request (platform `.remove()` ## Build -**Package manager:** `npm` (also compatible with `pnpm`) +**Package manager:** `npm` (also compatible with `pnpm`) **SDK dependency:** `@gcoredev/proxy-wasm-sdk-as ^1.2.3` ```sh @@ -199,3 +199,168 @@ Upload `build/apiKey.wasm` to the FastEdge portal and attach it to a CDN applica - proxy-wasm-sdk-as reference (full `Context`, `RootContext`, `FilterHeadersStatusValues` API) - FastEdge secrets management (how to set and rotate application secrets) - CDN app deployment guide + +## Source Material + +### FILE: examples/apiKey/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + HeaderPair, + log, + LogLevelValues, + makeHeaderPair, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class ApiKeyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new ApiKeyContext(context_id, this); + } +} + +class ApiKeyContext extends Context { + constructor(context_id: u32, root_context: ApiKeyRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const expectedKey = getSecret("API_KEY"); + if (expectedKey === "") { + log(LogLevelValues.error, "API_KEY secret not configured"); + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const providedKey = stream_context.headers.request.get("X-API-Key"); + + if (providedKey === "") { + const authHeaders = new Array(); + authHeaders.push(makeHeaderPair("WWW-Authenticate", "API-Key")); + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Missing X-API-Key header"), + authHeaders, + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (providedKey !== expectedKey) { + log(LogLevelValues.info, "API key validation failed"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid API key"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // .remove() sets the header value to "" rather than deleting it entirely — + // the upstream will see X-API-Key: "" rather than a missing header. + stream_context.headers.request.remove("X-API-Key"); + + log(LogLevelValues.info, "API key validated successfully"); + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new ApiKeyRoot(context_id); +}, "apiKey"); +``` + + +### FILE: examples/apiKey/package.json + +```json +{ + "name": "fastedge-as-example-api-key", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: API Key — validate X-API-Key header against a secret", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/apiKey/README.md + +``` +[← Back to examples](../README.md) + +# API Key + +This application validates requests using an `X-API-Key` header checked against a stored secret. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the expected API key from the `API_KEY` secret. +2. Checks the `X-API-Key` request header. +3. Returns `401 Unauthorized` if the header is missing. +4. Returns `403 Forbidden` if the key does not match. +5. On success, clears the `X-API-Key` header before forwarding to the upstream origin (proxy-wasm `.remove()` sets the header value to an empty string rather than deleting it). + +This is a simpler alternative to JWT validation when you need basic API authentication without token expiry or claims. + +> **Production note:** The key comparison (`providedKey !== expectedKey`) is not constant-time, which opens a timing side-channel for a high-volume attacker. For production use, replace the comparison with a constant-time HMAC equality check or use the `jwt` example which includes proper cryptographic validation. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `API_KEY` | Secret | The expected API key value | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/apiKey.wasm` | Optimised release binary — upload this to FastEdge | +| `build/apiKey-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/apiKey.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `API_KEY` secret in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md index bc2390d..ca16447 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- capabilities: @@ -62,7 +62,7 @@ Reads a named secret variable configured on the FastEdge application. - **Parameter**: `name` — secret variable name (string) - **Returns**: secret value as a UTF-8 string, or `null` if not found -- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` +- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` without encoding ```typescript const secret = getSecret("SECRET"); @@ -82,6 +82,7 @@ Verifies a JWT token against an HMAC-SHA256 secret. - `secret` — HMAC signing secret (string) - **Returns**: `JwtValidation` enum value - **Package**: `@gcoredev/as-jwt` (separate dependency, not part of proxy-wasm-sdk-as) +- **Behavior**: does not throw; always check the return value against `JwtValidation.Ok` ### `JwtValidation` enum @@ -105,11 +106,11 @@ Sends an immediate HTTP response and stops the request. Body must be encoded as ### `setLogLevel(level: LogLevelValues): void` -Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. +Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. Present in source for demonstration purposes only — explicitly setting the default is optional. ### `log(level: LogLevelValues, message: string): void` -Emits a log entry. Used to record token rejection reasons. +Emits a log entry. Used to record token rejection reasons (e.g. `"Token Expired"`, `"Bad Token"`). --- @@ -150,7 +151,23 @@ All blocked responses return `FilterHeadersStatusValues.StopIteration`. |---|---|---| | `SECRET` | HMAC-SHA256 signing key | String; minimum 256 bits / 32 characters | -Configure this secret variable on the FastEdge application before deployment. For secret rotation, see `getSecretEffectiveAt` in the FastEdge secrets reference. +Configure this secret variable on the FastEdge application before deployment. For secret rotation, use `getSecretEffectiveAt` instead of `getSecret` — see the FastEdge secrets reference. + +--- + +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` --- @@ -167,6 +184,14 @@ Configure this secret variable on the FastEdge application before deployment. Fo - `@gcoredev/as-jwt` is a required peer dependency — it is NOT bundled in proxy-wasm-sdk-as. - `assemblyscript-json` is declared as a dependency but not directly used in this example. +**Dev dependencies:** +```json +{ + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" +} +``` + --- ## Build @@ -208,6 +233,7 @@ Root context name: `"auth"`. - The `Authorization` header is validated in two steps: first a null check (missing header → 401), then a `startsWith("Bearer ")` check (wrong scheme → 401). An empty-string header would fail the Bearer scheme check. - `jwtVerify` does not throw; always check the return value against `JwtValidation.Ok`. - Validation happens in `onRequestHeaders` only. There is no body or response hook in this example. +- The `setLogLevel(LogLevelValues.info)` call in `createContext` is present for demonstration only — `info` is the default level and the call is not required. - For HMAC secret rotation using slot-based secrets, use `getSecretEffectiveAt` instead of `getSecret`. See the FastEdge secrets reference. --- @@ -217,4 +243,4 @@ Root context name: `"auth"`. - proxy-wasm-sdk-as SDK reference (AssemblyScript) - FastEdge secrets reference (`getSecret`, `getSecretEffectiveAt`, rotation slots) - CDN app platform overview -- `@gcoredev/as-jwt` package (npmjs.com) +- `@gcoredev/as-jwt` package on npmjs.com diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md index 6c00fe2..e0e5128 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -337,3 +337,216 @@ When modifying body content: - host-services-rust (for equivalent Rust patterns) - platform-overview (CDN app lifecycle, hook execution order) - examples-headers-as (header-only manipulation without body buffering) + +## Source Material + +### FILE: examples/body/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BufferTypeValues, + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_buffer_bytes, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class HttpBodyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug + return new HttpBody(context_id, this); + } +} + +class HttpBody extends Context { + constructor(context_id: u32, root_context: HttpBodyRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool + ): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); + // Remove the "content-length" header + stream_context.headers.request.remove("content-length"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); + if (bodyStr.includes("Client")) { + const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; + set_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length, + String.UTF8.encode(newBody) + ); + } + } + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); + + // Remove "content-length" header as the body size will change + stream_context.headers.response.remove("content-length"); + + // Set "transfer-encoding" to "chunked" + stream_context.headers.response.replace("transfer-encoding", "Chunked"); + + const contentType = stream_context.headers.response.get("content-type"); + if (contentType.length > 0) { + set_property("response.content_type", String.UTF8.encode(contentType)); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + + // Retrieve the request URL + const urlBytes = get_property("request.url"); + const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } + + // Retrieve the response content type stored in onResponseHeaders + const contentTypeBytes = get_property("response.content_type"); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); + } + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HttpBodyRoot(context_id); +}, "httpbody"); +``` + +### FILE: examples/body/package.json + +```json +{ + "name": "fastedge-as-example-body", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Body manipulation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/body/README.md + +``` +[← Back to examples](../README.md) + +# Body + +This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. + +## What it does + +1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. + +2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. + +3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. + +4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. + +This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: + +- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). +- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ----------------------- | -------------------------------------------------- | +| `build/body.wasm` | Optimised release binary — upload this to FastEdge | +| `build/body-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md index 0010810..f135ed3 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -228,3 +228,191 @@ Build targets: - FastEdge CDN app scaffold reference - platform-overview (CDN application lifecycle and hook execution model) - best-practices (header mutation, environment variable patterns) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | +| Other | `public, max-age=600` | 10 minutes | +| Error responses (4xx/5xx) | `no-store` | — | + +Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). + +## Configuration + +All environment variables are optional — sensible defaults are applied when unset. + +| Variable | Default | Description | +|----------|---------|-------------| +| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | +| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | +| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cacheControl-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md index f36b937..baedbd0 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # CDN Example: Convert Image (Rust) @@ -202,300 +202,3 @@ Transformation is skipped (returning `Action::Continue` without modifying the re - FastEdge CDN app platform overview - FastEdge SDK Rust reference - FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/convert_image/src/lib.rs - -```rust -use image::*; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::{env, env::VarError, io::Cursor, str::from_utf8}; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); -}} - -struct ConvertImageRoot; - -impl Context for ConvertImageRoot {} - -impl RootContext for ConvertImageRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(ConvertImageContext)) - } -} - -struct ConvertImageContext; - -impl Context for ConvertImageContext {} - -impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // this header is used to select correct image version from cache - self.add_http_request_header("Image-Format", "original"); - - // get extension - let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); - println!("request.path={path:?}"); - let raw_ext = self.get_property(vec!["request.extension"]); - println!("request.extension={raw_ext:?}"); - let Some(ext) = raw_ext else { - println!("No extension in request path, not transforming"); - return Action::Continue; - }; - let Ok(ext) = from_utf8(&ext) else { - println!("Invalid UTF-8 in request extension, not transforming"); - return Action::Continue; - }; - if ext.is_empty() { - println!("No extension in request path, not transforming"); - return Action::Continue; - } - - // FORMATS_TO_TRANSFORM contains list of file extensions to transfor - // note that jpg and jpeg are different extensions - let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { - println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); - return Action::Continue; - }; - if !image_list.split(',').any(|entry| entry == ext) { - println!( - "extension {} is not in the list of formats to transform: {}, not transforming", - ext, image_list - ); - return Action::Continue; - } - - // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed - let Some(ua) = self.get_http_request_header("User-Agent") else { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - }; - if ua.is_empty() { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - } - if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { - if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { - println!("User-Agent is in ignore list, not transforming"); - return Action::Continue; - } - } - - // indicator for on_response_headers and for cache key - self.set_http_request_header("Image-Format", Some("image/avif")); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // only process 200 responses - if let Some(status) = self.rsp_status() { - if status != 200 { - println!( - "Response status is {} instead of expected 200, not transforming", - status - ); - return Action::Continue; - } - } else { - println!("Response status is not set, not transforming"); - return Action::Continue; - } - - // if "Image-Format" request header is not set, don't convert the image - let Some(content_type) = self.get_http_request_header("Image-Format") else { - return Action::Continue; - }; - // instruct cache to vary by this header so "original" and "image/avif" are cached separately - self.add_http_response_header("Vary", "Image-Format"); - - if content_type == "original" { - return Action::Continue; - }; - - // image to be transformed, set headers accordingly - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some(content_type.as_str())); - - // indicate to on_http_response_body that transformation is needed - self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // wait till we get complete body - return Action::Pause; - } - - let Some(content_type) = self.get_property(vec!["response.content-type"]) else { - return Action::Continue; - }; - - let Ok(content_type) = from_utf8(&content_type) else { - // should never happen - println!("Invalid UTF-8 in Content-Type"); - self.send_http_response(500, vec![], None); - return Action::Pause; - }; - - if content_type != "image/avif" { - // should never happen - println!( - "Content-Type {} is not supported, not transforming", - content_type - ); - return Action::Continue; - } - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let buf = body_bytes.as_bytes(); - let img = match load_from_memory(buf) { - Ok(i) => i, - Err(e) => { - println!("cannot load image to memory {}, not converting", e); - return Action::Continue; - } - }; - - let mut out = Vec::new(); - let mut c = Cursor::new(&mut out); - let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( - &mut c, - u8_param("AVIF_SPEED", 1, 10, 5), - u8_param("AVIF_QUALITY", 1, 100, 70), - )); - - match res { - Ok(_) => { - println!( - "{} bytes -> {} bytes {}", - body_size, - out.len(), - content_type - ); - self.set_http_response_body(0, body_size, &out) - } - Err(e) => println!("cannot store transformed image {}", e), - } - } else { - println!("No response body to transform"); - } - - Action::Continue - } -} - -impl ConvertImageContext { - fn rsp_status(&mut self) -> Option { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() != 2 { - println!("HTTP status property is not 2 bytes"); - return None; - } - return Some(u16::from_be_bytes([status[0], status[1]])); - } - None - } -} - -fn str_param(name: &str) -> Result { - let val = env::var(name)?; - if val.is_empty() { - return Err(VarError::NotPresent); - } - - Ok(val) -} - -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { - let Ok(val) = env::var(name) else { - println!("Param {} is not set, using default value {}", name, default); - return default; - }; - if val.is_empty() { - println!("Param {} is not set, using default value {}", name, default); - return default; - } - - let val = match val.parse() { - Err(_) => { - println!( - "Param {} is not a valid number, using default value {}", - name, default - ); - return default; - } - Ok(v) => v, - }; - if val < min { - println!( - "Param {} is below minimum {}, using default value {}", - name, min, default - ); - return default; - } - if val > max { - println!( - "Param {} is above maximum {}, using default value {}", - name, max, default - ); - return default; - } - - val -} -``` - - -### FILE: examples/cdn/convert_image/Cargo.toml - -```toml -[workspace] - -[package] -name = "convert_image" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -image = "0.25" -``` - - -### FILE: examples/cdn/convert_image/README.md - -``` -[← Back to examples](../../README.md) - -# Convert Image (CDN) - -Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. - -## Configuration - -- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) -- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip -- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) -- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) - -## How it works - -1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation -2. **on_response_headers** — sets response headers for AVIF content type on 200 responses -3. **on_response_body** — decodes the original image and re-encodes it as AVIF -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md index c1ff635..613b6e1 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CORS — AssemblyScript (CDN) @@ -193,3 +193,159 @@ pnpm run asbuild - FastEdge CDN application environment variables configuration - platform-overview (CDN app lifecycle and OPTIONS handling) - best-practices (header mutation patterns, Vary usage) + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md index 3ccdd4c..9ec6651 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -171,6 +171,24 @@ set_buffer_bytes( | other 5xx | Server Error | | other | Error | +### Descriptions + +| Code | Description | +|---|---| +| 400 | The server could not understand the request due to invalid syntax. | +| 401 | You need to authenticate to access this resource. | +| 403 | You do not have permission to access this resource. | +| 404 | The requested page could not be found. It may have been moved or deleted. | +| 405 | The request method is not supported for this resource. | +| 408 | The server timed out waiting for the request. | +| 429 | You have sent too many requests. Please try again later. | +| 500 | The server encountered an unexpected condition that prevented it from fulfilling the request. | +| 502 | The server received an invalid response from the upstream server. | +| 503 | The server is temporarily unavailable. Please try again later. | +| 504 | The server did not receive a timely response from the upstream server. | +| other 5xx | The server encountered an error processing your request. | +| other | An error occurred processing your request. | + ### Categories | Range | Category label | diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md index 1fe604d..90a1205 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> ## Custom Error Pages — CDN (Rust) @@ -13,6 +13,8 @@ Intercepts 4xx and 5xx HTTP error responses at the CDN edge and replaces their bodies with branded HTML pages rendered via Handlebars templates. Use this pattern when you need consistent, styled error pages served from the edge regardless of what the origin returns. +A build script (`build.rs`) runs at compile time to embed images and messages from the `public/` folder into the WASM binary — there is no filesystem at runtime. + --- ### API Patterns @@ -188,6 +190,29 @@ let (message, description) = message_map }); ``` +**Pattern 4 — Root context and HTTP context wiring:** + +```rust +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(HttpBody)) + } +} +``` + --- ### Dependencies (`Cargo.toml`) @@ -203,11 +228,40 @@ regex = "1.10" base64 = "0.22" ``` -- `proxy-wasm`: CDN lifecycle hooks and context traits -- `handlebars`: template rendering for error pages and message strings -- `serde_json`: JSON data construction for template variables -- `regex`: available for pattern matching (used in build script or message processing) -- `base64` (build dependency only): encodes images into the generated map at compile time +| Crate | Role | +|-------|------| +| `proxy-wasm` | CDN lifecycle hooks and context traits | +| `handlebars` | Template rendering for error pages and message strings | +| `serde_json` | JSON data construction for template variables | +| `regex` | Pattern matching (available for use in build script or message processing) | +| `base64` (build-dep only) | Encodes images into the generated map at compile time | + +Crate type must be `cdylib`: + +```toml +[lib] +crate-type = ["cdylib"] +``` + +--- + +### Project Layout + +``` +custom_error_pages/ +├── build.rs # Generates image_map.rs and message_map.rs into OUT_DIR +├── Cargo.toml +├── src/ +│ └── lib.rs # HttpContext implementation +├── templates/ +│ └── error_page.hbs # Handlebars HTML page template +└── public/ + ├── styles.css # Embedded at compile time via include_str! + ├── images/ + │ └── .jpg # Per-status images; also 4000.jpg / 5000.jpg for fallbacks + └── messages/ + └── .hbs # First line = title, second line = description +``` --- @@ -215,7 +269,7 @@ base64 = "0.22" 1. Add an image: `public/images/.jpg` 2. Add a message file: `public/messages/.hbs` (first line = title, second line = description) -3. Recompile and redeploy — the build script regenerates the embedded maps +3. Recompile and redeploy — the build script regenerates the embedded maps automatically --- @@ -230,10 +284,11 @@ base64 = "0.22" - **Handlebars rendering is two-stage.** Message and description strings may themselves contain `{{status}}` placeholders. Render them first with `json!({ "status": ... })`, then pass the rendered strings into the final page template. Registering and rendering in a single pass will not expand variables inside message/description values. - **`handlebars::Handlebars::render` panics on template registration failure if `.unwrap()` is used.** In production code, handle `register_template_string` and `render` errors explicitly unless the templates are known-valid at compile time. - **`get_property` returns `Option>` in both hooks.** The property may be absent even for valid responses. Always handle the `None` case before attempting to decode. +- **Generic fallback keys use sentinel values, not HTTP status ranges.** The image and message maps use `4000` (not `400`) for the generic 4xx fallback and `5000` (not `500`) for the generic 5xx fallback. These are not valid HTTP status codes — they are sentinel keys used only in the embedded maps. --- -### Related +### See Also - CDN apps reference — proxy-wasm lifecycle hooks (`on_http_response_headers`, `on_http_response_body`), `HttpContext` trait, `Action` enum, and `get_property` / `set_http_response_header` / `set_http_response_body` signatures - Host services reference — KV store, secrets, and dictionary APIs available in CDN apps diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md index bbfd2a1..ec7dc83 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -250,3 +250,123 @@ self.send_http_response(status_code: u32, headers: Vec<(&str, &str)>, body: Opti - CDN app pattern guide (see the _docs-pattern-cdn reference) - Host services reference for Rust (see the host-services-rust reference) - Platform overview (see the platform-overview reference) + +## Source Material + +### FILE: examples/cdn/custom/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::time::Duration; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); +}} + +const BAD_REQUEST: u32 = 400; + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, _context_id: u32) -> Option> { + Some(Box::new(HttpHeaders)) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders; + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Some(path) = self.get_property(vec!["request.path"]) else { + self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); + return Action::Pause; + }; + + let Ok(path) = std::str::from_utf8(&path) else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - not utf8 string"), + ); + return Action::Pause; + }; + + //trim first '/' + let path = if path.starts_with('/') { + &path[1..] + } else { + path + }; + let mut segments = path.split('/'); + + let Some(status_code) = segments.next() else { + return Action::Continue; + }; + + if let Some(delay) = segments.next() { + if let Ok(delay) = delay.parse::() { + std::thread::sleep(Duration::from_millis(delay)); + } + } + + let Ok(status_code) = status_code.parse::() else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - invalid status code"), + ); + return Action::Pause; + }; + + match status_code { + 0 | 200 => Action::Continue, + code if code < 600 => { + self.send_http_response(code, vec![], None); + Action::Pause + } + _ => { + self.send_http_response(BAD_REQUEST, vec![], None); + Action::Pause + } + } + } +} +``` + +### FILE: examples/cdn/custom/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/custom/README.md + +``` +[← Back to examples](../../README.md) + +# Custom (CDN) + +Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md index 0c9c162..541ac7c 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- type: example @@ -225,3 +225,140 @@ import { - examples-headers-cdn-as (request/response header manipulation patterns) - platform-overview (secret management and deployment configuration) - best-practices (logging levels and secret handling guidelines) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. + +To test locally with the visual debugger, create `fixtures/.env`: + +``` +FASTEDGE_VAR_ENV_USERNAME=my-username +FASTEDGE_VAR_SECRET_PASSWORD=my-password +``` + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| -------------------------------------- | -------------------------------------------------- | +| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | +| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/variablesAndSecrets.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md index e954ccc..61f35b9 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -199,6 +199,17 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { } ``` +## Multi-Value Headers + +`new-header-03` is deliberately added twice — `add()` is called with the same name twice. This produces a multi-value header with two separate `new-header-03` entries. The validation pattern uses `Set` of `"name:value"` pairs to assert both values are present. + +## Cross-Phase Response Header Writes + +`stream_context.headers.response.add(...)` can be called during `onRequestHeaders`. Headers written in the request phase appear in the final response alongside those set in `onResponseHeaders`. The distinction: + +- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase +- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase + ## Error Response Codes Used | Code | Meaning | Trigger | @@ -239,9 +250,7 @@ onLog(): void { **`replace()` upserts on FastEdge**: `replace()` creates the header with the given value if the named header does not exist — it does not behave as a no-op on absent headers. Guard calls to `replace()` with a prior `get()` length check when you intend to update only an existing header. -**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. The distinction: -- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase -- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase +**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. ## Build diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md index b327ddc..97aea43 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -230,3 +230,356 @@ All error paths return `Action::Pause`. - examples-body-cdn-rust (body manipulation API) - examples-shared-data-cdn-rust (shared data API) - host-services-rust reference (full ABI surface) + +## Source Material + +### FILE: examples/cdn/headers/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::collections::HashSet; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); +}} + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, context_id: u32) -> Option> { + Some(Box::new(HttpHeaders { context_id })) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders { + context_id: u32, +} + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_request_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_request_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_request_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_request_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_request_header("new-header-01", "value-01"); + self.add_http_request_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_request_header("new-header-02", "value-02"); + self.add_http_request_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_request_header("new-header-03", "value-03"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_request_header("new-header-01", None); + self.set_http_request_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_request_header("new-header-02", Some("new-value-02")); + self.set_http_request_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_request_header("new-header-03", "value-03-a"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // try to set/add response headers + self.add_http_response_header("new-response-header", "value-01"); + self.set_http_response_header("cache-control", None); + self.set_http_response_header("new-response-header", Some("value-02")); + + // get new headers + let headers = self + .get_http_request_headers() + .into_iter() + .collect::>(); + let headers_bytes = self + .get_http_request_headers_bytes() + .into_iter() + .collect::>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::>(); + + let diff = headers + .difference(&original_headers) + .collect::>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::>(); + + let diff = diff.difference(&expected).collect::>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + // check if the response header is not returned + if self.get_http_response_header("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + if self.get_http_response_header_bytes("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + + let response_headers = self.get_http_response_headers(); + if response_headers.len() != 1 { + self.send_http_response(555, vec![], None); + return Action::Pause; + } + let Some((name, value)) = response_headers.into_iter().next() else { + self.send_http_response(555, vec![], None); + return Action::Pause; + }; + if name != "new-response-header" || value != "value-02" { + self.send_http_response(556, vec![], None); + return Action::Pause; + } + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_response_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_response_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_response_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_response_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_response_header("new-header-01", "value-01"); + self.add_http_response_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_response_header("new-header-02", "value-02"); + self.add_http_response_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_response_header("new-header-03", "value-03"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_response_header("new-header-01", None); + self.set_http_response_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_response_header("new-header-02", Some("new-value-02")); + self.set_http_response_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_response_header("new-header-03", "value-03-a"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // get new headers + let headers = self + .get_http_response_headers() + .into_iter() + .collect::>(); + let headers_bytes = self + .get_http_response_headers_bytes() + .into_iter() + .collect::>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::>(); + + let diff = headers + .difference(&original_headers) + .collect::>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::>(); + + let diff = diff.difference(&expected).collect::>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + Action::Continue + } +} +``` + +### FILE: examples/cdn/headers/Cargo.toml + +```toml +[workspace] + +[package] +name = "headers" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/headers/README.md + +``` +[← Back to examples](../../README.md) + +# Headers (CDN) + +Validates and manipulates HTTP request and response headers using the proxy-wasm ABI. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md index 3e5ef9b..9509c21 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> ## Overview @@ -123,6 +123,8 @@ import { } from "@gcoredev/proxy-wasm-sdk-as/assembly"; import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +const INTERNAL_SERVER_ERROR: u32 = 500; + function handleHttpCallResponse( ctx: BaseContext, hdrs: u32, @@ -133,10 +135,12 @@ function handleHttpCallResponse( log(LogLevelValues.error, "HTTP call failed — no response received"); return; } + const userAgent = stream_context.headers.http_callback.get("user-agent"); if (userAgent !== "") { log(LogLevelValues.info, "User-Agent: " + userAgent); } + if (bodySize > 0) { const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpCallResponseBody, @@ -144,30 +148,43 @@ function handleHttpCallResponse( bodySize as u32, ); const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "Response body: " + bodyStr); + log( + LogLevelValues.info, + "Response body (" + bodySize.toString() + " bytes): " + bodyStr, + ); + } else { + log(LogLevelValues.info, "Response body: empty"); } } -class MyRoot extends RootContext { +class HttpCallRoot extends RootContext { createContext(context_id: u32): Context { setLogLevel(LogLevelValues.info); - return new MyContext(context_id, this); + return new HttpCallContext(context_id, this); } } -class MyContext extends Context { +class HttpCallContext extends Context { httpCallDispatched: bool = false; - constructor(context_id: u32, root_context: MyRoot) { + constructor(context_id: u32, root_context: HttpCallRoot) { super(context_id, root_context); } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Latch prevents re-dispatch on second invocation after callback fires. + // FastEdge re-invokes this hook after the httpCall response is processed. + // The latch gates re-dispatch so the second invocation returns Continue + // instead of firing another HTTP call. if (this.httpCallDispatched) { + log( + LogLevelValues.info, + "HTTP call response received, resuming request.", + ); return FilterHeadersStatusValues.Continue; } + log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); + const headers = new Array(); headers.push(makeHeaderPair(":scheme", "https")); headers.push(makeHeaderPair(":authority", "httpbin.org")); @@ -175,7 +192,8 @@ class MyContext extends Context { headers.push(makeHeaderPair(":method", "GET")); headers.push(makeHeaderPair("User-Agent", "fastedge")); - const result = (this.root_context as MyRoot).httpCall( + // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. + const result = (this.root_context as HttpCallRoot).httpCall( "httpbin.org", headers, new ArrayBuffer(0), @@ -186,8 +204,12 @@ class MyContext extends Context { ); if (result != WasmResultValues.Ok) { + log( + LogLevelValues.error, + "Failed to dispatch HTTP call: " + result.toString(), + ); send_http_response( - 500, + INTERNAL_SERVER_ERROR, "internal server error", String.UTF8.encode("Failed to dispatch HTTP call"), [], @@ -196,13 +218,15 @@ class MyContext extends Context { } this.httpCallDispatched = true; + log(LogLevelValues.info, "HTTP call dispatched, pausing request"); + return FilterHeadersStatusValues.StopIteration; } } registerRootContext((context_id: u32) => { - return new MyRoot(context_id); -}, "myApp"); + return new HttpCallRoot(context_id); +}, "httpCall"); ``` ### Callback: check failure, read headers, read body @@ -239,7 +263,7 @@ function handleHttpCallResponse( ### Error response on dispatch failure ```typescript -const result = (this.root_context as MyRoot).httpCall( +const result = (this.root_context as HttpCallRoot).httpCall( "upstream-host", headers, new ArrayBuffer(0), @@ -260,16 +284,37 @@ if (result != WasmResultValues.Ok) { } ``` +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +| File | Description | +|------|-------------| +| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | +| `build/httpCall-debug.wasm` | Debug binary with source maps | + +Dependencies (from `package.json`): + +| Package | Role | +|---------|------| +| `@gcoredev/proxy-wasm-sdk-as` | SDK — required runtime dep (`^1.2.3`) | +| `assemblyscript` | AssemblyScript compiler (`^0.28.9`) | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (`^0.1.0`) | + ## Gotchas - **No closures over mutable state.** AssemblyScript does not support closures that capture mutable variables. Capture state on the `Context` instance (instance fields) or use `set_property`/`get_property`. The `httpCallDispatched: bool` latch is an instance field for this reason. - **FastEdge resume model differs from canonical proxy-wasm.** After the callback fires, the runtime **re-invokes `onRequestHeaders` on the same `Context` instance**. Do not call `continueRequest()` — it has no effect on FastEdge. Use a latch field to distinguish the second invocation. -- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as MyRoot).httpCall(...)`. +- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as HttpCallRoot).httpCall(...)`. - **Instance state survives re-entry within the same context** but does not persist across the nginx→core-proxy hop. Do not rely on instance fields across separate request lifecycles. - **Tune `timeoutMs` per upstream.** The example uses `3000` ms to accommodate cold DNS and variable network conditions. Set a tighter value for low-latency upstreams and a looser value for slow third-party services. - **No try/catch.** AssemblyScript has no exception handling at runtime. Always check `WasmResultValues.Ok` explicitly after `httpCall()` returns. - **Pseudo-headers are required.** Include `:scheme`, `:authority`, `:path`, and `:method` in the headers array. Missing pseudo-headers will cause dispatch to fail. - **`hdrs == 0` means failure, not "no headers".** Inside the callback, `hdrs == 0` signals that the HTTP call itself failed (timeout, DNS error, invalid cluster). Do not skip this check. +- **Log the dispatch failure result.** When `result != WasmResultValues.Ok`, log `result.toString()` to surface the specific `WasmResultValues` variant for debugging. ## Related diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md index b2bf6a4..3959d1b 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # HTTP Call — CDN (Rust) @@ -244,27 +244,7 @@ on_http_call_response(token_id, num_headers, body_size, _) --- -## Gotchas - -- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. -- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. -- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. -- The callback `on_http_call_response` is defined on the `Context` trait, not `HttpContext` — implement it on the per-request struct, not the root context. -- `num_headers == 0` is the only reliable signal for call failure; do not rely on `body_size` or `token_id` for failure detection. -- `:authority` in headers must match the `upstream` argument passed to `dispatch_http_call`. - ---- - -## See Also - -- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) -- CDN app scaffold blueprint -- FastEdge platform overview -- FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/http_call/src/lib.rs +## Complete Example ```rust use proxy_wasm::traits::*; @@ -305,7 +285,6 @@ impl Context for HttpHeaders { println!( "Received http call response with token id: {token_id}, num_headers: {num_headers}" ); - //If num_headers is 0, then the HTTP call failed. if num_headers != 0 { let headers = self.get_http_call_response_headers(); let headers_str = headers @@ -315,7 +294,7 @@ impl Context for HttpHeaders { .join(","); println!("Response headers: [{}]", headers_str); - self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. + self.state = 1; self.resume_http_request(); } else { @@ -376,32 +355,22 @@ fn to_status_code(status: Status) -> u32 { } ``` +--- -### FILE: examples/cdn/http_call/Cargo.toml - -```toml -[workspace] - -[package] -name = "http_call" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` - +## Gotchas -### FILE: examples/cdn/http_call/README.md +- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. +- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. +- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. +- The callback `on_http_call_response` is defined on the `Context` trait, not `HttpContext` — implement it on the per-request struct, not the root context. +- `num_headers == 0` is the only reliable signal for call failure; do not rely on `body_size` or `token_id` for failure detection. +- `:authority` in headers must match the `upstream` argument passed to `dispatch_http_call`. -``` -[← Back to examples](../../README.md) +--- -# HTTP Call (CDN) +## See Also -Makes asynchronous HTTP calls to external services with timeout handling using the proxy-wasm ABI. -``` +- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) +- CDN app scaffold blueprint +- FastEdge platform overview +- FastEdge error codes reference diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md index 3517ddd..dd4a9e0 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -321,6 +321,8 @@ npm run asbuild:release # release only - `onResponseBody` must buffer until `end_of_stream` is `true`; return `StopIterationAndBuffer` otherwise - `min` and `max` for `zrange` are received as strings from query params and must be parsed with `parseFloat` - `response.status` set via `set_property` in `onResponseBody` is advisory only — the origin HTTP status passes through to the client; the JSON error body is the authoritative error signal +- Empty query string is treated as an error — app responds with `545` and a JSON error body +- `validateQueryParams` returns a map containing key `"error"` on failure; the caller must check `params.has("error")` before proceeding --- diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md index eecbdf0..e88ed80 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -65,7 +65,7 @@ self.send_http_response(BAD_REQUEST, vec![], None); **Steps**: 1. Reads `Content-Type` response header via `self.get_http_response_header("Content-Type")`. -2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. +2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. Detection uses `starts_with`, so parameters (e.g. `text/plain; charset=utf-8`) are also matched. 3. If matched: - Removes `Content-Length` by setting to `None` (required before body replacement to avoid length mismatch). - Sets `Transfer-Encoding` to `"Chunked"`. diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-properties-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-properties-as.md index 7d4571b..f293ce8 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-properties-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CDN Runtime Properties — AssemblyScript @@ -117,17 +117,26 @@ set_property("request.path", String.UTF8.encode("/new-path")); const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); - const params = queryString.split("&").map>((pair) => pair.split("=")); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map>((pair) => pair.split("=")); + for (let i = 0; i < params.length; i++) { const param = params[i]; - if (param.length !== 2) continue; + if (param.length !== 2) { + continue; // Skip invalid query parameters + } const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); set_property(REQUEST_PATH, String.UTF8.encode(value)); } } @@ -178,11 +187,75 @@ class Properties extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Error codes 551–559 identify the absent property via the HTTP response status. - // request.extension (555) and request.query (556) are optional — never trigger an error. - // Read, log, and expose all known properties as response headers. - // Return FilterHeadersStatusValues.StopIteration on any required missing property. - // Return FilterHeadersStatusValues.Continue on success. + // Error codes 551–559 identify the absent property via the HTTP response status: + // 551=uri 552=host 553=path 554=scheme 555=extension 556=query 557=x_real_ip 558=country 559=city + if (!this.handleProperty(REQUEST_URI, 551, "uri", "request-uri")) { + return FilterHeadersStatusValues.StopIteration; + } + + // host must be present for upstream routing; validated but not logged or exposed as a response header + if (!this.handleProperty(REQUEST_HOST, 552)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_PATH, 553, "path", "request-path")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_SCHEME, 554, "scheme", "request-scheme")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_EXTENSION, 555, "extension", "request-extension", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_QUERY, 556, "query", "request-query", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_X_REAL_IP, 557, "client_ip", "request-x-real-ip")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_COUNTRY, 558, "country", "request-country")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_CITY, 559, "city", "request-city")) { + return FilterHeadersStatusValues.StopIteration; + } + + // Handle query parameters — override request.url, request.host, request.path via url=, host=, path= params + const query = get_property(REQUEST_QUERY); + if (query.byteLength !== 0) { + const queryString = String.UTF8.decode(query); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map>((pair) => pair.split("=")); + + for (let i = 0; i < params.length; i++) { + const param = params[i]; + if (param.length !== 2) { + continue; + } + const key = param[0]; + const value = param[1]; + if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); + set_property(REQUEST_URI, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); + set_property(REQUEST_HOST, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); + set_property(REQUEST_PATH, String.UTF8.encode(value)); + } + } + } + + return FilterHeadersStatusValues.Continue; } onLog(): void { @@ -208,7 +281,12 @@ class Properties extends Context { } return true; } - send_http_response(errorCode, "internal server error", String.UTF8.encode("Internal server error"), []); + send_http_response( + errorCode, + "internal server error", + String.UTF8.encode("Internal server error"), + [] + ); return false; } const value = String.UTF8.decode(valueArr); @@ -323,6 +401,7 @@ Upload `build/properties.wasm` to the FastEdge portal and attach to a CDN applic - **`request.extension` and `request.query` are optional**: They may legitimately be absent (no file extension in path, no query string). Use `allowEmpty: true` — log an empty value and continue without adding a response header. - **`request.host` is validated but not exposed**: It must be non-empty for upstream routing to work correctly. It is not logged and not added as a response header. - **`set_property` on request properties takes effect immediately** for subsequent property reads and downstream filter processing within the same request. +- **Query parameter override logs change**: When a query parameter overrides a property, the change is logged with a message of the form `` `change url to: ${value}` ``, `` `change host to: ${value}` ``, or `` `change path to: ${value}` ``. - **Lifecycle constraint**: Request properties (`request.*`) are only meaningful during request processing phases. Attempting to read them outside `onRequestHeaders` or `onRequestBody` may yield empty buffers. - **Query parameter parsing is manual**: The SDK provides no built-in query string parser. Split on `&`, then on `=`, and validate `param.length === 2` before accessing indices. - **`setLogLevel` must be called in `createContext`**: Log level is set to `LogLevelValues.info` inside `PropertiesRoot.createContext`, not in the constructor of the `Properties` context class. diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md index 0d5fa22..d1e2c6e 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> # A/B Testing — FastEdge Example @@ -207,3 +207,121 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **`xid` range**: `Math.random()` can return `0` but not `1`. `slice(1, 5)` on `"0.473..."` yields `".473"` — always a 4-character string starting with `.`. - **Empty cookie after strip**: If `x-fastedge-abid` was the only cookie, the `cookie` header is deleted entirely rather than set to an empty string. - **Last variant not assigned**: If `xid * 100` falls exactly at or beyond the sum of all normalized percentages (due to floating-point rounding), no variant header is set for that test. In practice this is extremely rare given `xid` is always `< 1`. + +## Source Material + +### FILE: examples/ab-testing/src/index.js + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(request); + + const headers = createAbTestHeaders(slicedHeaders, testConfig, xid); + + // This is the URL of the outbound service - i.e. could be a url to your origin + // e.g. https://template-invoice-ab-test-123456.fastedge.cdn.gc.onl/ + const outboundUrl = getEnv('OUTBOUND_URL'); + if (!outboundUrl || !String(outboundUrl).trim()) { + return new Response('OUTBOUND_URL environment variable is not configured', { + status: 500, + }); + } + + const response = await fetch(outboundUrl, { headers }); + + // Request/Response Headers are immutable, so we need to create a new Headers object + const resHeaders = new Headers(response.headers); + resHeaders.set( + 'set-cookie', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); + +const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { + // Request/Response Headers are immutable, so we need to create a new Headers object + const headers = new Headers(reqHeaders); + const cookie = headers.get('cookie') || ''; + // Read the existing `xid` cookie value. + const xid = (cookie.match(/(?:^|;) *x-fastedge-abid=((0|1|)\.\d+) *(?:;|$)/u) || [])[1]; + if (xid) { + // Request contains A/B cookie, hide it from the origin + const newCookie = cookie.replace(/x-fastedge-abid=[^;]+;?\s*/gu, ''); + if (newCookie) { + headers.set('cookie', newCookie); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + return headers; +}; +``` + +### FILE: examples/ab-testing/package.json + +```json +{ + "name": "fastedge-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge JS example: cookie-based A/B testing", + "type": "module", + "main": "src/index.js", + "scripts": { + "build": "fastedge-build src/index.js dist/ab-testing.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md index 1dd6bda..d754a7e 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -153,114 +153,3 @@ cargo build --release --target wasm32-wasip1 - platform-overview (FastEdge request lifecycle, outbound request capabilities) - sdk-reference-rust (`fastedge::send_request`, `Body`, HTTP types) - best-practices (body buffering considerations, error handling patterns) - -## Source Material - -### FILE: examples/http/basic/backend/src/lib.rs - -```rust -use anyhow::{anyhow, Error, Result}; -use fastedge::body::Body; -use fastedge::http::{Method, Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request) -> Result> { - let (parts, body) = req.into_parts(); - let query = parts - .uri - .query() - .ok_or(anyhow!("missing uri query parameter"))?; - let params = querystring::querify(query); - let url = params - .iter() - .find(|(k, _)| k == &"url") - .ok_or(anyhow!("missing url parameter"))?; - let url = urlencoding::decode(url.1)?.to_string(); - println!("url = {:?}", url); - let request = Request::builder().uri(url).method(Method::GET).body(body)?; - - let response = fastedge::send_request(request).map_err(Error::msg)?; - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "len = {}, content-type = {:?}", - response.body().len(), - response.headers().get("Content-Type") - ))) - .map_err(Error::msg) -} -``` - - -### FILE: examples/http/basic/backend/Cargo.toml - -```toml -[workspace] - -[package] -name = "backend" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -querystring = "1.1" -urlencoding = "2.1" -``` - - -### FILE: examples/http/basic/backend/README.md - -``` -[← Back to examples](../../../README.md) - -# Backend (URL Proxy) - -A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. - -> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). - -## What it does - -1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). -2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. -3. Returns HTTP 200 with a plain-text body: - ``` - len = , content-type = - ``` -4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::send_request(request)` | Blocking outbound HTTP request | -| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | -| `fastedge::body::Body` | Request and response bodies | -| `querystring::querify` | Parse query string into key-value pairs | -| `urlencoding::decode` | Percent-decode the `?url=` value | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/backend.wasm -``` - -## Expected behavior - -| Request | Response status | Response body | -|---|---|---| -| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = , content-type = Some("")` | -| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | -| `GET /` (no query string) | 500 | `missing uri query parameter` | - -The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option` debug string (e.g. `Some("application/json")`). -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md index c836e97..21d90c6 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -150,3 +150,162 @@ Uses the `wstd` WASI Component Model HTTP server macro. - KV Store provisioning and population via FastEdge API - examples-bloom-filter-denylist-js (JavaScript mirror of this example) - platform-overview (KV Store concepts) + +## Source Material + +### FILE: examples/http/wasi/bloom_filter_denylist/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Bloom-filter IP denylist example. + +Checks the client IP (from the `x-real-ip` request header, falling back to +`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 +on a hit, 200 otherwise. + +Required configuration: + - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) + +The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; +populate the filter out of band. + +Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::key_value::{Error as StoreError, Store}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +const BLOOM_KEY: &str = "blocked-ips"; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let store_name = match env::var("DENYLIST_STORE") { + Ok(s) if !s.trim().is_empty() => s, + _ => { + return json_response( + 500, + json!({ "error": "DENYLIST_STORE environment variable is not configured" }), + ); + } + }; + + let headers = req.headers(); + let client_ip = headers + .get("x-real-ip") + .or_else(|| headers.get("x-forwarded-for")) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let Some(ip) = client_ip else { + return json_response(500, json!({ "error": "client IP not available" })); + }; + + let store = match Store::open(&store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return json_response( + 403, + json!({ "error": "access denied opening denylist store" }), + ); + } + Err(e) => { + return json_response(500, json!({ "error": format!("store open error: {e}") })); + } + }; + + let blocked = store + .bf_exists(BLOOM_KEY, ip) + .map_err(|e| anyhow!("bf_exists error: {e}"))?; + + if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. + return json_response(403, json!({ "allowed": false, "ip": ip })); + } + + json_response(200, json!({ "allowed": true, "ip": ip })) +} + +fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(value.to_string()))?) +} +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml + +```toml +[workspace] + +[package] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/README.md + +``` +[← Back to examples](../../../README.md) + +# Bloom Filter — IP Denylist (WASI) + +Rejects requests from IPs present in a KV Store bloom filter. Reads the client IP from the +`x-real-ip` request header (falling back to `x-forwarded-for`), checks it against a +pre-populated bloom filter, and returns **403** on a hit or **200** otherwise. + +Demonstrates `fastedge::key_value::Store` + `bf_exists()` and the conventional way to obtain +the client IP from a Component Model HTTP handler. + +## Configuration + +- Environment variable `DENYLIST_STORE` — name of the KV store that holds the bloom filter. +- Bloom-filter key — hardcoded to `blocked-ips`. Change `BLOOM_KEY` in `src/lib.rs` if your + key is different. + +## Behaviour + +| `bf_exists("blocked-ips", ip)` | Response | +| --- | --- | +| `true` | `403` `{ "allowed": false, "ip": "..." }` | +| `false` | `200` `{ "allowed": true, "ip": "..." }` | + +## Tradeoff: false positives + +Bloom filters answer "**definitely not** in set" vs "**maybe** in set". When `bf_exists` +returns `true`, the IP *probably* was added — but a small fraction of hits will be false +positives, meaning some legitimate IPs will be over-blocked. Acceptable for a denylist; for +allowlists or anything requiring exact membership, use `store.get()` against a regular key +instead. + +## Populating the filter + +The edge handler is read-only. Populate `blocked-ips` out of band (for example, via the +FastEdge API). + +## Related + +Mirror of `FastEdge-sdk-js/examples/bloom-filter-denylist/`. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md index 28ff740..4193334 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -188,3 +188,197 @@ cargo build --release - HTTP outbound requests via wstd (sdk-reference-rust) - Environment variable configuration (platform-overview) - HTTP app deploy workflow (deploy skill) + +## Source Material + +### FILE: examples/http/wasi/cache/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Example app demonstrating response caching and cache purging via the cache interface. + +The app reads ORIGIN_HOST from the environment, forwards the incoming request +to that origin, and caches the response body keyed by the request path. +On subsequent requests for the same path the cached body is returned directly +without hitting the origin. + +Cache reads and writes use the synchronous `fastedge::cache` API; upstream +HTTP I/O still uses the async `wstd` client. + +Special purge routes (handled before any origin call): + GET /purge — purge all cached keys; returns 200 with deleted count + GET /purge/ — purge keys whose cache key starts with cache:/ + +Environment variables: + ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com + CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) + +Build: + cargo build --release +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::cache; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let origin = env::var("ORIGIN_HOST") + .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; + + let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + env::var("CACHE_TTL_MS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(60_000) + }); + + // Build cache key from the request path (and query string if present) + let path_and_query = req + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + + // Handle purge requests before any cache/origin logic + if path_and_query == "/purge" { + let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + if let Some(prefix) = path_and_query.strip_prefix("/purge/") { + let prefix = format!("cache:/{prefix}"); + let deleted = cache::purge_prefix(&prefix)?; + println!("purge prefix '{prefix}': {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + if let Some(prefix) = path_and_query.strip_prefix("/delete/") { + let prefix = format!("cache:/{prefix}"); + cache::delete(&prefix)?; + println!("prefix '{prefix}': removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + let cache_key = format!("cache:{path_and_query}"); + + // Return cached response if available + if let Some(cached) = cache::get(&cache_key)? { + println!("cache hit: {cache_key}"); + return Ok(Response::builder() + .status(200) + .header("content-type", "application/octet-stream") + .header("x-cache", "hit") + .body(Body::from(cached))?); + } + + // Cache miss — forward request to origin + let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); + println!("cache miss: {cache_key} → {upstream_url}"); + + let upstream_req = Request::get(&upstream_url) + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("upstream request failed: {e}"))?; + + let status = upstream_resp.status(); + let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + // Read body bytes + let mut body = upstream_resp.into_body(); + let body_bytes = body.contents().await?.to_vec(); + + // Only cache successful responses + if status.is_success() { + cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; + } + + // Replay original response + let mut builder = Response::builder() + .status(status) + .header("x-cache", "miss"); + for (k, v) in &headers { + builder = builder.header(k, v); + } + Ok(builder.body(Body::from(body_bytes))?) +} +``` + + +### FILE: examples/http/wasi/cache/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/cache/README.md + +``` +[← Back to examples](../../../README.md) + +# Cache (WASI) + +Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | +| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | + +## How it works + +GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) +GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) + +Cache key is `cache:?`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. + +## Build + +cargo build --release +# Output: target/wasm32-wasip2/release/cache_wasi.wasm + +## APIs used + +- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` +- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry +- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md index 99f2107..fe37566 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,152 @@ Use `key=value` pairs separated by spaces (`logfmt`-ish format). Benefits: - host-services-rust (other host service integrations available to Rust WASI apps) - CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, different module path (see CDN apps reference) - examples-kv-store-wasi-rust (another Rust WASI HTTP example showing KV Store usage) + +## Source Material + +### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Diagnostic logging example. + +Tiny pass-through proxy that writes a single `set_user_diag` tag per request +summarising the outcome (config_error, origin_unreachable, or proxied). The +tag appears in the FastEdge platform's per-request log viewer and is distinct +from stdout — it's intended for filterable outcome labels, not verbose +traces. + +Required configuration: + - Environment variable: ORIGIN_URL (origin to proxy to) + +Uses `logfmt`-ish formatting (`outcome= key=value ...`) so the tag is +easy to slice in log search tooling. +*/ + +use std::env; + +use fastedge::utils::set_user_diag; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let method = req.method().as_str().to_string(); + let path = req.uri().path().to_string(); + + let origin = match env::var("ORIGIN_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + set_user_diag("outcome=config_error reason=origin_missing"); + return Ok(Response::builder() + .status(500) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("ORIGIN_URL is not configured"))?); + } + }; + + let outbound = Request::get(&origin).body(Body::empty())?; + let resp = match Client::new().send(outbound).await { + Ok(r) => r, + Err(e) => { + set_user_diag(&format!( + "outcome=origin_unreachable method={method} path={path} err={e}" + )); + return Ok(Response::builder() + .status(502) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("origin unreachable"))?); + } + }; + + let status = resp.status().as_u16(); + set_user_diag(&format!( + "outcome=proxied method={method} path={path} status={status}" + )); + + let (parts, mut body) = resp.into_parts(); + let bytes = body.contents().await?; + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .body(Body::from(bytes))?) +} +``` + + +### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml + +```toml +[workspace] + +[package] +name = "diagnostic_logging_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/diagnostic_logging/README.md + +``` +[← Back to examples](../../../README.md) + +# Diagnostic Logging (WASI) + +Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request +summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, +distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at +per-request outcomes. + +## Configuration + +- `ORIGIN_URL` environment variable — the origin that requests are proxied to. + +## Outcomes + +Each request writes exactly one of: + +| Condition | Tag | +| --- | --- | +| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | +| Origin unreachable | `outcome=origin_unreachable method= path=

err=` | +| Request proxied | `outcome=proxied method= path=

status=` | + +## `set_user_diag` vs `println!` + +| | `println!` | `set_user_diag` | +| --- | --- | --- | +| Channel | stdout — general application logs | per-request structured tag in platform log viewer | +| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | +| Best for | verbose traces, debug details | a single filterable outcome label | +| Forbidden | — | secrets and PII (tags appear in platform logs) | + +## Convention + +Call `set_user_diag` **once**, on every branch, late enough in the handler to know the +outcome. The `logfmt`-ish format (`outcome= key=value key=value …`) is easy to slice in +log search tooling — keep keys short and stable so they make good filter terms. + +## Related + +CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, +different module path. See `docs/CDN_APPS.md`. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md index 2a9166c..41b44c6 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,117 @@ cargo build --release - host-services-rust — `wstd` HTTP types: `Request`, `Response`, `Body` - examples-headers-wasi-rust — general header reading patterns - examples-env-vars-wasi-rust — environment variable access patterns + +## Source Material + +### FILE: examples/http/wasi/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example WASI-HTTP app demonstrating geo-based redirects. + +Reads the country code from the geoip-country-code request header +and redirects to a country-specific origin URL. Falls back to +BASE_ORIGIN when no country-specific mapping is configured. + +Required configuration: + - Environment variable: BASE_ORIGIN (fallback origin URL) + - Environment variable: (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use std::env; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let base_origin = match env::var("BASE_ORIGIN") { + Ok(origin) => origin, + Err(_) => { + return Ok(Response::builder() + .status(500) + .body(Body::from("BASE_ORIGIN is not set"))?); + } + }; + + let country_code = req + .headers() + .get("geoip-country-code") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let redirect_origin = if !country_code.is_empty() { + env::var(&country_code).unwrap_or(base_origin) + } else { + base_origin + }; + + Ok(Response::builder() + .status(302) + .header("location", &redirect_origin) + .body(Body::empty())?) +} +``` + +### FILE: examples/http/wasi/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + +### FILE: examples/http/wasi/geo_redirect/README.md + +``` +[← Back to examples](../../../README.md) + +# Geo Redirect (WASI) + +Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. + +Demonstrates reading request headers, reading environment variables, and returning redirect responses. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | +| `` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | + +## How it works + +``` +geoip-country-code: DE → env var DE is set → 302 to DE value +geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN +(no header) → 302 to BASE_ORIGIN +BASE_ORIGIN not set → 500 +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm +``` + +## APIs used + +- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge +- `std::env::var(country_code)` — dynamic env var lookup by country code +- `Response::builder().status(302).header("location", url)` — redirect response +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md index 8b6fef2..2f17c5e 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -133,3 +133,77 @@ Hello, you made a wasi request to http:///? - fastedge-docs reference: best-practices - fastedge-docs reference: error-codes - Scaffold skill blueprints: http/base (Rust) + +## Source Material + +### FILE: examples/http/wasi/hello_world/src/lib.rs + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let url = request.uri().to_string(); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!( + "Hello, you made a wasi request to {url}" + )))?) +} +``` + +### FILE: examples/http/wasi/hello_world/Cargo.toml + +```toml +[workspace] + +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + +### FILE: examples/http/wasi/hello_world/README.md + +``` +[← Back to examples](../../../README.md) + +# Hello World (WASI) + +The simplest possible async FastEdge application — echoes the full request URI in the response body. + +Demonstrates the `#[wstd::http_server]` entry-point macro and the async handler signature used by all WASI HTTP examples. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/plain;charset=UTF-8 + +Hello, you made a wasi request to http:///? +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/hello_world.wasm +``` + +## APIs used + +- `#[wstd::http_server]` — WASI HTTP entry-point macro +- `wstd::http::{Request, Response}` — request/response types +- `wstd::http::body::Body` — response body construction +- `request.uri().to_string()` — full absolute request URI +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-js.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-js.md index a906fce..2b8dbeb 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-js.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> ## KV Store — Example Reference @@ -132,6 +132,14 @@ Validation is performed by `validateQueryParams(queryParams: URLSearchParams)` i - `min` and `max` are required for: `zrange`. - `item` is required for: `bfExists`. +**Type definitions (from `utils.ts`):** +```ts +const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; +export type Action = (typeof ALL_ACTIONS)[number]; +type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; +type Params = { [key in ParamKey]: string }; +``` + --- ### Error Handling @@ -246,10 +254,31 @@ addEventListener('fetch', (event: FetchEvent) => { Decodes an `ArrayBuffer` to a UTF-8 string. Returns `''` if `arrVal` is `null`. +```ts +export const decodeValueArray = (arrVal: ArrayBuffer | null) => { + if (arrVal) { + const decoder = new TextDecoder(); + return decoder.decode(arrVal); + } + return ''; +}; +``` + #### `stringifyValueScoreTuples(tupleList: Array<[ArrayBuffer, number]>): string` Formats sorted-set result tuples as a string: `[{ Value: , Score: }, ...]`. +```ts +export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { + let strResponse = '['; + for (const tuple of tupleList) { + strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; + } + strResponse += ']'; + return strResponse; +}; +``` + --- ### Build Configuration @@ -299,3 +328,4 @@ TypeScript types for FastEdge globals (`FetchEvent`, etc.) are provided by `@gco - `"type": "module"` must be set in `package.json` for ESM compatibility with `fastedge-build`. - The SDK dependency version is `^2.3.0`. - `tsconfig.json` `target` is `ES2023`; `moduleResolution` is `Bundler`; `lib` is `["ES2023"]`; `types` is `["@gcoredev/fastedge-sdk-js"]`. +- Empty string values for required query parameters are treated as missing — validation rejects them the same as absent params. diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md index bc4dd90..3766914 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -378,3 +378,277 @@ cargo build --release - platform-overview (store provisioning and access control) - host-services-rust (other host service integrations) - examples-kv-store-js (JavaScript equivalent) + +## Source Material + +### FILE: examples/http/wasi/key_value/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating KV Store operations via the WASI-HTTP interface. + +Supports all KV Store operations via query parameters: + ?store=&action=get&key= + ?store=&action=scan&match= + ?store=&action=zrange&key=&min=&max= + ?store=&action=zscan&key=&match= + ?store=&action=bfExists&key=&item= + +Defaults to action=get if not specified. +*/ + +use std::collections::HashMap; + +use anyhow::anyhow; +use fastedge::key_value::{Store, Error as StoreError}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let query = req.uri().query().ok_or(anyhow!("no query parameters"))?; + let params: HashMap<&str, &str> = querystring::querify(query).into_iter().collect(); + + let store_name = *params + .get("store") + .ok_or(anyhow!("missing param 'store'"))?; + + let action = params.get("action").copied().unwrap_or("get"); + + let store = match Store::open(store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return Ok(Response::builder() + .status(403) + .header("content-type", "application/json") + .body(Body::from(json!({"error": "access denied"}).to_string()))?); + } + Err(e) => { + return Ok(Response::builder() + .status(500) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("store open error: {e}")}).to_string()))?); + } + }; + + let body = match action { + "get" => handle_get(&store, ¶ms)?, + "scan" => handle_scan(&store, ¶ms)?, + "zrange" => handle_zrange(&store, ¶ms)?, + "zscan" => handle_zscan(&store, ¶ms)?, + "bfExists" => handle_bf_exists(&store, ¶ms)?, + _ => { + return Ok(Response::builder() + .status(400) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("Invalid action '{action}'. Supported: get, scan, zrange, zscan, bfExists")}).to_string()))?); + } + }; + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(body))?) +} + +fn handle_get(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + match store.get(key) { + Ok(Some(value)) => { + let value_str = String::from_utf8_lossy(&value); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": value_str.as_ref() + }).to_string()) + } + Ok(None) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": null + }).to_string()), + Err(e) => Err(anyhow!("KV get error: {e}")), + } +} + +fn handle_scan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + match store.scan(pattern) { + Ok(keys) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "scan", + "match": pattern, + "response": keys + }).to_string()), + Err(e) => Err(anyhow!("KV scan error: {e}")), + } +} + +fn handle_zrange(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let min: f64 = params + .get("min") + .ok_or(anyhow!("missing param 'min'"))? + .parse() + .map_err(|_| anyhow!("invalid 'min': must be a number"))?; + let max: f64 = params + .get("max") + .ok_or(anyhow!("missing param 'max'"))? + .parse() + .map_err(|_| anyhow!("invalid 'max': must be a number"))?; + + match store.zrange_by_score(key, min, max) { + Ok(entries) => { + let entries_json: Vec = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zrange", + "key": key, + "min": min, + "max": max, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zrange error: {e}")), + } +} + +fn handle_zscan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + + match store.zscan(key, pattern) { + Ok(entries) => { + let entries_json: Vec = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zscan", + "key": key, + "match": pattern, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zscan error: {e}")), + } +} + +fn handle_bf_exists(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let item = *params + .get("item") + .ok_or(anyhow!("missing param 'item'"))?; + + match store.bf_exists(key, item) { + Ok(exists) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "bfExists", + "key": key, + "item": item, + "response": exists + }).to_string()), + Err(e) => Err(anyhow!("KV bfExists error: {e}")), + } +} +``` + +### FILE: examples/http/wasi/key_value/Cargo.toml + +```toml +[workspace] + +[package] +name = "key_value_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +querystring = "1.1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/key_value/README.md + +``` +[← Back to examples](../../../README.md) + +# Key Value (WASI) + +Demonstrates all KV store operations via a query-parameter driven HTTP API: get, scan, zrange, zscan, and bfExists. + +## Usage + +All operations require `?store=` and `?action=` query parameters: + +| Action | Additional params | Description | +|---|---|---| +| `get` | `key=` | Fetch a single value by key | +| `scan` | `match=` | List keys matching a glob pattern | +| `zrange` | `key=&min=&max=` | Fetch sorted-set members by score range | +| `zscan` | `key=&match=` | List sorted-set members matching a pattern | +| `bfExists` | `key=&item=` | Check bloom filter membership | + +`action` defaults to `get` if omitted. + +## Example + +``` +GET /?store=my-store&action=get&key=hello +→ 200 {"store":"my-store","action":"get","key":"hello","response":"world"} + +GET /?store=my-store&action=scan&match=user:* +→ 200 {"store":"my-store","action":"scan","match":"user:*","response":["user:1","user:2"]} +``` + +## Error responses + +| Condition | Status | Body | +|---|---|---| +| Store not found / access denied | 403 | `{"error":"access denied"}` | +| Missing required params | 530 | Runtime error | +| Store open error | 500 | `{"error":"store open error: ..."}` | +| Invalid action | 400 | `{"error":"Invalid action '...'. Supported: ..."}` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/key_value_wasi.wasm +``` + +## APIs used + +- `fastedge::key_value::Store::open(name)` — open a named KV store +- `store.get(key)` — fetch value by key; returns `Ok(Option>)` +- `store.scan(pattern)` — list keys by glob pattern +- `store.zrange_by_score(key, min, max)` — range query on sorted set +- `store.zscan(key, pattern)` — pattern scan on sorted set +- `store.bf_exists(key, item)` — bloom filter membership test +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md index 43e3c89..f98adf3 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -51,14 +51,14 @@ Only `GET` and `HEAD` are accepted. ## Core Flow -1. Read `BASE` env var; strip trailing `/`. +1. Read `BASE` env var; strip trailing `/` via `base.trim_end_matches('/')`. 2. Validate request path is non-empty and not `/`. 3. Construct outbound `GET` request: `BASE + path`, `User-Agent: fastedge`. 4. Call `fastedge::send_request` via internal `request()` helper. -5. Follow redirects up to `MAX_REDIRECTS = 5` hops. -6. Decode response body as UTF-8; failure → `500`. -7. Parse Markdown with `Parser::new_ext(md, Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. -8. Render HTML with `pulldown_cmark::html::push_html`. +5. Follow redirects up to `MAX_REDIRECTS = 5` hops via `request_inner(req, depth)`. +6. Decode response body as UTF-8 via `String::from_utf8(rsp.body().to_vec())`; failure → `500`. +7. Parse Markdown with `Parser::new_ext(md.as_str(), Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. +8. Render HTML with `pulldown_cmark::html::push_html(&mut html, parser)`. 9. Wrap in `...`; optionally inject `HEAD` env var into ``. 10. Return `200 OK`, `Content-Type: text/html`. @@ -193,6 +193,7 @@ Build target: `wasm32-wasip1`. - `String::from_utf8(rsp.body().to_vec())` fails on binary responses (e.g. images) — returns `500`. Ensure `BASE` points to a text/Markdown origin. - Redirect following is implemented manually; `fastedge::send_request` does not auto-follow redirects. - `HEAD` env var content is injected as raw HTML — no escaping or validation. Only inject trusted content. +- `request_inner` only returns `Ok(rsp)` for `200 OK` responses; all other non-redirect status codes are returned as `Err(status)` and forwarded as empty-body responses. --- diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md index 142235f..f25c8e7 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -186,3 +186,120 @@ cargo build --release - sdk-reference-rust (full `fastedge` crate API reference) - examples-basic-http-rust (minimal sync handler without outbound fetch) - platform-overview (FastEdge execution model and WASM target context) + +## Source Material + +### FILE: examples/http/basic/outbound_fetch/src/lib.rs + +```rust +use anyhow::{Error, Result}; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use serde_json::{json, Value}; + +#[fastedge::http] +fn main(_req: Request) -> Result> { + let upstream_req = Request::builder() + .uri("http://jsonplaceholder.typicode.com/users") + .body(Body::empty())?; + + let upstream_resp = fastedge::send_request(upstream_req).map_err(Error::msg)?; + + let body_bytes = upstream_resp.body().to_vec(); + let users: Value = serde_json::from_slice(&body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(Body::from(result.to_string())) + .map_err(Into::into) +} +``` + + +### FILE: examples/http/basic/outbound_fetch/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_fetch" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` + + +### FILE: examples/http/basic/outbound_fetch/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Fetch (Basic HTTP) + +Demonstrates outbound HTTP from a FastEdge application using the **legacy sync handler** (`#[fastedge::http]`). Fetches user data from the [JSONPlaceholder](https://jsonplaceholder.typicode.com) public API, selects the first 5 users, and returns them in a paginated JSON envelope. + +> **When to use this example:** If you need a synchronous, single-function HTTP handler with outbound requests targeting `wasm32-wasip1`. For new apps, prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +On any incoming request: + +1. Makes a GET request to `http://jsonplaceholder.typicode.com/users` using `fastedge::send_request`. +2. Parses the JSON response body. +3. Takes the first 5 users from the array. +4. Returns a JSON envelope with pagination metadata. + +Example response body: + +```json +{ + "users": [ { "id": 1, "name": "Leanne Graham", ... }, ... ], + "total": 5, + "skip": 0, + "limit": 30 +} +``` + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request` | Outbound HTTP request to upstream API | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Request and response bodies | +| `serde_json` | JSON parsing and serialisation | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/outbound_fetch.wasm +``` + +## Expected behavior + +| Request | Response status | Response content-type | Response body | +|---|---|---|---| +| `GET /` | 200 | `application/json` | JSON object with `users` (array of ≤5), `total`, `skip`, `limit` | +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md index 5666f87..6be9762 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -182,3 +182,102 @@ Ok(Response::builder() - outbound-modify-response example (JS) - wstd HTTP client documentation - serde_json crate documentation + +## Source Material + +### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Outbound fetch with response transformation. + +Fetches JSON from an upstream origin, reads and parses the body, reshapes it +into a new JSON object (first 5 users with pagination metadata), and returns +it with a fresh `content-type: application/json` header. + +This is the stepping-stone beyond `outbound_fetch/` which just passes the +upstream response through unchanged. + +Mirror of the FastEdge-sdk-js `outbound-modify-response` example. +*/ + +use anyhow::anyhow; +use serde_json::{Value, json}; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (_, mut body) = upstream_resp.into_parts(); + let body_bytes = body.contents().await?; + let users: Value = serde_json::from_slice(body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} +``` + +### FILE: examples/http/wasi/outbound_modify_response/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_modify_response_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/outbound_modify_response/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Modify Response (WASI) + +Fetch data from an outbound HTTP origin, transform the JSON response (slice to first 5 +users), and return it with a fresh `content-type: application/json` header. + +Demonstrates reading the upstream body with `body.contents().await`, parsing JSON with +`serde_json`, and composing a new response from scratch. + +## Related + +- [outbound_fetch](../outbound_fetch/) — the simpler variant that just passes the upstream + response through unchanged. +- Mirror of `FastEdge-sdk-js/examples/outbound-modify-response/`. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md index ac1fdd7..2f2a35a 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -171,6 +171,7 @@ cargo build --release - `MAX_FILE_SIZE` is silently ignored (no error, no log) when set to a non-numeric string. - On S3 success, the response body is replaced entirely with the clean object URL — the original S3 response body is discarded. - The `UrlStyle::Path` style is used for `Bucket::new` — bucket name appears in the URL path, not the hostname. +- Query params (`?name=...`) are parsed manually using `split('&')` and `splitn(2, '=')` into a `HashMap`. --- diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md index d97a52d..3d9715b 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # Secret Access — Rust HTTP Example @@ -251,3 +251,185 @@ node tools/fixture-validator/index.mjs \ - fastedge-docs skill reference: sdk-reference-rust - fastedge-docs skill reference: error-codes - manage skill: secret management subcommands (set, list, delete) + +## Source Material + +### FILE: examples/http/basic/secret/src/lib.rs + +```rust +use anyhow::{Error, Result}; +use std::time::SystemTime; + +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use fastedge::secret; + +#[allow(dead_code)] +#[fastedge::http] +fn main(_req: Request) -> Result> { + let value = match secret::get("SECRET") { + Ok(value) => value, + Err(secret::Error::AccessDenied) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .map_err(Error::msg); + } + Err(secret::Error::Other(msg)) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from(msg)) + .map_err(Error::msg); + } + Err(secret::Error::DecryptError) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .map_err(Error::msg); + } + }; + + if value.is_none() { + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .map_err(Error::msg); + } + + let ts = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let effective_at_value = match secret::get_effective_at("SECRET", ts as u32) { + Ok(value) => value, + Err(secret::Error::AccessDenied) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .map_err(Error::msg); + } + Err(secret::Error::Other(msg)) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from(msg)) + .map_err(Error::msg); + } + Err(secret::Error::DecryptError) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .map_err(Error::msg); + } + }; + + if effective_at_value.is_none() { + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .map_err(Error::msg); + } + + Response::builder() + .status(StatusCode::OK) + .body(Body::from(format!( + "get={:?}\nget_efective_at={:?}\n", + value, effective_at_value + ))) + .map_err(Error::msg) +} +``` + + +### FILE: examples/http/basic/secret/Cargo.toml + +```toml +[workspace] + +[package] +name = "secret" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/basic/secret/README.md + +``` +[← Back to examples](../../../README.md) + +# Secret + +Demonstrates accessing encrypted secrets injected by the FastEdge platform using `secret::get()` and `secret::get_effective_at()`. Shows how to handle all error variants (access denied, decrypt error) and the time-based secret rotation API. + +## What it does + +On every request, the handler: + +1. Calls `secret::get("SECRET")` — retrieves the current value of the secret named `SECRET`. +2. If the secret is missing (returned as `None`), returns **404**. +3. Calls `secret::get_effective_at("SECRET", )` — retrieves the secret value effective at the current Unix timestamp, demonstrating the rotation/versioning API. +4. If that value is also missing, returns **404**. +5. On success, returns **200** with both values in the body (Debug format). + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::secret::get(key)` | Retrieve the current value of a named secret | +| `fastedge::secret::get_effective_at(key, timestamp)` | Retrieve the secret value effective at a specific Unix timestamp | +| `fastedge::secret::Error` | Error variants: `AccessDenied`, `Other(msg)`, `DecryptError` | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Response body | + +## Secret error variants + +| Variant | HTTP response | Meaning | +|---|---|---| +| `Ok(Some(value))` | 200 with body | Secret found | +| `Ok(None)` | 404 empty | Secret name is valid but not set | +| `Err(AccessDenied)` | 403 empty | App is not permitted to read this secret | +| `Err(Other(msg))` | 403 with `msg` body | Other denial with a human-readable message | +| `Err(DecryptError)` | 500 empty | Secret exists but could not be decrypted | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/secret.wasm +``` + +## Expected behavior + +| Scenario | Secret `SECRET` | Response status | Response body | +|---|---|---|---| +| Happy path | `"my-value"` | 200 | `get=Some("my-value")\nget_efective_at=Some("my-value")\n` | +| Secret not set | (absent) | 404 | (empty) | +| Access denied | — | 403 | (empty) | + +> **Note:** The response body contains a typo in the field name (`get_efective_at` instead of `get_effective_at`). This is a known cosmetic issue in the source. + +## Local testing + +Inject the secret via a `.env` file in your fixtures directory using the `FASTEDGE_VAR_SECRET_` prefix: + +``` +# fixtures/.env +FASTEDGE_VAR_SECRET_SECRET=my-test-value +``` + +Run with the fixture validator: + +```sh +node tools/fixture-validator/index.mjs \ + FastEdge-sdk-rust/examples/http/basic/secret/ \ + --wasm FastEdge-sdk-rust/examples/http/basic/secret/target/wasm32-wasip1/release/secret.wasm +``` +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md index 824cf69..3370f1b 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -147,3 +147,123 @@ package = "component:simple_fetch" - sdk-reference-rust - examples-simple-request-rust (basic sync HTTP handler, `fastedge` crate) - host-services-rust (outbound fetch via host services) + +## Source Material + +### FILE: examples/http/wasi/simple_fetch/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating the WASI-HTTP interface via the wstd crate. + +The app receives an incoming HTTP request and makes an outbound HTTP request +to the URL specified in the `x-fetch-url` header (defaults to https://httpbin.org/get). + +Build with cargo-component: + cargo component build --release +*/ + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let target_url = request + .headers() + .get("x-fetch-url") + .and_then(|v| v.to_str().ok()) + .unwrap_or("https://httpbin.org/get") + .to_string(); + + println!("Fetching: {target_url}"); + + let upstream_req = Request::get(&target_url) + .header("accept", "application/json") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let client = Client::new(); + let response = client + .send(upstream_req) + .await + .map_err(|e| anyhow!("request failed: {e}"))?; + + println!("Response status: {}", response.status()); + + Ok(response) +} +``` + + +### FILE: examples/http/wasi/simple_fetch/Cargo.toml + +```toml +[workspace] + +[package] +name = "simple_fetch" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" + +[package.metadata.component] +package = "component:simple_fetch" +``` + + +### FILE: examples/http/wasi/simple_fetch/README.md + +``` +[← Back to examples](../../../README.md) + +# Simple Fetch + +A minimal example demonstrating outbound HTTP requests using the [WASI-HTTP](https://github.com/WebAssembly/wasi-http) interface via the [`wstd`](https://crates.io/crates/wstd) crate. + +Uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). The same async pattern is used by all examples in `examples/http/wasi/`. + +## How it works + +The app receives an incoming request, reads the target URL from the `x-fetch-url` header, makes an outbound GET request to that URL, and streams the response back to the caller. + +If the `x-fetch-url` header is absent, it defaults to `https://httpbin.org/get`. + +## Request headers + +| Header | Required | Description | +|--------|----------|-------------| +| `x-fetch-url` | No | URL to fetch. Defaults to `https://httpbin.org/get` | + +## Example + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + +## Build + +```bash +cargo build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Key differences from basic HTTP examples + +| | Basic HTTP (`fastedge` crate) | WASI HTTP (`wstd` crate) | +|---|---|---| +| Handler | `fn main(req)` — sync | `async fn main(req)` — async | +| Macro | `#[fastedge::http]` | `#[wstd::http_server]` | +| Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | +| Build target | `wasm32-wasip1` | `wasm32-wasip2` | +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md index 7584018..b8e56f8 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -143,3 +143,105 @@ chunk 4 - examples-streaming-js reference (JavaScript mirror of this example) - wstd HTTP body reference - FastEdge SDK Rust reference + +## Source Material + +### FILE: examples/http/wasi/streaming/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https:///` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} +``` + + +### FILE: examples/http/wasi/streaming/Cargo.toml + +```toml +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" +``` + + +### FILE: examples/http/wasi/streaming/README.md + +``` +[← Back to examples](../../../README.md) + +# Streaming Response (WASI) + +Generates a response body on the fly — five text chunks, one every 200 ms — using +`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it +is produced, not all at once at the end. + +Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async +stream generation, and `wstd::time::Timer` for per-chunk delays. + +## Testing the streaming behaviour + +```sh +curl -N https://.fastedge.cdn.gc.onl/ +``` + +`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a +time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. + +## Other streaming patterns + +- **Pass-through streaming** — return an upstream response's body directly. See + [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. +- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, + then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. +- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where + `chunks` is any iterable of `Into`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/streaming/`. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/quickstart-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/quickstart-as.md index e408331..05333df 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/quickstart-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/quickstart-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # Quickstart: AssemblyScript CDN Apps on FastEdge @@ -118,7 +118,7 @@ registerRootContext((context_id: u32) => { |------|---------| | `export * from ".../assembly/proxy"` | Exposes wasm entry points the host runtime calls into. Required in every app. Must be the first line. | | `RootContext` subclass | Created once per worker. `createContext` produces a `Context` instance for each hook invocation. | -| `Context` subclass | Handles a single lifecycle hook phase. Instance fields do not persist across hooks (hook state isolation). | +| `Context` subclass | Handles a single lifecycle hook phase. A fresh instance is created for each hook phase — instance fields do not persist across hooks (hook state isolation). | | `registerRootContext` | Registers the root context factory with the proxy runtime. | ### Lifecycle hooks diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/sdk-reference-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/sdk-reference-as.md index 8b77309..8d3c4f2 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/sdk-reference-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/sdk-reference-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # AssemblyScript Proxy-Wasm SDK Reference @@ -65,6 +65,8 @@ registerRootContext( ); ``` +**No default parameters on nested functions**: AssemblyScript compiles functions defined inside a method body to `call_indirect` entries in the WebAssembly element table. Default parameter values are applied at direct call sites only — for indirect calls, unspecified argument slots receive `0` (a null pointer). If those slots are used as strings or objects, the wasm traps with a memory access out-of-bounds error at runtime. Fix: promote the helper to a `private` class method (dispatched as a direct call; defaults apply correctly), or pass all arguments explicitly at every call site. + --- ## Build Configuration @@ -873,14 +875,22 @@ function getEnv(name: string): string; Returns the value of the environment variable, or an empty string if the variable is not set. +**Empty string fallback**: AssemblyScript's `||` operator performs a pointer-non-null check, not a value-falsy check. `getEnv("X") || "default"` returns `""` rather than `"default"` when the variable is unset, because the empty-string object has a non-zero pointer. Use an explicit check instead: + +```typescript +const raw = getEnv("X"); +const val = raw === "" ? "default" : raw; +``` + +The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getEnv } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; import { log, LogLevelValues } from "@gcoredev/proxy-wasm-sdk-as/assembly"; -const blocklist = getEnv("BLOCKLIST"); -if (blocklist.length == 0) { - log(LogLevelValues.warn, "BLOCKLIST env var is not set"); -} +const raw = getEnv("BLOCKLIST"); +const blocklist = raw === "" ? "none" : raw; +log(LogLevelValues.info, "Blocklist: " + blocklist); ``` ### Dictionary (getDictionary) @@ -919,6 +929,8 @@ function getSecretEffectiveAt(name: string, effectiveAt: u32): string; `getSecretEffectiveAt`: reads a secret from a specific rotation slot. Slots are defined in the FastEdge UI and are always numeric (e.g., incremental integers, or timestamp-style values representing a point in time). Use this for secret rotation: pass the current Unix timestamp in seconds as `effectiveAt`. The host selects the slot where `effectiveAt >= secret_slots.slot`. +**Empty string fallback**: AssemblyScript's `||` operator does not fall back on empty strings. `getSecret("KEY") || "default"` returns `""` rather than `"default"` when the secret is unset. Use an explicit check: `const raw = getSecret("KEY"); const key = raw === "" ? "default" : raw;`. The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getSecret, @@ -959,7 +971,7 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the | `scan(pattern: string): string[]` | `string[]` | Glob-style key scan. Pattern must include a wildcard (e.g., `"foo*"`). | | `zrangeByScore(key: string, min: f64, max: f64): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set range query: returns entries where `min <= score <= max`. | | `zscan(key: string, pattern: string): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set pattern scan: matches values against the glob pattern. | -| `bfExists(key: string, item: string): bool` | `bool` | Bloom filter membership check. Returns `true` if item may exist. | +| `bfExists(key: string, item: string): boolean` | `boolean` | Bloom filter membership check. Returns `true` if item may exist. | `get` returns `ArrayBuffer | null` — the caller must decode the buffer. @@ -970,9 +982,9 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the ```typescript class ValueScoreTuple { value: ArrayBuffer; // The stored value bytes - score: f64; // The associated score + score: number; // The associated score (f64) - constructor(value: ArrayBuffer, score: f64); + constructor(value: ArrayBuffer, score: number); } ``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md new file mode 100644 index 0000000..91bb0f1 --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md @@ -0,0 +1,94 @@ + + +# FastEdge Bolt-On Templates + +Before writing authentication, cookie-hardening, or content-transform logic from scratch, check this catalog. Each entry is a maintained, tested FastEdge application that handles the security-sensitive parts a hand-rolled implementation would need to get right independently. Use or adapt these templates rather than building the same capability from scratch. + +## html2md — HTML-to-Markdown conversion / content transformation / Accept header negotiation + +**What it solves:** A CDN filter that transparently converts HTML origin responses to Markdown when the client sends `Accept: text/markdown`. Handles content negotiation, encoding, and caching concerns that a hand-rolled filter would need to get right independently. + +**How it deploys:** Single CDN Proxy-WASM app. Deployed via the Gcore portal template gallery (template ID 110). No origin changes required. + +**Runtime variants:** None. Conversion is fully automatic based on request `Accept` and response `Content-Type` headers. + +**Conversion conditions:** All three must be true — request `Accept` includes `text/markdown`, origin response `Content-Type` includes `text/html` with charset absent or `utf-8`, and full response body is available at end of stream. + +**What the filter does on conversion:** Adds request header `Convert: markdown` (cache differentiation), removes `Accept-Encoding` to avoid compressed payloads, removes `Content-Length`, sets `Content-Type: text/markdown; charset=utf-8`, sets `Transfer-Encoding: Chunked`, converts body at end of stream, adds `Vary: Convert` (merged with existing `Vary`). + +**Error conditions:** Returns `500` if origin body is not valid UTF-8 or if HTML-to-Markdown conversion fails. Non-HTML responses pass through unchanged. + +**Origin integration:** Configured entirely through request/response header inspection — no env vars, no origin code required. + +--- + +## harden-cookies — Cookie security hardening / Secure HttpOnly SameSite attributes / Set-Cookie rewriting + +**What it solves:** A CDN filter that adds `Secure`, `HttpOnly`, and `SameSite=Strict` attributes to targeted `Set-Cookie` response headers. Eliminates the need to modify backend code to enforce cookie security policy at the edge. + +**How it deploys:** Single CDN Proxy-WASM app. Deployed via the Gcore portal template gallery (template ID 184). No origin changes required. + +**Runtime variants:** None. Behavior is controlled entirely through environment variables. + +**Targeting behavior:** Only cookies whose name matches `COOKIE_NAME` are modified. Use `*` to match all cookies. All other `Set-Cookie` headers pass through unchanged. If `COOKIE_NAME` is unset, or none of `SECURE`, `HTTPONLY`, `SAMESITE` is set to `true`, responses pass through untouched. + +**Attribute application:** `Secure` and `HttpOnly` are added only if not already present. `SameSite=Strict` is set unconditionally, overriding any existing `SameSite` value. + +**Origin integration:** Configured entirely through environment variables (`COOKIE_NAME`, `SECURE`, `HTTPONLY`, `SAMESITE`) — no origin code required. + +--- + +## edge-sso — SSO / login / identity federation / Identity-Aware Proxy (Google, GitHub, Microsoft, Facebook, SAML) + +**What it solves:** A bolt-on Identity-Aware Proxy that adds multi-provider SSO (Google, GitHub, Microsoft, Facebook, SAML 2.0) to any existing site without changing the backend. Handles OAuth 2.0 / OIDC / SAML flows, session token issuance, and per-request enforcement. Building this from scratch requires correctly implementing multiple OAuth flows, token signing, and edge enforcement — this template has already done that. + +**How it deploys:** Two FastEdge apps deployed together, both via the Gcore portal template gallery: +- `cdn-filter/` — CDN Proxy-WASM app (Rust); sits in the CDN proxy layer, verifies session token on every request, redirects unauthenticated users to the auth app +- `auth-app/` — HTTP app (TypeScript/Hono); federates to the identity provider, issues a signed session token, sets it on the client + +**Runtime variants:** `SSO_VARIANT` selects the identity-delivery mode — the same value must be set on both apps: + +| Variant | What the origin receives | When to use | +|---|---|---| +| `gate-only` | Allow/deny only — no identity forwarded | Origin needs access control but not user context | +| `cookie` | Signed JWT in a cookie the origin can verify | Origin reads user identity from a verifiable token | +| `header` | Signed `x-sso-*` identity headers injected upstream | Origin trusts a header from the CDN layer | + +**Supported identity providers:** Google (OAuth 2.0), GitHub (OAuth 2.0), Microsoft (OAuth 2.0 / OIDC), Facebook (OAuth 2.0), SAML (SAML 2.0). + +**Key shared configuration requirements:** `SSO_VARIANT` and `SSO_AUDIENCE` must match on both apps. `SESSION_SECRET` is required in every variant. The `cookie` variant additionally requires an EC keypair (`SESSION_SIGNING_KEY` secret + `SESSION_PUBLIC_JWK` env var). + +**Origin integration:** The template is configured through environment variables and secrets on both apps. For the customer-side wiring contract — how the origin validates tokens or trusts identity headers depending on the chosen variant — see the `edge-sso` integration reference. + +--- + +## edge-totp — TOTP MFA / two-factor authentication / OTP challenge / RFC 6238 + +**What it solves:** Adds a TOTP (RFC 6238) two-factor authentication step in front of an existing site's login without modifying the backend. The customer's origin handles password validation; this app hosts the 6-digit OTP challenge, verifies the code with replay and brute-force protection, and issues a signed session assertion the origin trusts. Building this from scratch requires correctly implementing HOTP/TOTP, replay protection, brute-force throttling, signed cookie issuance, and CDN enforcement — this template has already done that. + +**How it deploys:** Two FastEdge apps deployed together via the Gcore portal template gallery: +- `otp-app/` — HTTP app (TypeScript/Hono, WASM); hosts the challenge, verify, enroll, self-service activate, logout, JWKS, and health endpoints; signs the `mfa_session` cookie (HS256) and the optional ES256 proof +- `otp-filter/` — CDN Proxy-WASM app (Rust); enforces `mfa_session` on protected paths; default-deny and fail-closed + +**Runtime variants (enforcement profiles):** + +| Profile | Mode | What the origin must do | +|---|---|---| +| **A** (default) | Filter enforces, zero origin code | Nothing — the CDN layer is the enforcement boundary | +| **B** (opt-in) | Origin verifies a one-time ES256 proof via JWKS | Origin validates the proof at its own session boundary using the app's JWKS endpoint | + +**Security-critical deployment requirements:** +- The origin must be locked to edge-only traffic (IP allowlist / origin auth / tunnel) — the gate is bypassed if the origin is directly reachable +- `MFA_AUDIENCE` must be set on both apps; the filter fail-closes (rejects every session) if it is unset +- `GCORE_API_TOKEN` has write access to every seed in the KV store — scope it to a single-tenant, per-customer isolated KV store + +**CDN wiring:** Attach `otp-app` as a CDN origin on the `{AUTH_PREFIX}/*` path rule of the customer's CDN resource; attach `otp-filter` as the CDN proxy app in front of protected paths, bypassing `{AUTH_PREFIX}` and `/health`. Both share the CDN host so `mfa_session` is first-party host-only. + +**Origin integration:** For the full customer-side wiring contract, trust model, and Profile B JWKS integration, see the `edge-totp` integration reference. diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-sso-integration.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-sso-integration.md new file mode 100644 index 0000000..ee18756 --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-sso-integration.md @@ -0,0 +1,214 @@ + + +# edge-sso — Origin Integration Reference + +Reference for origins integrating with an already-deployed `edge-sso` template pair (auth-app + cdn-filter). Covers the contract the origin relies on: identity delivery, auth routes, login page customization, and redirect validation. + +--- + +## 1. What `SSO_VARIANT` Means for the Origin + +`SSO_VARIANT` is set identically on both the auth-app and the cdn-filter. It controls how the edge delivers identity to the origin and what the origin is responsible for verifying. + +### `gate-only` + +- **What the origin receives**: nothing — the filter enforces allow/deny at the edge; only authenticated requests reach the origin. +- **Origin responsibility**: none. The origin does not receive a token or identity headers; it only sees requests that passed the gate. +- **Signing algorithm**: HS256 (shared `SESSION_SECRET`). The origin never verifies the token; the filter handles all verification. +- **Session cookie**: stripped before forwarding — the origin never sees the raw JWT. + +### `cookie` + +- **What the origin receives**: the `sso_session` cookie (configurable via `SESSION_COOKIE`) containing a signed JWT. The cookie passes through to the origin unchanged. +- **Origin responsibility**: the origin verifies the cookie itself using the published JWKS endpoint. + - **JWKS endpoint**: `GET /auth/.well-known/jwks.json` — mounted on the auth-app only when `SSO_VARIANT=cookie` and `SESSION_PUBLIC_JWK` is configured. Use a standard JWKS client (e.g., `createRemoteJWKSet` from the `jose` library) against this URL — do not implement verification manually. +- **Signing algorithm**: ES256 (asymmetric — EC private key `SESSION_SIGNING_KEY` signs; public JWK published via JWKS). The origin holds only the public key and verifies with it; it never holds a forge-capable secret. +- **Cookie attributes**: `HttpOnly; Secure; SameSite=Lax`. Under single-domain routing no `Domain=` is set — same-origin. +- **Token claims**: `sub`, `iat`, `exp`, `aud`, `iss` (optional), `email`, `name`, `picture`, `given_name`, `family_name`. + +### `header` + +- **What the origin receives**: verified identity injected as request headers. The session cookie is stripped before forwarding — the origin never sees the raw JWT. +- **Origin responsibility**: read and trust `x-sso-*` headers. The origin **must treat an empty `x-sso-*` header as absent** — the platform blanks a cleared header to empty rather than removing it, so an empty value means the claim was not present in the token. +- **Signing algorithm**: HS256 (shared `SESSION_SECRET`). The origin does not verify the token; the filter handles all verification before injecting headers. +- **Anti-spoofing contract**: the filter clears any client-supplied `x-sso-*` header before injecting verified values. A client cannot smuggle a spoofed identity header past the filter. The origin must not trust `x-sso-*` values from any path that bypasses the filter. + +**Complete list of injected headers (verbatim from `cdn-filter/src/lib.rs`):** + +| Header | Claim source | +|---|---| +| `x-sso-user` | `sub` | +| `x-sso-email` | `email` | +| `x-sso-name` | `name` | +| `x-sso-picture` | `picture` | +| `x-sso-given-name` | `given_name` | +| `x-sso-family-name` | `family_name` | + +Headers for claims absent from the token: if the claim was not present, the header is absent (or empty — treat empty as absent per the platform contract above). + +--- + +## 2. Auth-App Routes + +All routes are served under `AUTH_PREFIX` (default: `/auth`). Under single-domain routing, the CDN routes `AUTH_PREFIX/**` to the auth-app as an origin on the customer's own domain. The cdn-filter bypasses `AUTH_PREFIX/**` — it does not gate these routes. + +| Route | Method | Caller | Purpose | +|---|---|---|---| +| `/auth/` and `/auth` | GET | Browser | Hosted login page — server-rendered, branded, provider buttons. Honours `?redirect=`. | +| `/auth/providers` | GET | Browser / custom login UI | Provider data (JSON) — the enabled provider set with login URLs. Honours `?redirect=`. | +| `/auth/branding` | GET | Custom login page | Branding config (JSON) — current `LOGIN_PAGE_*` values for custom pages. | +| `/auth/login/google` | GET | Browser | Start Google OIDC. Honours `?redirect=`. | +| `/auth/login/github` | GET | Browser | Start GitHub OAuth. Honours `?redirect=`. | +| `/auth/login/microsoft` | GET | Browser | Start Microsoft OIDC. Honours `?redirect=`. | +| `/auth/login/facebook` | GET | Browser | Start Facebook OAuth. Honours `?redirect=`. | +| `/auth/login` | GET | Browser | Start SAML SSO. Honours `?redirect=`. | +| `/auth/logout` | GET | Browser | Sign out — clears `sso_session` (`Max-Age=0`), redirects to the validated `?redirect=` (defaults to `/`). Not gated by the filter. | +| `/auth/callback/` | GET | IdP only | OAuth/OIDC callback — used by the identity provider, not called directly. | +| `/auth/callback` | POST | IdP only | SAML ACS endpoint — used by the identity provider, not called directly. | +| `/auth/.well-known/jwks.json` | GET | Origin / JWKS client | Public JWK set — mounted only when `SSO_VARIANT=cookie` and `SESSION_PUBLIC_JWK` is set. | + +`?redirect=` is the post-login destination. After successful federation the auth-app sets the `sso_session` cookie and 302s to that URL. + +--- + +## 3. Login Page Customization Tiers + +### Tier 1 — Env Var Branding (recommended default) + +The built-in hosted login page reads these env vars per-request. No code changes required. + +| Env var | Default | Effect | +|---|---|---| +| `LOGIN_PAGE_TITLE` | `"Sign in"` | `` and `<h1>` | +| `LOGIN_PAGE_SUBTITLE` | `"Choose a sign-in method"` | Subheading below the title | +| `LOGIN_PAGE_LOGO_URL` | — | Logo image above the title | +| `LOGIN_PAGE_FAVICON_URL` | — | Tab favicon | +| `LOGIN_PAGE_ACCENT_COLOR` | `#0066cc` | Button/focus-ring color (CSS `--lp-accent`) | +| `LOGIN_PAGE_BACKGROUND_COLOR` | `#f0f2f5` | Page background (CSS `--lp-bg`) | +| `LOGIN_PAGE_CSS_URL` | — | Customer stylesheet linked last — overrides any built-in style | +| `IDP_LABEL` | `"SSO"` | Display name for the SAML provider button | +| `IDP_ICON_URL` | — | Icon URL for the SAML provider button | + +`LOGIN_PAGE_CSS_URL` is the deep-customization escape hatch — a `<link rel="stylesheet">` injected after built-in styles. The CSS variables `--lp-accent` and `--lp-bg` are intentional override points. + +### Tier 2 — Fully Custom Login Page (`LOGIN_PAGE_URL`) + +Set `LOGIN_PAGE_URL` on the **CDN filter** to redirect unauthenticated users to a page you own instead of the built-in one. That page calls `GET /auth/providers` for login URLs and, optionally, `GET /auth/branding` for consistent branding tokens. + +``` +LOGIN_PAGE_URL=https://shop.example.com/my-login +``` + +Your custom page handles the full UI; clicking a provider's button navigates to its `loginUrl` (relative, same-origin) which kicks off the standard federation flow. The default value of `LOGIN_PAGE_URL` is `/auth/` — set it only to opt out. + +### Tier 3 — Embed Sign-In Buttons on an Existing Page + +**Static links (simplest):** hard-code the provider routes. +```html +<a href="/auth/login/google?redirect=/account">Sign in with Google</a> +<a href="/auth/login/github?redirect=/account">Sign in with GitHub</a> +<a href="/auth/login?redirect=/account">Single Sign-On</a> +``` + +**Dynamic widget:** fetch `/auth/providers` and render whatever is enabled. +```js +const { providers } = await fetch("/auth/providers?redirect=/account").then(r => r.json()); +for (const p of providers) { + const a = document.createElement("a"); + a.href = p.loginUrl; // relative, same-origin, redirect already encoded + a.textContent = `Sign in with ${p.label}`; + loginContainer.append(a); +} +``` + +Adding or removing a provider (a secret or `SSO_PROVIDERS` change in the portal) updates the widget with no code change on the customer's side. + +--- + +## 4. JSON Contracts + +### `GET /auth/providers` + +```jsonc +// GET /auth/providers?redirect=/cart +{ + "providers": [ + { "id": "google", "label": "Google", "loginUrl": "/auth/login/google?redirect=%2Fcart" }, + { "id": "github", "label": "GitHub", "loginUrl": "/auth/login/github?redirect=%2Fcart" }, + { "id": "saml", "label": "SSO", "loginUrl": "/auth/login?redirect=%2Fcart" } + ] +} +``` + +- `id` — stable identifier; matches the value used in `SSO_PROVIDERS`. +- `label` — human label; the SAML label is overridden by `IDP_LABEL`. +- `loginUrl` — relative path including the encoded `redirect`. +- Order is stable (registry order), not allowlist order. +- The enabled provider set is resolved at runtime from `SSO_PROVIDERS` ∩ providers-whose-credentials-are-present. + +### `GET /auth/branding` + +```jsonc +{ + "title": "Sign in", + "subtitle": "Choose a sign-in method", + "logoUrl": "https://cdn.example.com/logo.png", + "faviconUrl": null, + "accentColor": "#e00", + "backgroundColor": "#f0f2f5", + "cssUrl": null +} +``` + +Returns the current `LOGIN_PAGE_*` env var values as a JSON object. Custom login pages (Tier 2) can `fetch("/auth/branding")` to auto-style themselves consistently without duplicating the env var set. + +--- + +## 5. Redirect Validation + +The `?redirect=` parameter is validated against `SSO_ALLOWED_ORIGINS` on every route that honours it and on every read of the `saml_relay` cookie. + +**Rules:** +- Relative URLs (starting with `/`) are always permitted. +- Off-origin absolute URLs are silently dropped — the post-login redirect falls back to `/`. +- Protocol-relative and backslash bypasses (e.g., `/\evil.com`) are rejected. +- To permit absolute redirects to specific origins, set `SSO_ALLOWED_ORIGINS` to a comma-separated list of allowed origins (e.g., `https://shop.example.com`). + +This is a security-load-bearing constraint, not cosmetic. An incorrect or missing `SSO_ALLOWED_ORIGINS` will silently drop redirect parameters that include absolute URLs, changing post-login destination behavior without error. + +--- + +## 6. Shared Config the Origin Needs to Know About + +These env vars affect the contract the origin operates under. They are set on the FastEdge apps (auth-app and/or cdn-filter), but the origin integration depends on their values. + +| Env var | Set on | Origin concern | +|---|---|---| +| `SSO_VARIANT` | Both apps (must match) | Determines what the origin receives: nothing (gate-only), a JWT cookie to verify (cookie), or identity headers (header). | +| `SSO_AUDIENCE` | Both apps (must match) | The `aud` claim in every minted token. The cookie variant origin must validate `aud` matches this value when verifying the JWT. | +| `AUTH_PREFIX` | Both apps | The path prefix reserved for auth routes (default: `/auth`). The origin must not serve conflicting routes under this prefix; the CDN routes it to the auth-app. | +| `SESSION_COOKIE` | Both apps | Cookie name (default: `sso_session`). The cookie variant origin reads the session token from this cookie name. | +| `SESSION_SECRET` | Auth-app + cdn-filter (gate-only/header) | HS256 signing secret. The origin does not use this directly; only the filter verifies with it. | +| `SESSION_SIGNING_KEY` / `SESSION_PUBLIC_JWK` | Auth-app (cookie variant) | EC keypair. The origin uses the JWKS endpoint (`GET /auth/.well-known/jwks.json`) — never `SESSION_SIGNING_KEY` directly. | +| `SSO_ALLOWED_ORIGINS` | Auth-app | Governs which absolute redirect URLs are permitted. Affects post-login destination. | +| `LOGIN_PAGE_URL` | CDN filter | Where unauthenticated users are redirected. Default `/auth/`. Set to opt out of the built-in hosted page. | +| `CANONICAL_HOST` | Auth-app | The auth-app 301-redirects requests arriving on non-canonical hosts. The origin and IdP callback URLs must use this domain. | + +**Provider credentials** (`GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, `MICROSOFT_CLIENT_ID`, `FACEBOOK_CLIENT_ID`, SAML `IDP_*`/`SP_*`) are internal to the auth-app and not visible to the origin. Provider availability at runtime is reflected in `GET /auth/providers`. + +--- + +## See Also + +- edge-sso template README (deployment and configuration of the auth-app and cdn-filter) +- auth-app `.env.example` and cdn-filter `.env.example` (authoritative, exhaustive env var lists with inline guidance) +- edge-sso architecture: auth-modes (SSO_VARIANT axis detail) +- edge-sso architecture: security (token trust model, known limitations including no revocation and no IdP Single Logout) +- edge-sso architecture: overview (two-app deployment model, signing strategy, CANONICAL_HOST) diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md new file mode 100644 index 0000000..4b3a82a --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md @@ -0,0 +1,120 @@ +<!-- + auto-updated: true + sources: + - id: fastedge-templates + ref: main + commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 + updated: 2026-08-17 +--> + +# edge-totp — Origin Integration + +Reference for wiring the TOTP second-factor step into an origin's existing password login. The `edge-totp` template (otp-app + otp-filter) is already deployed on the Gcore portal; this document covers the three origin-side code changes required to connect it. + +--- + +## The Three Origin-Side Changes + +Only the origin's auth module changes. The rest of the site is untouched. + +### 1. Split login into password-then-OTP + +After the password check succeeds, instead of immediately minting the full origin session: + +1. Sign a **handoff ticket** = `HMAC(HANDOFF_KEY)` over `{ sub: userId, next, exp: now + TICKET_TTL }`. +2. Set a short-lived **`pre_mfa`** cookie or marker so the origin remembers the password step passed (bind it to `sub`). +3. Issue a `303` redirect to `{AUTH_PREFIX}/challenge?t=<ticket>` (totp-app, same host via the CDN path rule). + +**Ticket constraints:** +- `sub` — the user identifier +- `next` — the URL to return to after MFA +- `exp` — absolute expiry (`now + TICKET_TTL`); totp-app independently requires this field and caps absolute age +- Signed with `HANDOFF_KEY` (HS256, shared between origin and edge) + +### 2. Finish login on return — choose a profile + +When the user returns to `next` after the TOTP step, the origin finishes minting its session. Pick one profile: + +**Profile A (default — zero origin crypto):** +- The Rust filter has already enforced `mfa_session` on protected paths before the request reaches the origin. +- The origin re-identifies its `pre_mfa` user and mints its own session — no proof verification, no crypto on the origin side. +- `HANDOFF_KEY` is the only key the origin holds. + +**Profile B (opt-in — longer sessions, signed proof):** +- The edge delivers a one-time **ES256** proof as a short-lived cookie (`MFA_PROOF_COOKIE`). The proof is never in a URL. +- The origin verifies the proof via totp-app's JWKS endpoint (`{AUTH_PREFIX}/.well-known/jwks.json`) using `createRemoteJWKSet` (public key only — the origin cannot forge proofs). +- The origin checks that the proof's `sub` matches `pre_mfa`. +- **Required:** the origin must also verify the proof's `aud` claim against `MFA_AUDIENCE`. Verifying the signature alone without checking `aud` accepts a proof that was never meant for this deployment — this is a required check, not optional. +- After verification, the origin mints its own revocable session with whatever lifetime it needs (safely longer than the 8h edge session). + +### 3. Enroll users + +No new origin endpoint is required. Two paths: + +- **Admin provisioning:** `POST {AUTH_PREFIX}/enroll` (gated by `ENROLL_API_KEY`). totp-app writes the seed to KV using `GCORE_API_TOKEN`. Call with `force: true` to re-provision a lost authenticator behind your own identity check. +- **Self-service:** Users can self-enroll on first login via `{AUTH_PREFIX}/activate`. Enabled by default (`ALLOW_SELF_ENROLLMENT=true`). Set `ALLOW_SELF_ENROLLMENT=false` to require admin provisioning; `/activate` returns 403 and `/challenge` refuses unenrolled users. + +--- + +## Profile A vs Profile B — Decision Guide + +This is a security-posture decision. A wrong choice here is a real vulnerability, not a style issue. + +| Criterion | Profile A | Profile B | +| --- | --- | --- | +| Origin crypto required | None — `HANDOFF_KEY` only | ES256 proof verification via JWKS | +| Origin knows *which* user passed MFA | No — the filter only verifies that *a* valid `mfa_session` exists | Yes — the ES256 proof carries `sub`; origin verifies and binds it to `pre_mfa` | +| Session lifetime | Edge-bounded (8h, non-sliding) | Origin mints its own session at whatever lifetime it needs | +| Origin-lockdown dependency | Hard requirement — Profile A collapses entirely if the origin is directly reachable | More robust — origin independently verifies the signed proof rather than trusting the request came through the gate | + +**Do not add an unsigned forwarded-identity header for the origin to trust under Profile A.** Forwarded-identity headers (`x-mfa-user` or similar) are a recurring source of auth-bypass CVEs (e.g. oauth2-proxy header smuggling). The Rust filter deliberately does not forward the user id to the origin because the origin already authenticated the password and re-identifies its `pre_mfa` user when minting its session. If you need the edge to assert *which* user passed MFA, use Profile B: the ES256 proof carries `sub`, the origin verifies it via JWKS, and the proof is signed rather than a bare header. This is the same signed-assertion pattern Cloudflare Access uses (`Cf-Access-Jwt-Assertion`). + +**Profile B `aud` check is mandatory.** After verifying the ES256 signature via JWKS, also assert that the proof's `aud` claim equals `MFA_AUDIENCE`. The edge embeds `MFA_AUDIENCE` as `aud` in both `mfa_session` and the Profile-B proof. `MFA_AUDIENCE` should be the CDN hostname (e.g. `https://app.example.com`). A proof signed by this deployment's private key but addressed to a different audience must be rejected. + +**Prefer Profile B when the origin cannot be fully locked to edge-only traffic.** Profile B independently verifies a signed assertion at the origin; Profile A relies entirely on the filter gate having run. + +--- + +## Shared Configuration + +Keys and endpoints shared between the origin and the edge. Both components must agree on these values. + +| Key / Endpoint | Origin | Edge | +| --- | --- | --- | +| `HANDOFF_KEY` (HS256) | Signs the handoff ticket on password success | Verifies the ticket on `{AUTH_PREFIX}/challenge` and `{AUTH_PREFIX}/verify` | +| **JWKS** endpoint (Profile B only) | Fetches `{AUTH_PREFIX}/.well-known/jwks.json` via `createRemoteJWKSet` (public key only — cannot forge proofs) | Signs the one-time ES256 proof with `MFA_PROOF_SIGNING_KEY` (private key, edge-internal); serves the JWKS endpoint | +| `MFA_AUDIENCE` | **Must** enforce as the `aud` claim on the Profile-B proof (required check after signature verification) | Embedded as `aud` in both `mfa_session` and the Profile-B proof; the Rust filter enforces it on `mfa_session` (Profile A); fail-closes without it | + +**Profile A shares only `HANDOFF_KEY`.** The origin holds no verification key for `mfa_session`; that is edge-internal (signed with `MFA_SESSION_KEY`, verified by the Rust filter). No symmetric secret crosses to the origin on the proof path under Profile B — only the JWKS public-key fetch. + +Both components must live on the **same CDN host** (CDN path rule `{AUTH_PREFIX}/*` → totp-app). The `mfa_session` cookie is first-party and host-only. Same-host is required: the proof and session are consumed at the edge into a host-only cookie, so there is no cross-host URL-token path. + +--- + +## Required Deployment Precondition + +**Lock the origin to edge-only traffic.** This is a hard requirement for both profiles, not a suggestion. + +Any edge gate — Profile A or B — is meaningless if the origin is directly reachable. If the origin's IP or hostname is accessible without going through the Gcore CDN, an attacker simply skips the CDN entirely; `mfa_session`, the Rust filter, and the signed proof never run. Restrict the origin to Gcore CDN ingress using an IP allowlist, origin authentication, or a tunnel. + +Profile B is more robust in environments where the origin cannot be fully locked down (the origin independently verifies the signed proof rather than trusting that the request arrived through the gate), but it does not eliminate this requirement — locking the origin is still necessary. + +--- + +## Recovery and Self-Service Enrollment Caveat + +Self-service enrollment (`{AUTH_PREFIX}/activate`) inherits the trust level of the password step — a user who can pass the password check can self-enroll a new authenticator. This is the correct default for most deployments, but for sensitive accounts this means a compromised password also compromises the TOTP second factor. + +**Decision point for sensitive accounts:** If your threat model requires that the second factor remain independent of the password (e.g. privileged users, admin accounts), disable self-service enrollment (`ALLOW_SELF_ENROLLMENT=false`) and require admin provisioning via `POST {AUTH_PREFIX}/enroll` (gated by `ENROLL_API_KEY`, behind your own identity check). Recovery for a lost authenticator also goes through `POST {AUTH_PREFIX}/enroll` with `force: true`. + +See the `edge-totp` threat model reference (threat-model.md, risk R5) before relying on self-service enrollment for sensitive accounts. + +--- + +## See Also + +- edge-totp storage and secrets reference (storage-and-secrets.md) — full env var and secret list for both otp-app and otp-filter +- edge-totp threat model reference (threat-model.md) — trust boundaries, residual risks, and risk register including R4 (KV token scope) and R5 (self-enrollment trust level) +- edge-totp architecture flow reference (flow.md) — why the seed is fetched at verify time and PoP replication behavior +- FastEdge KV store reference — `fastedge::kv` SDK binding and `storeRefs`/`kvStoreVars` deploy-time linking +- FastEdge secrets reference — `getSecret` API and dotenv sync workflow diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md index 52ac44f..d2b1159 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -222,3 +222,233 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide + +## Source Material + +### FILE: examples/abTesting/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; + +class AbTestingRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new AbTestingContext(context_id, this); + } +} + +class AbTestingContext extends Context { + constructor(context_id: u32, root_context: AbTestingRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const experimentName = getEnv("EXPERIMENT_NAME"); + if (experimentName === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const variantAPath = getEnv("VARIANT_A_PATH"); + const variantBPath = getEnv("VARIANT_B_PATH"); + if (variantAPath === "" || variantBPath === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode( + "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", + ), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Check for existing experiment cookie + const cookieName = "fe_exp_" + experimentName; + const cookieHeader = stream_context.headers.request.get("Cookie"); + let assignedVariant = this.getCookieValue(cookieHeader, cookieName); + + // Assign variant if not already set + if (assignedVariant !== "A" && assignedVariant !== "B") { + // Use current time as a simple entropy source for 50/50 split + const now = getCurrentTime(); + assignedVariant = now % 2 == 0 ? "A" : "B"; + } + + // Rewrite the request path to the variant path + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + return FilterHeadersStatusValues.Continue; + } + const originalPath = String.UTF8.decode(pathArrBuf); + const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; + const newPath = variantPath + originalPath; + + // Reconstruct request.url from its decomposed parts rather than splicing + // the path out of the full URL — splicing breaks when the path happens to + // appear inside the host, and it can silently lose the query string. + const schemeBuf = get_property("request.scheme"); + const hostBuf = get_property("request.host"); + if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { + const scheme = String.UTF8.decode(schemeBuf); + const host = String.UTF8.decode(hostBuf); + const queryBuf = get_property("request.query"); + const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; + const newUrl = + scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); + log(LogLevelValues.info, `A/B routing: ${newUrl}`); + set_property("request.url", String.UTF8.encode(newUrl)); + } + + // Add variant header for upstream visibility + stream_context.headers.request.add("X-Experiment", experimentName); + stream_context.headers.request.add("X-Variant", assignedVariant); + + log( + LogLevelValues.info, + `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, + ); + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Recover the assigned variant from the request header set in onRequestHeaders. + // Instance state (this.variant) does not survive the nginx -> core-proxy hop. + const variant = stream_context.headers.request.get("X-Variant"); + if (variant === "") { + return FilterHeadersStatusValues.Continue; + } + + const experimentName = getEnv("EXPERIMENT_NAME"); + const cookieName = "fe_exp_" + experimentName; + + // Set the experiment cookie so subsequent requests stick to the same variant + stream_context.headers.response.add( + "Set-Cookie", + cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", + ); + + // Add variant as response header for observability + stream_context.headers.response.add("X-Variant", variant); + + return FilterHeadersStatusValues.Continue; + } + + private getCookieValue(cookieHeader: string, name: string): string { + if (cookieHeader === "") return ""; + const pairs = cookieHeader.split(";"); + const prefix = name + "="; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].trim(); + if (pair.startsWith(prefix)) { + return pair.substring(prefix.length); + } + } + return ""; + } +} + +registerRootContext((context_id: u32) => { + return new AbTestingRoot(context_id); +}, "abTesting"); +``` + + +### FILE: examples/abTesting/package.json + +```json +{ + "name": "fastedge-as-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/abTesting/README.md + +``` +[← Back to examples](../README.md) + +# A/B Testing + +This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. + +## What it does + +In `onRequestHeaders`, the app: + +1. Checks for an existing experiment cookie (`fe_exp_<EXPERIMENT_NAME>`). +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-codex/skills/scaffold/reference/cdn/api-key-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-as.md index 218310a..436b145 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -122,6 +122,8 @@ class ApiKeyContext extends Context { } // 3. Strip key before forwarding to upstream + // .remove() sets the header value to "" rather than deleting it entirely — + // the upstream will see X-API-Key: "" rather than a missing header. stream_context.headers.request.remove("X-API-Key"); log(LogLevelValues.info, "API key validated successfully"); diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md index 4c21047..4e444bf 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -212,3 +212,122 @@ target = "wasm32-wasip1" - FastEdge secrets configuration (dashboard secret variable setup) - auth-jwt-rust reference (JWT-based authentication — use when token expiry and claims are needed) - platform-overview reference (CDN vs HTTP app type distinction) + +## Source Material + +### FILE: examples/cdn/api_key/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating API key validation. + +Validates requests using an X-API-Key header checked against a stored +secret. Simpler alternative to JWT when token expiry and claims are +not needed. + +Required configuration: + - Secret: API_KEY +*/ + +use fastedge::proxywasm::secret; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(ApiKeyRoot) }); +}} + +struct ApiKeyRoot; + +impl Context for ApiKeyRoot {} + +impl RootContext for ApiKeyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(ApiKeyContext)) + } +} + +struct ApiKeyContext; + +impl Context for ApiKeyContext {} + +impl HttpContext for ApiKeyContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let expected_key = match secret::get("API_KEY") { + Ok(Some(bytes)) => match String::from_utf8(bytes) { + Ok(s) if !s.is_empty() => s, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }; + + let provided_key = match self.get_http_request_header("X-API-Key") { + Some(k) if !k.is_empty() => k, + _ => { + self.send_http_response( + 401, + vec![("WWW-Authenticate", "API-Key")], + Some(b"Missing X-API-Key header"), + ); + return Action::Pause; + } + }; + + if provided_key != expected_key { + println!("API key validation failed"); + self.send_http_response(403, vec![], Some(b"Invalid API key")); + return Action::Pause; + } + + // Strip the API key header before forwarding to upstream + self.set_http_request_header("X-API-Key", None); + + println!("API key validated successfully"); + Action::Continue + } +} +``` + + +### FILE: examples/cdn/api_key/Cargo.toml + +```toml +[workspace] + +[package] +name = "api_key" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + + +### FILE: examples/cdn/api_key/README.md + +``` +[← Back to examples](../../README.md) + +# API Key (CDN) + +Validates requests using an `X-API-Key` header checked against a stored secret. Returns 401 if missing, 403 if invalid, and strips the header before forwarding to upstream. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md index 6b54958..ff3c689 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -234,6 +234,20 @@ registerRootContext((context_id: u32) => { | Token invalid (any other error) | 403 | `Invalid token` | | Token valid | — | Pass-through (`Continue`) | +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + ## Build Output | File | Description | @@ -253,3 +267,206 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint + +## Source Material + +### FILE: examples/jwt/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class AuthRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes + return new Auth(context_id, this); + } +} + +class Auth extends Context { + constructor(context_id: u32, root_context: AuthRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const secret = getSecret("SECRET"); + if (!secret) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const authHeader = stream_context.headers.request.get("Authorization"); + if (!authHeader) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("No Authorization header"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (!authHeader.startsWith("Bearer ")) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Authorization header must use Bearer scheme"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const token = authHeader.slice(7); // strip "Bearer " prefix + if (!token) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Token not found"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Decode the JWT token + const jwtResult = jwtVerify(token, secret); + if (jwtResult !== JwtValidation.Ok) { + if (jwtResult === JwtValidation.Expired) { + log(LogLevelValues.info, "Token Expired"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Expired token"), + [], + ); + } else { + log(LogLevelValues.info, "Bad Token"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid token"), + [], + ); + } + return FilterHeadersStatusValues.StopIteration; + } + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new AuthRoot(context_id); +}, "auth"); +``` + + +### FILE: examples/jwt/package.json + +```json +{ + "name": "fastedge-as-example-jwt", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: JWT validation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/as-jwt": "^1.0.3", + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", + "assemblyscript-json": "^1.1.0" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/jwt/README.md + +``` +[← Back to examples](../README.md) + +# JWT Validation + +This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. +2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. +3. Verifies the token signature and expiry using `jwtVerify()`. +4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. + +## Configuration + +Set the following secret variable on your FastEdge application: + +| Secret | Description | +| -------- | ------------------------------------------------------------------ | +| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | + +## Testing tokens + +**Expired token** (will return `403 Forbidden`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, will return `200 OK`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ---------------------- | -------------------------------------------------- | +| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | +| `build/jwt-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/jwt.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. + +For more on secrets and secret rotation slots, see the [FastEdge secrets documentation](https://gcore.com/docs/fastedge/secrets-manager/slots). +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md index f92ce4c..28c7eda 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -259,142 +259,3 @@ target = "wasm32-wasip1" - FastEdge SDK Rust reference (proxywasm module, secret API) - FastEdge secrets configuration (dashboard secret variable setup) - platform-overview reference (CDN vs HTTP app type distinction) - -## Source Material - -### FILE: examples/cdn/jwt/src/lib.rs - -```rust -use std::time::{SystemTime, UNIX_EPOCH}; -use headers::HeaderValue; -use headers::authorization::{Bearer, Credentials}; - -use fastedge::proxywasm::secret; -use jsonwebtoken::{decode, DecodingKey, Validation}; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde::Deserialize; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); -}} - -struct HttpHeadersRoot; - -impl Context for HttpHeadersRoot {} - -impl RootContext for HttpHeadersRoot { - fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(HttpHeaders {})) - } - - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } -} - -struct HttpHeaders {} - -impl Context for HttpHeaders {} - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -#[derive(Debug, Deserialize, Default)] -struct Claims { - exp: u64, -} - -impl HttpContext for HttpHeaders { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Ok(Some(secret)) = secret::get("secret") else { - println!("'secret' param not set"); - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - let Some(value) = self.get_http_request_header("Authorization") else { - println!("No auth header"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); - return Action::Pause; - }; - - if value.is_empty() { - println!("Auth header is empty"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); - return Action::Pause; - }; - - let Ok(header) = value.parse::<HeaderValue>() else { - println!("Auth header is invalid"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"Invalid Authorization header")); - return Action::Pause; - }; - - - let Some(bearer) = Bearer::decode(&header) else { - println!("Auth header doesn't contain token"); - self.send_http_response(FORBIDDEN, vec![], Some(b"Token not found")); - return Action::Pause; - }; - - let token = bearer.token(); - - let decoding_key = DecodingKey::from_secret(&secret); - let mut validation = Validation::default(); - validation.set_required_spec_claims(&["exp"]); - // skip validation af aud and nbf claims - validation.validate_aud = false; - validation.validate_nbf = false; - validation.validate_exp = false; // will validate expiration separately - - let token_data = match decode::<Claims>(token, &decoding_key, &validation) { - Ok(token_data) => token_data, - Err(error) => { - println!("Token is invalid"); - self.send_http_response(FORBIDDEN, vec![], Some(format!("Could not decode token {}: {}", token, error).as_bytes())); - return Action::Pause; - } - }; - - let claims = token_data.claims; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - if now > claims.exp { - println!("Token expired"); - self.send_http_response(FORBIDDEN, vec![], Some(b"Token expired")); - return Action::Pause; - } - - println!("Token ok"); - Action::Continue - } -} -``` - - -### FILE: examples/cdn/jwt/Cargo.toml - -```toml -[workspace] - -[package] -name = "jwt" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -jsonwebtoken = "9" -serde = { version = "1", features = ["derive"] } -headers = "0.4" -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md index 0138cee..8b560bd 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -14,8 +14,8 @@ languages: [assemblyscript] template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as -source_ref: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 -updated: 2026-05-20 +source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 +updated: 2026-08-17 --- # Base Skeleton: CDN AssemblyScript @@ -263,112 +263,3 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) - -## Source Material - -### FILE: examples/helloWorld/assembly/index.ts - -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; - -class HelloWorldRoot extends RootContext { - createContext(context_id: u32): Context { - return new HelloWorld(context_id, this); - } -} - -class HelloWorld extends Context { - constructor(context_id: u32, root_context: HelloWorldRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool, - ): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onRequestBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onResponseBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HelloWorldRoot(context_id); -}, "helloWorld"); - - -### FILE: examples/helloWorld/package.json - -{ - "name": "fastedge-as-example-hello-world", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} - - -### FILE: examples/helloWorld/asconfig.json - -{ - "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", - "targets": { - "debug": { - "outFile": "build/helloWorld-debug.wasm", - "textFile": "build/helloWorld-debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/helloWorld.wasm", - "textFile": "build/helloWorld.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "use": "abort=abort_proc_exit" - } -} diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md index 7b47146..d4f417a 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: cdn-base source_repo: https://github.com/G-Core/FastEdge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-07-23 +updated: 2026-08-17 --- # Base Skeleton: CDN Rust @@ -165,29 +165,6 @@ lerna-debug.log* .history/* ``` -## Logging Convention - -FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: - -1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. -2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - -**Do not use:** -- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime -- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer -- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - -If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - -Example pattern for CDN Rust: -```rust -fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - log::info!("incoming request"); // visible in FastEdge logs - // eprintln!("incoming request"); // DO NOT use — dropped - Action::Continue -} -``` - ## Build Configuration ```bash @@ -209,76 +186,25 @@ cargo build --release --target wasm32-wasip1 - **RootContext**: implements `get_type() -> Option<ContextType>` returning `ContextType::HttpContext` and `create_http_context(_: u32) -> Option<Box<dyn HttpContext>>` - **HttpContext**: all 4 hooks return `Action::Continue` in the base skeleton; `on_http_response_headers` adds `x-powered-by: FastEdge` response header via `self.add_http_response_header` -## Source Material - -### FILE: examples/cdn/hello_world/src/lib.rs - -```rust -use log::info; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HelloWorldRoot) }); -}} - -struct HelloWorldRoot; - -impl Context for HelloWorldRoot {} - -impl RootContext for HelloWorldRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(HelloWorld)) - } -} - -struct HelloWorld; +## Logging Convention -impl Context for HelloWorld {} +FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: -impl HttpContext for HelloWorld { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_headers"); - Action::Continue - } +1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. +2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_body"); - Action::Continue - } +**Do not use:** +- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime +- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer +- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - self.add_http_response_header("x-powered-by", "FastEdge"); - info!("Hello from on_http_response_headers"); - Action::Continue - } +If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_response_body"); - Action::Continue - } +Example pattern for CDN Rust: +```rust +fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + log::info!("incoming request"); // visible in FastEdge logs + // eprintln!("incoming request"); // DO NOT use — dropped + Action::Continue } ``` - -### FILE: examples/cdn/hello_world/Cargo.toml - -```toml -[workspace] - -[package] -name = "hello_world" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md index da93373..e3da1c6 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -70,7 +70,7 @@ registerRootContext() → registers the root context factory with a plugin name ```typescript class HttpBodyRoot extends RootContext { createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); + setLogLevel(LogLevelValues.info); // reduce to LogLevelValues.debug for more logging return new HttpBody(context_id, this); } } @@ -97,6 +97,7 @@ Called when request headers arrive. Must remove `content-length` before the body ```typescript onRequestHeaders(headers: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); stream_context.headers.request.remove("content-length"); return FilterHeadersStatusValues.Continue; } @@ -112,6 +113,7 @@ Called (possibly multiple times) as request body chunks arrive. Buffer until `en ```typescript onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } @@ -124,6 +126,7 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); if (bodyStr.includes("Client")) { const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; set_buffer_bytes( @@ -154,10 +157,11 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV ### `onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues` -Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. +Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. Captures the `content-type` header into a runtime property for use in `onResponseBody`. ```typescript onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); stream_context.headers.response.remove("content-length"); stream_context.headers.response.replace("transfer-encoding", "Chunked"); @@ -185,15 +189,31 @@ Called (possibly multiple times) as response body chunks arrive. Buffer until `e ```typescript onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + const urlBytes = get_property("request.url"); const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } const contentTypeBytes = get_property("response.content_type"); - const contentType = contentTypeBytes.byteLength === 0 ? "" : String.UTF8.decode(contentTypeBytes); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpResponseBody, @@ -203,7 +223,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "onHttpResponseBody >> bodyStr: " + bodyStr); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); } return FilterDataStatusValues.Continue; } @@ -217,13 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Used for final audit logging. - -```typescript -onLog(): void { - log(LogLevelValues.info, "onLog >> completed (contextId): " + this.context_id.toString()); -} -``` +Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. ## Registration @@ -334,7 +348,7 @@ pnpm run asbuild Build scripts defined in `package.json`: - `asbuild:debug` — `asc assembly/index.ts --target debug` - `asbuild:release` — `asc assembly/index.ts --target release` -- `asbuild` — runs both +- `asbuild` — runs both (`npm run asbuild:debug && npm run asbuild:release`) ## Deploy diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md index 725bf65..e8b9ba2 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -243,3 +243,125 @@ Requires `log = "0.4"` in `Cargo.toml`. Log level is set to `Trace` at startup v - proxy-wasm HttpContext trait reference - host-services-rust reference (property API, logging) - platform-overview reference (CDN app lifecycle) + +## Source Material + +### FILE: examples/cdn/body/src/lib.rs + +```rust +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpBody)) + } +} + +struct HttpBody; + +impl Context for HttpBody {} + +impl HttpContext for HttpBody { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + self.set_http_request_header("content-length", None); + Action::Continue + } + + fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + // Wait -- we'll be called again when the complete body is buffered + // at the host side. + return Action::Pause; + } + + if let Some(body_bytes) = self.get_http_request_body(0, body_size) { + let body_str = String::from_utf8(body_bytes).unwrap(); + if body_str.contains("Client") { + let new_body = + format!("Client's original message body ({body_size} bytes) redacted.\n"); + self.set_http_request_body(0, body_size, &new_body.into_bytes()); + } + } + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // remove content-length as we plan to change the body size + self.set_http_response_header("content-length", None); + // set transfer-encoding to chunked as we don't know body length + self.set_http_response_header("transfer-encoding", Some("Chunked")); + + if let Some(content_type) = self.get_http_response_header("content-type") { + self.set_property(vec!["response.content_type"], Some(content_type.as_bytes())); + } + + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + return Action::Pause; + } + + let url = if let Some(value) = self.get_property(vec!["request.url"]) { + let url = String::from_utf8_lossy(&value); + info!("url={}", url); + url.to_string() + } else { + "".to_string() + }; + + let content_type = + if let Some(content_type) = self.get_property(vec!["response.content_type"]) { + let content_type = String::from_utf8_lossy(&content_type); + info!("content_type={}", content_type); + content_type.to_string() + } else { + "NONE".to_string() + }; + + if let Some(body_bytes) = self.get_http_response_body(0, body_size) { + let body_str = String::from_utf8(body_bytes).unwrap(); + if body_str.contains("Client") { + let new_body = + format!("Original message body ({body_size} bytes) redacted.\nURL: {url}\nContent-Type: {content_type}\n"); + self.set_http_response_body(0, body_size, &new_body.into_bytes()); + } + } + Action::Continue + } +} +``` + +### FILE: examples/cdn/body/Cargo.toml + +```toml +[workspace] + +[package] +name = "body" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md index eff8100..2987a96 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -187,3 +187,191 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=<STATIC_MAX_AGE>, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=<HTML_MAX_AGE>, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=<API_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-codex/skills/scaffold/reference/cdn/cache-control-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-rust.md index 25517cf..b49ed2a 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -219,144 +219,3 @@ use std::env; - proxy-wasm HttpContext trait reference - FastEdge-sdk-rust CDN examples overview - host-services-rust reference (hostcalls, logging) - -## Source Material - -### FILE: examples/cdn/cache_control/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating content-type-aware cache control. - -Sets Cache-Control response headers based on the content type and -response status, providing fine-grained control over CDN caching. - -Optional configuration: - - Environment variable: STATIC_MAX_AGE (default: 31536000) - - Environment variable: HTML_MAX_AGE (default: 3600) - - Environment variable: API_MAX_AGE (default: 0 = no-cache) -*/ - -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::env; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(CacheControlRoot) }); -}} - -struct CacheControlRoot; - -impl Context for CacheControlRoot {} - -impl RootContext for CacheControlRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(CacheControlContext)) - } -} - -struct CacheControlContext; - -impl Context for CacheControlContext {} - -impl HttpContext for CacheControlContext { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Read response status - let status_code = self - .get_property(vec!["response.status"]) - .and_then(|bytes| { - if bytes.len() == 2 { - Some(u16::from_be_bytes([bytes[0], bytes[1]])) - } else { - None - } - }) - .unwrap_or(200); - - // Error responses should never be cached - if !(200..400).contains(&status_code) { - self.set_http_response_header("Cache-Control", Some("no-store")); - return Action::Continue; - } - - let content_type = self - .get_http_response_header("Content-Type") - .unwrap_or_default(); - - let static_max_age = env::var("STATIC_MAX_AGE").unwrap_or_else(|_| "31536000".to_string()); - let html_max_age = env::var("HTML_MAX_AGE").unwrap_or_else(|_| "3600".to_string()); - let api_max_age = env::var("API_MAX_AGE").unwrap_or_else(|_| "0".to_string()); - - let cache_control = if is_static_asset(&content_type) { - format!("public, max-age={}, immutable", static_max_age) - } else if content_type.contains("text/html") { - self.add_http_response_header("Vary", "Accept-Encoding"); - format!("public, max-age={}, must-revalidate", html_max_age) - } else if content_type.contains("application/json") - || content_type.contains("application/xml") - { - self.add_http_response_header("Vary", "Accept, Authorization"); - if api_max_age == "0" { - "no-cache, no-store, must-revalidate".to_string() - } else { - format!("private, max-age={}, must-revalidate", api_max_age) - } - } else { - "public, max-age=600".to_string() - }; - - self.set_http_response_header("Cache-Control", Some(&cache_control)); - - println!( - "Cache-Control: {} (content-type: {})", - cache_control, content_type - ); - - Action::Continue - } -} - -fn is_static_asset(content_type: &str) -> bool { - content_type.starts_with("image/") - || content_type.starts_with("font/") - || content_type.contains("application/javascript") - || content_type.contains("text/css") - || content_type.contains("text/javascript") - || content_type.contains("application/wasm") -} -``` - -### FILE: examples/cdn/cache_control/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_control" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -``` - -### FILE: examples/cdn/cache_control/README.md - -``` -[← Back to examples](../../README.md) - -# Cache Control (CDN) - -Sets `Cache-Control` response headers based on content type and response status, providing fine-grained control over CDN caching behaviour. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/convert-image-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/convert-image-rust.md index 9ce892f..69d0e83 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/convert-image-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -191,7 +191,7 @@ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Ac ## Helper: `rsp_status` -Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. +Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. Defined as an `impl ConvertImageContext` method. ```rust fn rsp_status(&mut self) -> Option<u16> { @@ -284,3 +284,300 @@ proxy_wasm::main! {{ - scaffold reference for CDN app type - FastEdge SDK Rust host services reference (proxy-wasm ABI, `get_property`, `set_property`) - platform overview (CDN app lifecycle, cache variation with `Vary` headers) + +## Source Material + +### FILE: examples/cdn/convert_image/src/lib.rs + +```rust +use image::*; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::{env, env::VarError, io::Cursor, str::from_utf8}; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(ConvertImageRoot) }); +}} + +struct ConvertImageRoot; + +impl Context for ConvertImageRoot {} + +impl RootContext for ConvertImageRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(ConvertImageContext)) + } +} + +struct ConvertImageContext; + +impl Context for ConvertImageContext {} + +impl HttpContext for ConvertImageContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + // this header is used to select correct image version from cache + self.add_http_request_header("Image-Format", "original"); + + // get extension + let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); + println!("request.path={path:?}"); + let raw_ext = self.get_property(vec!["request.extension"]); + println!("request.extension={raw_ext:?}"); + let Some(ext) = raw_ext else { + println!("No extension in request path, not transforming"); + return Action::Continue; + }; + let Ok(ext) = from_utf8(&ext) else { + println!("Invalid UTF-8 in request extension, not transforming"); + return Action::Continue; + }; + if ext.is_empty() { + println!("No extension in request path, not transforming"); + return Action::Continue; + } + + // FORMATS_TO_TRANSFORM contains list of file extensions to transfor + // note that jpg and jpeg are different extensions + let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { + println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); + return Action::Continue; + }; + if !image_list.split(',').any(|entry| entry == ext) { + println!( + "extension {} is not in the list of formats to transform: {}, not transforming", + ext, image_list + ); + return Action::Continue; + } + + // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed + let Some(ua) = self.get_http_request_header("User-Agent") else { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + }; + if ua.is_empty() { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + } + if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { + if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { + println!("User-Agent is in ignore list, not transforming"); + return Action::Continue; + } + } + + // indicator for on_response_headers and for cache key + self.set_http_request_header("Image-Format", Some("image/avif")); + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // only process 200 responses + if let Some(status) = self.rsp_status() { + if status != 200 { + println!( + "Response status is {} instead of expected 200, not transforming", + status + ); + return Action::Continue; + } + } else { + println!("Response status is not set, not transforming"); + return Action::Continue; + } + + // if "Image-Format" request header is not set, don't convert the image + let Some(content_type) = self.get_http_request_header("Image-Format") else { + return Action::Continue; + }; + // instruct cache to vary by this header so "original" and "image/avif" are cached separately + self.add_http_response_header("Vary", "Image-Format"); + + if content_type == "original" { + return Action::Continue; + }; + + // image to be transformed, set headers accordingly + self.set_http_response_header("Content-Length", None); + self.set_http_response_header("Transfer-Encoding", Some("Chunked")); + self.set_http_response_header("Content-Type", Some(content_type.as_str())); + + // indicate to on_http_response_body that transformation is needed + self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); + + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + // wait till we get complete body + return Action::Pause; + } + + let Some(content_type) = self.get_property(vec!["response.content-type"]) else { + return Action::Continue; + }; + + let Ok(content_type) = from_utf8(&content_type) else { + // should never happen + println!("Invalid UTF-8 in Content-Type"); + self.send_http_response(500, vec![], None); + return Action::Pause; + }; + + if content_type != "image/avif" { + // should never happen + println!( + "Content-Type {} is not supported, not transforming", + content_type + ); + return Action::Continue; + } + + if let Some(body_bytes) = self.get_http_response_body(0, body_size) { + let buf = body_bytes.as_bytes(); + let img = match load_from_memory(buf) { + Ok(i) => i, + Err(e) => { + println!("cannot load image to memory {}, not converting", e); + return Action::Continue; + } + }; + + let mut out = Vec::new(); + let mut c = Cursor::new(&mut out); + let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( + &mut c, + u8_param("AVIF_SPEED", 1, 10, 5), + u8_param("AVIF_QUALITY", 1, 100, 70), + )); + + match res { + Ok(_) => { + println!( + "{} bytes -> {} bytes {}", + body_size, + out.len(), + content_type + ); + self.set_http_response_body(0, body_size, &out) + } + Err(e) => println!("cannot store transformed image {}", e), + } + } else { + println!("No response body to transform"); + } + + Action::Continue + } +} + +impl ConvertImageContext { + fn rsp_status(&mut self) -> Option<u16> { + if let Some(status) = self.get_property(vec!["response.status"]) { + if status.len() != 2 { + println!("HTTP status property is not 2 bytes"); + return None; + } + return Some(u16::from_be_bytes([status[0], status[1]])); + } + None + } +} + +fn str_param(name: &str) -> Result<String, VarError> { + let val = env::var(name)?; + if val.is_empty() { + return Err(VarError::NotPresent); + } + + Ok(val) +} + +fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { + let Ok(val) = env::var(name) else { + println!("Param {} is not set, using default value {}", name, default); + return default; + }; + if val.is_empty() { + println!("Param {} is not set, using default value {}", name, default); + return default; + } + + let val = match val.parse() { + Err(_) => { + println!( + "Param {} is not a valid number, using default value {}", + name, default + ); + return default; + } + Ok(v) => v, + }; + if val < min { + println!( + "Param {} is below minimum {}, using default value {}", + name, min, default + ); + return default; + } + if val > max { + println!( + "Param {} is above maximum {}, using default value {}", + name, max, default + ); + return default; + } + + val +} +``` + + +### FILE: examples/cdn/convert_image/Cargo.toml + +```toml +[workspace] + +[package] +name = "convert_image" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +image = "0.25" +``` + + +### FILE: examples/cdn/convert_image/README.md + +``` +[← Back to examples](../../README.md) + +# Convert Image (CDN) + +Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. + +## Configuration + +- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) +- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip +- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) +- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) + +## How it works + +1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation +2. **on_response_headers** — sets response headers for AVIF content type on 200 responses +3. **on_response_body** — decodes the original image and re-encodes it as AVIF +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-as.md index dd83b67..8c95ff2 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -146,3 +146,157 @@ Build scripts: - proxy-wasm-sdk-as SDK reference - FastEdge CDN application environment variable configuration - FastEdge portal deployment guide + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-rust.md index 1c410d6..d8d3907 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -310,3 +310,207 @@ Edit `public/styles.css` directly. No build tools required. Styles are embedded - host-services-rust reference (property API) - platform-overview reference (CDN app lifecycle) - body-rust blueprint (general body manipulation pattern) + +## Source Material + +### FILE: examples/cdn/custom_error_pages/src/lib.rs + +```rust +use handlebars::Handlebars; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde_json::json; +use std::collections::HashMap; +use std::env; + +// Include the generated image map +include!(concat!(env!("OUT_DIR"), "/image_map.rs")); +include!(concat!(env!("OUT_DIR"), "/message_map.rs")); + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpBody)) + } +} + +struct HttpBody; + +impl Context for HttpBody {} + +impl HttpContext for HttpBody { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + if let Some(status) = self.get_property(vec!["response.status"]) { + if status.len() == 2 { + let status_code = u16::from_be_bytes([status[0], status[1]]); + if (400..600).contains(&status_code) { + // Remove the Content-Length header if it exists, we are going to change the response body + self.set_http_response_header("Content-Length", None); + self.set_http_response_header("Transfer-Encoding", Some("Chunked")); + self.set_http_response_header("Content-Type", Some("text/html")); + } + } + } + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + // only process 4xx/5xx error responses + let Some(status) = self.get_property(vec!["response.status"]) else { + return Action::Continue; + }; + if status.len() != 2 { + return Action::Continue; + } + let status_code = u16::from_be_bytes([status[0], status[1]]); + if !(400..600).contains(&status_code) { + return Action::Continue; + } + + if !end_of_stream { + // wait for complete body + return Action::Pause; + } + + // Get the image and message maps + let image_map = get_image_map(); + let message_map = get_message_map(); + + // Get the Base64-encoded image for the status code or its fallback + let base64_image = image_map + .get(&status_code) + .or_else(|| { + if (400..500).contains(&status_code) { + image_map.get(&4000) + } else if (500..600).contains(&status_code) { + image_map.get(&5000) + } else { + None + } + }) + .unwrap_or(&""); + + // Get the message and description for the status code or its fallback + let (message, description) = message_map + .get(&status_code) + .or_else(|| { + if (400..500).contains(&status_code) { + message_map.get(&4000) + } else if (500..600).contains(&status_code) { + message_map.get(&5000) + } else { + None + } + }) + .map(|(msg, desc)| (msg.to_string(), desc.to_string())) + .unwrap_or_else(|| { + ( + "Unexpected Error".to_string(), + "The server responded with a {{status}} error.".to_string(), + ) + }); + + let mut handlebars = Handlebars::new(); + // Use handlebars to complete message and description text allowing for usage of {{ status }} variable + handlebars + .register_template_string("message_template", message) + .unwrap(); + handlebars + .register_template_string("description_template", description) + .unwrap(); + + let msg_data = json!({ + "status": status_code.to_string(), + }); + + let complete_message = handlebars.render("message_template", &msg_data).unwrap(); + let complete_description = handlebars + .render("description_template", &msg_data) + .unwrap(); + + // Render the error page using Handlebars + let error_template = include_str!("../templates/error_page.hbs"); + handlebars + .register_template_string("error_template", error_template) + .unwrap(); + + let styles = include_str!("../public/styles.css"); + let page_data = json!({ + "styles": styles, + "status": status_code.to_string(), + "message": complete_message, + "description": complete_description, + "image": base64_image, + }); + + let html_body = handlebars.render("error_template", &page_data).unwrap(); + let body = html_body.as_bytes(); + self.set_http_response_body(0, body_size, body); + + Action::Continue + } +} +``` + +### FILE: examples/cdn/custom_error_pages/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom_error_pages" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +handlebars = "6.3" +serde_json = "1.0" +regex = "1.10" + +[build-dependencies] +base64 = "0.22" +``` + +### FILE: examples/cdn/custom_error_pages/README.md + +``` +[← Back to examples](../../README.md) + +# Custom Error Pages (CDN) + +Intercepts 4xx and 5xx error responses and replaces them with branded HTML error pages using Handlebars templates. + +## How it works + +A [build script](./build.rs) runs at compile time to embed images and messages from the `public/` folder into the WASM binary (since there is no filesystem at runtime). + +At runtime, when an error response is detected: +1. **on_response_headers** — sets `Content-Type` to `text/html` for error responses +2. **on_response_body** — looks up the status code in the embedded image/message maps, falls back to generic `4xx`/`5xx` templates, and renders the error page using Handlebars + +## Adding a custom error page + +1. Add an image: `public/images/<status>.jpg` +2. Add a message file: `public/messages/<status>.hbs` (first line = title, second line = description) +3. Recompile and deploy + +## Styling + +Styles are in [`public/styles.css`](./public/styles.css) — plain CSS, no build tools required. Edit directly. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-rust.md index 99d95cb..de7cb9f 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -260,3 +260,123 @@ Action::Pause - host-services-rust reference (property access, send_http_response ABI details) - sdk-reference-rust reference (proxy-wasm trait hierarchy) - best-practices reference (error handling patterns, Action semantics) + +## Source Material + +### FILE: examples/cdn/custom/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::time::Duration; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); +}} + +const BAD_REQUEST: u32 = 400; + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpHeaders)) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders; + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Some(path) = self.get_property(vec!["request.path"]) else { + self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); + return Action::Pause; + }; + + let Ok(path) = std::str::from_utf8(&path) else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - not utf8 string"), + ); + return Action::Pause; + }; + + //trim first '/' + let path = if path.starts_with('/') { + &path[1..] + } else { + path + }; + let mut segments = path.split('/'); + + let Some(status_code) = segments.next() else { + return Action::Continue; + }; + + if let Some(delay) = segments.next() { + if let Ok(delay) = delay.parse::<u64>() { + std::thread::sleep(Duration::from_millis(delay)); + } + } + + let Ok(status_code) = status_code.parse::<u32>() else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - invalid status code"), + ); + return Action::Pause; + }; + + match status_code { + 0 | 200 => Action::Continue, + code if code < 600 => { + self.send_http_response(code, vec![], None); + Action::Pause + } + _ => { + self.send_http_response(BAD_REQUEST, vec![], None); + Action::Pause + } + } + } +} +``` + +### FILE: examples/cdn/custom/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/custom/README.md + +``` +[← Back to examples](../../README.md) + +# Custom (CDN) + +Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-as.md index e2d8383..912df27 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -148,9 +148,9 @@ Configure both on the FastEdge application before deployment. The test runner maps environment variable names as follows: -| Env key | Maps to | -| ------------------------------ | ------------------ | -| `FASTEDGE_VAR_ENV_<NAME>` | `getEnv("NAME")` | +| Env key | Maps to | +| ------------------------------ | ------------------- | +| `FASTEDGE_VAR_ENV_<NAME>` | `getEnv("NAME")` | | `FASTEDGE_VAR_SECRET_<NAME>` | `getSecret("NAME")` | Use `"dotenv": {"enabled": true}` in the test fixture to load values from a `.env` file (e.g. `fixtures/.env`): @@ -179,3 +179,138 @@ Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to - cdn-base skeleton (base request/response handling structure) - fastedge-test reference (local WASM test runner and fixture format) - platform-overview reference (FastEdge application configuration and secret management) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_<NAME>` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_<NAME>` 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-codex/skills/scaffold/reference/cdn/env-secrets-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md index 598249f..9336819 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -175,3 +175,104 @@ println!("PASSWORD: {}", password); - FastEdge app secrets management (platform docs) - proxy-wasm HttpContext trait reference - fastedge crate proxywasm feature documentation + +## Source Material + +### FILE: examples/cdn/variables_and_secrets/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating environment variables and secrets access. + +Reads USERNAME from environment variables and PASSWORD from secrets, +then forwards both as request headers to the upstream origin. + +Required configuration: + - Environment variable: USERNAME + - Secret: PASSWORD +*/ + +use fastedge::proxywasm::secret; +use std::env; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(VariablesRoot) }); +}} + +struct VariablesRoot; + +impl Context for VariablesRoot {} + +impl RootContext for VariablesRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(VariablesContext)) + } +} + +struct VariablesContext; + +impl Context for VariablesContext {} + +impl HttpContext for VariablesContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let username = env::var("USERNAME").unwrap_or_default(); + let password = secret::get("PASSWORD") + .ok() + .flatten() + .and_then(|v| String::from_utf8(v).ok()) + .unwrap_or_default(); + + println!("USERNAME: {}", username); + // WARNING: Secrets are stored and retrieved as plaintext. Never log secret values + // in production code — platform logs are visible to operators and may be persisted. + // This line is shown for demonstration only; remove it in any real application. + println!("PASSWORD: {}", password); + + self.add_http_request_header("x-env-username", &username); + // WARNING: Forwarding a secret in a request header exposes it to the upstream origin + // and any intermediary that can inspect headers. Only do this when the upstream + // channel is trusted and the header is required by the destination API. + self.add_http_request_header("x-env-password", &password); + + Action::Continue + } +} +``` + +### FILE: examples/cdn/variables_and_secrets/Cargo.toml + +```toml +[workspace] + +[package] +name = "variables_and_secrets" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + +### FILE: examples/cdn/variables_and_secrets/README.md + +``` +[← Back to examples](../../README.md) + +# Variables and Secrets (CDN) + +Reads `USERNAME` from environment variables and `PASSWORD` from secrets for request forwarding using the proxy-wasm ABI. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md index ea43437..ae208d8 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -132,7 +132,20 @@ const countrySpecificOrigin = getEnv(countryCode); - Returns `""` (empty string) when no matching env var is set. - Do **not** use a null/undefined check; use an empty-string check. -### Step 4 — Read request path +### Step 4 — Read request host (optional) + +```typescript +const hostArrBuf = get_property("request.host"); +if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); +} +``` + +- `get_property("request.host")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. +- Reading the host is optional; absence does not stop iteration. + +### Step 5 — Read request path ```typescript const pathArrBuf = get_property("request.path"); @@ -151,7 +164,7 @@ const path = String.UTF8.decode(pathArrBuf); - `get_property("request.path")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. - Missing path → 500 response, stop iteration. -### Step 5 — Build and set target URL +### Step 6 — Build and set target URL ```typescript const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; @@ -212,7 +225,8 @@ registerRootContext((context_id: u32) => { ## Logging ```typescript -log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${matchedOrigin}`); +log(LogLevelValues.info, "onRequestHeaders >> "); +log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin}`); log(LogLevelValues.info, `Provided Host: ${host}`); log(LogLevelValues.info, `request-url: ${requestUrl}`); ``` @@ -252,3 +266,185 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) + +## Source Material + +### FILE: examples/geoRedirect/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const BAD_GATEWAY: u32 = 502; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class GeoRedirectRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new GeoRedirect(context_id, this); + } +} + +class GeoRedirect extends Context { + constructor(context_id: u32, root_context: GeoRedirectRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> "); + + const defaultOrigin = getEnv("DEFAULT"); + + if (!defaultOrigin) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured - DEFAULT must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const countryArrBuf = get_property("request.country"); + if (countryArrBuf.byteLength === 0) { + send_http_response( + BAD_GATEWAY, + "bad gateway", + String.UTF8.encode("Missing country information"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + const countryCode = String.UTF8.decode(countryArrBuf); + const countrySpecificOrigin = getEnv(countryCode); + + log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, + ); + + const hostArrBuf = get_property("request.host"); + if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); + } + + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Internal server error - no request path"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const path = String.UTF8.decode(pathArrBuf); + const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; + // remove trailing slashes from the origin + const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; + + const requestUrl = `${cleanedOrigin}${path}`; + + log(LogLevelValues.info, `request-url: ${requestUrl}`); + + set_property("request.url", String.UTF8.encode(requestUrl)); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new GeoRedirectRoot(context_id); +}, "geoRedirect"); +``` + + +### FILE: examples/geoRedirect/package.json + +```json +{ + "name": "fastedge-as-example-georedirect", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Geo Redirect", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/geoRedirect/README.md + +``` +[← Back to examples](../README.md) + +# Geo Redirect + +This application redirects requests to different origin URLs based on the client's country code. + +## What it does + +In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. + +- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. +- Otherwise it falls back to the `DEFAULT` origin. +- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. + +> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. + +> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | +| `<COUNTRY_CODE>` | `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-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md index 1fac82b..dab63e7 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -46,7 +46,7 @@ All routing logic executes in `on_http_request_headers`. No body hooks are used. 5. Read `request.path` property; default to `"/"` if absent. 6. Preserve the `Host` header by reading `request.host` and calling `set_http_request_header`. 7. Construct the target URL as `format!("{}{}", origin, path)`. -8. Log the target URL at `Info` level. +8. Log the target URL at `Info` level via `println!`. 9. Write the URL to `request.url` via `set_property` and return `Action::Continue`. ### Country Detection @@ -125,6 +125,8 @@ crate-type = ["cdylib"] proxy-wasm = "0.2" ``` +Note: the workspace-level `[workspace]` key is present in the source Cargo.toml but omitted here as it is not relevant to the library package configuration. + ## Struct Layout | Struct | Traits | Role | @@ -135,6 +137,15 @@ proxy-wasm = "0.2" `GeoRedirectRoot::get_type` returns `Some(ContextType::HttpContext)`. `GeoRedirectRoot::create_http_context` returns `Some(Box::new(GeoRedirectContext))`. +## Entry Point + +```rust +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(GeoRedirectRoot) }); +}} +``` + ## See Also - cdn-base skeleton reference diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md index 54555ac..c0544ac 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -158,8 +158,8 @@ registerRootContext((context_id: u32) => { Both blocked and allowed requests are logged at `INFO` level, providing an audit trail for all traffic decisions. -| Event | Log message | -| ---------------- | ---------------------------------------------- | +| Event | Log message | +| ---------------- | ------------------------------------------------ | | Request blocked | `"geoBlock: blocked request from " + countryStr` | | Request allowed | `"geoBlock: allowed request from " + countryStr` | @@ -169,11 +169,11 @@ Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. ## Status Codes Used -| Constant | Value | Condition | -| ----------------------- | ----- | ------------------------------------------------------- | -| `FORBIDDEN` | 403 | Request's country code is in the blacklist | -| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | -| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | +| Constant | Value | Condition | +| ----------------------- | ----- | ------------------------------------------------------ | +| `FORBIDDEN` | 403 | Request's country code is in the blacklist | +| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | +| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | --- @@ -256,23 +256,23 @@ Build scripts defined in `package.json`: ## Dependencies -| Package | Role | -| -------------------------------- | ------------------------------------------- | -| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | -| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | -| `assemblyscript` | AssemblyScript compiler (dev) | +| Package | Role | +| ----------------------------- | -------------------------------------- | +| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | +| `assemblyscript` | AssemblyScript compiler (dev) | --- ## Error Conditions -| Condition | Response | Status | -| --------------------------------------------- | ------------------------------------------------ | ------ | -| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | -| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | -| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | -| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | -| Country code not in blacklist | `Continue` | — | +| Condition | Response | Status | +| -------------------------------------------- | ---------------------------------------------------- | ------ | +| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | +| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | +| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | +| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | +| Country code not in blacklist | `Continue` | — | --- diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-rust.md index 031afb2..189abf0 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -155,7 +155,7 @@ Configure these in the FastEdge dashboard under the app's environment variables: | `BLACKLIST_TW_START` | No | Unix timestamp (u64) — start of time window during which blocking applies | | `BLACKLIST_TW_END` | No | Unix timestamp (u64) — end of time window during which blocking applies | -If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. +If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. If only one of the two is set, it is treated as absent and the block is permanent. ## Error Conditions @@ -206,3 +206,112 @@ Output binary: `target/wasm32-wasip1/release/<crate-name>.wasm` - FastEdge SDK Rust reference (proxy-wasm trait definitions and types) - Platform overview reference (request.country property and other injected properties) - deploy skill reference (uploading and registering the compiled WASM binary) + +## Source Material + +### FILE: examples/cdn/geoblock/src/lib.rs + +```rust +use std::env; +use std::time::{SystemTime, UNIX_EPOCH}; + +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(GeoblockRoot) }); +}} + +struct GeoblockRoot; + +impl Context for GeoblockRoot {} + +impl RootContext for GeoblockRoot { + fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(GeoblockContext {})) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct GeoblockContext {} + +impl Context for GeoblockContext {} + +const BAD_GATEWAY: u32 = 502; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +impl HttpContext for GeoblockContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(blacklist) = env::var("BLACKLIST") else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + + let mut blacklist = blacklist.split(','); + + let Some(country) = self.get_property(vec!["request.country"]) else { + self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - no country field")); + return Action::Pause; + }; + + let Ok(country) = std::str::from_utf8(&country) else { + self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - country not utf8 string")); + return Action::Pause; + }; + + if blacklist.any(|b| country.eq_ignore_ascii_case(b)) { + let tw_start = env::var("BLACKLIST_TW_START").ok(); + let tw_end = env::var("BLACKLIST_TW_END").ok(); + + if let Some((tw_start, tw_end)) = tw_start.zip(tw_end) { + let Ok(tw_start) = tw_start.parse::<u64>() else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + + let Ok(tw_end) = tw_end.parse::<u64>() else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if now >= tw_start && now <= tw_end { + self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); + return Action::Pause; + } + } else { + self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); + return Action::Pause; + } + } + + + Action::Continue + } +} +``` + +### FILE: examples/cdn/geoblock/Cargo.toml + +```toml +[workspace] + +[package] +name = "geoblock" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md index 6fc0857..f2b6798 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -246,14 +246,14 @@ let headers = self.get_http_request_headers() .collect::<HashSet<(String, String)>>(); // After add/set operations, expected new entries include: -// ("new-header-01", "") // removed → empty string -// ("new-header-bytes-01", "") // removed → empty string -// ("new-header-02", "new-value-02") // replaced -// ("new-header-bytes-02", "new-value-bytes-02") // replaced -// ("new-header-03", "value-03") // original add -// ("new-header-bytes-03", "value-bytes-03") // original add -// ("new-header-03", "value-03-a") // duplicate add preserved -// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved +// ("new-header-01", "") // removed → empty string +// ("new-header-bytes-01", "") // removed → empty string +// ("new-header-02", "new-value-02") // replaced +// ("new-header-bytes-02", "new-value-bytes-02") // replaced +// ("new-header-03", "value-03") // original add +// ("new-header-bytes-03", "value-bytes-03") // original add +// ("new-header-03", "value-03-a") // duplicate add preserved +// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved let diff = headers .difference(&original_headers) @@ -265,7 +265,7 @@ if !diff.is_empty() { } ``` -The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. +The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. The response phase (`on_http_response_headers`) applies an identical add/set/diff sequence using response header methods. --- diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/http-call-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/http-call-as.md index 2d323d6..590549a 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/http-call-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -91,12 +91,12 @@ function handleHttpCallResponse( **Parameters:** -| Parameter | Type | Description | -|------------|-------------|-------------| +| Parameter | Type | Description | +|------------|---------------|-------------| | `ctx` | `BaseContext` | Originating context | -| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | -| `bodySize` | `usize` | Size of the response body in bytes | -| `trls` | `u32` | Number of response trailers | +| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | +| `bodySize` | `usize` | Size of the response body in bytes | +| `trls` | `u32` | Number of response trailers | --- diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-as.md index 4377eda..717fbc3 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -205,6 +205,14 @@ The `validateQueryParams` function: - Validates `action` against `ALL_ACTIONS = ["get", "scan", "zscan", "zrange", "bfExists"]` - Checks all required parameters for the resolved action are present and non-empty +**Required parameters by action** (as defined in `validateQueryParams`): +- `store` — required for all actions +- `key` — required for `get`, `zrange`, `zscan`, `bfExists` +- `match` — required for `scan`, `zscan` +- `min` — required for `zrange` +- `max` — required for `zrange` +- `item` — required for `bfExists` + --- ## Error Handling @@ -248,6 +256,18 @@ set_buffer_bytes( The response body is a JSON object built from a `Map<string, string>` using `stringifyMap`. It includes fields such as `Store`, `Action`, `Key`, `Response`, and action-specific fields (`Match`, `Min`, `Max`, `Item`). +**`stringifyMap` format:** +```typescript +// Output: {"key1": "value1", "key2": "value2"} +function stringifyMap(map: Map<string, string>): string +``` + +**`stringifyValueScoreTuples` format:** +```typescript +// Output: "{ value: <val>, score: <score> }, ..." +function stringifyValueScoreTuples(arr: Array<ValueScoreTuple>): string +``` + --- ## Hook: `onResponseBody` @@ -295,6 +315,11 @@ registerRootContext → KvStoreRoot.createContext (sets log level to info) The KV Store must be configured and linked to the FastEdge application before use. `KvStore.open` will return `null` if the named store is not attached. The `store` query parameter at runtime must exactly match the binding name configured on the application. +KV Store setup steps: +1. Create a KV Store in the FastEdge portal under the Key-Value storage section. +2. Populate it with the keys and values you want to query. +3. Link the store to the app — when configuring the FastEdge application, add the store under the app's KV store bindings. The name given to the binding is what the `store` query parameter must match at runtime. + --- ## See Also diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md index d9a6ea5..8d280e2 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -76,14 +76,10 @@ class LargeDictionaryContext extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. const config = getDictionary("LARGE_CONFIG"); - // getDictionary returns "" (never null) when variable is not set - if (config.length === 0) { - log(LogLevelValues.info, "LARGE_CONFIG is not set"); - return FilterHeadersStatusValues.Continue; - } - const size = config.length; log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md index 925690d..a8334a3 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -153,111 +153,3 @@ println!("LARGE_CONFIG size: {} bytes", size); - proxy-wasm HttpContext trait reference - FastEdge environment variable configuration docs - `std::env::var` (standard Rust env access for values under 64KB) - -## Source Material - -### FILE: examples/cdn/large_env_variable/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating access to large environment variables. - -Uses `fastedge::proxywasm::dictionary` to read environment variables that -may exceed the 64KB WASI environment variable size limit. - -For normal-sized environment variables (< 64KB), prefer `std::env::var()` -instead. The dictionary API is only required when your variable value -may be larger than 64KB. - -Required configuration: - - Environment variable: LARGE_CONFIG (a large configuration payload, e.g. JSON) -*/ - -use fastedge::proxywasm::dictionary; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(LargeEnvRoot) }); -}} - -struct LargeEnvRoot; - -impl Context for LargeEnvRoot {} - -impl RootContext for LargeEnvRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(LargeEnvContext)) - } -} - -struct LargeEnvContext; - -impl Context for LargeEnvContext {} - -impl HttpContext for LargeEnvContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // Use dictionary::get for environment variables that may exceed 64KB. - // For normal-sized env vars, use std::env::var() instead. - let config = dictionary::get("LARGE_CONFIG").unwrap_or_default(); - - let size = config.len(); - println!("LARGE_CONFIG size: {} bytes", size); - - self.add_http_request_header("x-config-size", &size.to_string()); - - Action::Continue - } -} -``` - -### FILE: examples/cdn/large_env_variable/Cargo.toml - -```toml -[workspace] - -[package] -name = "large_env_variable" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -``` - -### FILE: examples/cdn/large_env_variable/README.md - -``` -[← Back to examples](../../README.md) - -# Large Environment Variable (CDN) - -Demonstrates how to read **large environment variables** (> 64KB) using `fastedge::proxywasm::dictionary`. - -## When to use `dictionary` vs `std::env` - -| Method | Use when | -|--------|----------| -| `std::env::var("KEY")` | Variable value is under 64KB (most cases) | -| `fastedge::proxywasm::dictionary::get("KEY")` | Variable value may exceed the 64KB WASI env var size limit | - -The WASI environment variable interface has a **64KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, certificates, policy documents), use the `dictionary` API which bypasses this limit. - -For all other environment variable access, prefer `std::env::var()` as it is the standard, idiomatic Rust approach. - -## Required configuration - -- **Environment variable**: `LARGE_CONFIG` - a large configuration payload (e.g. JSON, PEM certificate) -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md index 14d1bd8..f6472f8 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -130,7 +130,7 @@ set_property("request.path", String.UTF8.encode(newPath)); Parse the raw query string and rewrite properties based on matching parameter keys. ```typescript -const query = get_property("request.query"); +const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); const params = queryString @@ -145,11 +145,11 @@ if (query.byteLength !== 0) { const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { - set_property("request.url", String.UTF8.encode(value)); + set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { - set_property("request.host", String.UTF8.encode(value)); + set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { - set_property("request.path", String.UTF8.encode(value)); + set_property(REQUEST_PATH, String.UTF8.encode(value)); } } } diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-ts.md index 1e1f8f7..71565f9 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -252,3 +252,121 @@ Output: Headers - fastedge-sdk-js SDK reference - http-base skeleton - FastEdge build CLI reference + +## Source Material + +### FILE: examples/ab-testing/src/index.js + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(request); + + const headers = createAbTestHeaders(slicedHeaders, testConfig, xid); + + // This is the URL of the outbound service - i.e. could be a url to your origin + // e.g. https://template-invoice-ab-test-123456.fastedge.cdn.gc.onl/ + const outboundUrl = getEnv('OUTBOUND_URL'); + if (!outboundUrl || !String(outboundUrl).trim()) { + return new Response('OUTBOUND_URL environment variable is not configured', { + status: 500, + }); + } + + const response = await fetch(outboundUrl, { headers }); + + // Request/Response Headers are immutable, so we need to create a new Headers object + const resHeaders = new Headers(response.headers); + resHeaders.set( + 'set-cookie', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); + +const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { + // Request/Response Headers are immutable, so we need to create a new Headers object + const headers = new Headers(reqHeaders); + const cookie = headers.get('cookie') || ''; + // Read the existing `xid` cookie value. + const xid = (cookie.match(/(?:^|;) *x-fastedge-abid=((0|1|)\.\d+) *(?:;|$)/u) || [])[1]; + if (xid) { + // Request contains A/B cookie, hide it from the origin + const newCookie = cookie.replace(/x-fastedge-abid=[^;]+;?\s*/gu, ''); + if (newCookie) { + headers.set('cookie', newCookie); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + return headers; +}; +``` + +### FILE: examples/ab-testing/package.json + +```json +{ + "name": "fastedge-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge JS example: cookie-based A/B testing", + "type": "module", + "main": "src/index.js", + "scripts": { + "build": "fastedge-build src/index.js dist/ab-testing.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-wasi-rust.md index 49f2ccf..9f1a6a0 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -217,214 +217,3 @@ Response::builder() - fastedge-sdk-rust platform overview - host-services-rust reference (outbound HTTP, environment variables) - best-practices reference (cookie security, entropy sources) - -## Source Material - -### FILE: examples/http/wasi/ab_testing/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Cookie-based A/B testing example. - -Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically -assign the visitor to weighted variants of each configured test, then proxies the -request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-<name>` -headers. The origin response is returned verbatim with a `set-cookie` header so -returning visitors receive the same variants on subsequent visits. - -Required configuration: - - Environment variable: OUTBOUND_URL (downstream origin to proxy to) - -Mirror of the FastEdge-sdk-js `ab-testing` example. -*/ - -use std::env; -use std::time::{SystemTime, UNIX_EPOCH}; - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -struct VariantWeight { - variant: &'static str, - weight: f64, -} - -struct AbTest { - name: &'static str, - variants: &'static [VariantWeight], -} - -static TESTS: &[AbTest] = &[ - AbTest { - name: "logo", - variants: &[ - VariantWeight { variant: "hops", weight: 50.0 }, - VariantWeight { variant: "bottle", weight: 50.0 }, - ], - }, - AbTest { - name: "font", - variants: &[ - VariantWeight { variant: "exo2", weight: 40.0 }, - VariantWeight { variant: "gloria", weight: 65.0 }, - VariantWeight { variant: "standard", weight: 45.0 }, - ], - }, -]; - -const AB_COOKIE: &str = "x-fastedge-abid"; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let outbound_url = match env::var("OUTBOUND_URL") { - Ok(u) if !u.trim().is_empty() => u, - _ => { - return Ok(Response::builder() - .status(500) - .body(Body::from( - "OUTBOUND_URL environment variable is not configured", - ))?); - } - }; - - let raw_cookie = req - .headers() - .get("cookie") - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .to_string(); - - let (xid, cleaned_cookie) = match extract_abid(&raw_cookie) { - Some(existing) if is_valid_xid(existing) => { - (existing.to_string(), strip_abid(&raw_cookie)) - } - _ => (generate_xid(), raw_cookie.clone()), - }; - - // Build the outbound request: copy incoming headers except `host` and - // `cookie` (which we handle specially), replace the cookie with a version - // that has the abid stripped (so the origin never sees it), and attach an - // `ab-test-<name>` header for every configured test. - let mut builder = Request::get(&outbound_url); - for (name, value) in req.headers() { - let n = name.as_str(); - if n == "host" || n == "cookie" { - continue; - } - if let Ok(v) = value.to_str() { - builder = builder.header(n, v); - } - } - if !cleaned_cookie.trim().is_empty() { - builder = builder.header("cookie", cleaned_cookie); - } - for test in TESTS { - if let Some(variant) = assign_variant(&xid, test) { - builder = builder.header(format!("ab-test-{}", test.name), variant); - } - } - - let outbound_req = builder - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build outbound request: {e}"))?; - - let outbound_resp = Client::new() - .send(outbound_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - let (parts, mut body) = outbound_resp.into_parts(); - let body_bytes = body.contents().await?; - - let content_type = parts - .headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream") - .to_string(); - - let xid_cookie = - format!("{AB_COOKIE}={xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"); - - Ok(Response::builder() - .status(parts.status) - .header("content-type", content_type) - .header("set-cookie", xid_cookie) - .body(Body::from(body_bytes))?) -} - -fn extract_abid(cookie_header: &str) -> Option<&str> { - let needle = format!("{AB_COOKIE}="); - cookie_header - .split(';') - .map(str::trim) - .find_map(|p| p.strip_prefix(needle.as_str())) -} - -fn strip_abid(cookie_header: &str) -> String { - let needle = format!("{AB_COOKIE}="); - cookie_header - .split(';') - .map(str::trim) - .filter(|p| !p.is_empty()) - .filter(|p| !p.starts_with(needle.as_str())) - .collect::<Vec<_>>() - .join("; ") -} - -fn is_valid_xid(xid: &str) -> bool { - matches!(xid.parse::<f64>(), Ok(v) if (0.0..1.0).contains(&v)) -} - -/// Generate a pseudo-random A/B id of the form `"0.NNNN"`. -/// -/// Uses request-time nanoseconds as a weak entropy source. For production, -/// prefer a cryptographic RNG (e.g. `rand` wired to `wasi-random`). -fn generate_xid() -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - format!("0.{:04}", now.subsec_nanos() % 10000) -} - -fn assign_variant(xid: &str, test: &AbTest) -> Option<&'static str> { - let xid_value: f64 = xid.parse().ok()?; - let xid_percentage = xid_value * 100.0; - let total: f64 = test.variants.iter().map(|v| v.weight).sum(); - if total == 0.0 { - return None; - } - let mut start = 0.0; - for vw in test.variants { - let percentage = (vw.weight / total) * 100.0; - let end = start + percentage; - if xid_percentage >= start && xid_percentage < end { - return Some(vw.variant); - } - start = end; - } - None -} -``` - -### FILE: examples/http/wasi/ab_testing/Cargo.toml - -```toml -[workspace] - -[package] -name = "ab_testing_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/base-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/base-rust.md index a8d1e14..f2b3419 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/base-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-07-23 +updated: 2026-08-17 --- @@ -174,3 +174,45 @@ cargo build --release --target wasm32-wasip2 - **Handler signature**: `async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>>` - **Crate type**: `cdylib` (required for WASM output) - **Workspace**: Single-project workspace pattern (`[workspace]` declared in Cargo.toml) + +## Source Material + +### FILE: examples/http/wasi/hello_world/src/lib.rs + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { + let url = request.uri().to_string(); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!( + "Hello, you made a wasi request to {url}" + )))?) +} + +``` + + +### FILE: examples/http/wasi/hello_world/Cargo.toml + +```toml +[workspace] + +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" + +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-ts.md index 6b889c9..f81acb2 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -176,3 +176,67 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide + +## Source Material + +### FILE: examples/bloom-filter-denylist/src/index.js + +```js +import { getEnv } from 'fastedge::env'; +import { KvStore } from 'fastedge::kv'; + +const BLOOM_KEY = 'blocked-ips'; + +function app(event) { + const storeName = getEnv('DENYLIST_STORE'); + if (!storeName) { + return Response.json( + { error: 'DENYLIST_STORE environment variable is not configured' }, + { status: 500 }, + ); + } + + const ip = event.client.address; + if (!ip) { + return Response.json({ error: 'client address unavailable' }, { status: 500 }); + } + + let blocked; + try { + const store = KvStore.open(storeName); + blocked = store.bfExists(BLOOM_KEY, ip); + } catch (error) { + return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); + } + + if (blocked) { + // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. + // Acceptable for a denylist (you over-block some legitimate users); not acceptable for + // allowlists or anything requiring exact membership — use KvStore.get() for that. + return Response.json({ allowed: false, ip }, { status: 403 }); + } + + return Response.json({ allowed: true, ip }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/bloom-filter-denylist/package.json + +```json +{ + "name": "fastedge-example-bloom-filter-denylist", + "version": "1.0.0", + "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md index 07dd858..ad66102 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,117 @@ fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result<Respon - host-services-rust reference (KV store host service documentation) - http-base skeleton (entry point, request/response types, wstd::http_server macro) - examples-kv-rust (general KV store usage patterns) + +## Source Material + +### FILE: examples/http/wasi/bloom_filter_denylist/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Bloom-filter IP denylist example. + +Checks the client IP (from the `x-real-ip` request header, falling back to +`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 +on a hit, 200 otherwise. + +Required configuration: + - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) + +The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; +populate the filter out of band. + +Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::key_value::{Error as StoreError, Store}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +const BLOOM_KEY: &str = "blocked-ips"; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let store_name = match env::var("DENYLIST_STORE") { + Ok(s) if !s.trim().is_empty() => s, + _ => { + return json_response( + 500, + json!({ "error": "DENYLIST_STORE environment variable is not configured" }), + ); + } + }; + + let headers = req.headers(); + let client_ip = headers + .get("x-real-ip") + .or_else(|| headers.get("x-forwarded-for")) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let Some(ip) = client_ip else { + return json_response(500, json!({ "error": "client IP not available" })); + }; + + let store = match Store::open(&store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return json_response( + 403, + json!({ "error": "access denied opening denylist store" }), + ); + } + Err(e) => { + return json_response(500, json!({ "error": format!("store open error: {e}") })); + } + }; + + let blocked = store + .bf_exists(BLOOM_KEY, ip) + .map_err(|e| anyhow!("bf_exists error: {e}"))?; + + if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. + return json_response(403, json!({ "allowed": false, "ip": ip })); + } + + json_response(200, json!({ "allowed": true, "ip": ip })) +} + +fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result<Response<Body>> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(value.to_string()))?) +} +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml + +```toml +[workspace] + +[package] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-basic-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-basic-ts.md index ec77b72..8e33d7f 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-basic-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-basic-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -231,3 +231,117 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` - `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface - http-base skeleton — base HTTP handler structure this feature extends - deploy skill reference — building and uploading the compiled WASM binary + +## Source Material + +### FILE: examples/cache-basic/src/index.js + +```js +// FastEdge Cache — basic operations +// +// The `fastedge::cache` module gives you a fast, data-center-scoped +// key/value store. Values written here are stored in the same point of +// presence (POP) that runs the worker, so reads and writes are very fast, +// and writes from one POP are not visible to others. +// +// Use this for transient, request-time state — short-lived caches, hit +// counters, rate limit windows, deduplicated work. For globally +// replicated storage, use the `fastedge::kv` module instead. +// +// This example demonstrates the four most common operations: +// +// GET /?action=set&key=foo&value=bar -> Cache.set +// GET /?action=get&key=foo -> Cache.get +// GET /?action=exists&key=foo -> Cache.exists +// GET /?action=delete&key=foo -> Cache.delete + +import { Cache } from 'fastedge::cache'; + +async function eventHandler(event) { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + const key = url.searchParams.get('key'); + + if (!key) { + throw new Error('Missing required query parameter: "key"'); + } + + switch (action) { + case 'set': { + // Cache.set writes a value under `key`. Accepts strings, + // ArrayBuffers, ArrayBufferViews, ReadableStreams, and Response + // objects (the body is consumed; status and headers are not stored). + // + // The `{ ttl: 60 }` option means "expire 60 seconds from now". You + // can also use `ttlMs` for sub-second precision, or `expiresAt` for + // a fixed Unix-epoch deadline. Omit options entirely for no expiry. + const value = url.searchParams.get('value') ?? ''; + await Cache.set(key, value, { ttl: 60 }); + return Response.json({ action, key, value, ttl: 60 }); + } + + case 'get': { + // Cache.get returns a CacheEntry on a hit, or `null` on a miss + // (key absent or expired). The cache stores raw bytes, so on read + // you choose how to decode using one of: + // entry.text() -> Promise<string> (UTF-8) + // entry.json() -> Promise<unknown> (parsed JSON) + // entry.arrayBuffer() -> Promise<ArrayBuffer> + const entry = await Cache.get(key); + if (entry === null) { + return Response.json({ action, key, hit: false }); + } + const value = await entry.text(); + return Response.json({ action, key, hit: true, value }); + } + + case 'exists': { + // Cache.exists is a cheap presence check — useful when you only + // need to know whether a key is set without transferring its value + // (e.g. idempotency-key checks, "have we seen this token?"). + const present = await Cache.exists(key); + return Response.json({ action, key, present }); + } + + case 'delete': { + // Cache.delete removes the entry. It is a no-op if the key is + // already absent — no error is thrown. + await Cache.delete(key); + return Response.json({ action, key, deleted: true }); + } + + default: + throw new Error( + `Unknown action: "${action}". Use one of: set, get, exists, delete.`, + ); + } + } catch (error) { + // Validation errors (e.g. wrong types, conflicting WriteOptions fields) + // are thrown synchronously; host errors (access denied, internal error) + // arrive as Promise rejections. Both are caught by this single handler. + return Response.json({ error: error.message }, { status: 500 }); + } +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); +``` + +### FILE: examples/cache-basic/package.json + +```json +{ + "name": "fastedge-example-cache-basic", + "version": "1.0.0", + "description": "FastEdge JS example: simple Cache set/get/exists/delete operations", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/cache-basic.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-ts.md index c8c4e8a..5579117 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -315,3 +315,281 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise<Response> { + // `event.client.address` is the trusted-edge client IP. Sourced from + // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by + // the FastEdge POP, not the client, so they're safe to key on. + const ip = event.client.address || 'unknown'; + + const key = `rl:${ip}`; + + const count = await Cache.incr(key); + + // Only set the expiry on the first hit of a new window. If we set it + // on every request, the window would never close — each new request + // would push the deadline another 60 seconds out. + if (count === 1) { + await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); + } + + if (count > RATE_LIMIT_MAX) { + return Response.json( + { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, + { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, + ); + } + + return Response.json({ + pattern: 'rate-limit', + ip, + count, + remaining: RATE_LIMIT_MAX - count, + windowSeconds: RATE_LIMIT_WINDOW_S, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 2 — Origin-cache proxy with conditional caching +// --------------------------------------------------------------------------- +// +// Cache successful upstream responses for PROXY_TTL_S seconds; pass +// non-2xx and redirects through *without* caching, so a transient 404 +// or 500 doesn't get pinned for the rest of the window. The cache is +// a byte cache (no status/headers), so we only cache when "200 OK with +// application/octet-stream" is a faithful replay of the upstream. +// +// `getOrSet` is not used here because its populator can't signal +// "fetched, but don't cache" — we need that distinction to handle +// error responses safely. See Pattern 3 for `getOrSet` in a context +// where every populator output is cacheable. + +const PROXY_TTL_S = 30; + +async function proxy(url: string): Promise<Response> { + // Validate the URL before we use it as a cache key. + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); + } + + // Strip the fragment: fetch() never sends it to the origin, so + // `https://example.com/#a` and `#b` are the same upstream resource + // and must share one cache entry. + parsed.hash = ''; + + const key = `proxy:${parsed.toString()}`; + + // Cache hit — replay the bytes as 200 OK. Status/headers from the + // original response are not preserved by the byte cache. + const cached = await Cache.get(key); + if (cached !== null) { + return new Response(await cached.arrayBuffer(), { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'hit', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); + } + + // Cache miss — fetch upstream and only cache successful responses. + // Non-2xx and redirects flow through unchanged so callers see the + // real status code instead of a synthetic 200. + const upstream = await fetch(parsed.toString()); + if (!upstream.ok) { + return upstream; + } + + const bytes = await upstream.arrayBuffer(); + await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); + return new Response(bytes, { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'miss', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 3 — JSON memoisation via getOrSet with a computed populator +// --------------------------------------------------------------------------- +// +// Same shape as the proxy pattern, but the populator does CPU work +// instead of network I/O. Use this whenever you compute the same +// expensive answer many times in a row — search index lookups, +// signed-token verification, derived report rollups, JSON +// transformations of slow-changing source data. +// +// We embed `generatedAt` in the result so a client refreshing the +// page can see the timestamp stay constant within the cache window +// and update once it expires. + +const MEMO_TTL_S = 60; + +async function memo(): Promise<Response> { + const entry = await Cache.getOrSet( + 'memo:report', + () => { + // Stand-in for "expensive computation". The populator can be + // synchronous or async — both are accepted. + const report = { + generatedAt: new Date().toISOString(), + topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ + name, + score: Math.round(Math.random() * 1000) / 10, + rank: i + 1, + })), + }; + // The populator returns the value to store. Because we want + // structured JSON back later, we serialise here and re-parse + // via `entry.json()` on read. + return JSON.stringify(report); + }, + { ttl: MEMO_TTL_S }, + ); + + // `entry.json()` parses the cached UTF-8 bytes as JSON. Use + // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. + const report = await entry.json(); + + return Response.json({ + pattern: 'memo', + note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, + report, + }); +} + +// --------------------------------------------------------------------------- +// Default landing — usage menu when no action is supplied +// --------------------------------------------------------------------------- + +function landing(): Response { + return Response.json({ + name: 'FastEdge Cache patterns', + actions: { + 'rate-limit': '/?action=rate-limit', + proxy: '/?action=proxy&url=https://www.example.com', + memo: '/?action=memo', + }, + }); +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +async function eventHandler(event: FetchEvent): Promise<Response> { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'rate-limit': + return await rateLimit(event); + case 'proxy': + return await proxy(url.searchParams.get('url') ?? ''); + case 'memo': + return await memo(); + case null: + return landing(); + default: + return Response.json( + { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, + { status: 400 }, + ); + } + } catch (error: unknown) { + // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions + // fields) are synchronous; host errors arrive as Promise rejections. + // Both are caught by this single handler. + return Response.json({ error: (error as Error).message }, { status: 500 }); + } +} + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(eventHandler(event)); +}); +``` + + +### FILE: examples/cache/package.json + +```json +{ + "name": "fastedge-example-cache", + "version": "1.0.0", + "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", + "type": "module", + "scripts": { + "build": "fastedge-build -c" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` + + +### FILE: examples/cache/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-wasi-rust.md index f08fffb..20e88de 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -273,201 +273,3 @@ cargo build --release - http-base skeleton - outbound-http feature blueprint - platform-overview (environment variable configuration) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/<path_and_query> — purge keys whose cache key starts with cache:/<path_and_query> - -Environment variables: - ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com - CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) - -Build: - cargo build --release -*/ - -use std::env; - -use anyhow::anyhow; -use fastedge::cache; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let origin = env::var("ORIGIN_HOST") - .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; - - let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse().ok()) - .unwrap_or_else(|| { - env::var("CACHE_TTL_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(60_000) - }); - - // Build cache key from the request path (and query string if present) - let path_and_query = req - .uri() - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - - - // Handle purge requests before any cache/origin logic - if path_and_query == "/purge" { - let deleted = cache::purge()?; - println!("purge all: {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - if let Some(prefix) = path_and_query.strip_prefix("/purge/") { - let prefix = format!("cache:/{prefix}"); - let deleted = cache::purge_prefix(&prefix)?; - println!("purge prefix '{prefix}': {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - if let Some(prefix) = path_and_query.strip_prefix("/delete/") { - let prefix = format!("cache:/{prefix}"); - cache::delete(&prefix)?; - println!("prefix '{prefix}': removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - let cache_key = format!("cache:{path_and_query}"); - - // Return cached response if available - if let Some(cached) = cache::get(&cache_key)? { - println!("cache hit: {cache_key}"); - return Ok(Response::builder() - .status(200) - .header("content-type", "application/octet-stream") - .header("x-cache", "hit") - .body(Body::from(cached))?); - } - - // Cache miss — forward request to origin - let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); - println!("cache miss: {cache_key} → {upstream_url}"); - - let upstream_req = Request::get(&upstream_url) - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("upstream request failed: {e}"))?; - - let status = upstream_resp.status(); - let headers: Vec<(String, String)> = upstream_resp - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - // Read body bytes - let mut body = upstream_resp.into_body(); - let body_bytes = body.contents().await?.to_vec(); - - // Only cache successful responses - if status.is_success() { - cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; - } - - // Replay original response - let mut builder = Response::builder() - .status(status) - .header("x-cache", "miss"); - for (k, v) in &headers { - builder = builder.header(k, v); - } - Ok(builder.body(Body::from(body_bytes))?) -} -``` - - -### FILE: examples/http/wasi/cache/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/cache/README.md - -``` -[← Back to examples](../../../README.md) - -# Cache (WASI) - -Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | -| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | - -## How it works - -``` -GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) -GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) -``` - -Cache key is `cache:<path>?<query>`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/cache_wasi.wasm -``` - -## APIs used - -- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option<Vec<u8>>)` -- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry -- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md index 6d6aa86..3331544 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -40,7 +40,7 @@ Build script: `fastedge-build src/index.js dist/crypto-hmac-jwt.wasm` import { getSecret } from 'fastedge::secret'; ``` -`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons: +`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons reused across requests: ```js const encoder = new TextEncoder(); diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md index 0710498..64f2d27 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,103 @@ async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - http-base skeleton (base handler structure, entry point macro) - platform-overview (log viewer, per-request diagnostics context) - fastedge SDK Rust reference (full `fastedge::utils` API surface) + +## Source Material + +### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Diagnostic logging example. + +Tiny pass-through proxy that writes a single `set_user_diag` tag per request +summarising the outcome (config_error, origin_unreachable, or proxied). The +tag appears in the FastEdge platform's per-request log viewer and is distinct +from stdout — it's intended for filterable outcome labels, not verbose +traces. + +Required configuration: + - Environment variable: ORIGIN_URL (origin to proxy to) + +Uses `logfmt`-ish formatting (`outcome=<verb> key=value ...`) so the tag is +easy to slice in log search tooling. +*/ + +use std::env; + +use fastedge::utils::set_user_diag; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let method = req.method().as_str().to_string(); + let path = req.uri().path().to_string(); + + let origin = match env::var("ORIGIN_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + set_user_diag("outcome=config_error reason=origin_missing"); + return Ok(Response::builder() + .status(500) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("ORIGIN_URL is not configured"))?); + } + }; + + let outbound = Request::get(&origin).body(Body::empty())?; + let resp = match Client::new().send(outbound).await { + Ok(r) => r, + Err(e) => { + set_user_diag(&format!( + "outcome=origin_unreachable method={method} path={path} err={e}" + )); + return Ok(Response::builder() + .status(502) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("origin unreachable"))?); + } + }; + + let status = resp.status().as_u16(); + set_user_diag(&format!( + "outcome=proxied method={method} path={path} status={status}" + )); + + let (parts, mut body) = resp.into_parts(); + let bytes = body.contents().await?; + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .body(Body::from(bytes))?) +} +``` + +### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml + +```toml +[workspace] + +[package] +name = "diagnostic_logging_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-wasi-rust.md index d8d4004..85ad7d9 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -154,119 +154,3 @@ cargo build --release - http-base skeleton reference - FastEdge platform overview (geoip-country-code header injection) - deploy skill reference (uploading WASM binary and setting environment variables) - -## Source Material - -### FILE: examples/http/wasi/geo_redirect/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example WASI-HTTP app demonstrating geo-based redirects. - -Reads the country code from the geoip-country-code request header -and redirects to a country-specific origin URL. Falls back to -BASE_ORIGIN when no country-specific mapping is configured. - -Required configuration: - - Environment variable: BASE_ORIGIN (fallback origin URL) - - Environment variable: <COUNTRY_CODE> (optional per-country origin URLs, e.g. US, DE, GB) -*/ - -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let base_origin = match env::var("BASE_ORIGIN") { - Ok(origin) => origin, - Err(_) => { - return Ok(Response::builder() - .status(500) - .body(Body::from("BASE_ORIGIN is not set"))?); - } - }; - - let country_code = req - .headers() - .get("geoip-country-code") - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .to_string(); - - let redirect_origin = if !country_code.is_empty() { - env::var(&country_code).unwrap_or(base_origin) - } else { - base_origin - }; - - Ok(Response::builder() - .status(302) - .header("location", &redirect_origin) - .body(Body::empty())?) -} -``` - - -### FILE: examples/http/wasi/geo_redirect/Cargo.toml - -```toml -[workspace] - -[package] -name = "geo_redirect_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/geo_redirect/README.md - -``` -[← Back to examples](../../../README.md) - -# Geo Redirect (WASI) - -Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. - -Demonstrates reading request headers, reading environment variables, and returning redirect responses. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | -| `<COUNTRY_CODE>` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | - -## How it works - -``` -geoip-country-code: DE → env var DE is set → 302 to DE value -geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN -(no header) → 302 to BASE_ORIGIN -BASE_ORIGIN not set → 500 -``` - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm -``` - -## APIs used - -- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge -- `std::env::var(country_code)` — dynamic env var lookup by country code -- `Response::builder().status(302).header("location", url)` — redirect response -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-ts.md index b14c60e..ea38fe3 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -154,7 +154,6 @@ addEventListener('fetch', (event) => { }); ``` - ### FILE: examples/headers/package.json ```json diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-wasi-rust.md index 8c5712a..4cc23df 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/headers-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -164,7 +164,6 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { } ``` - ### FILE: examples/http/wasi/headers/Cargo.toml ```toml @@ -183,7 +182,6 @@ wstd = "0.6" anyhow = "1" ``` - ### FILE: examples/http/wasi/headers/README.md ``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-ts.md index f11cd59..bd5f092 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -286,186 +286,3 @@ Missing required parameters return HTTP 500 with JSON `{ "error": "..." }`. - The KV store must be pre-created in the Gcore dashboard or API before the app can use it. The store name is passed at runtime (e.g., as a query parameter). - Available KV operations: `get`, `scan`, `zrangeByScore`, `zscan`, `bfExists`. - `tsconfig.json` uses `"moduleResolution": "Bundler"` and `"target": "ES2023"`. Do not use `"moduleResolution": "Node"` or older ES targets with this SDK version. - -## Source Material - -### FILE: examples/kv-store/src/index.ts - -```ts -import { KvStore } from 'fastedge::kv'; - -import { Action, decodeValueArray, stringifyValueScoreTuples, validateQueryParams } from './utils'; - -async function eventHandler(event: FetchEvent): Promise<Response> { - try { - const { request: req } = event; - const url = new URL(req.url); - - const params = validateQueryParams(url.searchParams); - if (params.error) { - throw new Error(params.error); - } - - const myStore = KvStore.open(params.store); - const action = params.action as Action; - - const responseObj: Record<string, string> = { - Store: params.store, - Action: action, - }; - - switch (action) { - case 'get': { - const response = myStore.get(params.key); - responseObj.Key = params.key; - responseObj.Response = decodeValueArray(response); - break; - } - case 'scan': { - const response = myStore.scan(params.match); - responseObj.Match = params.match; - responseObj.Response = response.join(', '); - break; - } - case 'zrange': { - const { key, min, max } = params; - const response = myStore.zrangeByScore(key, Number.parseFloat(min), Number.parseFloat(max)); - responseObj.Key = key; - responseObj.Min = min; - responseObj.Max = max; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'zscan': { - const { key, match } = params; - const response = myStore.zscan(key, match); - responseObj.Key = key; - responseObj.Match = match; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'bfExists': { - const { key, item } = params; - const exists = myStore.bfExists(key, item); - responseObj.Key = key; - responseObj.Item = item; - responseObj.Response = exists ? 'true' : 'false'; - break; - } - default: - break; - } - - return Response.json(responseObj); - } catch (error: Error | unknown) { - return Response.json({ error: `${(error as Error).message}` }, { status: 500 }); - } -} - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store/src/utils.ts - -```ts -const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; - -export type Action = (typeof ALL_ACTIONS)[number]; - -type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; - -type Params = { [key in ParamKey]: string }; - -export function validateQueryParams(queryParams: URLSearchParams): Params { - const validParams = {} as Params; - - // Validate 'action' parameter - const action = queryParams.get('action') ?? 'get'; - if (ALL_ACTIONS.includes(action as Action)) { - validParams.action = action; - } else { - validParams.error = `Invalid action '${action}'. Supported actions are: ${ALL_ACTIONS.join( - ', ', - )}`; - return validParams; - } - - const requiredParameters = { - store: [...ALL_ACTIONS], - key: ['get', 'zrange', 'zscan', 'bfExists'], - match: ['scan', 'zscan'], - min: ['zrange'], - max: ['zrange'], - item: ['bfExists'], - } as Record<ParamKey, Array<string>>; - - for (const [key, actions] of Object.entries(requiredParameters)) { - if (actions.includes(action)) { - const value = queryParams.get(key); - if (value && value !== '') { - validParams[key as ParamKey] = value; - } else { - validParams.error = `Query parameters must provide '${key}' for a '${action}' action.`; - return validParams; - } - } - } - - return validParams; -} - -export const decodeValueArray = (arrVal: ArrayBuffer | null) => { - if (arrVal) { - const decoder = new TextDecoder(); - return decoder.decode(arrVal); - } - return ''; -}; - -export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { - let strResponse = '['; - for (const tuple of tupleList) { - strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; - } - strResponse += ']'; - return strResponse; -}; -``` - -### FILE: examples/kv-store/package.json - -```json -{ - "name": "fastedge-example-kv-store", - "version": "1.0.0", - "description": "FastEdge JS example: KV Store operations via query params", - "type": "module", - "scripts": { - "build": "fastedge-build -c" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` - -### FILE: examples/kv-store/tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2023", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "lib": ["ES2023"], - "types": ["@gcoredev/fastedge-sdk-js"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/mcp-server-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/mcp-server-ts.md index 46c119b..54b0eef 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/mcp-server-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/mcp-server-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -400,3 +400,282 @@ The MCP server listens at `/mcp` on whatever domain the FastEdge app is deployed - Model Context Protocol specification (MCP HTTP transport, tool registration) - FastEdge build CLI reference (fastedge-build) - http-base blueprint (base HTTP worker skeleton) + +## Source Material + +### FILE: examples/mcp-server/src/index.ts + +```ts +import { StreamableHTTPTransport } from '@hono/mcp'; +import { Hono } from 'hono'; + +import server from './server.js'; + +const router = new Hono(); + +router.all('/mcp', async (c) => { + const transport = new StreamableHTTPTransport(); + await server.connect(transport); + return transport.handleRequest(c); +}); + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(router.fetch(event.request)); +}); +``` + + +### FILE: examples/mcp-server/src/server.ts + +```ts +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +import type { + AlertFeature, + AlertsResponse, + ForecastPeriod, + ForecastResponse, + PointsResponse, +} from './types.js'; + +const NWS_API_BASE = 'https://api.weather.gov'; +const USER_AGENT = 'weather-app/1.0'; + +// Helper function for making NWS API requests +async function makeNWSRequest<T>(url: string): Promise<T | null> { + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/geo+json', + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error('Error making NWS request:', error); + return null; + } +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || 'Unknown'}`, + `Area: ${props.areaDesc || 'Unknown'}`, + `Severity: ${props.severity || 'Unknown'}`, + `Status: ${props.status || 'Unknown'}`, + `Headline: ${props.headline || 'No headline'}`, + '---', + ].join('\n'); +} + +// Create server instance +const server = new McpServer({ + name: 'weather', + version: '1.0.0', +}); + +// Register weather tools +server.registerTool( + 'get-alerts', + { + title: 'Get Weather Alerts', + description: 'Get weather alerts for a US state', + inputSchema: z.object({ + state: z.string().length(2).describe('Two-letter state code (e.g. CA, NY)'), + }), + }, + async ({ state }) => { + const stateCode = state.toUpperCase(); + + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl); + + if (!alertsData) { + return { + content: [ + { + type: 'text', + text: 'Failed to retrieve alerts data', + }, + ], + }; + } + + const features = alertsData.features || []; + if (features.length === 0) { + return { + content: [ + { + type: 'text', + text: `No active alerts for ${stateCode}`, + }, + ], + }; + } + + const formattedAlerts = features.map(formatAlert); + const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`; + + return { + content: [ + { + type: 'text', + text: alertsText, + }, + ], + }; + }, +); + +server.registerTool( + 'get-forecast', + { + title: 'Get Weather Forecast', + description: 'Get weather forecast for a location', + inputSchema: z.object({ + latitude: z.number().min(-90).max(90).describe('Latitude of the location'), + longitude: z.number().min(-180).max(180).describe('Longitude of the location'), + }), + }, + async ({ latitude, longitude }) => { + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl); + + if (!pointsData) { + return { + content: [ + { + type: 'text', + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }, + ], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [ + { + type: 'text', + text: 'Failed to get forecast URL from grid point data', + }, + ], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl); + if (!forecastData) { + return { + content: [ + { + type: 'text', + text: 'Failed to retrieve forecast data', + }, + ], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No forecast periods available', + }, + ], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || 'Unknown'}:`, + `Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`, + `Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`, + `${period.shortForecast || 'No forecast available'}`, + '---', + ].join('\n'), + ); + + const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( + '\n', + )}`; + + return { + content: [ + { + type: 'text', + text: forecastText, + }, + ], + }; + }, +); + +export default server; +``` + + +### FILE: examples/mcp-server/package.json + +```json +{ + "name": "fastedge-example-mcp-server", + "version": "1.0.0", + "description": "Basic Example of running MCP Server on FastEdge", + "main": "src/index.ts", + "type": "module", + "bin": { + "weather": "./build/index.js" + }, + "scripts": { + "build": "npm run transpile && npm run build-wasm", + "build-wasm": "npx fastedge-build --input build/index.js --output build/weather.wasm --tsconfig tsconfig.json", + "transpile": "tsc" + }, + "files": [ + "build" + ], + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0", + "@hono/mcp": "^0.2.5", + "@modelcontextprotocol/sdk": "^1.29.0", + "hono": "^4.12.25", + "zod": "^4.3.6" + }, + "devDependencies": { + "typescript": "^5.9.2" + } +} +``` + + +### FILE: examples/mcp-server/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md index 871a96b..c1a13e7 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -127,90 +127,3 @@ Both errors propagate via `?` and result in a 500-class response from the FastEd - `outbound-modify-response` (WASI, Rust) — fetches upstream and reshapes the body into a new JSON response - `streaming` (WASI, Rust) — handler that generates its own streaming response without an upstream fetch - `outbound-fetch` (JavaScript) — mirror of this example in the FastEdge SDK JS - -## Source Material - -### FILE: examples/http/wasi/outbound_fetch/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Minimal outbound fetch example. - -Makes a GET request to an upstream HTTP origin and returns the upstream -response verbatim — status, headers, and body pass through unchanged. - -For a variant that reads and transforms the upstream body, see -`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. - -Mirror of the FastEdge-sdk-js `outbound-fetch` example. -*/ - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - // Return the upstream response verbatim. The body is passed through - // without calling `.contents()`, so it streams to the client as upstream - // produces it. - let (parts, body) = upstream_resp.into_parts(); - let mut response = Response::new(body); - *response.status_mut() = parts.status; - *response.headers_mut() = parts.headers; - Ok(response) -} -``` - - -### FILE: examples/http/wasi/outbound_fetch/Cargo.toml - -```toml -[workspace] - -[package] -name = "outbound_fetch" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/outbound_fetch/README.md - -``` -[← Back to examples](../../../README.md) - -# Outbound Fetch (WASI) - -Fetch data from an outbound HTTP origin and return the response directly — status, headers, -and body pass through unchanged. - -The body is never buffered (no `.contents().await`), so upstream chunks stream to the client -as they arrive. - -## Related - -- [outbound_modify_response](../outbound_modify_response/) — same fetch, but reads the body - and reshapes it into a new JSON response. -- [streaming](../streaming/) — a handler that generates its own streaming response body. -- Mirror of `FastEdge-sdk-js/examples/outbound-fetch/`. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-ts.md index 7fd769f..f802a94 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,49 @@ From `package.json`: - sdk-reference-js (fetch API, Response constructor, addEventListener) - deploy skill reference (uploading and registering the compiled WASM binary) - outbound-fetch feature blueprint (fetch without body transformation) + +## Source Material + +### FILE: examples/outbound-modify-response/src/index.js + +```js +async function app(event) { + const outboundResponse = await fetch('http://jsonplaceholder.typicode.com/users'); + const users = await outboundResponse.json(); + return new Response( + JSON.stringify({ + users: users.slice(0, 5), + total: 5, + skip: 0, + limit: 30, + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/outbound-modify-response/package.json + +```json +{ + "name": "fastedge-example-outbound-modify-response", + "version": "1.0.0", + "description": "FastEdge JS example: fetch and modify outbound response", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/outbound-modify-response.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md index fb8121f..9d4a66e 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -216,82 +216,3 @@ async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - outbound-fetch-wasi-rust (simpler variant — passes upstream response through unchanged) - http-base skeleton (base handler structure, `#[wstd::http_server]`, `Body`, `Request`, `Response`) - sdk-reference-rust (full `wstd` API surface) - -## Source Material - -### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Outbound fetch with response transformation. - -Fetches JSON from an upstream origin, reads and parses the body, reshapes it -into a new JSON object (first 5 users with pagination metadata), and returns -it with a fresh `content-type: application/json` header. - -This is the stepping-stone beyond `outbound_fetch/` which just passes the -upstream response through unchanged. - -Mirror of the FastEdge-sdk-js `outbound-modify-response` example. -*/ - -use anyhow::anyhow; -use serde_json::{Value, json}; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - let (_, mut body) = upstream_resp.into_parts(); - let body_bytes = body.contents().await?; - let users: Value = serde_json::from_slice(body_bytes)?; - - let sliced_users = match users.as_array() { - Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), - None => Value::Array(vec![]), - }; - - let result = json!({ - "users": sliced_users, - "total": 5, - "skip": 0, - "limit": 30, - }); - - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(Body::from(result.to_string()))?) -} -``` - -### FILE: examples/http/wasi/outbound_modify_response/Cargo.toml - -```toml -[workspace] - -[package] -name = "outbound_modify_response_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -serde_json = "1" -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rollover-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rollover-wasi-rust.md index c5cb2b8..7bbfb60 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -42,6 +42,15 @@ get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" Slot `0` serves as the baseline — always matched when no higher slot qualifies. +Usage as indices: +``` +get_effective_at("token-secret", 0) -> "original_password" +get_effective_at("token-secret", 3) -> "original_password" +get_effective_at("token-secret", 5) -> slot 5's value (if exists) +``` + +Usage as timestamps: a token's `iat` claim determines which password to validate against. `get_effective_at("token-secret", claims.iat)` returns the password that was effective when the token was issued. + ## API Reference ### `secret::get` @@ -158,6 +167,12 @@ let slot: u32 = request .as_secs() as u32 }); +let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + // Current (latest) value let current = secret::get(secret_name) .map_err(|e| anyhow!("secret::get failed: {e}"))?; @@ -165,59 +180,19 @@ let current = secret::get(secret_name) // Value effective at the given slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; -``` -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/secret_rollover.wasm +let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, +}); ``` -## See Also - -- fastedge::secret module reference (Rust SDK reference) -- http-base skeleton (base HTTP app structure) -- platform-overview (secret management configuration) -- best-practices (secret rotation strategies) - -## Source Material - -### FILE: examples/http/wasi/secret_rollover/src/lib.rs +## Full Source ```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Secret rollover example using slot-based secret retrieval. - -Demonstrates how to use `secret::get_effective_at()` with slots to support -secret rotation. Slots use a greatest matching rule: the slot with the highest -value that is <= the requested `effective_at` is returned. - -Example secret configuration: -{ - "secret": { - "name": "token-secret", - "secret_slots": [ - { "slot": 0, "value": "original_password" }, - { "slot": 1741790697, "value": "new_password" } - ] - } -} - -Usage as indices: - get_effective_at("token-secret", 0) -> "original_password" - get_effective_at("token-secret", 3) -> "original_password" - get_effective_at("token-secret", 5) -> slot 5's value (if exists) - -Usage as timestamps: - A token's `iat` claim determines which password to validate against. - get_effective_at("token-secret", claims.iat) returns the password - that was effective when the token was issued. -*/ - use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::anyhow; @@ -228,7 +203,6 @@ use wstd::http::{Request, Response}; #[wstd::http_server] async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { - // Read the slot from the x-slot header, defaulting to current timestamp let slot: u32 = request .headers() .get("x-slot") @@ -247,10 +221,8 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { .and_then(|v| v.to_str().ok()) .unwrap_or("TOKEN_SECRET"); - // Get the current secret value (latest slot) let current = secret::get(secret_name).map_err(|e| anyhow!("secret::get failed: {e}"))?; - // Get the secret effective at the requested slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; @@ -269,73 +241,6 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { } ``` - -### FILE: examples/http/wasi/secret_rollover/Cargo.toml - -```toml -[workspace] - -[package] -name = "secret_rollover" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -serde_json = "1" -``` - - -### FILE: examples/http/wasi/secret_rollover/README.md - -``` -[← Back to examples](../../../README.md) - -# Secret Rollover (WASI) - -Demonstrates slot-based secret retrieval for secret rotation scenarios using `secret::get_effective_at()`. - -Compares the current secret value with the value effective at a given slot, returning both as JSON. This lets you validate that rotation is working correctly before removing the old slot. - -## Usage - -| Request header | Default | Description | -|---|---|---| -| `x-secret-name` | `TOKEN_SECRET` | Name of the secret to query | -| `x-slot` | current Unix timestamp | Slot value passed to `get_effective_at` | - -## How slots work - -Slots use a **greatest-match rule**: the slot with the highest value that is `<= effective_at` is returned. - -``` -Secret slots: { 0: "old-password", 1741790697: "new-password" } - -get_effective_at("TOKEN_SECRET", 0) → "old-password" -get_effective_at("TOKEN_SECRET", 100) → "old-password" -get_effective_at("TOKEN_SECRET", 1741790697) → "new-password" -get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" -``` - -When used with token `iat` (issued-at) timestamps, `get_effective_at(name, claims.iat)` returns the password that was active when the token was issued — enabling zero-downtime rotation without invalidating existing tokens. - -## What it returns - -```json -{ - "secret_name": "TOKEN_SECRET", - "slot": 0, - "current": "new-password", - "effective_at_slot": "old-password", - "is_same": false -} -``` - ## Build ```sh @@ -343,8 +248,9 @@ cargo build --release # Output: target/wasm32-wasip2/release/secret_rollover.wasm ``` -## APIs used +## See Also -- `fastedge::secret::get(name)` — current (latest-slot) secret value; `Ok(Option<String>)` -- `fastedge::secret::get_effective_at(name, slot)` — secret value at a given slot; `Ok(Option<String>)` -``` +- fastedge::secret module reference (Rust SDK reference) +- http-base skeleton (base HTTP app structure) +- platform-overview (secret management configuration) +- best-practices (secret rotation strategies) diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-ts.md index 5fe2b27..8155e9b 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -73,6 +73,7 @@ function app(event) { const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 5; i++) { + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 200); }); controller.enqueue(encoder.encode(`chunk ${i}\n`)); } @@ -114,51 +115,3 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference - -## Source Material - -### FILE: examples/streaming/src/index.js - -```js -function app(event) { - const encoder = new TextEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { setTimeout(resolve, 200); }); - controller.enqueue(encoder.encode(`chunk ${i}\n`)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - - -### FILE: examples/streaming/package.json - -```json -{ - "name": "fastedge-example-streaming", - "version": "1.0.0", - "description": "FastEdge JS example: streaming response with ReadableStream", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/streaming.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md index 765e332..e011ba1 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -141,67 +141,3 @@ async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - FastEdge-sdk-rust HTTP examples (other HTTP feature blueprints) - wstd crate documentation (Runtime, Body, Timer APIs) - futures-lite crate documentation (stream combinators, unfold) - -## Source Material - -### FILE: examples/http/wasi/streaming/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Streaming response example. - -Generates a response body on the fly — five text chunks, one every 200ms — -using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime -polls the stream as the body is sent, so chunks flow to the client as they -are produced instead of all at once at the end. - -Watch it stream with `curl -N https://<app-url>/` (`-N` disables client-side -buffering). - -Mirror of the FastEdge-sdk-js `streaming` example. -*/ - -use futures_lite::stream; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; -use wstd::time::{Duration, Timer}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let chunk_stream = stream::unfold(0u32, |i| async move { - if i >= 5 { - return None; - } - Timer::after(Duration::from_millis(200)).wait().await; - Some((format!("chunk {i}\n"), i + 1)) - }); - - Ok(Response::builder() - .status(200) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from_stream(chunk_stream))?) -} -``` - - -### FILE: examples/http/wasi/streaming/Cargo.toml - -```toml -[workspace] - -[package] -name = "streaming_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -futures-lite = "1" -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ts.md index 43e1a3d..5d7002e 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -202,87 +202,3 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference - -## Source Material - -### FILE: examples/template-invoice/src/index.js - -```js -import Handlebars from 'handlebars'; - -import { cssStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { logoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler() { - const rawHtmlTemplate = htmlTemplate(); - - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logoBrand, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler()); -}); -``` - - -### FILE: examples/template-invoice/package.json - -```json -{ - "name": "fastedge-example-template-invoice", - "version": "1.0.0", - "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md index c06bc83..c23693c 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -135,89 +135,3 @@ cargo build --release - deploy skill reference (uploading the compiled `.wasm` binary) - manage skill reference (setting environment variables and secrets on an app) - FastEdge platform overview (secret storage model) - -## Source Material - -### FILE: examples/http/wasi/variables_and_secrets/src/lib.rs - -```rust -use fastedge::secret; -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let username = env::var("USERNAME").unwrap_or_default(); - let password = match secret::get("PASSWORD") { - Ok(Some(value)) => value, - _ => String::new(), - }; - - Ok(Response::builder() - .status(200) - .body(Body::from(format!( - "Username: {username}, Password: {password}" - )))?) -} -``` - - -### FILE: examples/http/wasi/variables_and_secrets/Cargo.toml - -```toml -[workspace] - -[package] -name = "variables_and_secrets" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/variables_and_secrets/README.md - -``` -[← Back to examples](../../../README.md) - -# Variables and Secrets (WASI) - -Demonstrates reading an environment variable (`USERNAME`) and a secret (`PASSWORD`), returning both in the response body. - -Environment variables are set via the FastEdge app configuration and accessed with `std::env::var`. Secrets are stored encrypted and accessed with `fastedge::secret::get` — they are never exposed in platform logs or configuration UIs. - -## Configuration - -| Key | Type | Required | Description | -|---|---|---|---| -| `USERNAME` | Environment variable | No | Username to include in response. Empty string if unset. | -| `PASSWORD` | Secret | No | Password to include in response. Empty string if unset or unavailable. | - -## What it returns - -``` -HTTP/1.1 200 OK - -Username: <USERNAME value>, Password: <PASSWORD value> -``` - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/variables_and_secrets.wasm -``` - -## APIs used - -- `std::env::var("USERNAME").unwrap_or_default()` — read env var with fallback -- `fastedge::secret::get("PASSWORD")` — read secret by name; returns `Ok(Some(String))` on success -``` diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/dotenv.md b/plugins/gcore-fastedge-codex/skills/test/reference/dotenv.md index 9088970..30b38d6 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/dotenv.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/dotenv.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Dotenv — Runtime Secrets and Environment Variables diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/runner-internals.md b/plugins/gcore-fastedge-codex/skills/test/reference/runner-internals.md index b75c2eb..5501551 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/runner-internals.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/runner-internals.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Runner Internals — Low-Level Runner API diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/server-api.md b/plugins/gcore-fastedge-codex/skills/test/reference/server-api.md index 1e7af77..057aa7a 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/server-api.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/server-api.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Server API — REST and WebSocket Endpoints diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/test-config.md b/plugins/gcore-fastedge-codex/skills/test/reference/test-config.md index 164f73f..af1c4cb 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/test-config.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/test-config.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # test-config Reference diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/test-framework.md b/plugins/gcore-fastedge-codex/skills/test/reference/test-framework.md index 3c928e7..b65dfde 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/test-framework.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/test-framework.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # FastEdge Test Framework API diff --git a/plugins/gcore-fastedge-codex/skills/test/reference/vscode-debugger.md b/plugins/gcore-fastedge-codex/skills/test/reference/vscode-debugger.md index 457d95d..28df592 100644 --- a/plugins/gcore-fastedge-codex/skills/test/reference/vscode-debugger.md +++ b/plugins/gcore-fastedge-codex/skills/test/reference/vscode-debugger.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # FastEdge Visual Debugger diff --git a/plugins/gcore-fastedge-cursor/.cursor-plugin/plugin.json b/plugins/gcore-fastedge-cursor/.cursor-plugin/plugin.json index bfc9e8d..ed7ccd5 100644 --- a/plugins/gcore-fastedge-cursor/.cursor-plugin/plugin.json +++ b/plugins/gcore-fastedge-cursor/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "gcore-fastedge", "displayName": "Gcore FastEdge", "description": "Build, deploy, and manage serverless WebAssembly applications on Gcore FastEdge — Wasm compute on 210+ global edge PoPs.", - "version": "0.2.5", + "version": "0.2.6", "author": { "name": "Gcore", "email": "support@gcore.com", diff --git a/plugins/gcore-fastedge-cursor/docs-index.json b/plugins/gcore-fastedge-cursor/docs-index.json index 2f223e6..369f101 100644 --- a/plugins/gcore-fastedge-cursor/docs-index.json +++ b/plugins/gcore-fastedge-cursor/docs-index.json @@ -1,9 +1,9 @@ { "schema_version": "1.0.0", - "generated_at": "2026-07-24T07:50:35.350Z", + "generated_at": "2026-08-18T07:33:55.154Z", "source": { "repo": "fastedge-plugin", - "commit": "05c2493", + "commit": "067d44e", "pipeline": "sync-reference-docs" }, "topics": [ @@ -532,7 +532,7 @@ { "id": "cdn-examples-ab-testing-as", "title": "A/B Testing — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md", "skill": "fastedge-docs", "category": "examples", @@ -554,9 +554,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "cookie-based", "traffic", @@ -566,7 +566,7 @@ "requests", "different" ], - "content_sha256": "419863edee5fea328eaa0d29bec26e3e2baab36e05c21d29fe5d8837fb0a4c9b", + "content_sha256": "9546c33b6d3c8657538a75f442e7738541cdad753fc7b88a1cd759cc036629f0", "sections": [ { "id": "cdn-examples-ab-testing-as#introduction", @@ -583,9 +583,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "assemblyscript", "cdn", @@ -694,7 +694,7 @@ "level": 2, "anchor": "request-flow-—-onrequestheaders", "line_start": 44, - "line_end": 118, + "line_end": 120, "keywords": [ "request", "flow", @@ -729,8 +729,8 @@ "heading": "Response Flow — `onResponseHeaders`", "level": 2, "anchor": "response-flow-—-onresponseheaders", - "line_start": 119, - "line_end": 151, + "line_start": 121, + "line_end": 153, "keywords": [ "response", "flow", @@ -773,8 +773,8 @@ "heading": "API Surface", "level": 2, "anchor": "api-surface", - "line_start": 152, - "line_end": 169, + "line_start": 154, + "line_end": 171, "keywords": [ "api", "surface", @@ -822,8 +822,8 @@ "heading": "Cookie Convention", "level": 2, "anchor": "cookie-convention", - "line_start": 170, - "line_end": 181, + "line_start": 172, + "line_end": 183, "keywords": [ "cookie", "convention", @@ -846,8 +846,8 @@ "heading": "Error Conditions", "level": 2, "anchor": "error-conditions", - "line_start": 182, - "line_end": 192, + "line_start": 184, + "line_end": 194, "keywords": [ "error", "conditions", @@ -884,8 +884,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 193, - "line_end": 208, + "line_start": 195, + "line_end": 210, "keywords": [ "build", "pnpm", @@ -917,8 +917,8 @@ "heading": "AssemblyScript-Specific Constraints", "level": 2, "anchor": "assemblyscript-specific-constraints", - "line_start": 209, - "line_end": 218, + "line_start": 211, + "line_end": 220, "keywords": [ "assemblyscript-specific", "constraints", @@ -967,8 +967,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 219, - "line_end": 225, + "line_start": 221, + "line_end": 227, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -1000,7 +1000,7 @@ { "id": "cdn-examples-ab-testing-rust", "title": "A/B Testing — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1024,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1034,7 +1034,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "e0967ff8a5a76ceb6efaaf12499f28d2197799098608bbdbe5e3637b1b9e8495", + "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1053,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1650,13 +1650,63 @@ "structure", "examples-a" ] + }, + { + "id": "cdn-examples-ab-testing-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 252, + "line_end": 439, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "ab_testing", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "traffic", + "splitting", + "layer.", + "uses", + "cookie", + "assign", + "users", + "variant", + "rewrites", + "request", + "path", + "route", + "different", + "parts", + "origin", + "server.", + "required", + "configuration", + "environment", + "variable", + "experiment_name", + "variant_a_path", + "prefix" + ] } ] }, { "id": "cdn-examples-api-key-as", "title": "CDN Example: API Key Authentication (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1678,9 +1728,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -1690,7 +1740,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "144feff24d4893ee721a26628b201437fd48d6c34b14d465c821cab5cff05f07", + "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1707,9 +1757,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -2064,6 +2114,176 @@ "deployment", "guide" ] + }, + { + "id": "cdn-examples-api-key-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 203, + "line_end": 326, + "keywords": [ + "source", + "material", + "file", + "examples", + "apikey", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "getsecret", + "setloglevel", + "fastedge", + "const", + "unauthorized", + "u32", + "401", + "forbidd" + ] + }, + { + "id": "cdn-examples-api-key-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 327, + "line_end": 340, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "expected", + "api", + "key", + "api_key", + "secret.", + "checks", + "x-api-key", + "request", + "header.", + "returns", + "401", + "unauthorized", + "header", + "missing.", + "403", + "forbidden", + "match.", + "success", + "clears", + "forwarding", + "upstream", + "origin", + "proxy-wasm", + ".remove", + "sets", + "value", + "empty", + "string", + "rather", + "deleting", + "simpler", + "alternative", + "jwt", + "validation", + "you" + ] + }, + { + "id": "cdn-examples-api-key-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 341, + "line_end": 348, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "api_key", + "secret", + "expected", + "api", + "key", + "value" + ] + }, + { + "id": "cdn-examples-api-key-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 349, + "line_end": 362, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "apikey.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "apikey-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-api-key-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 363, + "line_end": 367, + "keywords": [ + "deploy", + "upload", + "build", + "apikey.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "api_key", + "secret", + "application", + "settings." + ] } ] }, @@ -2374,7 +2594,7 @@ { "id": "cdn-examples-auth-jwt-as", "title": "JWT Validation — CDN App (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md", "skill": "fastedge-docs", "category": "examples", @@ -2398,9 +2618,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2411,7 +2631,7 @@ "example", "app_type" ], - "content_sha256": "6facd62118f8f4e03232cb0574acfa59cfdf3cd113a070d7d46847c6e62084dd", + "content_sha256": "c9792a91f6b66ee092a92fc63a8c0a961358dc9ee9cd2b42f8e0d5a9791869d5", "sections": [ { "id": "cdn-examples-auth-jwt-as#introduction", @@ -2428,9 +2648,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2520,7 +2740,7 @@ "level": 2, "anchor": "key-apis", "line_start": 57, - "line_end": 115, + "line_end": 116, "keywords": [ "key", "apis", @@ -2546,6 +2766,8 @@ "pass", "directly", "jwtverify", + "without", + "encoding", "typescript", "const", "send_http_response", @@ -2553,7 +2775,7 @@ "internal", "server", "error", - "string.utf8.encode" + "strin" ] }, { @@ -2561,8 +2783,8 @@ "heading": "Validation Flow", "level": 2, "anchor": "validation-flow", - "line_start": 116, - "line_end": 131, + "line_start": 117, + "line_end": 132, "keywords": [ "validation", "flow", @@ -2602,8 +2824,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 132, - "line_end": 146, + "line_start": 133, + "line_end": 147, "keywords": [ "error", "responses", @@ -2640,8 +2862,8 @@ "heading": "Required Secret", "level": 2, "anchor": "required-secret", - "line_start": 147, - "line_end": 156, + "line_start": 148, + "line_end": 157, "keywords": [ "required", "secret", @@ -2663,19 +2885,48 @@ "application", "deployment.", "rotation", - "see", + "use", "getsecreteffectiveat", + "instead", + "getsecret", + "see", "secrets", "reference." ] }, + { + "id": "cdn-examples-auth-jwt-as#testing-tokens", + "heading": "Testing Tokens", + "level": 2, + "anchor": "testing-tokens", + "line_start": 158, + "line_end": 173, + "keywords": [ + "testing", + "tokens", + "use", + "secret", + "a-string-secret-at-least-256-bits-long-thats-hard-to-break", + "expired", + "token", + "returns", + "403", + "forbidden", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiylcjlehaiojk3odmxmdg2mx0.egssdoddahz8kqee7be9n168cdewoioej96idm2c1yq", + "valid", + "expiry", + "2035-01-01", + "200", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte" + ] + }, { "id": "cdn-examples-auth-jwt-as#dependencies", "heading": "Dependencies", "level": 2, "anchor": "dependencies", - "line_start": 157, - "line_end": 171, + "line_start": 174, + "line_end": 196, "keywords": [ "dependencies", "json", @@ -2695,6 +2946,11 @@ "directly", "used", "example.", + "dev", + "assemblyscript", + "wasi-shim", + "0.1.0", + "0.28.9", "---" ] }, @@ -2703,8 +2959,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 172, - "line_end": 190, + "line_start": 197, + "line_end": 215, "keywords": [ "build", "pnpm", @@ -2738,8 +2994,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 191, - "line_end": 202, + "line_start": 216, + "line_end": 227, "keywords": [ "registration", "typescript", @@ -2761,8 +3017,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 203, - "line_end": 214, + "line_start": 228, + "line_end": 240, "keywords": [ "gotchas", "secret", @@ -2811,8 +3067,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 215, - "line_end": 221, + "line_start": 241, + "line_end": 247, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -3396,7 +3652,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3418,9 +3674,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3430,7 +3686,7 @@ "app_type", "languages" ], - "content_sha256": "131d3cb75bfb089b33860c1da8965a6ff2dbf7ba478f8e12cdb37384d5412f3e", + "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3447,9 +3703,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3876,6 +4132,139 @@ "body", "buffering" ] + }, + { + "id": "cdn-examples-body-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 341, + "line_end": 519, + "keywords": [ + "source", + "material", + "file", + "examples", + "body", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "buffertypevalues", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_buffer_bytes", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "set_property", + "stream_context" + ] + }, + { + "id": "cdn-examples-body-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 520, + "line_end": 534, + "keywords": [ + "onrequestheaders", + "removes", + "content-length", + "header", + "required", + "because", + "body", + "content", + "altered.", + "onrequestbody", + "buffers", + "full", + "request", + "checks", + "whether", + "contains", + "word", + "client", + "found", + "replaced", + "redaction", + "notice.", + "onresponseheaders", + "sets", + "transfer-encoding", + "chunked", + "captures", + "content-type", + "runtime", + "property.", + "onresponsebody", + "logs", + "url" + ] + }, + { + "id": "cdn-examples-body-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 535, + "line_end": 548, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "-----------------------", + "--------------------------------------------------", + "body.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "body-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-body-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 549, + "line_end": 553, + "keywords": [ + "deploy", + "upload", + "build", + "body.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application." + ] } ] }, @@ -4338,7 +4727,7 @@ { "id": "cdn-examples-cache-control-as", "title": "Cache Control — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md", "skill": "fastedge-docs", "category": "examples", @@ -4362,9 +4751,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4374,7 +4763,7 @@ "cache-control", "response-headers" ], - "content_sha256": "a088ac66592e155e209debe7d4c503dbc1cd638a3570ff06890f35c6258c1fa8", + "content_sha256": "1addef442e8b046f2689030c6ded25b7bd7ce8c303a7d46d5249db12d9955e1f", "sections": [ { "id": "cdn-examples-cache-control-as#introduction", @@ -4391,9 +4780,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4784,6 +5173,184 @@ "variable", "patterns" ] + }, + { + "id": "cdn-examples-cache-control-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 232, + "line_end": 376, + "keywords": [ + "source", + "material", + "file", + "examples", + "cachecontrol", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "cachecontrolroot", + "extends", + "createcontext", + "context_id", + "u32" + ] + }, + { + "id": "cdn-examples-cache-control-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 377, + "line_end": 390, + "keywords": [ + "onresponseheaders", + "app", + "inspects", + "content-type", + "response.status", + "apply", + "appropriate", + "caching", + "policy", + "content", + "type", + "cache", + "default", + "max-age", + "---", + "images", + "fonts", + "css", + "wasm", + "public", + "static_max_age", + "immutable", + "year", + "31536000s", + "text", + "html", + "html_max_age", + "must-revalidate", + "hour", + "3600s", + "application", + "json", + "xml", + "private", + "api_max_age", + "no-cache", + "no-" + ] + }, + { + "id": "cdn-examples-cache-control-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 391, + "line_end": 400, + "keywords": [ + "configuration", + "environment", + "variables", + "optional", + "sensible", + "defaults", + "applied", + "unset.", + "variable", + "default", + "description", + "----------", + "---------", + "-------------", + "static_max_age", + "31536000", + "max-age", + "static", + "assets", + "seconds", + "html_max_age", + "3600", + "html", + "responses", + "api_max_age", + "api", + "no-cache" + ] + }, + { + "id": "cdn-examples-cache-control-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 401, + "line_end": 414, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cachecontrol.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cachecontrol-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cache-control-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 415, + "line_end": 419, + "keywords": [ + "deploy", + "upload", + "build", + "cachecontrol.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "optionally", + "configure", + "max-age", + "environment", + "variables." + ] } ] }, @@ -5376,7 +5943,7 @@ { "id": "cdn-examples-convert-image-rust", "title": "CDN Example: Convert Image (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5400,7 +5967,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "example", "convert", "image", @@ -5410,7 +5977,7 @@ "avif", "format" ], - "content_sha256": "b2e6e63bcb77be6336941490ac3a834292ab5f939ac981b1d4d2e13942ece297", + "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5429,7 +5996,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "cdn", "example", "convert", @@ -5894,129 +6461,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-convert-image-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 206, - "line_end": 488, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "convert_image", - "src", - "lib.rs", - "rust", - "use", - "image", - "proxy_wasm", - "traits", - "types", - "std", - "env", - "varerror", - "cursor", - "str", - "from_utf8", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "convertimageroot", - "struct", - "impl", - "context", - "get_type", - "self" - ] - }, - { - "id": "cdn-examples-convert-image-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 489, - "line_end": 495, - "keywords": [ - "configuration", - "environment", - "variable", - "formats_to_transform", - "comma-separated", - "list", - "file", - "extensions", - "convert", - "e.g.", - "jpg", - "jpeg", - "png", - "ignored_ua_list", - "optional", - "user-agent", - "substrings", - "skip", - "avif_speed", - "avif", - "encoding", - "speed", - "1-10", - "default", - "avif_quality", - "quality", - "1-100" - ] - }, - { - "id": "cdn-examples-convert-image-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 496, - "line_end": 502, - "keywords": [ - "works", - "on_request_headers", - "checks", - "file", - "extension", - "user-agent", - "sets", - "image-format", - "header", - "cache", - "variation", - "on_response_headers", - "response", - "headers", - "avif", - "content", - "type", - "200", - "responses", - "on_response_body", - "decodes", - "original", - "image", - "re-encodes" - ] } ] }, { "id": "cdn-examples-cors-as", "title": "CORS — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6038,9 +6489,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "response-header", "injection", @@ -6050,7 +6501,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "9d28e66e98ae483455490d57761e0994771a24b3e869097c66a3190591c02e2e", + "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6067,9 +6518,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "assemblyscript", "cdn", @@ -6449,6 +6900,189 @@ "vary", "usage" ] + }, + { + "id": "cdn-examples-cors-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 197, + "line_end": 318, + "keywords": [ + "source", + "material", + "file", + "examples", + "cors", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "corsroot", + "extends", + "createcontext", + "context_id", + "u32", + "loglev" + ] + }, + { + "id": "cdn-examples-cors-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 319, + "line_end": 324, + "keywords": [ + "onresponseheaders", + "requests", + "allowed", + "origins", + "app", + "adds", + "access-control-allow-origin", + "vary", + "origin", + "response", + "headers.", + "optionally", + "exposes", + "additional", + "headers", + "via", + "access-control-expose-headers", + "disallowed", + "pass", + "unchanged", + "cors", + "added", + "note", + "options", + "preflights", + "fastedge", + "edge", + "layer", + "answers", + "preflight", + "directly", + "proxy-wasm", + "hooks", + "fire", + "options.", + "configure", + "behaviour", + "meth" + ] + }, + { + "id": "cdn-examples-cors-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 325, + "line_end": 333, + "keywords": [ + "configuration", + "set", + "following", + "environment", + "variables", + "your", + "fastedge", + "application", + "variable", + "example", + "description", + "----------", + "---------", + "-------------", + "allowed_origins", + "https", + "example.com", + "app.example.com", + "comma-separated", + "allowed", + "origins", + "required", + "expose_headers", + "x-request-id", + "x-trace-id", + "response", + "headers", + "expose", + "browser", + "optional" + ] + }, + { + "id": "cdn-examples-cors-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 334, + "line_end": 347, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cors.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cors-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cors-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 348, + "line_end": 352, + "keywords": [ + "deploy", + "upload", + "build", + "cors.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "allowed_origins", + "environment", + "variable", + "application", + "settings." + ] } ] }, @@ -7009,7 +7643,7 @@ { "id": "cdn-examples-custom-error-pages-as", "title": "Custom Error Pages — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7031,9 +7665,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7043,7 +7677,7 @@ "response-headers", "response-body" ], - "content_sha256": "bc71a12495f399f47434a5c8529feafb96e975dc762a1cd99a49ea8b8df2465f", + "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7060,9 +7694,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7248,7 +7882,7 @@ "level": 2, "anchor": "error-code-coverage", "line_start": 154, - "line_end": 182, + "line_end": 200, "keywords": [ "error", "code", @@ -7283,13 +7917,10 @@ "unavailable", "504", "5xx", - "categories", - "range", - "category", - "label", - "499", - "client", - "599" + "descriptions", + "description", + "understand", + "due" ] }, { @@ -7297,8 +7928,8 @@ "heading": "HTML Output Structure", "level": 2, "anchor": "html-output-structure", - "line_start": 183, - "line_end": 210, + "line_start": 201, + "line_end": 228, "keywords": [ "html", "output", @@ -7339,8 +7970,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 211, - "line_end": 229, + "line_start": 229, + "line_end": 247, "keywords": [ "build", "pnpm", @@ -7373,8 +8004,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 230, - "line_end": 235, + "line_start": 248, + "line_end": 253, "keywords": [ "deploy", "upload", @@ -7397,8 +8028,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 236, - "line_end": 248, + "line_start": 254, + "line_end": 266, "keywords": [ "constraints", "gotchas", @@ -7442,8 +8073,8 @@ "heading": "Imports", "level": 2, "anchor": "imports", - "line_start": 249, - "line_end": 270, + "line_start": 267, + "line_end": 288, "keywords": [ "imports", "import", @@ -7473,8 +8104,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 271, - "line_end": 277, + "line_start": 289, + "line_end": 295, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -7501,7 +8132,7 @@ { "id": "cdn-examples-custom-error-pages-rust", "title": "cdn-examples-custom-error-pages-rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7525,7 +8156,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "custom", "error", "pages", @@ -7535,7 +8166,7 @@ "5xx", "http" ], - "content_sha256": "e2e516ccf39ab64b58e9c2111f315294d4ae02c35a28c5ae42705b56fbce70e1", + "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", "sections": [ { "id": "cdn-examples-custom-error-pages-rust#introduction", @@ -7554,7 +8185,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -7563,7 +8194,7 @@ "level": 2, "anchor": "custom-error-pages-—-cdn-rust", "line_start": 10, - "line_end": 242, + "line_end": 297, "keywords": [ "custom", "error", @@ -7595,16 +8226,16 @@ "regardless", "origin", "returns.", - "---", - "api", - "patterns", - "logic", + "build", + "script", + "build.rs", "runs", - "inside", - "proxy_wasm", - "traits", - "httpcontext", - "implementation." + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] @@ -7612,7 +8243,7 @@ { "id": "cdn-examples-custom-rust", "title": "CDN Example: Custom — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7636,7 +8267,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -7646,7 +8277,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "45a2197a3910b5dbb1f0873d4726f360fe12ced198feef50a806e0d3c08505d9", + "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -7665,7 +8296,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -8047,13 +8678,59 @@ "overview", "platform-overview" ] + }, + { + "id": "cdn-examples-custom-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 373, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "custom", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "const", + "bad_request", + "u32", + "400", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id" + ] } ] }, { "id": "cdn-examples-env-secrets-as", "title": "Environment Variables and Secrets — CDN (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md", "skill": "fastedge-docs", "category": "examples", @@ -8075,9 +8752,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8088,7 +8765,7 @@ "secrets", "environment" ], - "content_sha256": "34d6cb6344af07831efe048bd621e270ce2ad9377a1645d26b883ede493e6cb5", + "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8105,9 +8782,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8518,6 +9195,221 @@ "handling", "guidelines" ] + }, + { + "id": "cdn-examples-env-secrets-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 229, + "line_end": 313, + "keywords": [ + "source", + "material", + "file", + "examples", + "variablesandsecrets", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "getsecret", + "setloglevel", + "fastedge" + ] + }, + { + "id": "cdn-examples-env-secrets-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 314, + "line_end": 326, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "username", + "environment", + "variable", + "using", + "getenv", + "password", + "secret", + "getsecret", + "logs", + "values", + "retrieved", + "without", + "logging", + "value", + "itself", + "injects", + "them", + "x-env-username", + "x-env-password", + "request", + "headers", + "upstream", + "receives", + "them.", + "useful", + "reference", + "understanding", + "access", + "variables", + "secrets", + "within", + "fastedge", + "plugin.", + "security", + "warning", + "neve" + ] + }, + { + "id": "cdn-examples-env-secrets-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 327, + "line_end": 335, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "----------", + "--------------------", + "--------------------------------------", + "username", + "environment", + "variable", + "value", + "forward", + "upstream", + "password", + "secret" + ] + }, + { + "id": "cdn-examples-env-secrets-as#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 336, + "line_end": 346, + "keywords": [ + "local", + "testing", + "fixture", + "fixtures", + "happy-path.test.json", + "uses", + "dotenv", + "enabled", + "true", + "load", + "values", + ".env", + "runner", + "maps", + "fastedge_var_env_", + "name", + "getenv", + "fastedge_var_secret_", + "getsecret", + "test", + "locally", + "visual", + "debugger", + "create", + "fastedge_var_env_username", + "my-username", + "fastedge_var_secret_password", + "my-password" + ] + }, + { + "id": "cdn-examples-env-secrets-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 347, + "line_end": 360, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "--------------------------------------", + "--------------------------------------------------", + "variablesandsecrets.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "variablesandsecrets-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-env-secrets-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 361, + "line_end": 365, + "keywords": [ + "deploy", + "upload", + "build", + "variablesandsecrets.wasm", + "fastedge", + "portal", + "https", + "portal.gcore.com", + "attach", + "your", + "cdn", + "application.", + "configure", + "username", + "environment", + "variable", + "password", + "secret", + "application", + "settings." + ] } ] }, @@ -11210,7 +12102,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11234,9 +12126,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11246,7 +12138,7 @@ "headers", "request-headers" ], - "content_sha256": "ce0dbc8a8e9aa6790dfde6e27b5a069105063a9725c0593334e625084387b9f7", + "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11263,9 +12155,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11572,13 +12464,82 @@ "hea" ] }, + { + "id": "cdn-examples-headers-as#multi-value-headers", + "heading": "Multi-Value Headers", + "level": 2, + "anchor": "multi-value-headers", + "line_start": 202, + "line_end": 205, + "keywords": [ + "multi-value", + "headers", + "new-header-03", + "deliberately", + "added", + "twice", + "add", + "called", + "same", + "name", + "twice.", + "produces", + "header", + "two", + "separate", + "entries.", + "validation", + "pattern", + "uses", + "set", + "string", + "value", + "pairs", + "assert", + "values", + "present." + ] + }, + { + "id": "cdn-examples-headers-as#cross-phase-response-header-writes", + "heading": "Cross-Phase Response Header Writes", + "level": 2, + "anchor": "cross-phase-response-header-writes", + "line_start": 206, + "line_end": 212, + "keywords": [ + "cross-phase", + "response", + "header", + "writes", + "stream_context.headers.response.add", + "...", + "called", + "onrequestheaders", + "headers", + "written", + "request", + "phase", + "appear", + "final", + "alongside", + "set", + "onresponseheaders", + "distinction", + "new-response-header", + "value-01", + "safe", + "stream_context.headers.response.get", + "panics" + ] + }, { "id": "cdn-examples-headers-as#error-response-codes-used", "heading": "Error Response Codes Used", "level": 2, "anchor": "error-response-codes-used", - "line_start": 202, - "line_end": 211, + "line_start": 213, + "line_end": 222, "keywords": [ "error", "response", @@ -11611,8 +12572,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 212, - "line_end": 235, + "line_start": 223, + "line_end": 246, "keywords": [ "logging", "log", @@ -11651,8 +12612,8 @@ "heading": "Known Issues", "level": 2, "anchor": "known-issues", - "line_start": 236, - "line_end": 245, + "line_start": 247, + "line_end": 254, "keywords": [ "known", "issues", @@ -11694,8 +12655,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 246, - "line_end": 265, + "line_start": 255, + "line_end": 274, "keywords": [ "build", "pnpm", @@ -11730,8 +12691,8 @@ "heading": "Deployment", "level": 2, "anchor": "deployment", - "line_start": 266, - "line_end": 269, + "line_start": 275, + "line_end": 278, "keywords": [ "deployment", "upload", @@ -11752,8 +12713,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 270, - "line_end": 276, + "line_start": 279, + "line_end": 285, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -11774,7 +12735,7 @@ { "id": "cdn-examples-headers-rust", "title": "Headers — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -11798,7 +12759,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -11808,7 +12769,7 @@ "headers", "request-headers" ], - "content_sha256": "9b8fa82906d3a7e8498f68496bb247ca4f942000fddfef9a2e4bf8b65faf16cc", + "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -11827,7 +12788,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -12167,13 +13128,58 @@ "abi", "surface" ] + }, + { + "id": "cdn-examples-headers-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 234, + "line_end": 586, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "headers", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "collections", + "hashset", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "context_id", + "u32", + "option", + "httpcont" + ] } ] }, { "id": "cdn-examples-http-call-as", "title": "cdn-examples-http-call-as", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12197,9 +13203,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "overview", "httpcall", "dispatches", @@ -12210,7 +13216,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "11691976691add5d7914e80eff68a71b36c33bc8d12beac13c0bbacdb1576e55", + "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12227,9 +13233,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20" + "2026-08-17" ] }, { @@ -12328,7 +13334,7 @@ "level": 2, "anchor": "common-patterns", "line_start": 102, - "line_end": 262, + "line_end": 286, "keywords": [ "common", "patterns", @@ -12360,13 +13366,60 @@ "proxy-wasm-sdk" ] }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 287, + "line_end": 306, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps", + "dependencies", + "package.json", + "package", + "role", + "---------", + "gcoredev", + "proxy-wasm-sdk-as", + "sdk", + "required", + "runtime", + "dep", + "1.2.3", + "assemblyscript", + "compiler", + "0.28.9", + "wasi-shim", + "wasi", + "compatibility" + ] + }, { "id": "cdn-examples-http-call-as#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 263, - "line_end": 273, + "line_start": 307, + "line_end": 318, "keywords": [ "gotchas", "closures", @@ -12410,8 +13463,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 274, - "line_end": 279, + "line_start": 319, + "line_end": 324, "keywords": [ "related", "sdk", @@ -12460,7 +13513,7 @@ { "id": "cdn-examples-http-call-rust", "title": "HTTP Call — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12484,7 +13537,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "makes", @@ -12494,7 +13547,7 @@ "services", "timeout" ], - "content_sha256": "a877fd720fb921fd1b8c38d69e81717621c2c7ffa28ee41f98bbded108fea44c", + "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -12513,7 +13566,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "cdn", @@ -12909,13 +13962,54 @@ "resume_http_request" ] }, + { + "id": "cdn-examples-http-call-rust#complete-example", + "heading": "Complete Example", + "level": 2, + "anchor": "complete-example", + "line_start": 247, + "line_end": 359, + "keywords": [ + "complete", + "example", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id", + "u32", + "option", + "httpcontext", + "httpheaders", + "sta" + ] + }, { "id": "cdn-examples-http-call-rust#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 247, - "line_end": 257, + "line_start": 360, + "line_end": 370, "keywords": [ "gotchas", "action", @@ -12964,8 +14058,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 258, - "line_end": 264, + "line_start": 371, + "line_end": 377, "keywords": [ "see", "proxy-wasm", @@ -12986,58 +14080,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-http-call-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 265, - "line_end": 408, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "http_call", - "src", - "lib.rs", - "rust", - "use", - "proxy_wasm", - "traits", - "types", - "std", - "time", - "duration", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "httpheadersroot", - "struct", - "impl", - "context", - "create_http_context", - "self", - "_context_id", - "u32", - "option", - "httpcontext" - ] } ] }, { "id": "cdn-examples-kv-store-as", "title": "KV Store — AssemblyScript CDN Example", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13061,9 +14110,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13073,7 +14122,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "5818392d7f01341518fb38b62a6267eece48af77d2334793bc92cda935fb596a", + "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -13090,9 +14139,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13631,7 +14680,7 @@ "level": 2, "anchor": "gotchas", "line_start": 316, - "line_end": 326, + "line_end": 328, "keywords": [ "gotchas", "kvstore.get", @@ -13675,8 +14724,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 327, - "line_end": 332, + "line_start": 329, + "line_end": 334, "keywords": [ "see", "kvstore", @@ -15332,7 +16381,7 @@ { "id": "cdn-examples-md2html-rust", "title": "CDN Example: Markdown to HTML (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15356,7 +16405,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15366,7 +16415,7 @@ "response-body-transform", "request-path-rewrite" ], - "content_sha256": "e22ac7622cc26345ca2016244b7a82727792c8c2294d27f9cfa4c87008fafd64", + "content_sha256": "796fc091efb4397c28b107d9b27eda72793c58776c4ecb1f23300362f6b68f86", "sections": [ { "id": "cdn-examples-md2html-rust#introduction", @@ -15385,7 +16434,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15814,7 +16863,7 @@ { "id": "cdn-examples-properties-as", "title": "CDN Runtime Properties — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-properties-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15836,9 +16885,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "runtime", "properties", "language", @@ -15848,7 +16897,7 @@ "type", "overview" ], - "content_sha256": "92e45ac0e88b8037fef119fc2a3a3010b2409de712b9b40a8a1db75a1b75b5a4", + "content_sha256": "487273a2f72dca3787758f1b60af9110a2c173879ba02842d23c21af59930534", "sections": [ { "id": "cdn-examples-properties-as#introduction", @@ -15865,9 +16914,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cdn", "runtime", "properties", @@ -16051,7 +17100,7 @@ "level": 2, "anchor": "usage-patterns", "line_start": 86, - "line_end": 138, + "line_end": 147, "keywords": [ "usage", "patterns", @@ -16088,8 +17137,8 @@ "heading": "Complete Example Structure", "level": 2, "anchor": "complete-example-structure", - "line_start": 139, - "line_end": 236, + "line_start": 148, + "line_end": 314, "keywords": [ "complete", "example", @@ -16127,8 +17176,8 @@ "heading": "Error Handling Pattern", "level": 2, "anchor": "error-handling-pattern", - "line_start": 237, - "line_end": 268, + "line_start": 315, + "line_end": 346, "keywords": [ "error", "handling", @@ -16173,8 +17222,8 @@ "heading": "Expected Output", "level": 2, "anchor": "expected-output", - "line_start": 269, - "line_end": 288, + "line_start": 347, + "line_end": 366, "keywords": [ "expected", "output", @@ -16211,8 +17260,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 312, + "line_start": 367, + "line_end": 390, "keywords": [ "build", "pnpm", @@ -16254,8 +17303,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 313, - "line_end": 318, + "line_start": 391, + "line_end": 396, "keywords": [ "deploy", "upload", @@ -16277,8 +17326,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 319, - "line_end": 331, + "line_start": 397, + "line_end": 410, "keywords": [ "gotchas", "get_property", @@ -16327,8 +17376,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 332, - "line_end": 338, + "line_start": 411, + "line_end": 417, "keywords": [ "see", "cdn", @@ -17091,7 +18140,7 @@ { "id": "dotenv", "title": "Dotenv — Runtime Secrets and Environment Variables", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/dotenv.md", "skill": "test", "category": "testing", @@ -17108,11 +18157,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17123,7 +18172,7 @@ "debugger", "inject" ], - "content_sha256": "cae196f2afaa09f7a84db329df3c77208afb1e4e206ad0e023cd22fef12f6491", + "content_sha256": "c7c39cf06d60e6c2cfb502b636db8a85f3ad05d79ed5acbcc9ded2e54390e567", "sections": [ { "id": "dotenv#introduction", @@ -17138,11 +18187,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17708,7 +18757,7 @@ { "id": "http-examples-ab-testing-js", "title": "A/B Testing — FastEdge Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -17732,7 +18781,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example", @@ -17743,7 +18792,7 @@ "visitors", "test" ], - "content_sha256": "e840de6f715610ad3709f719b391b7dab00230196769178c24e05dd501f87a85", + "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -17762,7 +18811,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example" @@ -18227,6 +19276,47 @@ "keys", "vary" ] + }, + { + "id": "http-examples-ab-testing-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 211, + "line_end": 328, + "keywords": [ + "source", + "material", + "file", + "examples", + "ab-testing", + "src", + "index.js", + "import", + "getenv", + "fastedge", + "env", + "const", + "testconfig", + "logo", + "variant", + "hops", + "weight", + "bottle", + "font", + "exo2", + "gloria", + "standard", + "async", + "function", + "eventhandler", + "request", + "xid", + "slicedheaders", + "sliceabtestidfromcookie", + "headers", + "createabtesthe" + ] } ] }, @@ -19695,7 +20785,7 @@ { "id": "http-examples-backend-basic-rust", "title": "Example: Backend (URL Proxy) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19719,7 +20809,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -19729,7 +20819,7 @@ "outbound-http", "query-params" ], - "content_sha256": "a2b700d31b619ab571bf016b276a2063db60d54b0a63308088097e1a1b5f4446", + "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -19748,7 +20838,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20170,207 +21260,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20394,7 +21290,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20404,7 +21300,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "19e3d3e017e89bd5e2f2d3cc57067a1b5b2d83502c762e0ada31536984710a19", + "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -20423,7 +21319,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20876,6 +21772,188 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 281, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "bloom_filter_denylist", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "bloom-filter", + "denylist", + "example.", + "checks", + "client", + "x-real-ip", + "request", + "header", + "falling", + "back", + "x-forwarded-for", + "against", + "bloom", + "filter", + "stored", + "fastedge", + "kv.", + "returns", + "403", + "hit", + "200", + "otherwise.", + "required", + "configuration", + "environment" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 282, + "line_end": 287, + "keywords": [ + "configuration", + "environment", + "variable", + "denylist_store", + "name", + "store", + "holds", + "bloom", + "filter.", + "bloom-filter", + "key", + "hardcoded", + "blocked-ips", + "change", + "bloom_key", + "src", + "lib.rs", + "your", + "different." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#behaviour", + "heading": "Behaviour", + "level": 2, + "anchor": "behaviour", + "line_start": 288, + "line_end": 294, + "keywords": [ + "behaviour", + "bf_exists", + "blocked-ips", + "response", + "---", + "true", + "403", + "allowed", + "false", + "...", + "200" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#tradeoff-false-positives", + "heading": "Tradeoff: false positives", + "level": 2, + "anchor": "tradeoff-false-positives", + "line_start": 295, + "line_end": 302, + "keywords": [ + "tradeoff", + "false", + "positives", + "bloom", + "filters", + "answer", + "definitely", + "set", + "maybe", + "bf_exists", + "returns", + "true", + "probably", + "added", + "small", + "fraction", + "hits", + "meaning", + "legitimate", + "ips", + "over-blocked.", + "acceptable", + "denylist", + "allowlists", + "anything", + "requiring", + "exact", + "membership", + "use", + "store.get", + "against", + "regular", + "key", + "instead." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#populating-the-filter", + "heading": "Populating the filter", + "level": 2, + "anchor": "populating-the-filter", + "line_start": 303, + "line_end": 307, + "keywords": [ + "populating", + "filter", + "edge", + "handler", + "read-only.", + "populate", + "blocked-ips", + "out", + "band", + "example", + "via", + "fastedge", + "api" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 308, + "line_end": 312, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "bloom-filter-denylist" + ] } ] }, @@ -21724,7 +22802,7 @@ { "id": "http-examples-cache-wasi-rust", "title": "Cache (WASI) — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21748,7 +22826,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -21758,7 +22836,7 @@ "cache", "outbound-http" ], - "content_sha256": "e0929deee35dbe662b0a26183b2655f95b7796526be190276c8e4ca0bce0eddf", + "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -21777,7 +22855,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22126,13 +23204,201 @@ "workflow", "skill" ] + }, + { + "id": "http-examples-cache-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 192, + "line_end": 359, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "response", + "caching", + "purging", + "via", + "interface.", + "reads", + "origin_host", + "environment", + "forwards", + "incoming", + "request", + "origin", + "caches", + "body", + "keyed", + "path.", + "subsequent", + "requests", + "same", + "path", + "cached", + "returned" + ] + }, + { + "id": "http-examples-cache-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 360, + "line_end": 366, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "origin_host", + "yes", + "base", + "url", + "upstream", + "origin", + "e.g.", + "https", + "api.example.com", + "returns", + "500", + "unset.", + "cache_ttl_ms", + "long", + "cache", + "responses", + "milliseconds.", + "default", + "60000" + ] + }, + { + "id": "http-examples-cache-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 367, + "line_end": 373, + "keywords": [ + "works", + "get", + "data", + "cache", + "miss", + "forward", + "origin_host", + "2xx", + "body", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "path", + "query", + "only", + "responses", + "origin", + "error", + "pass", + "without", + "stored.", + "response", + "headers", + "replayed", + "cache-hit", + "use", + "content-type", + "application", + "octet-stream", + "sinc" + ] + }, + { + "id": "http-examples-cache-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 374, + "line_end": 378, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "cache_wasi.wasm" + ] + }, + { + "id": "http-examples-cache-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 379, + "line_end": 385, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry", + "wstd", + "http", + "client", + "new", + ".send", + "req", + ".await", + "async", + "outbound", + "request", + "origin" + ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22156,7 +23422,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22166,7 +23432,7 @@ "languages", "diagnostic" ], - "content_sha256": "07465c714cb1fa963a3f6a8ff6759449d473393481ff4aac12103dbef85e899f", + "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -22185,7 +23451,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22686,125 +23952,342 @@ "store", "usage" ] - } - ] - }, - { - "id": "http-examples-fetch-js", - "title": "http-examples-fetch-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-fetch-js.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "http" - ], - "languages": [ - "javascript" - ], - "tags": [ - "examples", - "fastedge-docs", - "http", - "javascript", - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23", - "fetch", - "outbound", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url" - ], - "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", - "sections": [ + }, { - "id": "http-examples-fetch-js#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, + "id": "http-examples-diagnostic-logging-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 194, + "line_end": 307, "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23" + "source", + "material", + "file", + "examples", + "http", + "wasi", + "diagnostic_logging", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "diagnostic", + "logging", + "example.", + "tiny", + "pass-through", + "proxy", + "writes", + "single", + "set_user_diag", + "tag", + "per", + "request", + "summarising", + "outcome", + "config_error", + "origin_unreachable", + "proxied", + "appears", + "fastedge", + "platform", + "per-request", + "log", + "viewer", + "distinct", + "stdout" ] }, { - "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", - "heading": "fetch — Outbound HTTP Requests", + "id": "http-examples-diagnostic-logging-wasi-rust#configuration", + "heading": "Configuration", "level": 2, - "anchor": "fetch-—-outbound-http-requests", - "line_start": 10, - "line_end": 93, + "anchor": "configuration", + "line_start": 308, + "line_end": 311, "keywords": [ - "fetch", - "outbound", - "http", + "configuration", + "origin_url", + "environment", + "variable", + "origin", "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url", - "options", - "api.", - "runtime", - "exposes", - "subset", - "browser", - "api", - "sufficient", - "downstream", - "service", - "calls.", + "proxied", + "to." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", + "heading": "Outcomes", + "level": 2, + "anchor": "outcomes", + "line_start": 312, + "line_end": 321, + "keywords": [ + "outcomes", + "request", + "writes", + "exactly", + "one", + "condition", + "tag", "---", - "signature", - "string", - "requestinit", - "promise", - "response", - "parameters", - "parameter", - "type", - "required", - "description", - "-----------", - "------", - "----------", - "-------------", - "yes", - "absolute", - "resou" + "origin_url", + "missing", + "outcome", + "config_error", + "reason", + "origin_missing", + "origin", + "unreachable", + "origin_unreachable", + "method", + "path", + "err", + "proxied", + "status" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", + "heading": "`set_user_diag` vs `println!`", + "level": 2, + "anchor": "set_user_diag-vs-println", + "line_start": 322, + "line_end": 330, + "keywords": [ + "set_user_diag", + "println", + "---", + "channel", + "stdout", + "general", + "application", + "logs", + "per-request", + "structured", + "tag", + "platform", + "log", + "viewer", + "cardinality", + "many", + "per", + "request", + "one", + "multiple", + "calls", + "leave", + "only", + "last", + "concatenated", + "undefined", + "best", + "verbose", + "traces", + "debug", + "details", + "single", + "filterable", + "outcome", + "label", + "forbidden", + "secrets", + "pii", + "tags", + "appear" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#convention", + "heading": "Convention", + "level": 2, + "anchor": "convention", + "line_start": 331, + "line_end": 336, + "keywords": [ + "convention", + "call", + "set_user_diag", + "once", + "branch", + "late", + "enough", + "handler", + "know", + "outcome.", + "logfmt", + "-ish", + "format", + "outcome", + "verb", + "key", + "value", + "easy", + "slice", + "log", + "search", + "tooling", + "keep", + "keys", + "short", + "stable", + "they", + "make", + "good", + "filter", + "terms." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 337, + "line_end": 342, + "keywords": [ + "related", + "cdn", + "proxy-wasm", + "variant", + "fastedge", + "proxywasm", + "utils", + "set_user_diag", + "same", + "semantics", + "different", + "module", + "path.", + "see", + "docs", + "cdn_apps.md" ] } ] }, { - "id": "http-examples-geo-redirect-js", - "title": "http-examples-geo-redirect-js", + "id": "http-examples-fetch-js", + "title": "http-examples-fetch-js", "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-fetch-js.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "http" + ], + "languages": [ + "javascript" + ], + "tags": [ + "examples", + "fastedge-docs", + "http", + "javascript", + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23", + "fetch", + "outbound", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url" + ], + "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", + "sections": [ + { + "id": "http-examples-fetch-js#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23" + ] + }, + { + "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", + "heading": "fetch — Outbound HTTP Requests", + "level": 2, + "anchor": "fetch-—-outbound-http-requests", + "line_start": 10, + "line_end": 93, + "keywords": [ + "fetch", + "outbound", + "http", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url", + "options", + "api.", + "runtime", + "exposes", + "subset", + "browser", + "api", + "sufficient", + "downstream", + "service", + "calls.", + "---", + "signature", + "string", + "requestinit", + "promise", + "response", + "parameters", + "parameter", + "type", + "required", + "description", + "-----------", + "------", + "----------", + "-------------", + "yes", + "absolute", + "resou" + ] + } + ] + }, + { + "id": "http-examples-geo-redirect-js", + "title": "http-examples-geo-redirect-js", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -22916,7 +24399,7 @@ { "id": "http-examples-geo-redirect-wasi-rust", "title": "Geo Redirect — WASI HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22940,7 +24423,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22950,7 +24433,7 @@ "geo-redirect", "headers" ], - "content_sha256": "77ed57c49c605d2b3301511ccb2591fd39dc6726111761bc541c9229ec53ca23", + "content_sha256": "5c4595ed117a31ae93362494563368610e2f957a6ae0f6fcdd2e9d9ca9570e2a", "sections": [ { "id": "http-examples-geo-redirect-wasi-rust#introduction", @@ -22969,7 +24452,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -23375,6 +24858,167 @@ "examples-env-vars-wasi-rust", "access" ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 149, + "line_end": 232, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "geo_redirect", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "wasi-http", + "app", + "demonstrating", + "geo-based", + "redirects.", + "reads", + "country", + "code", + "geoip-country-code", + "request", + "header", + "redirects", + "country-specific", + "origin", + "url.", + "falls", + "back", + "base_origin", + "mapping", + "configured.", + "required", + "configuration", + "environment", + "variable" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 233, + "line_end": 239, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "base_origin", + "yes", + "fallback", + "redirect", + "url", + "e.g.", + "https", + "example.com", + "returns", + "500", + "unset.", + "country_code", + "per-country", + "keyed", + "2-letter", + "country", + "code", + "falls", + "back", + "set." + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 240, + "line_end": 248, + "keywords": [ + "works", + "geoip-country-code", + "env", + "var", + "set", + "302", + "value", + "base_origin", + "header", + "500" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 249, + "line_end": 255, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "geo_redirect_wasi.wasm" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 256, + "line_end": 262, + "keywords": [ + "apis", + "used", + "request.headers", + ".get", + "geoip-country-code", + "read", + "geo", + "header", + "injected", + "fastedge", + "edge", + "std", + "env", + "var", + "country_code", + "dynamic", + "lookup", + "country", + "code", + "response", + "builder", + ".status", + "302", + ".header", + "location", + "url", + "redirect" + ] } ] }, @@ -24569,7 +26213,7 @@ { "id": "http-examples-hello-world-wasi-rust", "title": "Example: Hello World (WASI) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24593,7 +26237,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24603,7 +26247,7 @@ "hello-world", "wasi" ], - "content_sha256": "1777edec022e50301971e706c675717cea56db7b51e3e487b9379ceb00d95d08", + "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -24622,7 +26266,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -25015,6 +26659,127 @@ "base", "rust" ] + }, + { + "id": "http-examples-hello-world-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 186, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "hello_world", + "src", + "lib.rs", + "rust", + "use", + "wstd", + "body", + "request", + "response", + "http_server", + "async", + "main", + "anyhow", + "result", + "let", + "url", + "request.uri", + ".to_string", + "builder", + ".status", + "200", + ".header", + "content-type", + "text", + "plain", + "charset", + "utf-8", + ".body", + "format", + "hello", + "you", + "made" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 187, + "line_end": 195, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "plain", + "charset", + "utf-8", + "hello", + "you", + "made", + "wasi", + "request", + "host", + "path", + "query" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 196, + "line_end": 202, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "hello_world.wasm" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 203, + "line_end": 210, + "keywords": [ + "apis", + "used", + "wstd", + "http_server", + "wasi", + "http", + "entry-point", + "macro", + "request", + "response", + "types", + "body", + "construction", + "request.uri", + ".to_string", + "full", + "absolute", + "uri" + ] } ] }, @@ -25302,7 +27067,7 @@ { "id": "http-examples-kv-store-js", "title": "http-examples-kv-store-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-js.md", "skill": "fastedge-docs", "category": "examples", @@ -25326,7 +27091,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "store", "example", "reference", @@ -25338,7 +27103,7 @@ "fastedge", "edge" ], - "content_sha256": "1ffcb44a8617f30c59fe878b41487d54f63062a0862ae4578a2ad41f1016b76d", + "content_sha256": "d3c4dbab0c624edfd7e3590c6413091cc12881c8a06e43b9a74fe7008ce7e515", "sections": [ { "id": "http-examples-kv-store-js#introduction", @@ -25357,7 +27122,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -25366,7 +27131,7 @@ "level": 2, "anchor": "kv-store-—-example-reference", "line_start": 10, - "line_end": 302, + "line_end": 332, "keywords": [ "store", "example", @@ -25413,7 +27178,7 @@ { "id": "http-examples-kv-store-wasi-rust", "title": "KV Store — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25437,7 +27202,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25447,7 +27212,7 @@ "languages", "store" ], - "content_sha256": "b6fb89540d2ef2384d671a94a18bf815537190adacbc1f2e03b861eae1e5ff83", + "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -25466,7 +27231,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25893,6 +27658,222 @@ "javascript", "equivalent" ] + }, + { + "id": "http-examples-kv-store-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 382, + "line_end": 605, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "key_value", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "store", + "operations", + "via", + "wasi-http", + "interface.", + "supports", + "query", + "parameters", + "name", + "action", + "get", + "key", + "scan", + "match", + "pattern", + "zrange", + "min", + "f64", + "max", + "zscan", + "bfexists" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 606, + "line_end": 619, + "keywords": [ + "usage", + "operations", + "require", + "store", + "name", + "action", + "query", + "parameters", + "additional", + "params", + "description", + "---", + "get", + "key", + "fetch", + "single", + "value", + "scan", + "match", + "pattern", + "list", + "keys", + "matching", + "glob", + "zrange", + "min", + "f64", + "max", + "sorted-set", + "members", + "score", + "range", + "zscan", + "bfexists", + "item", + "check", + "bloom", + "filt" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 620, + "line_end": 629, + "keywords": [ + "example", + "get", + "store", + "my-store", + "action", + "key", + "hello", + "200", + "response", + "world", + "scan", + "match", + "user" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#error-responses", + "heading": "Error responses", + "level": 2, + "anchor": "error-responses", + "line_start": 630, + "line_end": 638, + "keywords": [ + "error", + "responses", + "condition", + "status", + "body", + "---", + "store", + "found", + "access", + "denied", + "403", + "missing", + "required", + "params", + "530", + "runtime", + "open", + "500", + "...", + "invalid", + "action", + "400", + "supported" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 639, + "line_end": 645, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "key_value_wasi.wasm" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 646, + "line_end": 655, + "keywords": [ + "apis", + "used", + "fastedge", + "key_value", + "store", + "open", + "name", + "named", + "store.get", + "key", + "fetch", + "value", + "returns", + "option", + "vec", + "store.scan", + "pattern", + "list", + "keys", + "glob", + "store.zrange_by_score", + "min", + "max", + "range", + "query", + "sorted", + "set", + "store.zscan", + "scan", + "store.bf_exists", + "item", + "bloom", + "filter", + "membership", + "test" + ] } ] }, @@ -26288,7 +28269,7 @@ { "id": "http-examples-markdown-render-basic-rust", "title": "HTTP Example: Markdown Render (Rust, Basic)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26312,7 +28293,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26322,7 +28303,7 @@ "outbound-fetch", "env-vars" ], - "content_sha256": "f72f4be89063520134023c34d38ba6d901f66977bbeab889a4f8640e36fa288b", + "content_sha256": "2c31f5e301b8fd9c5eede29a285d26452eeb1606d1d009ebb4bbe5d5bc1fafc9", "sections": [ { "id": "http-examples-markdown-render-basic-rust#introduction", @@ -26341,7 +28322,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26472,6 +28453,8 @@ "var", "strip", "trailing", + "via", + "base.trim_end_matches", "validate", "request", "path", @@ -26483,27 +28466,25 @@ "fastedge", "call", "send_request", - "via", "internal", "helper.", "follow", "redirects", "max_redirects", - "hops.", + "hops", + "request_inner", + "req", + "depth", "decode", "response", "body", "utf-8", + "string", + "from_utf8", + "rsp.body", + ".to_vec", "failure", - "500", - "parse", - "markdown", - "parser", - "new_ext", - "options", - "enable_tables", - "enable_footnotes", - "render" + "500" ] }, { @@ -26728,7 +28709,7 @@ "level": 2, "anchor": "gotchas", "line_start": 189, - "line_end": 198, + "line_end": 199, "keywords": [ "gotchas", "base.trim_end_matches", @@ -26777,8 +28758,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 199, - "line_end": 206, + "line_start": 200, + "line_end": 207, "keywords": [ "see", "fastedge-sdk-rust", @@ -26814,7 +28795,7 @@ { "id": "http-examples-outbound-fetch-basic-rust", "title": "Example: Outbound Fetch — Basic HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26838,7 +28819,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26848,7 +28829,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "700a80efb4a443683f2d328b4b720fcbb8bdcfacaa14c271fd4afa30db17ae2d", + "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -26867,7 +28848,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27235,6 +29216,171 @@ "target", "context" ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 190, + "line_end": 262, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "outbound_fetch", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "fastedge", + "body", + "request", + "response", + "statuscode", + "serde_json", + "json", + "value", + "main", + "_req", + "let", + "upstream_req", + "builder", + ".uri", + "jsonplaceholder.typicode.com", + "users", + ".body", + "empty", + "upstream_resp", + "send_request", + ".map_err" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 263, + "line_end": 282, + "keywords": [ + "incoming", + "request", + "makes", + "get", + "http", + "jsonplaceholder.typicode.com", + "users", + "using", + "fastedge", + "send_request", + "parses", + "json", + "response", + "body.", + "takes", + "first", + "array.", + "returns", + "envelope", + "pagination", + "metadata.", + "example", + "body", + "name", + "leanne", + "graham", + "...", + "total", + "skip", + "limit" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 283, + "line_end": 292, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "send_request", + "outbound", + "request", + "upstream", + "response", + "statuscode", + "types", + "body", + "bodies", + "serde_json", + "json", + "parsing", + "serialisation" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 293, + "line_end": 299, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "outbound_fetch.wasm" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 300, + "line_end": 306, + "keywords": [ + "expected", + "behavior", + "request", + "response", + "status", + "content-type", + "body", + "---", + "get", + "200", + "application", + "json", + "object", + "users", + "array", + "total", + "skip", + "limit" + ] } ] }, @@ -27691,7 +29837,7 @@ { "id": "http-examples-outbound-modify-response-wasi-rust", "title": "Example: Outbound Modify Response (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27715,7 +29861,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27725,7 +29871,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "17e973e59daaef6de41a976cc9c3cbb1c037242bd84aee527c2d6a840100df18", + "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -27744,7 +29890,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -28060,6 +30206,78 @@ "serde_json", "crate" ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 277, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "outbound_modify_response", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "outbound", + "fetch", + "response", + "transformation.", + "fetches", + "json", + "upstream", + "origin", + "reads", + "parses", + "body", + "reshapes", + "new", + "object", + "first", + "users", + "pagination", + "metadata", + "returns", + "fresh", + "content-type", + "application", + "header.", + "stepping-stone", + "beyond" + ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 278, + "line_end": 284, + "keywords": [ + "related", + "outbound_fetch", + "simpler", + "variant", + "passes", + "upstream", + "response", + "unchanged.", + "mirror", + "fastedge-sdk-js", + "examples", + "outbound-modify-response" + ] } ] }, @@ -29212,7 +31430,7 @@ { "id": "http-examples-s3upload-basic-rust", "title": "S3 Upload — HTTP Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29236,7 +31454,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29246,7 +31464,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "16c81799dc962bed122dbb754b1370cc70719e1fb425adbb9b343a066ddea4fc", + "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -29265,7 +31483,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29630,7 +31848,7 @@ "level": 2, "anchor": "key-constraints-and-gotchas", "line_start": 166, - "line_end": 176, + "line_end": 177, "keywords": [ "key", "constraints", @@ -29679,8 +31897,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 177, - "line_end": 184, + "line_start": 178, + "line_end": 185, "keywords": [ "see", "fastedge-docs", @@ -29710,7 +31928,7 @@ { "id": "http-examples-secret-basic-rust", "title": "Secret Access — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29734,7 +31952,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "example", @@ -29744,7 +31962,7 @@ "fastedge", "platform" ], - "content_sha256": "3036554dd9530db71002d821ac1c653a53831fbcaf9598cd6ac93742ef7edba8", + "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -29763,7 +31981,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "rust", @@ -30153,6 +32371,277 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 370, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "secret", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "std", + "time", + "systemtime", + "fastedge", + "body", + "request", + "response", + "statuscode", + "allow", + "dead_code", + "main", + "_req", + "let", + "value", + "match", + "get", + "err", + "accessdenied", + "return", + "builder" + ] + }, + { + "id": "http-examples-secret-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 371, + "line_end": 380, + "keywords": [ + "request", + "handler", + "calls", + "secret", + "get", + "retrieves", + "current", + "value", + "named", + "missing", + "returned", + "none", + "returns", + "404", + "get_effective_at", + "unix_ts", + "effective", + "unix", + "timestamp", + "demonstrating", + "rotation", + "versioning", + "api.", + "success", + "200", + "values", + "body", + "debug" + ] + }, + { + "id": "http-examples-secret-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 381, + "line_end": 391, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "secret", + "get", + "key", + "retrieve", + "current", + "value", + "named", + "get_effective_at", + "timestamp", + "effective", + "specific", + "unix", + "error", + "variants", + "accessdenied", + "msg", + "decrypterror", + "request", + "response", + "statuscode", + "types", + "body", + "respo" + ] + }, + { + "id": "http-examples-secret-basic-rust#secret-error-variants", + "heading": "Secret error variants", + "level": 2, + "anchor": "secret-error-variants", + "line_start": 392, + "line_end": 401, + "keywords": [ + "secret", + "error", + "variants", + "variant", + "http", + "response", + "meaning", + "---", + "value", + "200", + "body", + "found", + "none", + "404", + "empty", + "name", + "valid", + "set", + "err", + "accessdenied", + "403", + "app", + "permitted", + "read", + "msg", + "denial", + "human-readable", + "message", + "decrypterror", + "500", + "exists", + "decrypted" + ] + }, + { + "id": "http-examples-secret-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 402, + "line_end": 408, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] + }, + { + "id": "http-examples-secret-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 409, + "line_end": 418, + "keywords": [ + "expected", + "behavior", + "scenario", + "secret", + "response", + "status", + "body", + "---", + "happy", + "path", + "my-value", + "200", + "get", + "nget_efective_at", + "set", + "absent", + "404", + "empty", + "access", + "denied", + "403", + "note", + "contains", + "typo", + "field", + "name", + "get_efective_at", + "instead", + "get_effective_at", + "known", + "cosmetic", + "issue", + "source." + ] + }, + { + "id": "http-examples-secret-basic-rust#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 419, + "line_end": 436, + "keywords": [ + "local", + "testing", + "inject", + "secret", + "via", + ".env", + "file", + "your", + "fixtures", + "directory", + "using", + "fastedge_var_secret_", + "name", + "prefix", + "fastedge_var_secret_secret", + "my-test-value", + "run", + "fixture", + "validator", + "node", + "tools", + "fixture-validator", + "index.mjs", + "fastedge-sdk-rust", + "examples", + "http", + "basic", + "--wasm", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] } ] }, @@ -30616,7 +33105,7 @@ { "id": "http-examples-simple-fetch-wasi-rust", "title": "Example: Simple Fetch (WASI HTTP, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -30640,7 +33129,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -30650,7 +33139,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "59940cbcac813462e867ba44395edd99653c63a1b5dad9fd847815d05a4a3ce8", + "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -30669,7 +33158,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31034,6 +33523,186 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 151, + "line_end": 235, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "simple_fetch", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "wasi-http", + "interface", + "via", + "wstd", + "crate.", + "receives", + "incoming", + "request", + "makes", + "outbound", + "url", + "specified", + "x-fetch-url", + "header", + "defaults", + "https", + "httpbin.org", + "get", + "build", + "cargo-component", + "cargo", + "component" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 236, + "line_end": 241, + "keywords": [ + "works", + "app", + "receives", + "incoming", + "request", + "reads", + "target", + "url", + "x-fetch-url", + "header", + "makes", + "outbound", + "get", + "streams", + "response", + "back", + "caller.", + "absent", + "defaults", + "https", + "httpbin.org" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#request-headers", + "heading": "Request headers", + "level": 2, + "anchor": "request-headers", + "line_start": 242, + "line_end": 247, + "keywords": [ + "request", + "headers", + "header", + "required", + "description", + "--------", + "----------", + "-------------", + "x-fetch-url", + "url", + "fetch.", + "defaults", + "https", + "httpbin.org", + "get" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 248, + "line_end": 253, + "keywords": [ + "example", + "bash", + "curl", + "x-fetch-url", + "https", + "httpbin.org", + "uuid", + "your-app-domain" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 254, + "line_end": 260, + "keywords": [ + "build", + "bash", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "simple_fetch.wasm" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#key-differences-from-basic-http-examples", + "heading": "Key differences from basic HTTP examples", + "level": 2, + "anchor": "key-differences-from-basic-http-examples", + "line_start": 261, + "line_end": 270, + "keywords": [ + "key", + "differences", + "basic", + "http", + "examples", + "fastedge", + "crate", + "wasi", + "wstd", + "---", + "handler", + "main", + "req", + "sync", + "async", + "macro", + "http_server", + "outbound", + "send_request", + "client", + "new", + ".send", + ".await", + "build", + "target", + "wasm32-wasip1", + "wasm32-wasip2" + ] } ] }, @@ -32109,7 +34778,7 @@ { "id": "http-examples-streaming-wasi-rust", "title": "Streaming Response — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32133,7 +34802,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32143,7 +34812,7 @@ "streaming", "async" ], - "content_sha256": "d43182bd8af1e158b7f960a742038ab49a7da9f93a10b9cd8a9163b91e79cb1d", + "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -32162,7 +34831,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32491,6 +35160,144 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, @@ -36775,7 +39582,7 @@ { "id": "quickstart-as", "title": "Quickstart: AssemblyScript CDN Apps on FastEdge", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/quickstart-as.md", "skill": "fastedge-docs", "category": "reference", @@ -36796,9 +39603,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "cdn", "apps", @@ -36809,7 +39616,7 @@ "compile", "webassembly" ], - "content_sha256": "5fc2440dbcf9811f874674b3390cc4c3227b2c2d095903c73343095e2f218549", + "content_sha256": "41ad91b4703ac00f166601df66080f0e3bd82de89c6e9fd8abfe5f3cf36abc80", "sections": [ { "id": "quickstart-as#introduction", @@ -36826,9 +39633,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "assemblyscript", "cdn", @@ -37900,7 +40707,7 @@ { "id": "runner-internals", "title": "Runner Internals — Low-Level Runner API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/runner-internals.md", "skill": "test", "category": "testing", @@ -37914,11 +40721,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -37930,7 +40737,7 @@ "use", "you" ], - "content_sha256": "2281ea5aaf755391320fbb293d097ebbb9ca95c272b510df303c43541b5c4806", + "content_sha256": "3ece5464164e143468e6f52883fffde960a434dddb1352dc3f9b65f188f0a4f8", "sections": [ { "id": "runner-internals#introduction", @@ -37945,11 +40752,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -38295,7 +41102,7 @@ { "id": "sdk-reference-as", "title": "AssemblyScript Proxy-Wasm SDK Reference", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/sdk-reference-as.md", "skill": "fastedge-docs", "category": "sdk", @@ -38316,9 +41123,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "proxy-wasm", "reference", "gcoredev", @@ -38328,7 +41135,7 @@ "writing", "cdn" ], - "content_sha256": "41b8129100103bce01866f20179662cdbe02e850593df1728abec65ffa86b79a", + "content_sha256": "353effa70cde17d46a928071309fca138aa211b6c6768542059770adcf6cc021", "sections": [ { "id": "sdk-reference-as#introduction", @@ -38345,9 +41152,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "assemblyscript", "proxy-wasm", "sdk", @@ -38383,7 +41190,7 @@ "level": 2, "anchor": "entry-point-pattern", "line_start": 20, - "line_end": 69, + "line_end": 71, "keywords": [ "entry", "point", @@ -38429,8 +41236,8 @@ "heading": "Build Configuration", "level": 2, "anchor": "build-configuration", - "line_start": 70, - "line_end": 111, + "line_start": 72, + "line_end": 113, "keywords": [ "build", "configuration", @@ -38471,8 +41278,8 @@ "heading": "Proxy-Wasm Lifecycle", "level": 2, "anchor": "proxy-wasm-lifecycle", - "line_start": 112, - "line_end": 317, + "line_start": 114, + "line_end": 319, "keywords": [ "proxy-wasm", "lifecycle", @@ -38518,8 +41325,8 @@ "heading": "Return Value Enums", "level": 2, "anchor": "return-value-enums", - "line_start": 318, - "line_end": 397, + "line_start": 320, + "line_end": 399, "keywords": [ "return", "value", @@ -38551,8 +41358,8 @@ "heading": "Request and Response Manipulation", "level": 2, "anchor": "request-and-response-manipulation", - "line_start": 398, - "line_end": 668, + "line_start": 400, + "line_end": 670, "keywords": [ "request", "response", @@ -38587,8 +41394,8 @@ "heading": "Outbound HTTP (httpCall)", "level": 2, "anchor": "outbound-http-httpcall", - "line_start": 669, - "line_end": 814, + "line_start": 671, + "line_end": 816, "keywords": [ "outbound", "http", @@ -38632,8 +41439,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 815, - "line_end": 837, + "line_start": 817, + "line_end": 839, "keywords": [ "logging", "import", @@ -38675,8 +41482,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 838, - "line_end": 859, + "line_start": 840, + "line_end": 861, "keywords": [ "registration", "import", @@ -38723,8 +41530,8 @@ "heading": "FastEdge Host APIs", "level": 2, "anchor": "fastedge-host-apis", - "line_start": 860, - "line_end": 1067, + "line_start": 862, + "line_end": 1079, "keywords": [ "fastedge", "host", @@ -38771,8 +41578,8 @@ "heading": "Deprecated Functions", "level": 2, "anchor": "deprecated-functions", - "line_start": 1068, - "line_end": 1081, + "line_start": 1080, + "line_end": 1093, "keywords": [ "deprecated", "functions", @@ -38804,8 +41611,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 1082, - "line_end": 1088, + "line_start": 1094, + "line_end": 1100, "keywords": [ "see", "fastedge", @@ -39844,7 +42651,7 @@ { "id": "server-api", "title": "Server API — REST and WebSocket Endpoints", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/server-api.md", "skill": "test", "category": "testing", @@ -39858,11 +42665,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -39874,7 +42681,7 @@ "interfaces", "loading" ], - "content_sha256": "d03fdff0b2a4179f0a7e45bbd2519f4edb1ac604a36369aba6b661c45a82d201", + "content_sha256": "96fce5e89649293442cc67e66685fffc2282f052c1d92ac935c72b815ae71822", "sections": [ { "id": "server-api#introduction", @@ -39889,11 +42696,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -40096,10 +42903,1055 @@ } ] }, + { + "id": "templates-catalog", + "title": "FastEdge Bolt-On Templates", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check" + ], + "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "sections": [ + { + "id": "templates-catalog#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 13, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check", + "catalog.", + "entry", + "maintained", + "tested", + "application", + "handles", + "security-sensitive", + "parts", + "hand-rolled", + "implementation", + "get", + "right", + "independently.", + "use", + "adapt", + "templat" + ] + }, + { + "id": "templates-catalog#html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "heading": "html2md — HTML-to-Markdown conversion / content transformation / Accept header negotiation", + "level": 2, + "anchor": "html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "line_start": 14, + "line_end": 31, + "keywords": [ + "html2md", + "html-to-markdown", + "conversion", + "content", + "transformation", + "accept", + "header", + "negotiation", + "solves", + "cdn", + "filter", + "transparently", + "converts", + "html", + "origin", + "responses", + "markdown", + "client", + "sends", + "text", + "handles", + "encoding", + "caching", + "concerns", + "hand-rolled", + "get", + "right", + "independently.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "110", + "changes" + ] + }, + { + "id": "templates-catalog#harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "heading": "harden-cookies — Cookie security hardening / Secure HttpOnly SameSite attributes / Set-Cookie rewriting", + "level": 2, + "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "line_start": 32, + "line_end": 47, + "keywords": [ + "harden-cookies", + "cookie", + "security", + "hardening", + "secure", + "httponly", + "samesite", + "attributes", + "set-cookie", + "rewriting", + "solves", + "cdn", + "filter", + "adds", + "strict", + "targeted", + "response", + "headers.", + "eliminates", + "modify", + "backend", + "code", + "enforce", + "policy", + "edge.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "184", + "origin", + "changes", + "required.", + "runtime" + ] + }, + { + "id": "templates-catalog#edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "heading": "edge-sso — SSO / login / identity federation / Identity-Aware Proxy (Google, GitHub, Microsoft, Facebook, SAML)", + "level": 2, + "anchor": "edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "line_start": 48, + "line_end": 71, + "keywords": [ + "edge-sso", + "sso", + "login", + "identity", + "federation", + "identity-aware", + "proxy", + "google", + "github", + "microsoft", + "facebook", + "saml", + "solves", + "bolt-on", + "adds", + "multi-provider", + "2.0", + "existing", + "site", + "without", + "changing", + "backend.", + "handles", + "oauth", + "oidc", + "flows", + "session", + "token", + "issuance", + "per-request", + "enforcement.", + "building", + "scratch", + "requires", + "correctly", + "implementing", + "multiple", + "signing", + "edge", + "enforcem" + ] + }, + { + "id": "templates-catalog#edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "heading": "edge-totp — TOTP MFA / two-factor authentication / OTP challenge / RFC 6238", + "level": 2, + "anchor": "edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "line_start": 72, + "line_end": 95, + "keywords": [ + "edge-totp", + "totp", + "mfa", + "two-factor", + "authentication", + "otp", + "challenge", + "rfc", + "6238", + "solves", + "adds", + "step", + "front", + "existing", + "site", + "login", + "without", + "modifying", + "backend.", + "customer", + "origin", + "handles", + "password", + "validation", + "app", + "hosts", + "6-digit", + "verifies", + "code", + "replay", + "brute-force", + "protection", + "issues", + "signed", + "session", + "assertion", + "trusts.", + "building", + "scratch", + "requires" + ] + } + ] + }, + { + "id": "templates-edge-sso-integration", + "title": "edge-sso — Origin Integration Reference", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-sso-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app" + ], + "content_sha256": "7301aea1c160744e3ee14e1d9dbe28aa1fb33e22fd28c5a4bc99be69b5bbc361", + "sections": [ + { + "id": "templates-edge-sso-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "reference", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app", + "cdn-filter", + "covers", + "contract", + "relies", + "identity", + "delivery", + "auth", + "routes", + "login", + "page", + "customization", + "redirect", + "validation.", + "---" + ] + }, + { + "id": "templates-edge-sso-integration#1-what-sso_variant-means-for-the-origin", + "heading": "1. What `SSO_VARIANT` Means for the Origin", + "level": 2, + "anchor": "1-what-sso_variant-means-for-the-origin", + "line_start": 16, + "line_end": 57, + "keywords": [ + "sso_variant", + "means", + "origin", + "set", + "identically", + "auth-app", + "cdn-filter.", + "controls", + "edge", + "delivers", + "identity", + "responsible", + "verifying.", + "gate-only", + "receives", + "nothing", + "filter", + "enforces", + "allow", + "deny", + "only", + "authenticated", + "requests", + "reach", + "origin.", + "responsibility", + "none.", + "receive", + "token", + "headers", + "sees", + "pas" + ] + }, + { + "id": "templates-edge-sso-integration#2-auth-app-routes", + "heading": "2. Auth-App Routes", + "level": 2, + "anchor": "2-auth-app-routes", + "line_start": 58, + "line_end": 80, + "keywords": [ + "auth-app", + "routes", + "served", + "under", + "auth_prefix", + "default", + "auth", + "single-domain", + "routing", + "cdn", + "origin", + "customer", + "own", + "domain.", + "cdn-filter", + "bypasses", + "gate", + "routes.", + "route", + "method", + "caller", + "purpose", + "---", + "get", + "browser", + "hosted", + "login", + "page", + "server-rendered", + "branded", + "provider", + "buttons.", + "honours", + "redirect", + "providers" + ] + }, + { + "id": "templates-edge-sso-integration#3-login-page-customization-tiers", + "heading": "3. Login Page Customization Tiers", + "level": 2, + "anchor": "3-login-page-customization-tiers", + "line_start": 81, + "line_end": 134, + "keywords": [ + "login", + "page", + "customization", + "tiers", + "tier", + "env", + "var", + "branding", + "recommended", + "default", + "built-in", + "hosted", + "reads", + "vars", + "per-request.", + "code", + "changes", + "required.", + "effect", + "---", + "login_page_title", + "sign", + "title", + "login_page_subtitle", + "choose", + "sign-in", + "method", + "subheading", + "login_page_logo_url", + "logo", + "image", + "login_page_favicon_url", + "tab", + "favicon", + "login_page_accent_" + ] + }, + { + "id": "templates-edge-sso-integration#4-json-contracts", + "heading": "4. JSON Contracts", + "level": 2, + "anchor": "4-json-contracts", + "line_start": 135, + "line_end": 173, + "keywords": [ + "json", + "contracts", + "get", + "auth", + "providers", + "jsonc", + "redirect", + "cart", + "google", + "label", + "loginurl", + "login", + "2fcart", + "github", + "saml", + "sso", + "stable", + "identifier", + "matches", + "value", + "used", + "sso_providers", + "human" + ] + }, + { + "id": "templates-edge-sso-integration#5-redirect-validation", + "heading": "5. Redirect Validation", + "level": 2, + "anchor": "5-redirect-validation", + "line_start": 174, + "line_end": 187, + "keywords": [ + "redirect", + "validation", + "parameter", + "validated", + "against", + "sso_allowed_origins", + "route", + "honours", + "read", + "saml_relay", + "cookie.", + "rules", + "relative", + "urls", + "starting", + "always", + "permitted.", + "off-origin", + "absolute", + "silently", + "dropped", + "post-login", + "falls", + "back", + "protocol-relative", + "backslash", + "bypasses", + "e.g.", + "evil.com", + "rejected.", + "permit", + "redirects", + "specific", + "origins", + "set" + ] + }, + { + "id": "templates-edge-sso-integration#6-shared-config-the-origin-needs-to-know-about", + "heading": "6. Shared Config the Origin Needs to Know About", + "level": 2, + "anchor": "6-shared-config-the-origin-needs-to-know-about", + "line_start": 188, + "line_end": 207, + "keywords": [ + "shared", + "config", + "origin", + "needs", + "know", + "about", + "env", + "vars", + "affect", + "contract", + "operates", + "under.", + "they", + "set", + "fastedge", + "apps", + "auth-app", + "cdn-filter", + "integration", + "depends", + "their", + "values.", + "var", + "concern", + "---", + "sso_variant", + "match", + "determines", + "receives", + "nothing", + "gate-only", + "jwt", + "cookie", + "verify", + "identity", + "headers", + "header", + "sso_audience" + ] + }, + { + "id": "templates-edge-sso-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 208, + "line_end": 215, + "keywords": [ + "see", + "edge-sso", + "template", + "readme", + "deployment", + "configuration", + "auth-app", + "cdn-filter", + ".env.example", + "authoritative", + "exhaustive", + "env", + "var", + "lists", + "inline", + "guidance", + "architecture", + "auth-modes", + "sso_variant", + "axis", + "detail", + "security", + "token", + "trust", + "model", + "known", + "limitations", + "including", + "revocation", + "idp", + "single", + "logout", + "overview", + "two-app", + "signing", + "strategy", + "canonical_host" + ] + } + ] + }, + { + "id": "templates-edge-totp-integration", + "title": "edge-totp — Origin Integration", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password" + ], + "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "sections": [ + { + "id": "templates-edge-totp-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "reference", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password", + "login.", + "template", + "otp-app", + "otp-filter", + "already", + "deployed", + "gcore", + "portal", + "document", + "covers", + "three", + "origin-side", + "code", + "changes", + "required", + "connect", + "it.", + "---" + ] + }, + { + "id": "templates-edge-totp-integration#the-three-origin-side-changes", + "heading": "The Three Origin-Side Changes", + "level": 2, + "anchor": "the-three-origin-side-changes", + "line_start": 16, + "line_end": 58, + "keywords": [ + "three", + "origin-side", + "changes", + "only", + "origin", + "auth", + "module", + "changes.", + "rest", + "site", + "untouched.", + "split", + "login", + "password-then-otp", + "password", + "check", + "succeeds", + "instead", + "immediately", + "minting", + "full", + "session", + "sign", + "handoff", + "ticket", + "hmac", + "handoff_key", + "over", + "sub", + "userid", + "next", + "exp", + "now", + "ticket_ttl", + "set", + "short-lived", + "pre_mfa", + "cookie", + "marker", + "remembers" + ] + }, + { + "id": "templates-edge-totp-integration#profile-a-vs-profile-b-—-decision-guide", + "heading": "Profile A vs Profile B — Decision Guide", + "level": 2, + "anchor": "profile-a-vs-profile-b-—-decision-guide", + "line_start": 59, + "line_end": 77, + "keywords": [ + "profile", + "decision", + "guide", + "security-posture", + "decision.", + "wrong", + "choice", + "here", + "real", + "vulnerability", + "style", + "issue.", + "criterion", + "---", + "origin", + "crypto", + "required", + "none", + "handoff_key", + "only", + "es256", + "proof", + "verification", + "via", + "jwks", + "knows", + "user", + "passed", + "mfa", + "filter", + "verifies", + "valid", + "mfa_session", + "exists", + "yes", + "carries", + "sub", + "binds", + "pre_mfa", + "session" + ] + }, + { + "id": "templates-edge-totp-integration#shared-configuration", + "heading": "Shared Configuration", + "level": 2, + "anchor": "shared-configuration", + "line_start": 78, + "line_end": 93, + "keywords": [ + "shared", + "configuration", + "keys", + "endpoints", + "origin", + "edge.", + "components", + "agree", + "values.", + "key", + "endpoint", + "edge", + "---", + "handoff_key", + "hs256", + "signs", + "handoff", + "ticket", + "password", + "success", + "verifies", + "auth_prefix", + "challenge", + "verify", + "jwks", + "profile", + "only", + "fetches", + ".well-known", + "jwks.json", + "via", + "createremotejwkset", + "public", + "cannot", + "forge", + "proofs", + "one" + ] + }, + { + "id": "templates-edge-totp-integration#required-deployment-precondition", + "heading": "Required Deployment Precondition", + "level": 2, + "anchor": "required-deployment-precondition", + "line_start": 94, + "line_end": 103, + "keywords": [ + "required", + "deployment", + "precondition", + "lock", + "origin", + "edge-only", + "traffic.", + "hard", + "requirement", + "profiles", + "suggestion.", + "edge", + "gate", + "profile", + "meaningless", + "directly", + "reachable.", + "hostname", + "accessible", + "without", + "going", + "gcore", + "cdn", + "attacker", + "simply", + "skips", + "entirely", + "mfa_session", + "rust", + "filter", + "signed", + "proof", + "never", + "run.", + "restrict", + "ingress", + "using", + "allowlist", + "authenticat" + ] + }, + { + "id": "templates-edge-totp-integration#recovery-and-self-service-enrollment-caveat", + "heading": "Recovery and Self-Service Enrollment Caveat", + "level": 2, + "anchor": "recovery-and-self-service-enrollment-caveat", + "line_start": 104, + "line_end": 113, + "keywords": [ + "recovery", + "self-service", + "enrollment", + "caveat", + "auth_prefix", + "activate", + "inherits", + "trust", + "level", + "password", + "step", + "user", + "pass", + "check", + "self-enroll", + "new", + "authenticator.", + "correct", + "default", + "deployments", + "sensitive", + "accounts", + "means", + "compromised", + "compromises", + "totp", + "second", + "factor.", + "decision", + "point", + "your", + "threat", + "model", + "requires", + "factor", + "remain", + "independent" + ] + }, + { + "id": "templates-edge-totp-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 114, + "line_end": 121, + "keywords": [ + "see", + "edge-totp", + "storage", + "secrets", + "reference", + "storage-and-secrets.md", + "full", + "env", + "var", + "secret", + "list", + "otp-app", + "otp-filter", + "threat", + "model", + "threat-model.md", + "trust", + "boundaries", + "residual", + "risks", + "risk", + "register", + "including", + "token", + "scope", + "self-enrollment", + "level", + "architecture", + "flow", + "flow.md", + "seed", + "fetched", + "verify", + "time", + "pop", + "replication", + "behavior", + "fastedge", + "store", + "sdk" + ] + } + ] + }, { "id": "test-config", "title": "test-config Reference", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/test-config.md", "skill": "test", "category": "testing", @@ -40113,11 +43965,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40129,7 +43981,7 @@ "request", "cdn" ], - "content_sha256": "009f7caa6b308ade8af63bdd256f2e553729893224f02cf420a8b5275e280424", + "content_sha256": "cf3e921e482468b97114a9494e3123f97e275060da3afffe2206862b7ffe0469", "sections": [ { "id": "test-config#introduction", @@ -40144,11 +43996,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40425,7 +44277,7 @@ { "id": "test-framework", "title": "FastEdge Test Framework API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/test-framework.md", "skill": "test", "category": "testing", @@ -40442,11 +44294,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "framework", "api", @@ -40457,7 +44309,7 @@ "runner", "apps." ], - "content_sha256": "538ad571eed95fce3e11226e6ac4355e725c8370b05c21a4bf3854ba991b527b", + "content_sha256": "e125a779b09525ed8048d05e09b9979c2ceea4c1c90bbc4a988c1ce98a9fcda1", "sections": [ { "id": "test-framework#introduction", @@ -40472,11 +44324,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "test", "framework", @@ -40841,7 +44693,7 @@ { "id": "vscode-debugger", "title": "FastEdge Visual Debugger", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge-cursor/skills/test/reference/vscode-debugger.md", "skill": "test", "category": "testing", @@ -40855,11 +44707,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", @@ -40871,7 +44723,7 @@ "gcoredev", "same" ], - "content_sha256": "dca66acac8714a6340718db344e69a3189c65884f062ac5b3a725420213a3e85", + "content_sha256": "627d9b7700750420febf279bfb44b3cea3ea212faa41800f660fa1f74a5965e5", "sections": [ { "id": "vscode-debugger#introduction", @@ -40886,11 +44738,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", diff --git a/plugins/gcore-fastedge-cursor/rules/fastedge-knowledge.mdc b/plugins/gcore-fastedge-cursor/rules/fastedge-knowledge.mdc index 95b0186..6313825 100644 --- a/plugins/gcore-fastedge-cursor/rules/fastedge-knowledge.mdc +++ b/plugins/gcore-fastedge-cursor/rules/fastedge-knowledge.mdc @@ -40,6 +40,11 @@ Only after all three are confirmed, use **parallel sub-agents** to research. Do - Explicit: "I want a CDN filter", "HTTP app in TypeScript", "Proxy-WASM filter in Rust" - Ambiguous (always ask): "gateway for CDN resources", "auth for CDN traffic", "edge middleware", "SAML at the edge", "protect my CDN" +**Before scaffolding anything**, check `skills/fastedge-docs/reference/templates/catalog.md` — it's +the current list of maintained, ready-to-deploy bolt-on templates (kept up to date automatically +as new ones are added), and hand-building a capability it already covers is very likely the wrong +call. + ### Scaffolding new projects — HARD CONSTRAINT **Never manually create project files (package.json, tsconfig.json, src/index.ts, Cargo.toml, etc.) from scratch.** Always use the `/gcore-fastedge:scaffold` skill, which creates projects from **blueprint reference files** in `skills/scaffold/reference/`. diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md index a278deb..8d0191c 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md @@ -89,6 +89,19 @@ The reference directory is organised in two layers: - `./reference/http/` — HTTP-app patterns extracted from `FastEdge-sdk-js/examples/` (fetch, headers, kv-store, cache, geo-redirect, ab-testing, plus Hono routing/middleware, auth-bearer/JWT, proxy-with-transform) - `./reference/cdn/` — CDN-app patterns from `FastEdge-sdk-rust/examples/cdn/` and `proxy-wasm-sdk-as/examples/` (jwt, geoblock, headers, body, env-secrets, kv-store, properties, ab-testing, etc.) +### Bolt-on templates (pipeline-generated, from `FastEdge-templates`) + +**Read `./reference/templates/catalog.md` before scaffolding or hand-building any capability.** +It lists Gcore's maintained, ready-to-deploy bolt-on templates (installed via the Gcore portal +template gallery, not `/gcore-fastedge:scaffold`) and is kept current automatically as new +templates are added — do not rely on this file's prose to know what's covered, always check the +catalog itself. If the user's request matches an entry, recommend the template instead of +hand-building the same capability. + +If the user has **already deployed** a template and needs help wiring it into their own origin +codebase, the catalog links each template to its own `templates/<name>-integration.md` reference +(when one exists) — use that for the origin-side contract instead of scaffolding anything. + ## Common Questions **Q: How do I set environment variables for my app?** diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md index eff6c0f..0404634 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # A/B Testing — AssemblyScript (CDN) @@ -101,6 +101,8 @@ set_property("request.url", String.UTF8.encode(newUrl)) `set_property` / `get_property` key: `"request.url"`, `"request.path"`, `"request.scheme"`, `"request.host"`, `"request.query"`. +URL rewrite is skipped entirely if `schemeBuf.byteLength === 0` or `hostBuf.byteLength === 0`. + ### Step 5 — Add upstream headers ``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md index 60d90b9..0205b81 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -248,3 +248,191 @@ No additional dependencies. Uses `std::env` and `std::time::UNIX_EPOCH` from the - FastEdge environment variable configuration (setting `EXPERIMENT_NAME`, `VARIANT_A_PATH`, `VARIANT_B_PATH` at deploy time) - examples-geoblock-rust reference (similar CDN proxy-wasm filter structure) - examples-auth-jwt-rust reference (CDN Rust example with cross-hook state pattern) + +## Source Material + +### FILE: examples/cdn/ab_testing/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating A/B traffic splitting at the CDN layer. + +Uses a cookie to assign users to variant A or B, then rewrites the +request path to route to different parts of the origin server. + +Required configuration: + - Environment variable: EXPERIMENT_NAME + - Environment variable: VARIANT_A_PATH (path prefix for variant A) + - Environment variable: VARIANT_B_PATH (path prefix for variant B) +*/ + +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::env; +use std::time::UNIX_EPOCH; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(AbTestingRoot) }); +}} + +struct AbTestingRoot; + +impl Context for AbTestingRoot {} + +impl RootContext for AbTestingRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(AbTestingContext)) + } +} + +struct AbTestingContext; + +impl Context for AbTestingContext {} + +impl HttpContext for AbTestingContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(experiment_name) = env::var("EXPERIMENT_NAME") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - EXPERIMENT_NAME must be set"), + ); + return Action::Pause; + }; + + let Ok(variant_a_path) = env::var("VARIANT_A_PATH") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - VARIANT_A_PATH must be set"), + ); + return Action::Pause; + }; + + let Ok(variant_b_path) = env::var("VARIANT_B_PATH") else { + self.send_http_response( + 500, + vec![], + Some(b"App misconfigured - VARIANT_B_PATH must be set"), + ); + return Action::Pause; + }; + + let cookie_name = format!("fe_exp_{}", experiment_name); + + // Check for existing experiment cookie + let cookie_header = self + .get_http_request_header("Cookie") + .unwrap_or_default(); + let mut assigned = get_cookie_value(&cookie_header, &cookie_name); + + // Assign variant if not already set + if assigned != "A" && assigned != "B" { + let now = self + .get_current_time() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + assigned = if now % 2 == 0 { "A" } else { "B" }.to_string(); + } + + // Rewrite request path + let path = self + .get_property(vec!["request.path"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| "/".to_string()); + + let variant_path = if assigned == "A" { + &variant_a_path + } else { + &variant_b_path + }; + let new_path = format!("{}{}", variant_path, path); + + // Update the request path directly to avoid ambiguous URL rewriting. + self.set_property(vec!["request.path"], Some(new_path.as_bytes())); + + // Add variant headers for upstream visibility + self.add_http_request_header("X-Experiment", &experiment_name); + self.add_http_request_header("X-Variant", &assigned); + + println!( + "A/B test \"{}\": variant {}, path {}", + experiment_name, assigned, new_path + ); + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // Recover the assigned variant and experiment name from the request headers set in + // on_http_request_headers. Instance state does not survive the nginx -> core-proxy hop. + let Some(variant) = self.get_http_request_header("X-Variant") else { + return Action::Continue; + }; + let Some(experiment_name) = self.get_http_request_header("X-Experiment") else { + return Action::Continue; + }; + + let cookie = format!( + "fe_exp_{}={}; Path=/; Max-Age=86400; SameSite=Lax", + experiment_name, variant + ); + self.add_http_response_header("Set-Cookie", &cookie); + self.add_http_response_header("X-Variant", &variant); + + Action::Continue + } +} + +fn get_cookie_value(cookie_header: &str, name: &str) -> String { + if cookie_header.is_empty() { + return String::new(); + } + let prefix = format!("{}=", name); + for pair in cookie_header.split(';') { + let pair = pair.trim(); + if let Some(value) = pair.strip_prefix(&prefix) { + return value.to_string(); + } + } + String::new() +} +``` + + +### FILE: examples/cdn/ab_testing/Cargo.toml + +```toml +[workspace] + +[package] +name = "ab_testing" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + + +### FILE: examples/cdn/ab_testing/README.md + +``` +[← Back to examples](../../README.md) + +# A/B Testing (CDN) + +Cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md index 29a7d5b..fe6573a 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -27,8 +27,8 @@ Validates incoming requests by checking the `X-API-Key` request header against a ## Entry Point -**File:** `assembly/index.ts` -**Root context name:** `"apiKey"` +**File:** `assembly/index.ts` +**Root context name:** `"apiKey"` **Hook:** `onRequestHeaders` --- @@ -164,7 +164,7 @@ Header `X-API-Key` is set to `""` on the forwarded request (platform `.remove()` ## Build -**Package manager:** `npm` (also compatible with `pnpm`) +**Package manager:** `npm` (also compatible with `pnpm`) **SDK dependency:** `@gcoredev/proxy-wasm-sdk-as ^1.2.3` ```sh @@ -199,3 +199,168 @@ Upload `build/apiKey.wasm` to the FastEdge portal and attach it to a CDN applica - proxy-wasm-sdk-as reference (full `Context`, `RootContext`, `FilterHeadersStatusValues` API) - FastEdge secrets management (how to set and rotate application secrets) - CDN app deployment guide + +## Source Material + +### FILE: examples/apiKey/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + HeaderPair, + log, + LogLevelValues, + makeHeaderPair, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class ApiKeyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new ApiKeyContext(context_id, this); + } +} + +class ApiKeyContext extends Context { + constructor(context_id: u32, root_context: ApiKeyRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const expectedKey = getSecret("API_KEY"); + if (expectedKey === "") { + log(LogLevelValues.error, "API_KEY secret not configured"); + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const providedKey = stream_context.headers.request.get("X-API-Key"); + + if (providedKey === "") { + const authHeaders = new Array<HeaderPair>(); + 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-cursor/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md index bc2390d..ca16447 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- capabilities: @@ -62,7 +62,7 @@ Reads a named secret variable configured on the FastEdge application. - **Parameter**: `name` — secret variable name (string) - **Returns**: secret value as a UTF-8 string, or `null` if not found -- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` +- **Type note**: returns `string`, not `ArrayBuffer`; pass directly to `jwtVerify` without encoding ```typescript const secret = getSecret("SECRET"); @@ -82,6 +82,7 @@ Verifies a JWT token against an HMAC-SHA256 secret. - `secret` — HMAC signing secret (string) - **Returns**: `JwtValidation` enum value - **Package**: `@gcoredev/as-jwt` (separate dependency, not part of proxy-wasm-sdk-as) +- **Behavior**: does not throw; always check the return value against `JwtValidation.Ok` ### `JwtValidation` enum @@ -105,11 +106,11 @@ Sends an immediate HTTP response and stops the request. Body must be encoded as ### `setLogLevel(level: LogLevelValues): void` -Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. +Sets the log verbosity. Default is `LogLevelValues.info`. Called in `createContext`. Present in source for demonstration purposes only — explicitly setting the default is optional. ### `log(level: LogLevelValues, message: string): void` -Emits a log entry. Used to record token rejection reasons. +Emits a log entry. Used to record token rejection reasons (e.g. `"Token Expired"`, `"Bad Token"`). --- @@ -150,7 +151,23 @@ All blocked responses return `FilterHeadersStatusValues.StopIteration`. |---|---|---| | `SECRET` | HMAC-SHA256 signing key | String; minimum 256 bits / 32 characters | -Configure this secret variable on the FastEdge application before deployment. For secret rotation, see `getSecretEffectiveAt` in the FastEdge secrets reference. +Configure this secret variable on the FastEdge application before deployment. For secret rotation, use `getSecretEffectiveAt` instead of `getSecret` — see the FastEdge secrets reference. + +--- + +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` --- @@ -167,6 +184,14 @@ Configure this secret variable on the FastEdge application before deployment. Fo - `@gcoredev/as-jwt` is a required peer dependency — it is NOT bundled in proxy-wasm-sdk-as. - `assemblyscript-json` is declared as a dependency but not directly used in this example. +**Dev dependencies:** +```json +{ + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" +} +``` + --- ## Build @@ -208,6 +233,7 @@ Root context name: `"auth"`. - The `Authorization` header is validated in two steps: first a null check (missing header → 401), then a `startsWith("Bearer ")` check (wrong scheme → 401). An empty-string header would fail the Bearer scheme check. - `jwtVerify` does not throw; always check the return value against `JwtValidation.Ok`. - Validation happens in `onRequestHeaders` only. There is no body or response hook in this example. +- The `setLogLevel(LogLevelValues.info)` call in `createContext` is present for demonstration only — `info` is the default level and the call is not required. - For HMAC secret rotation using slot-based secrets, use `getSecretEffectiveAt` instead of `getSecret`. See the FastEdge secrets reference. --- @@ -217,4 +243,4 @@ Root context name: `"auth"`. - proxy-wasm-sdk-as SDK reference (AssemblyScript) - FastEdge secrets reference (`getSecret`, `getSecretEffectiveAt`, rotation slots) - CDN app platform overview -- `@gcoredev/as-jwt` package (npmjs.com) +- `@gcoredev/as-jwt` package on npmjs.com diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md index 6c00fe2..e0e5128 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -337,3 +337,216 @@ When modifying body content: - host-services-rust (for equivalent Rust patterns) - platform-overview (CDN app lifecycle, hook execution order) - examples-headers-as (header-only manipulation without body buffering) + +## Source Material + +### FILE: examples/body/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BufferTypeValues, + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_buffer_bytes, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class HttpBodyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug + return new HttpBody(context_id, this); + } +} + +class HttpBody extends Context { + constructor(context_id: u32, root_context: HttpBodyRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool + ): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); + // Remove the "content-length" header + stream_context.headers.request.remove("content-length"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + <u32>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, + <u32>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, + <u32>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-cursor/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md index 0010810..f135ed3 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -228,3 +228,191 @@ Build targets: - FastEdge CDN app scaffold reference - platform-overview (CDN application lifecycle and hook execution model) - best-practices (header mutation, environment variable patterns) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=<STATIC_MAX_AGE>, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=<HTML_MAX_AGE>, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=<API_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-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md index f36b937..baedbd0 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # CDN Example: Convert Image (Rust) @@ -202,300 +202,3 @@ Transformation is skipped (returning `Action::Continue` without modifying the re - FastEdge CDN app platform overview - FastEdge SDK Rust reference - FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/convert_image/src/lib.rs - -```rust -use image::*; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::{env, env::VarError, io::Cursor, str::from_utf8}; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(ConvertImageRoot) }); -}} - -struct ConvertImageRoot; - -impl Context for ConvertImageRoot {} - -impl RootContext for ConvertImageRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(ConvertImageContext)) - } -} - -struct ConvertImageContext; - -impl Context for ConvertImageContext {} - -impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // this header is used to select correct image version from cache - self.add_http_request_header("Image-Format", "original"); - - // get extension - let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); - println!("request.path={path:?}"); - let raw_ext = self.get_property(vec!["request.extension"]); - println!("request.extension={raw_ext:?}"); - let Some(ext) = raw_ext else { - println!("No extension in request path, not transforming"); - return Action::Continue; - }; - let Ok(ext) = from_utf8(&ext) else { - println!("Invalid UTF-8 in request extension, not transforming"); - return Action::Continue; - }; - if ext.is_empty() { - println!("No extension in request path, not transforming"); - return Action::Continue; - } - - // FORMATS_TO_TRANSFORM contains list of file extensions to transfor - // note that jpg and jpeg are different extensions - let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { - println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); - return Action::Continue; - }; - if !image_list.split(',').any(|entry| entry == ext) { - println!( - "extension {} is not in the list of formats to transform: {}, not transforming", - ext, image_list - ); - return Action::Continue; - } - - // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed - let Some(ua) = self.get_http_request_header("User-Agent") else { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - }; - if ua.is_empty() { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - } - if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { - if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { - println!("User-Agent is in ignore list, not transforming"); - return Action::Continue; - } - } - - // indicator for on_response_headers and for cache key - self.set_http_request_header("Image-Format", Some("image/avif")); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // only process 200 responses - if let Some(status) = self.rsp_status() { - if status != 200 { - println!( - "Response status is {} instead of expected 200, not transforming", - status - ); - return Action::Continue; - } - } else { - println!("Response status is not set, not transforming"); - return Action::Continue; - } - - // if "Image-Format" request header is not set, don't convert the image - let Some(content_type) = self.get_http_request_header("Image-Format") else { - return Action::Continue; - }; - // instruct cache to vary by this header so "original" and "image/avif" are cached separately - self.add_http_response_header("Vary", "Image-Format"); - - if content_type == "original" { - return Action::Continue; - }; - - // image to be transformed, set headers accordingly - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some(content_type.as_str())); - - // indicate to on_http_response_body that transformation is needed - self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // wait till we get complete body - return Action::Pause; - } - - let Some(content_type) = self.get_property(vec!["response.content-type"]) else { - return Action::Continue; - }; - - let Ok(content_type) = from_utf8(&content_type) else { - // should never happen - println!("Invalid UTF-8 in Content-Type"); - self.send_http_response(500, vec![], None); - return Action::Pause; - }; - - if content_type != "image/avif" { - // should never happen - println!( - "Content-Type {} is not supported, not transforming", - content_type - ); - return Action::Continue; - } - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let buf = body_bytes.as_bytes(); - let img = match load_from_memory(buf) { - Ok(i) => i, - Err(e) => { - println!("cannot load image to memory {}, not converting", e); - return Action::Continue; - } - }; - - let mut out = Vec::new(); - let mut c = Cursor::new(&mut out); - let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( - &mut c, - u8_param("AVIF_SPEED", 1, 10, 5), - u8_param("AVIF_QUALITY", 1, 100, 70), - )); - - match res { - Ok(_) => { - println!( - "{} bytes -> {} bytes {}", - body_size, - out.len(), - content_type - ); - self.set_http_response_body(0, body_size, &out) - } - Err(e) => println!("cannot store transformed image {}", e), - } - } else { - println!("No response body to transform"); - } - - Action::Continue - } -} - -impl ConvertImageContext { - fn rsp_status(&mut self) -> Option<u16> { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() != 2 { - println!("HTTP status property is not 2 bytes"); - return None; - } - return Some(u16::from_be_bytes([status[0], status[1]])); - } - None - } -} - -fn str_param(name: &str) -> Result<String, VarError> { - let val = env::var(name)?; - if val.is_empty() { - return Err(VarError::NotPresent); - } - - Ok(val) -} - -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { - let Ok(val) = env::var(name) else { - println!("Param {} is not set, using default value {}", name, default); - return default; - }; - if val.is_empty() { - println!("Param {} is not set, using default value {}", name, default); - return default; - } - - let val = match val.parse() { - Err(_) => { - println!( - "Param {} is not a valid number, using default value {}", - name, default - ); - return default; - } - Ok(v) => v, - }; - if val < min { - println!( - "Param {} is below minimum {}, using default value {}", - name, min, default - ); - return default; - } - if val > max { - println!( - "Param {} is above maximum {}, using default value {}", - name, max, default - ); - return default; - } - - val -} -``` - - -### FILE: examples/cdn/convert_image/Cargo.toml - -```toml -[workspace] - -[package] -name = "convert_image" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -image = "0.25" -``` - - -### FILE: examples/cdn/convert_image/README.md - -``` -[← Back to examples](../../README.md) - -# Convert Image (CDN) - -Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. - -## Configuration - -- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) -- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip -- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) -- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) - -## How it works - -1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation -2. **on_response_headers** — sets response headers for AVIF content type on 200 responses -3. **on_response_body** — decodes the original image and re-encodes it as AVIF -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md index c1ff635..613b6e1 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CORS — AssemblyScript (CDN) @@ -193,3 +193,159 @@ pnpm run asbuild - FastEdge CDN application environment variables configuration - platform-overview (CDN app lifecycle and OPTIONS handling) - best-practices (header mutation patterns, Vary usage) + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md index 3ccdd4c..9ec6651 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -171,6 +171,24 @@ set_buffer_bytes( | other 5xx | Server Error | | other | Error | +### Descriptions + +| Code | Description | +|---|---| +| 400 | The server could not understand the request due to invalid syntax. | +| 401 | You need to authenticate to access this resource. | +| 403 | You do not have permission to access this resource. | +| 404 | The requested page could not be found. It may have been moved or deleted. | +| 405 | The request method is not supported for this resource. | +| 408 | The server timed out waiting for the request. | +| 429 | You have sent too many requests. Please try again later. | +| 500 | The server encountered an unexpected condition that prevented it from fulfilling the request. | +| 502 | The server received an invalid response from the upstream server. | +| 503 | The server is temporarily unavailable. Please try again later. | +| 504 | The server did not receive a timely response from the upstream server. | +| other 5xx | The server encountered an error processing your request. | +| other | An error occurred processing your request. | + ### Categories | Range | Category label | diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md index 1fe604d..90a1205 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> ## Custom Error Pages — CDN (Rust) @@ -13,6 +13,8 @@ Intercepts 4xx and 5xx HTTP error responses at the CDN edge and replaces their bodies with branded HTML pages rendered via Handlebars templates. Use this pattern when you need consistent, styled error pages served from the edge regardless of what the origin returns. +A build script (`build.rs`) runs at compile time to embed images and messages from the `public/` folder into the WASM binary — there is no filesystem at runtime. + --- ### API Patterns @@ -188,6 +190,29 @@ let (message, description) = message_map }); ``` +**Pattern 4 — Root context and HTTP context wiring:** + +```rust +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpBody)) + } +} +``` + --- ### Dependencies (`Cargo.toml`) @@ -203,11 +228,40 @@ regex = "1.10" base64 = "0.22" ``` -- `proxy-wasm`: CDN lifecycle hooks and context traits -- `handlebars`: template rendering for error pages and message strings -- `serde_json`: JSON data construction for template variables -- `regex`: available for pattern matching (used in build script or message processing) -- `base64` (build dependency only): encodes images into the generated map at compile time +| Crate | Role | +|-------|------| +| `proxy-wasm` | CDN lifecycle hooks and context traits | +| `handlebars` | Template rendering for error pages and message strings | +| `serde_json` | JSON data construction for template variables | +| `regex` | Pattern matching (available for use in build script or message processing) | +| `base64` (build-dep only) | Encodes images into the generated map at compile time | + +Crate type must be `cdylib`: + +```toml +[lib] +crate-type = ["cdylib"] +``` + +--- + +### Project Layout + +``` +custom_error_pages/ +├── build.rs # Generates image_map.rs and message_map.rs into OUT_DIR +├── Cargo.toml +├── src/ +│ └── lib.rs # HttpContext implementation +├── templates/ +│ └── error_page.hbs # Handlebars HTML page template +└── public/ + ├── styles.css # Embedded at compile time via include_str! + ├── images/ + │ └── <status>.jpg # Per-status images; also 4000.jpg / 5000.jpg for fallbacks + └── messages/ + └── <status>.hbs # First line = title, second line = description +``` --- @@ -215,7 +269,7 @@ base64 = "0.22" 1. Add an image: `public/images/<status>.jpg` 2. Add a message file: `public/messages/<status>.hbs` (first line = title, second line = description) -3. Recompile and redeploy — the build script regenerates the embedded maps +3. Recompile and redeploy — the build script regenerates the embedded maps automatically --- @@ -230,10 +284,11 @@ base64 = "0.22" - **Handlebars rendering is two-stage.** Message and description strings may themselves contain `{{status}}` placeholders. Render them first with `json!({ "status": ... })`, then pass the rendered strings into the final page template. Registering and rendering in a single pass will not expand variables inside message/description values. - **`handlebars::Handlebars::render` panics on template registration failure if `.unwrap()` is used.** In production code, handle `register_template_string` and `render` errors explicitly unless the templates are known-valid at compile time. - **`get_property` returns `Option<Vec<u8>>` in both hooks.** The property may be absent even for valid responses. Always handle the `None` case before attempting to decode. +- **Generic fallback keys use sentinel values, not HTTP status ranges.** The image and message maps use `4000` (not `400`) for the generic 4xx fallback and `5000` (not `500`) for the generic 5xx fallback. These are not valid HTTP status codes — they are sentinel keys used only in the embedded maps. --- -### Related +### See Also - CDN apps reference — proxy-wasm lifecycle hooks (`on_http_response_headers`, `on_http_response_body`), `HttpContext` trait, `Action` enum, and `get_property` / `set_http_response_header` / `set_http_response_body` signatures - Host services reference — KV store, secrets, and dictionary APIs available in CDN apps diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md index bbfd2a1..ec7dc83 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -250,3 +250,123 @@ self.send_http_response(status_code: u32, headers: Vec<(&str, &str)>, body: Opti - CDN app pattern guide (see the _docs-pattern-cdn reference) - Host services reference for Rust (see the host-services-rust reference) - Platform overview (see the platform-overview reference) + +## Source Material + +### FILE: examples/cdn/custom/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::time::Duration; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); +}} + +const BAD_REQUEST: u32 = 400; + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpHeaders)) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders; + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Some(path) = self.get_property(vec!["request.path"]) else { + self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); + return Action::Pause; + }; + + let Ok(path) = std::str::from_utf8(&path) else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - not utf8 string"), + ); + return Action::Pause; + }; + + //trim first '/' + let path = if path.starts_with('/') { + &path[1..] + } else { + path + }; + let mut segments = path.split('/'); + + let Some(status_code) = segments.next() else { + return Action::Continue; + }; + + if let Some(delay) = segments.next() { + if let Ok(delay) = delay.parse::<u64>() { + std::thread::sleep(Duration::from_millis(delay)); + } + } + + let Ok(status_code) = status_code.parse::<u32>() else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - invalid status code"), + ); + return Action::Pause; + }; + + match status_code { + 0 | 200 => Action::Continue, + code if code < 600 => { + self.send_http_response(code, vec![], None); + Action::Pause + } + _ => { + self.send_http_response(BAD_REQUEST, vec![], None); + Action::Pause + } + } + } +} +``` + +### FILE: examples/cdn/custom/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/custom/README.md + +``` +[← Back to examples](../../README.md) + +# Custom (CDN) + +Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md index 0c9c162..541ac7c 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- type: example @@ -225,3 +225,140 @@ import { - examples-headers-cdn-as (request/response header manipulation patterns) - platform-overview (secret management and deployment configuration) - best-practices (logging levels and secret handling guidelines) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_<NAME>` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_<NAME>` 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-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md index e954ccc..61f35b9 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -199,6 +199,17 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { } ``` +## Multi-Value Headers + +`new-header-03` is deliberately added twice — `add()` is called with the same name twice. This produces a multi-value header with two separate `new-header-03` entries. The validation pattern uses `Set<string>` of `"name:value"` pairs to assert both values are present. + +## Cross-Phase Response Header Writes + +`stream_context.headers.response.add(...)` can be called during `onRequestHeaders`. Headers written in the request phase appear in the final response alongside those set in `onResponseHeaders`. The distinction: + +- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase +- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase + ## Error Response Codes Used | Code | Meaning | Trigger | @@ -239,9 +250,7 @@ onLog(): void { **`replace()` upserts on FastEdge**: `replace()` creates the header with the given value if the named header does not exist — it does not behave as a no-op on absent headers. Guard calls to `replace()` with a prior `get()` length check when you intend to update only an existing header. -**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. The distinction: -- `stream_context.headers.response.add("new-response-header", "value-01")` — **safe** in request phase -- `stream_context.headers.response.get("new-response-header")` — **panics** in request phase +**Response header reads during request phase**: Reading `stream_context.headers.response` (e.g. via `get()`) during `onRequestHeaders` causes a runtime panic because response headers are not available in the request phase. Writing response headers via `add()` during `onRequestHeaders` is safe and those headers appear in the final response. ## Build diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md index b327ddc..97aea43 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -230,3 +230,356 @@ All error paths return `Action::Pause`. - examples-body-cdn-rust (body manipulation API) - examples-shared-data-cdn-rust (shared data API) - host-services-rust reference (full ABI surface) + +## Source Material + +### FILE: examples/cdn/headers/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::collections::HashSet; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); +}} + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpHeaders { context_id })) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders { + context_id: u32, +} + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_request_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_request_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_request_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_request_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_request_header("new-header-01", "value-01"); + self.add_http_request_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_request_header("new-header-02", "value-02"); + self.add_http_request_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_request_header("new-header-03", "value-03"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_request_header("new-header-01", None); + self.set_http_request_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_request_header("new-header-02", Some("new-value-02")); + self.set_http_request_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_request_header("new-header-03", "value-03-a"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // try to set/add response headers + self.add_http_response_header("new-response-header", "value-01"); + self.set_http_response_header("cache-control", None); + self.set_http_response_header("new-response-header", Some("value-02")); + + // get new headers + let headers = self + .get_http_request_headers() + .into_iter() + .collect::<HashSet<(String, String)>>(); + let headers_bytes = self + .get_http_request_headers_bytes() + .into_iter() + .collect::<HashSet<(String, Bytes)>>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::<HashSet<_>>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::<HashSet<_>>(); + + let diff = headers + .difference(&original_headers) + .collect::<HashSet<_>>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::<HashSet<_>>(); + + let diff = diff.difference(&expected).collect::<Vec<_>>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::<Vec<_>>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + // check if the response header is not returned + if self.get_http_response_header("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + if self.get_http_response_header_bytes("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + + let response_headers = self.get_http_response_headers(); + if response_headers.len() != 1 { + self.send_http_response(555, vec![], None); + return Action::Pause; + } + let Some((name, value)) = response_headers.into_iter().next() else { + self.send_http_response(555, vec![], None); + return Action::Pause; + }; + if name != "new-response-header" || value != "value-02" { + self.send_http_response(556, vec![], None); + return Action::Pause; + } + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_response_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_response_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_response_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_response_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_response_header("new-header-01", "value-01"); + self.add_http_response_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_response_header("new-header-02", "value-02"); + self.add_http_response_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_response_header("new-header-03", "value-03"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_response_header("new-header-01", None); + self.set_http_response_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_response_header("new-header-02", Some("new-value-02")); + self.set_http_response_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_response_header("new-header-03", "value-03-a"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // get new headers + let headers = self + .get_http_response_headers() + .into_iter() + .collect::<HashSet<(String, String)>>(); + let headers_bytes = self + .get_http_response_headers_bytes() + .into_iter() + .collect::<HashSet<(String, Bytes)>>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::<HashSet<_>>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::<HashSet<_>>(); + + let diff = headers + .difference(&original_headers) + .collect::<HashSet<_>>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::<HashSet<_>>(); + + let diff = diff.difference(&expected).collect::<Vec<_>>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::<Vec<_>>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + Action::Continue + } +} +``` + +### FILE: examples/cdn/headers/Cargo.toml + +```toml +[workspace] + +[package] +name = "headers" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/headers/README.md + +``` +[← Back to examples](../../README.md) + +# Headers (CDN) + +Validates and manipulates HTTP request and response headers using the proxy-wasm ABI. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md index 3e5ef9b..9509c21 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> ## Overview @@ -123,6 +123,8 @@ import { } from "@gcoredev/proxy-wasm-sdk-as/assembly"; import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +const INTERNAL_SERVER_ERROR: u32 = 500; + function handleHttpCallResponse( ctx: BaseContext, hdrs: u32, @@ -133,10 +135,12 @@ function handleHttpCallResponse( log(LogLevelValues.error, "HTTP call failed — no response received"); return; } + const userAgent = stream_context.headers.http_callback.get("user-agent"); if (userAgent !== "") { log(LogLevelValues.info, "User-Agent: " + userAgent); } + if (bodySize > 0) { const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpCallResponseBody, @@ -144,30 +148,43 @@ function handleHttpCallResponse( bodySize as u32, ); const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "Response body: " + bodyStr); + log( + LogLevelValues.info, + "Response body (" + bodySize.toString() + " bytes): " + bodyStr, + ); + } else { + log(LogLevelValues.info, "Response body: empty"); } } -class MyRoot extends RootContext { +class HttpCallRoot extends RootContext { createContext(context_id: u32): Context { setLogLevel(LogLevelValues.info); - return new MyContext(context_id, this); + return new HttpCallContext(context_id, this); } } -class MyContext extends Context { +class HttpCallContext extends Context { httpCallDispatched: bool = false; - constructor(context_id: u32, root_context: MyRoot) { + constructor(context_id: u32, root_context: HttpCallRoot) { super(context_id, root_context); } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Latch prevents re-dispatch on second invocation after callback fires. + // FastEdge re-invokes this hook after the httpCall response is processed. + // The latch gates re-dispatch so the second invocation returns Continue + // instead of firing another HTTP call. if (this.httpCallDispatched) { + log( + LogLevelValues.info, + "HTTP call response received, resuming request.", + ); return FilterHeadersStatusValues.Continue; } + log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); + const headers = new Array<HeaderPair>(); headers.push(makeHeaderPair(":scheme", "https")); headers.push(makeHeaderPair(":authority", "httpbin.org")); @@ -175,7 +192,8 @@ class MyContext extends Context { headers.push(makeHeaderPair(":method", "GET")); headers.push(makeHeaderPair("User-Agent", "fastedge")); - const result = (this.root_context as MyRoot).httpCall( + // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. + const result = (this.root_context as HttpCallRoot).httpCall( "httpbin.org", headers, new ArrayBuffer(0), @@ -186,8 +204,12 @@ class MyContext extends Context { ); if (result != WasmResultValues.Ok) { + log( + LogLevelValues.error, + "Failed to dispatch HTTP call: " + result.toString(), + ); send_http_response( - 500, + INTERNAL_SERVER_ERROR, "internal server error", String.UTF8.encode("Failed to dispatch HTTP call"), [], @@ -196,13 +218,15 @@ class MyContext extends Context { } this.httpCallDispatched = true; + log(LogLevelValues.info, "HTTP call dispatched, pausing request"); + return FilterHeadersStatusValues.StopIteration; } } registerRootContext((context_id: u32) => { - return new MyRoot(context_id); -}, "myApp"); + return new HttpCallRoot(context_id); +}, "httpCall"); ``` ### Callback: check failure, read headers, read body @@ -239,7 +263,7 @@ function handleHttpCallResponse( ### Error response on dispatch failure ```typescript -const result = (this.root_context as MyRoot).httpCall( +const result = (this.root_context as HttpCallRoot).httpCall( "upstream-host", headers, new ArrayBuffer(0), @@ -260,16 +284,37 @@ if (result != WasmResultValues.Ok) { } ``` +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +| File | Description | +|------|-------------| +| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | +| `build/httpCall-debug.wasm` | Debug binary with source maps | + +Dependencies (from `package.json`): + +| Package | Role | +|---------|------| +| `@gcoredev/proxy-wasm-sdk-as` | SDK — required runtime dep (`^1.2.3`) | +| `assemblyscript` | AssemblyScript compiler (`^0.28.9`) | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (`^0.1.0`) | + ## Gotchas - **No closures over mutable state.** AssemblyScript does not support closures that capture mutable variables. Capture state on the `Context` instance (instance fields) or use `set_property`/`get_property`. The `httpCallDispatched: bool` latch is an instance field for this reason. - **FastEdge resume model differs from canonical proxy-wasm.** After the callback fires, the runtime **re-invokes `onRequestHeaders` on the same `Context` instance**. Do not call `continueRequest()` — it has no effect on FastEdge. Use a latch field to distinguish the second invocation. -- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as MyRoot).httpCall(...)`. +- **`httpCall` must be called on `RootContext`.** Request-stream `Context` instances do not expose `httpCall` directly. Cast with `(this.root_context as HttpCallRoot).httpCall(...)`. - **Instance state survives re-entry within the same context** but does not persist across the nginx→core-proxy hop. Do not rely on instance fields across separate request lifecycles. - **Tune `timeoutMs` per upstream.** The example uses `3000` ms to accommodate cold DNS and variable network conditions. Set a tighter value for low-latency upstreams and a looser value for slow third-party services. - **No try/catch.** AssemblyScript has no exception handling at runtime. Always check `WasmResultValues.Ok` explicitly after `httpCall()` returns. - **Pseudo-headers are required.** Include `:scheme`, `:authority`, `:path`, and `:method` in the headers array. Missing pseudo-headers will cause dispatch to fail. - **`hdrs == 0` means failure, not "no headers".** Inside the callback, `hdrs == 0` signals that the HTTP call itself failed (timeout, DNS error, invalid cluster). Do not skip this check. +- **Log the dispatch failure result.** When `result != WasmResultValues.Ok`, log `result.toString()` to surface the specific `WasmResultValues` variant for debugging. ## Related diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md index b2bf6a4..3959d1b 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # HTTP Call — CDN (Rust) @@ -244,27 +244,7 @@ on_http_call_response(token_id, num_headers, body_size, _) --- -## Gotchas - -- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. -- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. -- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. -- The callback `on_http_call_response` is defined on the `Context` trait, not `HttpContext` — implement it on the per-request struct, not the root context. -- `num_headers == 0` is the only reliable signal for call failure; do not rely on `body_size` or `token_id` for failure detection. -- `:authority` in headers must match the `upstream` argument passed to `dispatch_http_call`. - ---- - -## See Also - -- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) -- CDN app scaffold blueprint -- FastEdge platform overview -- FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/http_call/src/lib.rs +## Complete Example ```rust use proxy_wasm::traits::*; @@ -305,7 +285,6 @@ impl Context for HttpHeaders { println!( "Received http call response with token id: {token_id}, num_headers: {num_headers}" ); - //If num_headers is 0, then the HTTP call failed. if num_headers != 0 { let headers = self.get_http_call_response_headers(); let headers_str = headers @@ -315,7 +294,7 @@ impl Context for HttpHeaders { .join(","); println!("Response headers: [{}]", headers_str); - self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. + self.state = 1; self.resume_http_request(); } else { @@ -376,32 +355,22 @@ fn to_status_code(status: Status) -> u32 { } ``` +--- -### FILE: examples/cdn/http_call/Cargo.toml - -```toml -[workspace] - -[package] -name = "http_call" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` - +## Gotchas -### FILE: examples/cdn/http_call/README.md +- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. +- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. +- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. +- The callback `on_http_call_response` is defined on the `Context` trait, not `HttpContext` — implement it on the per-request struct, not the root context. +- `num_headers == 0` is the only reliable signal for call failure; do not rely on `body_size` or `token_id` for failure detection. +- `:authority` in headers must match the `upstream` argument passed to `dispatch_http_call`. -``` -[← Back to examples](../../README.md) +--- -# HTTP Call (CDN) +## See Also -Makes asynchronous HTTP calls to external services with timeout handling using the proxy-wasm ABI. -``` +- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) +- CDN app scaffold blueprint +- FastEdge platform overview +- FastEdge error codes reference diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md index 3517ddd..dd4a9e0 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -321,6 +321,8 @@ npm run asbuild:release # release only - `onResponseBody` must buffer until `end_of_stream` is `true`; return `StopIterationAndBuffer` otherwise - `min` and `max` for `zrange` are received as strings from query params and must be parsed with `parseFloat` - `response.status` set via `set_property` in `onResponseBody` is advisory only — the origin HTTP status passes through to the client; the JSON error body is the authoritative error signal +- Empty query string is treated as an error — app responds with `545` and a JSON error body +- `validateQueryParams` returns a map containing key `"error"` on failure; the caller must check `params.has("error")` before proceeding --- diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md index eecbdf0..e88ed80 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -65,7 +65,7 @@ self.send_http_response(BAD_REQUEST, vec![], None); **Steps**: 1. Reads `Content-Type` response header via `self.get_http_response_header("Content-Type")`. -2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. +2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. Detection uses `starts_with`, so parameters (e.g. `text/plain; charset=utf-8`) are also matched. 3. If matched: - Removes `Content-Length` by setting to `None` (required before body replacement to avoid length mismatch). - Sets `Transfer-Encoding` to `"Chunked"`. diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-properties-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-properties-as.md index 7d4571b..f293ce8 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-properties-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # CDN Runtime Properties — AssemblyScript @@ -117,17 +117,26 @@ set_property("request.path", String.UTF8.encode("/new-path")); const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); - const params = queryString.split("&").map<Array<string>>((pair) => pair.split("=")); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map<Array<string>>((pair) => pair.split("=")); + for (let i = 0; i < params.length; i++) { const param = params[i]; - if (param.length !== 2) continue; + if (param.length !== 2) { + continue; // Skip invalid query parameters + } const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); set_property(REQUEST_PATH, String.UTF8.encode(value)); } } @@ -178,11 +187,75 @@ class Properties extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Error codes 551–559 identify the absent property via the HTTP response status. - // request.extension (555) and request.query (556) are optional — never trigger an error. - // Read, log, and expose all known properties as response headers. - // Return FilterHeadersStatusValues.StopIteration on any required missing property. - // Return FilterHeadersStatusValues.Continue on success. + // Error codes 551–559 identify the absent property via the HTTP response status: + // 551=uri 552=host 553=path 554=scheme 555=extension 556=query 557=x_real_ip 558=country 559=city + if (!this.handleProperty(REQUEST_URI, 551, "uri", "request-uri")) { + return FilterHeadersStatusValues.StopIteration; + } + + // host must be present for upstream routing; validated but not logged or exposed as a response header + if (!this.handleProperty(REQUEST_HOST, 552)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_PATH, 553, "path", "request-path")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_SCHEME, 554, "scheme", "request-scheme")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_EXTENSION, 555, "extension", "request-extension", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_QUERY, 556, "query", "request-query", true)) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_X_REAL_IP, 557, "client_ip", "request-x-real-ip")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_COUNTRY, 558, "country", "request-country")) { + return FilterHeadersStatusValues.StopIteration; + } + + if (!this.handleProperty(REQUEST_CITY, 559, "city", "request-city")) { + return FilterHeadersStatusValues.StopIteration; + } + + // Handle query parameters — override request.url, request.host, request.path via url=, host=, path= params + const query = get_property(REQUEST_QUERY); + if (query.byteLength !== 0) { + const queryString = String.UTF8.decode(query); + log(LogLevelValues.info, "query=" + queryString); + const params = queryString + .split("&") + .map<Array<string>>((pair) => pair.split("=")); + + for (let i = 0; i < params.length; i++) { + const param = params[i]; + if (param.length !== 2) { + continue; + } + const key = param[0]; + const value = param[1]; + if (key.toLowerCase() === "url") { + log(LogLevelValues.info, `change url to: ${value}`); + set_property(REQUEST_URI, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "host") { + log(LogLevelValues.info, `change host to: ${value}`); + set_property(REQUEST_HOST, String.UTF8.encode(value)); + } else if (key.toLowerCase() === "path") { + log(LogLevelValues.info, `change path to: ${value}`); + set_property(REQUEST_PATH, String.UTF8.encode(value)); + } + } + } + + return FilterHeadersStatusValues.Continue; } onLog(): void { @@ -208,7 +281,12 @@ class Properties extends Context { } return true; } - send_http_response(errorCode, "internal server error", String.UTF8.encode("Internal server error"), []); + send_http_response( + errorCode, + "internal server error", + String.UTF8.encode("Internal server error"), + [] + ); return false; } const value = String.UTF8.decode(valueArr); @@ -323,6 +401,7 @@ Upload `build/properties.wasm` to the FastEdge portal and attach to a CDN applic - **`request.extension` and `request.query` are optional**: They may legitimately be absent (no file extension in path, no query string). Use `allowEmpty: true` — log an empty value and continue without adding a response header. - **`request.host` is validated but not exposed**: It must be non-empty for upstream routing to work correctly. It is not logged and not added as a response header. - **`set_property` on request properties takes effect immediately** for subsequent property reads and downstream filter processing within the same request. +- **Query parameter override logs change**: When a query parameter overrides a property, the change is logged with a message of the form `` `change url to: ${value}` ``, `` `change host to: ${value}` ``, or `` `change path to: ${value}` ``. - **Lifecycle constraint**: Request properties (`request.*`) are only meaningful during request processing phases. Attempting to read them outside `onRequestHeaders` or `onRequestBody` may yield empty buffers. - **Query parameter parsing is manual**: The SDK provides no built-in query string parser. Split on `&`, then on `=`, and validate `param.length === 2` before accessing indices. - **`setLogLevel` must be called in `createContext`**: Log level is set to `LogLevelValues.info` inside `PropertiesRoot.createContext`, not in the constructor of the `Properties` context class. diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md index 0d5fa22..d1e2c6e 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> # A/B Testing — FastEdge Example @@ -207,3 +207,121 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **`xid` range**: `Math.random()` can return `0` but not `1`. `slice(1, 5)` on `"0.473..."` yields `".473"` — always a 4-character string starting with `.`. - **Empty cookie after strip**: If `x-fastedge-abid` was the only cookie, the `cookie` header is deleted entirely rather than set to an empty string. - **Last variant not assigned**: If `xid * 100` falls exactly at or beyond the sum of all normalized percentages (due to floating-point rounding), no variant header is set for that test. In practice this is extremely rare given `xid` is always `< 1`. + +## Source Material + +### FILE: examples/ab-testing/src/index.js + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(request); + + const headers = createAbTestHeaders(slicedHeaders, testConfig, xid); + + // This is the URL of the outbound service - i.e. could be a url to your origin + // e.g. https://template-invoice-ab-test-123456.fastedge.cdn.gc.onl/ + const outboundUrl = getEnv('OUTBOUND_URL'); + if (!outboundUrl || !String(outboundUrl).trim()) { + return new Response('OUTBOUND_URL environment variable is not configured', { + status: 500, + }); + } + + const response = await fetch(outboundUrl, { headers }); + + // Request/Response Headers are immutable, so we need to create a new Headers object + const resHeaders = new Headers(response.headers); + resHeaders.set( + 'set-cookie', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); + +const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { + // Request/Response Headers are immutable, so we need to create a new Headers object + const headers = new Headers(reqHeaders); + const cookie = headers.get('cookie') || ''; + // Read the existing `xid` cookie value. + const xid = (cookie.match(/(?:^|;) *x-fastedge-abid=((0|1|)\.\d+) *(?:;|$)/u) || [])[1]; + if (xid) { + // Request contains A/B cookie, hide it from the origin + const newCookie = cookie.replace(/x-fastedge-abid=[^;]+;?\s*/gu, ''); + if (newCookie) { + headers.set('cookie', newCookie); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + return headers; +}; +``` + +### FILE: examples/ab-testing/package.json + +```json +{ + "name": "fastedge-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge JS example: cookie-based A/B testing", + "type": "module", + "main": "src/index.js", + "scripts": { + "build": "fastedge-build src/index.js dist/ab-testing.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md index 1dd6bda..d754a7e 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -153,114 +153,3 @@ cargo build --release --target wasm32-wasip1 - platform-overview (FastEdge request lifecycle, outbound request capabilities) - sdk-reference-rust (`fastedge::send_request`, `Body`, HTTP types) - best-practices (body buffering considerations, error handling patterns) - -## Source Material - -### FILE: examples/http/basic/backend/src/lib.rs - -```rust -use anyhow::{anyhow, Error, Result}; -use fastedge::body::Body; -use fastedge::http::{Method, Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request<Body>) -> Result<Response<Body>> { - let (parts, body) = req.into_parts(); - let query = parts - .uri - .query() - .ok_or(anyhow!("missing uri query parameter"))?; - let params = querystring::querify(query); - let url = params - .iter() - .find(|(k, _)| k == &"url") - .ok_or(anyhow!("missing url parameter"))?; - let url = urlencoding::decode(url.1)?.to_string(); - println!("url = {:?}", url); - let request = Request::builder().uri(url).method(Method::GET).body(body)?; - - let response = fastedge::send_request(request).map_err(Error::msg)?; - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "len = {}, content-type = {:?}", - response.body().len(), - response.headers().get("Content-Type") - ))) - .map_err(Error::msg) -} -``` - - -### FILE: examples/http/basic/backend/Cargo.toml - -```toml -[workspace] - -[package] -name = "backend" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -querystring = "1.1" -urlencoding = "2.1" -``` - - -### FILE: examples/http/basic/backend/README.md - -``` -[← Back to examples](../../../README.md) - -# Backend (URL Proxy) - -A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. - -> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). - -## What it does - -1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). -2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. -3. Returns HTTP 200 with a plain-text body: - ``` - len = <body-length>, content-type = <upstream-content-type> - ``` -4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::send_request(request)` | Blocking outbound HTTP request | -| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | -| `fastedge::body::Body` | Request and response bodies | -| `querystring::querify` | Parse query string into key-value pairs | -| `urlencoding::decode` | Percent-decode the `?url=` value | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/backend.wasm -``` - -## Expected behavior - -| Request | Response status | Response body | -|---|---|---| -| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = <N>, content-type = Some("<mime>")` | -| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | -| `GET /` (no query string) | 500 | `missing uri query parameter` | - -The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option<HeaderValue>` debug string (e.g. `Some("application/json")`). -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md index c836e97..21d90c6 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -150,3 +150,162 @@ Uses the `wstd` WASI Component Model HTTP server macro. - KV Store provisioning and population via FastEdge API - examples-bloom-filter-denylist-js (JavaScript mirror of this example) - platform-overview (KV Store concepts) + +## Source Material + +### FILE: examples/http/wasi/bloom_filter_denylist/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Bloom-filter IP denylist example. + +Checks the client IP (from the `x-real-ip` request header, falling back to +`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 +on a hit, 200 otherwise. + +Required configuration: + - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) + +The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; +populate the filter out of band. + +Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::key_value::{Error as StoreError, Store}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +const BLOOM_KEY: &str = "blocked-ips"; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let store_name = match env::var("DENYLIST_STORE") { + Ok(s) if !s.trim().is_empty() => s, + _ => { + return json_response( + 500, + json!({ "error": "DENYLIST_STORE environment variable is not configured" }), + ); + } + }; + + let headers = req.headers(); + let client_ip = headers + .get("x-real-ip") + .or_else(|| headers.get("x-forwarded-for")) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let Some(ip) = client_ip else { + return json_response(500, json!({ "error": "client IP not available" })); + }; + + let store = match Store::open(&store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return json_response( + 403, + json!({ "error": "access denied opening denylist store" }), + ); + } + Err(e) => { + return json_response(500, json!({ "error": format!("store open error: {e}") })); + } + }; + + let blocked = store + .bf_exists(BLOOM_KEY, ip) + .map_err(|e| anyhow!("bf_exists error: {e}"))?; + + if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. + return json_response(403, json!({ "allowed": false, "ip": ip })); + } + + json_response(200, json!({ "allowed": true, "ip": ip })) +} + +fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result<Response<Body>> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(value.to_string()))?) +} +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml + +```toml +[workspace] + +[package] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/README.md + +``` +[← Back to examples](../../../README.md) + +# Bloom Filter — IP Denylist (WASI) + +Rejects requests from IPs present in a KV Store bloom filter. Reads the client IP from the +`x-real-ip` request header (falling back to `x-forwarded-for`), checks it against a +pre-populated bloom filter, and returns **403** on a hit or **200** otherwise. + +Demonstrates `fastedge::key_value::Store` + `bf_exists()` and the conventional way to obtain +the client IP from a Component Model HTTP handler. + +## Configuration + +- Environment variable `DENYLIST_STORE` — name of the KV store that holds the bloom filter. +- Bloom-filter key — hardcoded to `blocked-ips`. Change `BLOOM_KEY` in `src/lib.rs` if your + key is different. + +## Behaviour + +| `bf_exists("blocked-ips", ip)` | Response | +| --- | --- | +| `true` | `403` `{ "allowed": false, "ip": "..." }` | +| `false` | `200` `{ "allowed": true, "ip": "..." }` | + +## Tradeoff: false positives + +Bloom filters answer "**definitely not** in set" vs "**maybe** in set". When `bf_exists` +returns `true`, the IP *probably* was added — but a small fraction of hits will be false +positives, meaning some legitimate IPs will be over-blocked. Acceptable for a denylist; for +allowlists or anything requiring exact membership, use `store.get()` against a regular key +instead. + +## Populating the filter + +The edge handler is read-only. Populate `blocked-ips` out of band (for example, via the +FastEdge API). + +## Related + +Mirror of `FastEdge-sdk-js/examples/bloom-filter-denylist/`. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md index 28ff740..4193334 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -188,3 +188,197 @@ cargo build --release - HTTP outbound requests via wstd (sdk-reference-rust) - Environment variable configuration (platform-overview) - HTTP app deploy workflow (deploy skill) + +## Source Material + +### FILE: examples/http/wasi/cache/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Example app demonstrating response caching and cache purging via the cache interface. + +The app reads ORIGIN_HOST from the environment, forwards the incoming request +to that origin, and caches the response body keyed by the request path. +On subsequent requests for the same path the cached body is returned directly +without hitting the origin. + +Cache reads and writes use the synchronous `fastedge::cache` API; upstream +HTTP I/O still uses the async `wstd` client. + +Special purge routes (handled before any origin call): + GET /purge — purge all cached keys; returns 200 with deleted count + GET /purge/<path_and_query> — purge keys whose cache key starts with cache:/<path_and_query> + +Environment variables: + ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com + CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) + +Build: + cargo build --release +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::cache; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let origin = env::var("ORIGIN_HOST") + .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; + + let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + env::var("CACHE_TTL_MS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(60_000) + }); + + // Build cache key from the request path (and query string if present) + let path_and_query = req + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + + // Handle purge requests before any cache/origin logic + if path_and_query == "/purge" { + let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + if let Some(prefix) = path_and_query.strip_prefix("/purge/") { + let prefix = format!("cache:/{prefix}"); + let deleted = cache::purge_prefix(&prefix)?; + println!("purge prefix '{prefix}': {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + if let Some(prefix) = path_and_query.strip_prefix("/delete/") { + let prefix = format!("cache:/{prefix}"); + cache::delete(&prefix)?; + println!("prefix '{prefix}': removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + let cache_key = format!("cache:{path_and_query}"); + + // Return cached response if available + if let Some(cached) = cache::get(&cache_key)? { + println!("cache hit: {cache_key}"); + return Ok(Response::builder() + .status(200) + .header("content-type", "application/octet-stream") + .header("x-cache", "hit") + .body(Body::from(cached))?); + } + + // Cache miss — forward request to origin + let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); + println!("cache miss: {cache_key} → {upstream_url}"); + + let upstream_req = Request::get(&upstream_url) + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("upstream request failed: {e}"))?; + + let status = upstream_resp.status(); + let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + // Read body bytes + let mut body = upstream_resp.into_body(); + let body_bytes = body.contents().await?.to_vec(); + + // Only cache successful responses + if status.is_success() { + cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; + } + + // Replay original response + let mut builder = Response::builder() + .status(status) + .header("x-cache", "miss"); + for (k, v) in &headers { + builder = builder.header(k, v); + } + Ok(builder.body(Body::from(body_bytes))?) +} +``` + + +### FILE: examples/http/wasi/cache/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/cache/README.md + +``` +[← Back to examples](../../../README.md) + +# Cache (WASI) + +Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | +| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | + +## How it works + +GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) +GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) + +Cache key is `cache:<path>?<query>`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. + +## Build + +cargo build --release +# Output: target/wasm32-wasip2/release/cache_wasi.wasm + +## APIs used + +- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option<Vec<u8>>)` +- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry +- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md index 99f2107..fe37566 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,152 @@ Use `key=value` pairs separated by spaces (`logfmt`-ish format). Benefits: - host-services-rust (other host service integrations available to Rust WASI apps) - CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, different module path (see CDN apps reference) - examples-kv-store-wasi-rust (another Rust WASI HTTP example showing KV Store usage) + +## Source Material + +### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Diagnostic logging example. + +Tiny pass-through proxy that writes a single `set_user_diag` tag per request +summarising the outcome (config_error, origin_unreachable, or proxied). The +tag appears in the FastEdge platform's per-request log viewer and is distinct +from stdout — it's intended for filterable outcome labels, not verbose +traces. + +Required configuration: + - Environment variable: ORIGIN_URL (origin to proxy to) + +Uses `logfmt`-ish formatting (`outcome=<verb> key=value ...`) so the tag is +easy to slice in log search tooling. +*/ + +use std::env; + +use fastedge::utils::set_user_diag; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let method = req.method().as_str().to_string(); + let path = req.uri().path().to_string(); + + let origin = match env::var("ORIGIN_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + set_user_diag("outcome=config_error reason=origin_missing"); + return Ok(Response::builder() + .status(500) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("ORIGIN_URL is not configured"))?); + } + }; + + let outbound = Request::get(&origin).body(Body::empty())?; + let resp = match Client::new().send(outbound).await { + Ok(r) => r, + Err(e) => { + set_user_diag(&format!( + "outcome=origin_unreachable method={method} path={path} err={e}" + )); + return Ok(Response::builder() + .status(502) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("origin unreachable"))?); + } + }; + + let status = resp.status().as_u16(); + set_user_diag(&format!( + "outcome=proxied method={method} path={path} status={status}" + )); + + let (parts, mut body) = resp.into_parts(); + let bytes = body.contents().await?; + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .body(Body::from(bytes))?) +} +``` + + +### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml + +```toml +[workspace] + +[package] +name = "diagnostic_logging_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/diagnostic_logging/README.md + +``` +[← Back to examples](../../../README.md) + +# Diagnostic Logging (WASI) + +Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request +summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, +distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at +per-request outcomes. + +## Configuration + +- `ORIGIN_URL` environment variable — the origin that requests are proxied to. + +## Outcomes + +Each request writes exactly one of: + +| Condition | Tag | +| --- | --- | +| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | +| Origin unreachable | `outcome=origin_unreachable method=<M> path=<P> err=<E>` | +| Request proxied | `outcome=proxied method=<M> path=<P> status=<S>` | + +## `set_user_diag` vs `println!` + +| | `println!` | `set_user_diag` | +| --- | --- | --- | +| Channel | stdout — general application logs | per-request structured tag in platform log viewer | +| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | +| Best for | verbose traces, debug details | a single filterable outcome label | +| Forbidden | — | secrets and PII (tags appear in platform logs) | + +## Convention + +Call `set_user_diag` **once**, on every branch, late enough in the handler to know the +outcome. The `logfmt`-ish format (`outcome=<verb> key=value key=value …`) is easy to slice in +log search tooling — keep keys short and stable so they make good filter terms. + +## Related + +CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, +different module path. See `docs/CDN_APPS.md`. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md index 2a9166c..41b44c6 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,117 @@ cargo build --release - host-services-rust — `wstd` HTTP types: `Request`, `Response`, `Body` - examples-headers-wasi-rust — general header reading patterns - examples-env-vars-wasi-rust — environment variable access patterns + +## Source Material + +### FILE: examples/http/wasi/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example WASI-HTTP app demonstrating geo-based redirects. + +Reads the country code from the geoip-country-code request header +and redirects to a country-specific origin URL. Falls back to +BASE_ORIGIN when no country-specific mapping is configured. + +Required configuration: + - Environment variable: BASE_ORIGIN (fallback origin URL) + - Environment variable: <COUNTRY_CODE> (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use std::env; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let base_origin = match env::var("BASE_ORIGIN") { + Ok(origin) => origin, + Err(_) => { + return Ok(Response::builder() + .status(500) + .body(Body::from("BASE_ORIGIN is not set"))?); + } + }; + + let country_code = req + .headers() + .get("geoip-country-code") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let redirect_origin = if !country_code.is_empty() { + env::var(&country_code).unwrap_or(base_origin) + } else { + base_origin + }; + + Ok(Response::builder() + .status(302) + .header("location", &redirect_origin) + .body(Body::empty())?) +} +``` + +### FILE: examples/http/wasi/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + +### FILE: examples/http/wasi/geo_redirect/README.md + +``` +[← Back to examples](../../../README.md) + +# Geo Redirect (WASI) + +Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. + +Demonstrates reading request headers, reading environment variables, and returning redirect responses. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | +| `<COUNTRY_CODE>` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | + +## How it works + +``` +geoip-country-code: DE → env var DE is set → 302 to DE value +geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN +(no header) → 302 to BASE_ORIGIN +BASE_ORIGIN not set → 500 +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm +``` + +## APIs used + +- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge +- `std::env::var(country_code)` — dynamic env var lookup by country code +- `Response::builder().status(302).header("location", url)` — redirect response +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md index 8b6fef2..2f17c5e 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -133,3 +133,77 @@ Hello, you made a wasi request to http://<host>/<path>?<query> - fastedge-docs reference: best-practices - fastedge-docs reference: error-codes - Scaffold skill blueprints: http/base (Rust) + +## Source Material + +### FILE: examples/http/wasi/hello_world/src/lib.rs + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { + let url = request.uri().to_string(); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!( + "Hello, you made a wasi request to {url}" + )))?) +} +``` + +### FILE: examples/http/wasi/hello_world/Cargo.toml + +```toml +[workspace] + +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + +### FILE: examples/http/wasi/hello_world/README.md + +``` +[← Back to examples](../../../README.md) + +# Hello World (WASI) + +The simplest possible async FastEdge application — echoes the full request URI in the response body. + +Demonstrates the `#[wstd::http_server]` entry-point macro and the async handler signature used by all WASI HTTP examples. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/plain;charset=UTF-8 + +Hello, you made a wasi request to http://<host>/<path>?<query> +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/hello_world.wasm +``` + +## APIs used + +- `#[wstd::http_server]` — WASI HTTP entry-point macro +- `wstd::http::{Request, Response}` — request/response types +- `wstd::http::body::Body` — response body construction +- `request.uri().to_string()` — full absolute request URI +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-js.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-js.md index a906fce..2b8dbeb 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-js.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> ## KV Store — Example Reference @@ -132,6 +132,14 @@ Validation is performed by `validateQueryParams(queryParams: URLSearchParams)` i - `min` and `max` are required for: `zrange`. - `item` is required for: `bfExists`. +**Type definitions (from `utils.ts`):** +```ts +const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; +export type Action = (typeof ALL_ACTIONS)[number]; +type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; +type Params = { [key in ParamKey]: string }; +``` + --- ### Error Handling @@ -246,10 +254,31 @@ addEventListener('fetch', (event: FetchEvent) => { Decodes an `ArrayBuffer` to a UTF-8 string. Returns `''` if `arrVal` is `null`. +```ts +export const decodeValueArray = (arrVal: ArrayBuffer | null) => { + if (arrVal) { + const decoder = new TextDecoder(); + return decoder.decode(arrVal); + } + return ''; +}; +``` + #### `stringifyValueScoreTuples(tupleList: Array<[ArrayBuffer, number]>): string` Formats sorted-set result tuples as a string: `[{ Value: <decoded>, Score: <number> }, ...]`. +```ts +export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { + let strResponse = '['; + for (const tuple of tupleList) { + strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; + } + strResponse += ']'; + return strResponse; +}; +``` + --- ### Build Configuration @@ -299,3 +328,4 @@ TypeScript types for FastEdge globals (`FetchEvent`, etc.) are provided by `@gco - `"type": "module"` must be set in `package.json` for ESM compatibility with `fastedge-build`. - The SDK dependency version is `^2.3.0`. - `tsconfig.json` `target` is `ES2023`; `moduleResolution` is `Bundler`; `lib` is `["ES2023"]`; `types` is `["@gcoredev/fastedge-sdk-js"]`. +- Empty string values for required query parameters are treated as missing — validation rejects them the same as absent params. diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md index bc4dd90..3766914 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -378,3 +378,277 @@ cargo build --release - platform-overview (store provisioning and access control) - host-services-rust (other host service integrations) - examples-kv-store-js (JavaScript equivalent) + +## Source Material + +### FILE: examples/http/wasi/key_value/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating KV Store operations via the WASI-HTTP interface. + +Supports all KV Store operations via query parameters: + ?store=<name>&action=get&key=<key> + ?store=<name>&action=scan&match=<pattern> + ?store=<name>&action=zrange&key=<key>&min=<f64>&max=<f64> + ?store=<name>&action=zscan&key=<key>&match=<pattern> + ?store=<name>&action=bfExists&key=<key>&item=<item> + +Defaults to action=get if not specified. +*/ + +use std::collections::HashMap; + +use anyhow::anyhow; +use fastedge::key_value::{Store, Error as StoreError}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let query = req.uri().query().ok_or(anyhow!("no query parameters"))?; + let params: HashMap<&str, &str> = querystring::querify(query).into_iter().collect(); + + let store_name = *params + .get("store") + .ok_or(anyhow!("missing param 'store'"))?; + + let action = params.get("action").copied().unwrap_or("get"); + + let store = match Store::open(store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return Ok(Response::builder() + .status(403) + .header("content-type", "application/json") + .body(Body::from(json!({"error": "access denied"}).to_string()))?); + } + Err(e) => { + return Ok(Response::builder() + .status(500) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("store open error: {e}")}).to_string()))?); + } + }; + + let body = match action { + "get" => handle_get(&store, ¶ms)?, + "scan" => handle_scan(&store, ¶ms)?, + "zrange" => handle_zrange(&store, ¶ms)?, + "zscan" => handle_zscan(&store, ¶ms)?, + "bfExists" => handle_bf_exists(&store, ¶ms)?, + _ => { + return Ok(Response::builder() + .status(400) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("Invalid action '{action}'. Supported: get, scan, zrange, zscan, bfExists")}).to_string()))?); + } + }; + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(body))?) +} + +fn handle_get(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result<String> { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + match store.get(key) { + Ok(Some(value)) => { + let value_str = String::from_utf8_lossy(&value); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": value_str.as_ref() + }).to_string()) + } + Ok(None) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": null + }).to_string()), + Err(e) => Err(anyhow!("KV get error: {e}")), + } +} + +fn handle_scan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result<String> { + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + match store.scan(pattern) { + Ok(keys) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "scan", + "match": pattern, + "response": keys + }).to_string()), + Err(e) => Err(anyhow!("KV scan error: {e}")), + } +} + +fn handle_zrange(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result<String> { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let min: f64 = params + .get("min") + .ok_or(anyhow!("missing param 'min'"))? + .parse() + .map_err(|_| anyhow!("invalid 'min': must be a number"))?; + let max: f64 = params + .get("max") + .ok_or(anyhow!("missing param 'max'"))? + .parse() + .map_err(|_| anyhow!("invalid 'max': must be a number"))?; + + match store.zrange_by_score(key, min, max) { + Ok(entries) => { + let entries_json: Vec<serde_json::Value> = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zrange", + "key": key, + "min": min, + "max": max, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zrange error: {e}")), + } +} + +fn handle_zscan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result<String> { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + + match store.zscan(key, pattern) { + Ok(entries) => { + let entries_json: Vec<serde_json::Value> = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zscan", + "key": key, + "match": pattern, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zscan error: {e}")), + } +} + +fn handle_bf_exists(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result<String> { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let item = *params + .get("item") + .ok_or(anyhow!("missing param 'item'"))?; + + match store.bf_exists(key, item) { + Ok(exists) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "bfExists", + "key": key, + "item": item, + "response": exists + }).to_string()), + Err(e) => Err(anyhow!("KV bfExists error: {e}")), + } +} +``` + +### FILE: examples/http/wasi/key_value/Cargo.toml + +```toml +[workspace] + +[package] +name = "key_value_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +querystring = "1.1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/key_value/README.md + +``` +[← Back to examples](../../../README.md) + +# Key Value (WASI) + +Demonstrates all KV store operations via a query-parameter driven HTTP API: get, scan, zrange, zscan, and bfExists. + +## Usage + +All operations require `?store=<name>` and `?action=<op>` query parameters: + +| Action | Additional params | Description | +|---|---|---| +| `get` | `key=<key>` | Fetch a single value by key | +| `scan` | `match=<pattern>` | List keys matching a glob pattern | +| `zrange` | `key=<key>&min=<f64>&max=<f64>` | Fetch sorted-set members by score range | +| `zscan` | `key=<key>&match=<pattern>` | List sorted-set members matching a pattern | +| `bfExists` | `key=<key>&item=<item>` | Check bloom filter membership | + +`action` defaults to `get` if omitted. + +## Example + +``` +GET /?store=my-store&action=get&key=hello +→ 200 {"store":"my-store","action":"get","key":"hello","response":"world"} + +GET /?store=my-store&action=scan&match=user:* +→ 200 {"store":"my-store","action":"scan","match":"user:*","response":["user:1","user:2"]} +``` + +## Error responses + +| Condition | Status | Body | +|---|---|---| +| Store not found / access denied | 403 | `{"error":"access denied"}` | +| Missing required params | 530 | Runtime error | +| Store open error | 500 | `{"error":"store open error: ..."}` | +| Invalid action | 400 | `{"error":"Invalid action '...'. Supported: ..."}` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/key_value_wasi.wasm +``` + +## APIs used + +- `fastedge::key_value::Store::open(name)` — open a named KV store +- `store.get(key)` — fetch value by key; returns `Ok(Option<Vec<u8>>)` +- `store.scan(pattern)` — list keys by glob pattern +- `store.zrange_by_score(key, min, max)` — range query on sorted set +- `store.zscan(key, pattern)` — pattern scan on sorted set +- `store.bf_exists(key, item)` — bloom filter membership test +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md index 43e3c89..f98adf3 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -51,14 +51,14 @@ Only `GET` and `HEAD` are accepted. ## Core Flow -1. Read `BASE` env var; strip trailing `/`. +1. Read `BASE` env var; strip trailing `/` via `base.trim_end_matches('/')`. 2. Validate request path is non-empty and not `/`. 3. Construct outbound `GET` request: `BASE + path`, `User-Agent: fastedge`. 4. Call `fastedge::send_request` via internal `request()` helper. -5. Follow redirects up to `MAX_REDIRECTS = 5` hops. -6. Decode response body as UTF-8; failure → `500`. -7. Parse Markdown with `Parser::new_ext(md, Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. -8. Render HTML with `pulldown_cmark::html::push_html`. +5. Follow redirects up to `MAX_REDIRECTS = 5` hops via `request_inner(req, depth)`. +6. Decode response body as UTF-8 via `String::from_utf8(rsp.body().to_vec())`; failure → `500`. +7. Parse Markdown with `Parser::new_ext(md.as_str(), Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. +8. Render HTML with `pulldown_cmark::html::push_html(&mut html, parser)`. 9. Wrap in `<!DOCTYPE html><html><body>...</body></html>`; optionally inject `HEAD` env var into `<head>`. 10. Return `200 OK`, `Content-Type: text/html`. @@ -193,6 +193,7 @@ Build target: `wasm32-wasip1`. - `String::from_utf8(rsp.body().to_vec())` fails on binary responses (e.g. images) — returns `500`. Ensure `BASE` points to a text/Markdown origin. - Redirect following is implemented manually; `fastedge::send_request` does not auto-follow redirects. - `HEAD` env var content is injected as raw HTML — no escaping or validation. Only inject trusted content. +- `request_inner` only returns `Ok(rsp)` for `200 OK` responses; all other non-redirect status codes are returned as `Err(status)` and forwarded as empty-body responses. --- diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md index 142235f..f25c8e7 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -186,3 +186,120 @@ cargo build --release - sdk-reference-rust (full `fastedge` crate API reference) - examples-basic-http-rust (minimal sync handler without outbound fetch) - platform-overview (FastEdge execution model and WASM target context) + +## Source Material + +### FILE: examples/http/basic/outbound_fetch/src/lib.rs + +```rust +use anyhow::{Error, Result}; +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use serde_json::{json, Value}; + +#[fastedge::http] +fn main(_req: Request<Body>) -> Result<Response<Body>> { + let upstream_req = Request::builder() + .uri("http://jsonplaceholder.typicode.com/users") + .body(Body::empty())?; + + let upstream_resp = fastedge::send_request(upstream_req).map_err(Error::msg)?; + + let body_bytes = upstream_resp.body().to_vec(); + let users: Value = serde_json::from_slice(&body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(Body::from(result.to_string())) + .map_err(Into::into) +} +``` + + +### FILE: examples/http/basic/outbound_fetch/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_fetch" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` + + +### FILE: examples/http/basic/outbound_fetch/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Fetch (Basic HTTP) + +Demonstrates outbound HTTP from a FastEdge application using the **legacy sync handler** (`#[fastedge::http]`). Fetches user data from the [JSONPlaceholder](https://jsonplaceholder.typicode.com) public API, selects the first 5 users, and returns them in a paginated JSON envelope. + +> **When to use this example:** If you need a synchronous, single-function HTTP handler with outbound requests targeting `wasm32-wasip1`. For new apps, prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +On any incoming request: + +1. Makes a GET request to `http://jsonplaceholder.typicode.com/users` using `fastedge::send_request`. +2. Parses the JSON response body. +3. Takes the first 5 users from the array. +4. Returns a JSON envelope with pagination metadata. + +Example response body: + +```json +{ + "users": [ { "id": 1, "name": "Leanne Graham", ... }, ... ], + "total": 5, + "skip": 0, + "limit": 30 +} +``` + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request` | Outbound HTTP request to upstream API | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Request and response bodies | +| `serde_json` | JSON parsing and serialisation | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/outbound_fetch.wasm +``` + +## Expected behavior + +| Request | Response status | Response content-type | Response body | +|---|---|---|---| +| `GET /` | 200 | `application/json` | JSON object with `users` (array of ≤5), `total`, `skip`, `limit` | +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md index 5666f87..6be9762 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -182,3 +182,102 @@ Ok(Response::builder() - outbound-modify-response example (JS) - wstd HTTP client documentation - serde_json crate documentation + +## Source Material + +### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Outbound fetch with response transformation. + +Fetches JSON from an upstream origin, reads and parses the body, reshapes it +into a new JSON object (first 5 users with pagination metadata), and returns +it with a fresh `content-type: application/json` header. + +This is the stepping-stone beyond `outbound_fetch/` which just passes the +upstream response through unchanged. + +Mirror of the FastEdge-sdk-js `outbound-modify-response` example. +*/ + +use anyhow::anyhow; +use serde_json::{Value, json}; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { + let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (_, mut body) = upstream_resp.into_parts(); + let body_bytes = body.contents().await?; + let users: Value = serde_json::from_slice(body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} +``` + +### FILE: examples/http/wasi/outbound_modify_response/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_modify_response_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +serde_json = "1" +``` + +### FILE: examples/http/wasi/outbound_modify_response/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Modify Response (WASI) + +Fetch data from an outbound HTTP origin, transform the JSON response (slice to first 5 +users), and return it with a fresh `content-type: application/json` header. + +Demonstrates reading the upstream body with `body.contents().await`, parsing JSON with +`serde_json`, and composing a new response from scratch. + +## Related + +- [outbound_fetch](../outbound_fetch/) — the simpler variant that just passes the upstream + response through unchanged. +- Mirror of `FastEdge-sdk-js/examples/outbound-modify-response/`. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md index ac1fdd7..2f2a35a 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -171,6 +171,7 @@ cargo build --release - `MAX_FILE_SIZE` is silently ignored (no error, no log) when set to a non-numeric string. - On S3 success, the response body is replaced entirely with the clean object URL — the original S3 response body is discarded. - The `UrlStyle::Path` style is used for `Bucket::new` — bucket name appears in the URL path, not the hostname. +- Query params (`?name=...`) are parsed manually using `split('&')` and `splitn(2, '=')` into a `HashMap<String, String>`. --- diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md index d97a52d..3d9715b 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> # Secret Access — Rust HTTP Example @@ -251,3 +251,185 @@ node tools/fixture-validator/index.mjs \ - fastedge-docs skill reference: sdk-reference-rust - fastedge-docs skill reference: error-codes - manage skill: secret management subcommands (set, list, delete) + +## Source Material + +### FILE: examples/http/basic/secret/src/lib.rs + +```rust +use anyhow::{Error, Result}; +use std::time::SystemTime; + +use fastedge::body::Body; +use fastedge::http::{Request, Response, StatusCode}; +use fastedge::secret; + +#[allow(dead_code)] +#[fastedge::http] +fn main(_req: Request<Body>) -> Result<Response<Body>> { + let value = match secret::get("SECRET") { + Ok(value) => value, + Err(secret::Error::AccessDenied) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .map_err(Error::msg); + } + Err(secret::Error::Other(msg)) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from(msg)) + .map_err(Error::msg); + } + Err(secret::Error::DecryptError) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .map_err(Error::msg); + } + }; + + if value.is_none() { + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .map_err(Error::msg); + } + + let ts = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let effective_at_value = match secret::get_effective_at("SECRET", ts as u32) { + Ok(value) => value, + Err(secret::Error::AccessDenied) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .map_err(Error::msg); + } + Err(secret::Error::Other(msg)) => { + return Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from(msg)) + .map_err(Error::msg); + } + Err(secret::Error::DecryptError) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .map_err(Error::msg); + } + }; + + if effective_at_value.is_none() { + return Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .map_err(Error::msg); + } + + Response::builder() + .status(StatusCode::OK) + .body(Body::from(format!( + "get={:?}\nget_efective_at={:?}\n", + value, effective_at_value + ))) + .map_err(Error::msg) +} +``` + + +### FILE: examples/http/basic/secret/Cargo.toml + +```toml +[workspace] + +[package] +name = "secret" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/basic/secret/README.md + +``` +[← Back to examples](../../../README.md) + +# Secret + +Demonstrates accessing encrypted secrets injected by the FastEdge platform using `secret::get()` and `secret::get_effective_at()`. Shows how to handle all error variants (access denied, decrypt error) and the time-based secret rotation API. + +## What it does + +On every request, the handler: + +1. Calls `secret::get("SECRET")` — retrieves the current value of the secret named `SECRET`. +2. If the secret is missing (returned as `None`), returns **404**. +3. Calls `secret::get_effective_at("SECRET", <unix_ts>)` — retrieves the secret value effective at the current Unix timestamp, demonstrating the rotation/versioning API. +4. If that value is also missing, returns **404**. +5. On success, returns **200** with both values in the body (Debug format). + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::secret::get(key)` | Retrieve the current value of a named secret | +| `fastedge::secret::get_effective_at(key, timestamp)` | Retrieve the secret value effective at a specific Unix timestamp | +| `fastedge::secret::Error` | Error variants: `AccessDenied`, `Other(msg)`, `DecryptError` | +| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | +| `fastedge::body::Body` | Response body | + +## Secret error variants + +| Variant | HTTP response | Meaning | +|---|---|---| +| `Ok(Some(value))` | 200 with body | Secret found | +| `Ok(None)` | 404 empty | Secret name is valid but not set | +| `Err(AccessDenied)` | 403 empty | App is not permitted to read this secret | +| `Err(Other(msg))` | 403 with `msg` body | Other denial with a human-readable message | +| `Err(DecryptError)` | 500 empty | Secret exists but could not be decrypted | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/secret.wasm +``` + +## Expected behavior + +| Scenario | Secret `SECRET` | Response status | Response body | +|---|---|---|---| +| Happy path | `"my-value"` | 200 | `get=Some("my-value")\nget_efective_at=Some("my-value")\n` | +| Secret not set | (absent) | 404 | (empty) | +| Access denied | — | 403 | (empty) | + +> **Note:** The response body contains a typo in the field name (`get_efective_at` instead of `get_effective_at`). This is a known cosmetic issue in the source. + +## Local testing + +Inject the secret via a `.env` file in your fixtures directory using the `FASTEDGE_VAR_SECRET_<NAME>` prefix: + +``` +# fixtures/.env +FASTEDGE_VAR_SECRET_SECRET=my-test-value +``` + +Run with the fixture validator: + +```sh +node tools/fixture-validator/index.mjs \ + FastEdge-sdk-rust/examples/http/basic/secret/ \ + --wasm FastEdge-sdk-rust/examples/http/basic/secret/target/wasm32-wasip1/release/secret.wasm +``` +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md index 824cf69..3370f1b 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -147,3 +147,123 @@ package = "component:simple_fetch" - sdk-reference-rust - examples-simple-request-rust (basic sync HTTP handler, `fastedge` crate) - host-services-rust (outbound fetch via host services) + +## Source Material + +### FILE: examples/http/wasi/simple_fetch/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating the WASI-HTTP interface via the wstd crate. + +The app receives an incoming HTTP request and makes an outbound HTTP request +to the URL specified in the `x-fetch-url` header (defaults to https://httpbin.org/get). + +Build with cargo-component: + cargo component build --release +*/ + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { + let target_url = request + .headers() + .get("x-fetch-url") + .and_then(|v| v.to_str().ok()) + .unwrap_or("https://httpbin.org/get") + .to_string(); + + println!("Fetching: {target_url}"); + + let upstream_req = Request::get(&target_url) + .header("accept", "application/json") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let client = Client::new(); + let response = client + .send(upstream_req) + .await + .map_err(|e| anyhow!("request failed: {e}"))?; + + println!("Response status: {}", response.status()); + + Ok(response) +} +``` + + +### FILE: examples/http/wasi/simple_fetch/Cargo.toml + +```toml +[workspace] + +[package] +name = "simple_fetch" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" + +[package.metadata.component] +package = "component:simple_fetch" +``` + + +### FILE: examples/http/wasi/simple_fetch/README.md + +``` +[← Back to examples](../../../README.md) + +# Simple Fetch + +A minimal example demonstrating outbound HTTP requests using the [WASI-HTTP](https://github.com/WebAssembly/wasi-http) interface via the [`wstd`](https://crates.io/crates/wstd) crate. + +Uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). The same async pattern is used by all examples in `examples/http/wasi/`. + +## How it works + +The app receives an incoming request, reads the target URL from the `x-fetch-url` header, makes an outbound GET request to that URL, and streams the response back to the caller. + +If the `x-fetch-url` header is absent, it defaults to `https://httpbin.org/get`. + +## Request headers + +| Header | Required | Description | +|--------|----------|-------------| +| `x-fetch-url` | No | URL to fetch. Defaults to `https://httpbin.org/get` | + +## Example + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https://<your-app-domain>/ +``` + +## Build + +```bash +cargo build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Key differences from basic HTTP examples + +| | Basic HTTP (`fastedge` crate) | WASI HTTP (`wstd` crate) | +|---|---|---| +| Handler | `fn main(req)` — sync | `async fn main(req)` — async | +| Macro | `#[fastedge::http]` | `#[wstd::http_server]` | +| Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | +| Build target | `wasm32-wasip1` | `wasm32-wasip2` | +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md index 7584018..b8e56f8 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -143,3 +143,105 @@ chunk 4 - examples-streaming-js reference (JavaScript mirror of this example) - wstd HTTP body reference - FastEdge SDK Rust reference + +## Source Material + +### FILE: examples/http/wasi/streaming/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https://<app-url>/` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} +``` + + +### FILE: examples/http/wasi/streaming/Cargo.toml + +```toml +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" +``` + + +### FILE: examples/http/wasi/streaming/README.md + +``` +[← Back to examples](../../../README.md) + +# Streaming Response (WASI) + +Generates a response body on the fly — five text chunks, one every 200 ms — using +`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it +is produced, not all at once at the end. + +Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async +stream generation, and `wstd::time::Timer` for per-chunk delays. + +## Testing the streaming behaviour + +```sh +curl -N https://<your-app>.fastedge.cdn.gc.onl/ +``` + +`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a +time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. + +## Other streaming patterns + +- **Pass-through streaming** — return an upstream response's body directly. See + [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. +- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, + then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. +- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where + `chunks` is any iterable of `Into<Bytes>`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/streaming/`. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/quickstart-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/quickstart-as.md index e408331..05333df 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/quickstart-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/quickstart-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # Quickstart: AssemblyScript CDN Apps on FastEdge @@ -118,7 +118,7 @@ registerRootContext((context_id: u32) => { |------|---------| | `export * from ".../assembly/proxy"` | Exposes wasm entry points the host runtime calls into. Required in every app. Must be the first line. | | `RootContext` subclass | Created once per worker. `createContext` produces a `Context` instance for each hook invocation. | -| `Context` subclass | Handles a single lifecycle hook phase. Instance fields do not persist across hooks (hook state isolation). | +| `Context` subclass | Handles a single lifecycle hook phase. A fresh instance is created for each hook phase — instance fields do not persist across hooks (hook state isolation). | | `registerRootContext` | Registers the root context factory with the proxy runtime. | ### Lifecycle hooks diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/sdk-reference-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/sdk-reference-as.md index 8b77309..8d3c4f2 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/sdk-reference-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/sdk-reference-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> # AssemblyScript Proxy-Wasm SDK Reference @@ -65,6 +65,8 @@ registerRootContext( ); ``` +**No default parameters on nested functions**: AssemblyScript compiles functions defined inside a method body to `call_indirect` entries in the WebAssembly element table. Default parameter values are applied at direct call sites only — for indirect calls, unspecified argument slots receive `0` (a null pointer). If those slots are used as strings or objects, the wasm traps with a memory access out-of-bounds error at runtime. Fix: promote the helper to a `private` class method (dispatched as a direct call; defaults apply correctly), or pass all arguments explicitly at every call site. + --- ## Build Configuration @@ -873,14 +875,22 @@ function getEnv(name: string): string; Returns the value of the environment variable, or an empty string if the variable is not set. +**Empty string fallback**: AssemblyScript's `||` operator performs a pointer-non-null check, not a value-falsy check. `getEnv("X") || "default"` returns `""` rather than `"default"` when the variable is unset, because the empty-string object has a non-zero pointer. Use an explicit check instead: + +```typescript +const raw = getEnv("X"); +const val = raw === "" ? "default" : raw; +``` + +The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getEnv } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; import { log, LogLevelValues } from "@gcoredev/proxy-wasm-sdk-as/assembly"; -const blocklist = getEnv("BLOCKLIST"); -if (blocklist.length == 0) { - log(LogLevelValues.warn, "BLOCKLIST env var is not set"); -} +const raw = getEnv("BLOCKLIST"); +const blocklist = raw === "" ? "none" : raw; +log(LogLevelValues.info, "Blocklist: " + blocklist); ``` ### Dictionary (getDictionary) @@ -919,6 +929,8 @@ function getSecretEffectiveAt(name: string, effectiveAt: u32): string; `getSecretEffectiveAt`: reads a secret from a specific rotation slot. Slots are defined in the FastEdge UI and are always numeric (e.g., incremental integers, or timestamp-style values representing a point in time). Use this for secret rotation: pass the current Unix timestamp in seconds as `effectiveAt`. The host selects the slot where `effectiveAt >= secret_slots.slot`. +**Empty string fallback**: AssemblyScript's `||` operator does not fall back on empty strings. `getSecret("KEY") || "default"` returns `""` rather than `"default"` when the secret is unset. Use an explicit check: `const raw = getSecret("KEY"); const key = raw === "" ? "default" : raw;`. The `!str` unary operator is correct and returns `true` for both `null` and `""`. + ```typescript import { getSecret, @@ -959,7 +971,7 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the | `scan(pattern: string): string[]` | `string[]` | Glob-style key scan. Pattern must include a wildcard (e.g., `"foo*"`). | | `zrangeByScore(key: string, min: f64, max: f64): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set range query: returns entries where `min <= score <= max`. | | `zscan(key: string, pattern: string): ValueScoreTuple[]` | `ValueScoreTuple[]` | Sorted set pattern scan: matches values against the glob pattern. | -| `bfExists(key: string, item: string): bool` | `bool` | Bloom filter membership check. Returns `true` if item may exist. | +| `bfExists(key: string, item: string): boolean` | `boolean` | Bloom filter membership check. Returns `true` if item may exist. | `get` returns `ArrayBuffer | null` — the caller must decode the buffer. @@ -970,9 +982,9 @@ Returns a `KvStore` instance, or `null` if the store cannot be opened (e.g., the ```typescript class ValueScoreTuple { value: ArrayBuffer; // The stored value bytes - score: f64; // The associated score + score: number; // The associated score (f64) - constructor(value: ArrayBuffer, score: f64); + constructor(value: ArrayBuffer, score: number); } ``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md new file mode 100644 index 0000000..91bb0f1 --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md @@ -0,0 +1,94 @@ +<!-- + auto-updated: true + sources: + - id: fastedge-templates + ref: main + commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 + updated: 2026-08-17 +--> + +# FastEdge Bolt-On Templates + +Before writing authentication, cookie-hardening, or content-transform logic from scratch, check this catalog. Each entry is a maintained, tested FastEdge application that handles the security-sensitive parts a hand-rolled implementation would need to get right independently. Use or adapt these templates rather than building the same capability from scratch. + +## html2md — HTML-to-Markdown conversion / content transformation / Accept header negotiation + +**What it solves:** A CDN filter that transparently converts HTML origin responses to Markdown when the client sends `Accept: text/markdown`. Handles content negotiation, encoding, and caching concerns that a hand-rolled filter would need to get right independently. + +**How it deploys:** Single CDN Proxy-WASM app. Deployed via the Gcore portal template gallery (template ID 110). No origin changes required. + +**Runtime variants:** None. Conversion is fully automatic based on request `Accept` and response `Content-Type` headers. + +**Conversion conditions:** All three must be true — request `Accept` includes `text/markdown`, origin response `Content-Type` includes `text/html` with charset absent or `utf-8`, and full response body is available at end of stream. + +**What the filter does on conversion:** Adds request header `Convert: markdown` (cache differentiation), removes `Accept-Encoding` to avoid compressed payloads, removes `Content-Length`, sets `Content-Type: text/markdown; charset=utf-8`, sets `Transfer-Encoding: Chunked`, converts body at end of stream, adds `Vary: Convert` (merged with existing `Vary`). + +**Error conditions:** Returns `500` if origin body is not valid UTF-8 or if HTML-to-Markdown conversion fails. Non-HTML responses pass through unchanged. + +**Origin integration:** Configured entirely through request/response header inspection — no env vars, no origin code required. + +--- + +## harden-cookies — Cookie security hardening / Secure HttpOnly SameSite attributes / Set-Cookie rewriting + +**What it solves:** A CDN filter that adds `Secure`, `HttpOnly`, and `SameSite=Strict` attributes to targeted `Set-Cookie` response headers. Eliminates the need to modify backend code to enforce cookie security policy at the edge. + +**How it deploys:** Single CDN Proxy-WASM app. Deployed via the Gcore portal template gallery (template ID 184). No origin changes required. + +**Runtime variants:** None. Behavior is controlled entirely through environment variables. + +**Targeting behavior:** Only cookies whose name matches `COOKIE_NAME` are modified. Use `*` to match all cookies. All other `Set-Cookie` headers pass through unchanged. If `COOKIE_NAME` is unset, or none of `SECURE`, `HTTPONLY`, `SAMESITE` is set to `true`, responses pass through untouched. + +**Attribute application:** `Secure` and `HttpOnly` are added only if not already present. `SameSite=Strict` is set unconditionally, overriding any existing `SameSite` value. + +**Origin integration:** Configured entirely through environment variables (`COOKIE_NAME`, `SECURE`, `HTTPONLY`, `SAMESITE`) — no origin code required. + +--- + +## edge-sso — SSO / login / identity federation / Identity-Aware Proxy (Google, GitHub, Microsoft, Facebook, SAML) + +**What it solves:** A bolt-on Identity-Aware Proxy that adds multi-provider SSO (Google, GitHub, Microsoft, Facebook, SAML 2.0) to any existing site without changing the backend. Handles OAuth 2.0 / OIDC / SAML flows, session token issuance, and per-request enforcement. Building this from scratch requires correctly implementing multiple OAuth flows, token signing, and edge enforcement — this template has already done that. + +**How it deploys:** Two FastEdge apps deployed together, both via the Gcore portal template gallery: +- `cdn-filter/` — CDN Proxy-WASM app (Rust); sits in the CDN proxy layer, verifies session token on every request, redirects unauthenticated users to the auth app +- `auth-app/` — HTTP app (TypeScript/Hono); federates to the identity provider, issues a signed session token, sets it on the client + +**Runtime variants:** `SSO_VARIANT` selects the identity-delivery mode — the same value must be set on both apps: + +| Variant | What the origin receives | When to use | +|---|---|---| +| `gate-only` | Allow/deny only — no identity forwarded | Origin needs access control but not user context | +| `cookie` | Signed JWT in a cookie the origin can verify | Origin reads user identity from a verifiable token | +| `header` | Signed `x-sso-*` identity headers injected upstream | Origin trusts a header from the CDN layer | + +**Supported identity providers:** Google (OAuth 2.0), GitHub (OAuth 2.0), Microsoft (OAuth 2.0 / OIDC), Facebook (OAuth 2.0), SAML (SAML 2.0). + +**Key shared configuration requirements:** `SSO_VARIANT` and `SSO_AUDIENCE` must match on both apps. `SESSION_SECRET` is required in every variant. The `cookie` variant additionally requires an EC keypair (`SESSION_SIGNING_KEY` secret + `SESSION_PUBLIC_JWK` env var). + +**Origin integration:** The template is configured through environment variables and secrets on both apps. For the customer-side wiring contract — how the origin validates tokens or trusts identity headers depending on the chosen variant — see the `edge-sso` integration reference. + +--- + +## edge-totp — TOTP MFA / two-factor authentication / OTP challenge / RFC 6238 + +**What it solves:** Adds a TOTP (RFC 6238) two-factor authentication step in front of an existing site's login without modifying the backend. The customer's origin handles password validation; this app hosts the 6-digit OTP challenge, verifies the code with replay and brute-force protection, and issues a signed session assertion the origin trusts. Building this from scratch requires correctly implementing HOTP/TOTP, replay protection, brute-force throttling, signed cookie issuance, and CDN enforcement — this template has already done that. + +**How it deploys:** Two FastEdge apps deployed together via the Gcore portal template gallery: +- `otp-app/` — HTTP app (TypeScript/Hono, WASM); hosts the challenge, verify, enroll, self-service activate, logout, JWKS, and health endpoints; signs the `mfa_session` cookie (HS256) and the optional ES256 proof +- `otp-filter/` — CDN Proxy-WASM app (Rust); enforces `mfa_session` on protected paths; default-deny and fail-closed + +**Runtime variants (enforcement profiles):** + +| Profile | Mode | What the origin must do | +|---|---|---| +| **A** (default) | Filter enforces, zero origin code | Nothing — the CDN layer is the enforcement boundary | +| **B** (opt-in) | Origin verifies a one-time ES256 proof via JWKS | Origin validates the proof at its own session boundary using the app's JWKS endpoint | + +**Security-critical deployment requirements:** +- The origin must be locked to edge-only traffic (IP allowlist / origin auth / tunnel) — the gate is bypassed if the origin is directly reachable +- `MFA_AUDIENCE` must be set on both apps; the filter fail-closes (rejects every session) if it is unset +- `GCORE_API_TOKEN` has write access to every seed in the KV store — scope it to a single-tenant, per-customer isolated KV store + +**CDN wiring:** Attach `otp-app` as a CDN origin on the `{AUTH_PREFIX}/*` path rule of the customer's CDN resource; attach `otp-filter` as the CDN proxy app in front of protected paths, bypassing `{AUTH_PREFIX}` and `/health`. Both share the CDN host so `mfa_session` is first-party host-only. + +**Origin integration:** For the full customer-side wiring contract, trust model, and Profile B JWKS integration, see the `edge-totp` integration reference. diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-sso-integration.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-sso-integration.md new file mode 100644 index 0000000..ee18756 --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-sso-integration.md @@ -0,0 +1,214 @@ +<!-- + auto-updated: true + sources: + - id: fastedge-templates + ref: main + commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 + updated: 2026-08-17 +--> + +# edge-sso — Origin Integration Reference + +Reference for origins integrating with an already-deployed `edge-sso` template pair (auth-app + cdn-filter). Covers the contract the origin relies on: identity delivery, auth routes, login page customization, and redirect validation. + +--- + +## 1. What `SSO_VARIANT` Means for the Origin + +`SSO_VARIANT` is set identically on both the auth-app and the cdn-filter. It controls how the edge delivers identity to the origin and what the origin is responsible for verifying. + +### `gate-only` + +- **What the origin receives**: nothing — the filter enforces allow/deny at the edge; only authenticated requests reach the origin. +- **Origin responsibility**: none. The origin does not receive a token or identity headers; it only sees requests that passed the gate. +- **Signing algorithm**: HS256 (shared `SESSION_SECRET`). The origin never verifies the token; the filter handles all verification. +- **Session cookie**: stripped before forwarding — the origin never sees the raw JWT. + +### `cookie` + +- **What the origin receives**: the `sso_session` cookie (configurable via `SESSION_COOKIE`) containing a signed JWT. The cookie passes through to the origin unchanged. +- **Origin responsibility**: the origin verifies the cookie itself using the published JWKS endpoint. + - **JWKS endpoint**: `GET /auth/.well-known/jwks.json` — mounted on the auth-app only when `SSO_VARIANT=cookie` and `SESSION_PUBLIC_JWK` is configured. Use a standard JWKS client (e.g., `createRemoteJWKSet` from the `jose` library) against this URL — do not implement verification manually. +- **Signing algorithm**: ES256 (asymmetric — EC private key `SESSION_SIGNING_KEY` signs; public JWK published via JWKS). The origin holds only the public key and verifies with it; it never holds a forge-capable secret. +- **Cookie attributes**: `HttpOnly; Secure; SameSite=Lax`. Under single-domain routing no `Domain=` is set — same-origin. +- **Token claims**: `sub`, `iat`, `exp`, `aud`, `iss` (optional), `email`, `name`, `picture`, `given_name`, `family_name`. + +### `header` + +- **What the origin receives**: verified identity injected as request headers. The session cookie is stripped before forwarding — the origin never sees the raw JWT. +- **Origin responsibility**: read and trust `x-sso-*` headers. The origin **must treat an empty `x-sso-*` header as absent** — the platform blanks a cleared header to empty rather than removing it, so an empty value means the claim was not present in the token. +- **Signing algorithm**: HS256 (shared `SESSION_SECRET`). The origin does not verify the token; the filter handles all verification before injecting headers. +- **Anti-spoofing contract**: the filter clears any client-supplied `x-sso-*` header before injecting verified values. A client cannot smuggle a spoofed identity header past the filter. The origin must not trust `x-sso-*` values from any path that bypasses the filter. + +**Complete list of injected headers (verbatim from `cdn-filter/src/lib.rs`):** + +| Header | Claim source | +|---|---| +| `x-sso-user` | `sub` | +| `x-sso-email` | `email` | +| `x-sso-name` | `name` | +| `x-sso-picture` | `picture` | +| `x-sso-given-name` | `given_name` | +| `x-sso-family-name` | `family_name` | + +Headers for claims absent from the token: if the claim was not present, the header is absent (or empty — treat empty as absent per the platform contract above). + +--- + +## 2. Auth-App Routes + +All routes are served under `AUTH_PREFIX` (default: `/auth`). Under single-domain routing, the CDN routes `AUTH_PREFIX/**` to the auth-app as an origin on the customer's own domain. The cdn-filter bypasses `AUTH_PREFIX/**` — it does not gate these routes. + +| Route | Method | Caller | Purpose | +|---|---|---|---| +| `/auth/` and `/auth` | GET | Browser | Hosted login page — server-rendered, branded, provider buttons. Honours `?redirect=`. | +| `/auth/providers` | GET | Browser / custom login UI | Provider data (JSON) — the enabled provider set with login URLs. Honours `?redirect=`. | +| `/auth/branding` | GET | Custom login page | Branding config (JSON) — current `LOGIN_PAGE_*` values for custom pages. | +| `/auth/login/google` | GET | Browser | Start Google OIDC. Honours `?redirect=`. | +| `/auth/login/github` | GET | Browser | Start GitHub OAuth. Honours `?redirect=`. | +| `/auth/login/microsoft` | GET | Browser | Start Microsoft OIDC. Honours `?redirect=`. | +| `/auth/login/facebook` | GET | Browser | Start Facebook OAuth. Honours `?redirect=`. | +| `/auth/login` | GET | Browser | Start SAML SSO. Honours `?redirect=`. | +| `/auth/logout` | GET | Browser | Sign out — clears `sso_session` (`Max-Age=0`), redirects to the validated `?redirect=` (defaults to `/`). Not gated by the filter. | +| `/auth/callback/<provider>` | GET | IdP only | OAuth/OIDC callback — used by the identity provider, not called directly. | +| `/auth/callback` | POST | IdP only | SAML ACS endpoint — used by the identity provider, not called directly. | +| `/auth/.well-known/jwks.json` | GET | Origin / JWKS client | Public JWK set — mounted only when `SSO_VARIANT=cookie` and `SESSION_PUBLIC_JWK` is set. | + +`?redirect=<url>` is the post-login destination. After successful federation the auth-app sets the `sso_session` cookie and 302s to that URL. + +--- + +## 3. Login Page Customization Tiers + +### Tier 1 — Env Var Branding (recommended default) + +The built-in hosted login page reads these env vars per-request. No code changes required. + +| Env var | Default | Effect | +|---|---|---| +| `LOGIN_PAGE_TITLE` | `"Sign in"` | `<title>` and `<h1>` | +| `LOGIN_PAGE_SUBTITLE` | `"Choose a sign-in method"` | Subheading below the title | +| `LOGIN_PAGE_LOGO_URL` | — | Logo image above the title | +| `LOGIN_PAGE_FAVICON_URL` | — | Tab favicon | +| `LOGIN_PAGE_ACCENT_COLOR` | `#0066cc` | Button/focus-ring color (CSS `--lp-accent`) | +| `LOGIN_PAGE_BACKGROUND_COLOR` | `#f0f2f5` | Page background (CSS `--lp-bg`) | +| `LOGIN_PAGE_CSS_URL` | — | Customer stylesheet linked last — overrides any built-in style | +| `IDP_LABEL` | `"SSO"` | Display name for the SAML provider button | +| `IDP_ICON_URL` | — | Icon URL for the SAML provider button | + +`LOGIN_PAGE_CSS_URL` is the deep-customization escape hatch — a `<link rel="stylesheet">` injected after built-in styles. The CSS variables `--lp-accent` and `--lp-bg` are intentional override points. + +### Tier 2 — Fully Custom Login Page (`LOGIN_PAGE_URL`) + +Set `LOGIN_PAGE_URL` on the **CDN filter** to redirect unauthenticated users to a page you own instead of the built-in one. That page calls `GET /auth/providers` for login URLs and, optionally, `GET /auth/branding` for consistent branding tokens. + +``` +LOGIN_PAGE_URL=https://shop.example.com/my-login +``` + +Your custom page handles the full UI; clicking a provider's button navigates to its `loginUrl` (relative, same-origin) which kicks off the standard federation flow. The default value of `LOGIN_PAGE_URL` is `/auth/` — set it only to opt out. + +### Tier 3 — Embed Sign-In Buttons on an Existing Page + +**Static links (simplest):** hard-code the provider routes. +```html +<a href="/auth/login/google?redirect=/account">Sign in with Google</a> +<a href="/auth/login/github?redirect=/account">Sign in with GitHub</a> +<a href="/auth/login?redirect=/account">Single Sign-On</a> +``` + +**Dynamic widget:** fetch `/auth/providers` and render whatever is enabled. +```js +const { providers } = await fetch("/auth/providers?redirect=/account").then(r => r.json()); +for (const p of providers) { + const a = document.createElement("a"); + a.href = p.loginUrl; // relative, same-origin, redirect already encoded + a.textContent = `Sign in with ${p.label}`; + loginContainer.append(a); +} +``` + +Adding or removing a provider (a secret or `SSO_PROVIDERS` change in the portal) updates the widget with no code change on the customer's side. + +--- + +## 4. JSON Contracts + +### `GET /auth/providers` + +```jsonc +// GET /auth/providers?redirect=/cart +{ + "providers": [ + { "id": "google", "label": "Google", "loginUrl": "/auth/login/google?redirect=%2Fcart" }, + { "id": "github", "label": "GitHub", "loginUrl": "/auth/login/github?redirect=%2Fcart" }, + { "id": "saml", "label": "SSO", "loginUrl": "/auth/login?redirect=%2Fcart" } + ] +} +``` + +- `id` — stable identifier; matches the value used in `SSO_PROVIDERS`. +- `label` — human label; the SAML label is overridden by `IDP_LABEL`. +- `loginUrl` — relative path including the encoded `redirect`. +- Order is stable (registry order), not allowlist order. +- The enabled provider set is resolved at runtime from `SSO_PROVIDERS` ∩ providers-whose-credentials-are-present. + +### `GET /auth/branding` + +```jsonc +{ + "title": "Sign in", + "subtitle": "Choose a sign-in method", + "logoUrl": "https://cdn.example.com/logo.png", + "faviconUrl": null, + "accentColor": "#e00", + "backgroundColor": "#f0f2f5", + "cssUrl": null +} +``` + +Returns the current `LOGIN_PAGE_*` env var values as a JSON object. Custom login pages (Tier 2) can `fetch("/auth/branding")` to auto-style themselves consistently without duplicating the env var set. + +--- + +## 5. Redirect Validation + +The `?redirect=` parameter is validated against `SSO_ALLOWED_ORIGINS` on every route that honours it and on every read of the `saml_relay` cookie. + +**Rules:** +- Relative URLs (starting with `/`) are always permitted. +- Off-origin absolute URLs are silently dropped — the post-login redirect falls back to `/`. +- Protocol-relative and backslash bypasses (e.g., `/\evil.com`) are rejected. +- To permit absolute redirects to specific origins, set `SSO_ALLOWED_ORIGINS` to a comma-separated list of allowed origins (e.g., `https://shop.example.com`). + +This is a security-load-bearing constraint, not cosmetic. An incorrect or missing `SSO_ALLOWED_ORIGINS` will silently drop redirect parameters that include absolute URLs, changing post-login destination behavior without error. + +--- + +## 6. Shared Config the Origin Needs to Know About + +These env vars affect the contract the origin operates under. They are set on the FastEdge apps (auth-app and/or cdn-filter), but the origin integration depends on their values. + +| Env var | Set on | Origin concern | +|---|---|---| +| `SSO_VARIANT` | Both apps (must match) | Determines what the origin receives: nothing (gate-only), a JWT cookie to verify (cookie), or identity headers (header). | +| `SSO_AUDIENCE` | Both apps (must match) | The `aud` claim in every minted token. The cookie variant origin must validate `aud` matches this value when verifying the JWT. | +| `AUTH_PREFIX` | Both apps | The path prefix reserved for auth routes (default: `/auth`). The origin must not serve conflicting routes under this prefix; the CDN routes it to the auth-app. | +| `SESSION_COOKIE` | Both apps | Cookie name (default: `sso_session`). The cookie variant origin reads the session token from this cookie name. | +| `SESSION_SECRET` | Auth-app + cdn-filter (gate-only/header) | HS256 signing secret. The origin does not use this directly; only the filter verifies with it. | +| `SESSION_SIGNING_KEY` / `SESSION_PUBLIC_JWK` | Auth-app (cookie variant) | EC keypair. The origin uses the JWKS endpoint (`GET /auth/.well-known/jwks.json`) — never `SESSION_SIGNING_KEY` directly. | +| `SSO_ALLOWED_ORIGINS` | Auth-app | Governs which absolute redirect URLs are permitted. Affects post-login destination. | +| `LOGIN_PAGE_URL` | CDN filter | Where unauthenticated users are redirected. Default `/auth/`. Set to opt out of the built-in hosted page. | +| `CANONICAL_HOST` | Auth-app | The auth-app 301-redirects requests arriving on non-canonical hosts. The origin and IdP callback URLs must use this domain. | + +**Provider credentials** (`GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, `MICROSOFT_CLIENT_ID`, `FACEBOOK_CLIENT_ID`, SAML `IDP_*`/`SP_*`) are internal to the auth-app and not visible to the origin. Provider availability at runtime is reflected in `GET /auth/providers`. + +--- + +## See Also + +- edge-sso template README (deployment and configuration of the auth-app and cdn-filter) +- auth-app `.env.example` and cdn-filter `.env.example` (authoritative, exhaustive env var lists with inline guidance) +- edge-sso architecture: auth-modes (SSO_VARIANT axis detail) +- edge-sso architecture: security (token trust model, known limitations including no revocation and no IdP Single Logout) +- edge-sso architecture: overview (two-app deployment model, signing strategy, CANONICAL_HOST) diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md new file mode 100644 index 0000000..4b3a82a --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md @@ -0,0 +1,120 @@ +<!-- + auto-updated: true + sources: + - id: fastedge-templates + ref: main + commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 + updated: 2026-08-17 +--> + +# edge-totp — Origin Integration + +Reference for wiring the TOTP second-factor step into an origin's existing password login. The `edge-totp` template (otp-app + otp-filter) is already deployed on the Gcore portal; this document covers the three origin-side code changes required to connect it. + +--- + +## The Three Origin-Side Changes + +Only the origin's auth module changes. The rest of the site is untouched. + +### 1. Split login into password-then-OTP + +After the password check succeeds, instead of immediately minting the full origin session: + +1. Sign a **handoff ticket** = `HMAC(HANDOFF_KEY)` over `{ sub: userId, next, exp: now + TICKET_TTL }`. +2. Set a short-lived **`pre_mfa`** cookie or marker so the origin remembers the password step passed (bind it to `sub`). +3. Issue a `303` redirect to `{AUTH_PREFIX}/challenge?t=<ticket>` (totp-app, same host via the CDN path rule). + +**Ticket constraints:** +- `sub` — the user identifier +- `next` — the URL to return to after MFA +- `exp` — absolute expiry (`now + TICKET_TTL`); totp-app independently requires this field and caps absolute age +- Signed with `HANDOFF_KEY` (HS256, shared between origin and edge) + +### 2. Finish login on return — choose a profile + +When the user returns to `next` after the TOTP step, the origin finishes minting its session. Pick one profile: + +**Profile A (default — zero origin crypto):** +- The Rust filter has already enforced `mfa_session` on protected paths before the request reaches the origin. +- The origin re-identifies its `pre_mfa` user and mints its own session — no proof verification, no crypto on the origin side. +- `HANDOFF_KEY` is the only key the origin holds. + +**Profile B (opt-in — longer sessions, signed proof):** +- The edge delivers a one-time **ES256** proof as a short-lived cookie (`MFA_PROOF_COOKIE`). The proof is never in a URL. +- The origin verifies the proof via totp-app's JWKS endpoint (`{AUTH_PREFIX}/.well-known/jwks.json`) using `createRemoteJWKSet` (public key only — the origin cannot forge proofs). +- The origin checks that the proof's `sub` matches `pre_mfa`. +- **Required:** the origin must also verify the proof's `aud` claim against `MFA_AUDIENCE`. Verifying the signature alone without checking `aud` accepts a proof that was never meant for this deployment — this is a required check, not optional. +- After verification, the origin mints its own revocable session with whatever lifetime it needs (safely longer than the 8h edge session). + +### 3. Enroll users + +No new origin endpoint is required. Two paths: + +- **Admin provisioning:** `POST {AUTH_PREFIX}/enroll` (gated by `ENROLL_API_KEY`). totp-app writes the seed to KV using `GCORE_API_TOKEN`. Call with `force: true` to re-provision a lost authenticator behind your own identity check. +- **Self-service:** Users can self-enroll on first login via `{AUTH_PREFIX}/activate`. Enabled by default (`ALLOW_SELF_ENROLLMENT=true`). Set `ALLOW_SELF_ENROLLMENT=false` to require admin provisioning; `/activate` returns 403 and `/challenge` refuses unenrolled users. + +--- + +## Profile A vs Profile B — Decision Guide + +This is a security-posture decision. A wrong choice here is a real vulnerability, not a style issue. + +| Criterion | Profile A | Profile B | +| --- | --- | --- | +| Origin crypto required | None — `HANDOFF_KEY` only | ES256 proof verification via JWKS | +| Origin knows *which* user passed MFA | No — the filter only verifies that *a* valid `mfa_session` exists | Yes — the ES256 proof carries `sub`; origin verifies and binds it to `pre_mfa` | +| Session lifetime | Edge-bounded (8h, non-sliding) | Origin mints its own session at whatever lifetime it needs | +| Origin-lockdown dependency | Hard requirement — Profile A collapses entirely if the origin is directly reachable | More robust — origin independently verifies the signed proof rather than trusting the request came through the gate | + +**Do not add an unsigned forwarded-identity header for the origin to trust under Profile A.** Forwarded-identity headers (`x-mfa-user` or similar) are a recurring source of auth-bypass CVEs (e.g. oauth2-proxy header smuggling). The Rust filter deliberately does not forward the user id to the origin because the origin already authenticated the password and re-identifies its `pre_mfa` user when minting its session. If you need the edge to assert *which* user passed MFA, use Profile B: the ES256 proof carries `sub`, the origin verifies it via JWKS, and the proof is signed rather than a bare header. This is the same signed-assertion pattern Cloudflare Access uses (`Cf-Access-Jwt-Assertion`). + +**Profile B `aud` check is mandatory.** After verifying the ES256 signature via JWKS, also assert that the proof's `aud` claim equals `MFA_AUDIENCE`. The edge embeds `MFA_AUDIENCE` as `aud` in both `mfa_session` and the Profile-B proof. `MFA_AUDIENCE` should be the CDN hostname (e.g. `https://app.example.com`). A proof signed by this deployment's private key but addressed to a different audience must be rejected. + +**Prefer Profile B when the origin cannot be fully locked to edge-only traffic.** Profile B independently verifies a signed assertion at the origin; Profile A relies entirely on the filter gate having run. + +--- + +## Shared Configuration + +Keys and endpoints shared between the origin and the edge. Both components must agree on these values. + +| Key / Endpoint | Origin | Edge | +| --- | --- | --- | +| `HANDOFF_KEY` (HS256) | Signs the handoff ticket on password success | Verifies the ticket on `{AUTH_PREFIX}/challenge` and `{AUTH_PREFIX}/verify` | +| **JWKS** endpoint (Profile B only) | Fetches `{AUTH_PREFIX}/.well-known/jwks.json` via `createRemoteJWKSet` (public key only — cannot forge proofs) | Signs the one-time ES256 proof with `MFA_PROOF_SIGNING_KEY` (private key, edge-internal); serves the JWKS endpoint | +| `MFA_AUDIENCE` | **Must** enforce as the `aud` claim on the Profile-B proof (required check after signature verification) | Embedded as `aud` in both `mfa_session` and the Profile-B proof; the Rust filter enforces it on `mfa_session` (Profile A); fail-closes without it | + +**Profile A shares only `HANDOFF_KEY`.** The origin holds no verification key for `mfa_session`; that is edge-internal (signed with `MFA_SESSION_KEY`, verified by the Rust filter). No symmetric secret crosses to the origin on the proof path under Profile B — only the JWKS public-key fetch. + +Both components must live on the **same CDN host** (CDN path rule `{AUTH_PREFIX}/*` → totp-app). The `mfa_session` cookie is first-party and host-only. Same-host is required: the proof and session are consumed at the edge into a host-only cookie, so there is no cross-host URL-token path. + +--- + +## Required Deployment Precondition + +**Lock the origin to edge-only traffic.** This is a hard requirement for both profiles, not a suggestion. + +Any edge gate — Profile A or B — is meaningless if the origin is directly reachable. If the origin's IP or hostname is accessible without going through the Gcore CDN, an attacker simply skips the CDN entirely; `mfa_session`, the Rust filter, and the signed proof never run. Restrict the origin to Gcore CDN ingress using an IP allowlist, origin authentication, or a tunnel. + +Profile B is more robust in environments where the origin cannot be fully locked down (the origin independently verifies the signed proof rather than trusting that the request arrived through the gate), but it does not eliminate this requirement — locking the origin is still necessary. + +--- + +## Recovery and Self-Service Enrollment Caveat + +Self-service enrollment (`{AUTH_PREFIX}/activate`) inherits the trust level of the password step — a user who can pass the password check can self-enroll a new authenticator. This is the correct default for most deployments, but for sensitive accounts this means a compromised password also compromises the TOTP second factor. + +**Decision point for sensitive accounts:** If your threat model requires that the second factor remain independent of the password (e.g. privileged users, admin accounts), disable self-service enrollment (`ALLOW_SELF_ENROLLMENT=false`) and require admin provisioning via `POST {AUTH_PREFIX}/enroll` (gated by `ENROLL_API_KEY`, behind your own identity check). Recovery for a lost authenticator also goes through `POST {AUTH_PREFIX}/enroll` with `force: true`. + +See the `edge-totp` threat model reference (threat-model.md, risk R5) before relying on self-service enrollment for sensitive accounts. + +--- + +## See Also + +- edge-totp storage and secrets reference (storage-and-secrets.md) — full env var and secret list for both otp-app and otp-filter +- edge-totp threat model reference (threat-model.md) — trust boundaries, residual risks, and risk register including R4 (KV token scope) and R5 (self-enrollment trust level) +- edge-totp architecture flow reference (flow.md) — why the seed is fetched at verify time and PoP replication behavior +- FastEdge KV store reference — `fastedge::kv` SDK binding and `storeRefs`/`kvStoreVars` deploy-time linking +- FastEdge secrets reference — `getSecret` API and dotenv sync workflow diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md index 52ac44f..d2b1159 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -222,3 +222,233 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide + +## Source Material + +### FILE: examples/abTesting/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; + +class AbTestingRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new AbTestingContext(context_id, this); + } +} + +class AbTestingContext extends Context { + constructor(context_id: u32, root_context: AbTestingRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const experimentName = getEnv("EXPERIMENT_NAME"); + if (experimentName === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const variantAPath = getEnv("VARIANT_A_PATH"); + const variantBPath = getEnv("VARIANT_B_PATH"); + if (variantAPath === "" || variantBPath === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode( + "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", + ), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Check for existing experiment cookie + const cookieName = "fe_exp_" + experimentName; + const cookieHeader = stream_context.headers.request.get("Cookie"); + let assignedVariant = this.getCookieValue(cookieHeader, cookieName); + + // Assign variant if not already set + if (assignedVariant !== "A" && assignedVariant !== "B") { + // Use current time as a simple entropy source for 50/50 split + const now = getCurrentTime(); + assignedVariant = now % 2 == 0 ? "A" : "B"; + } + + // Rewrite the request path to the variant path + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + return FilterHeadersStatusValues.Continue; + } + const originalPath = String.UTF8.decode(pathArrBuf); + const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; + const newPath = variantPath + originalPath; + + // Reconstruct request.url from its decomposed parts rather than splicing + // the path out of the full URL — splicing breaks when the path happens to + // appear inside the host, and it can silently lose the query string. + const schemeBuf = get_property("request.scheme"); + const hostBuf = get_property("request.host"); + if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { + const scheme = String.UTF8.decode(schemeBuf); + const host = String.UTF8.decode(hostBuf); + const queryBuf = get_property("request.query"); + const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; + const newUrl = + scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); + log(LogLevelValues.info, `A/B routing: ${newUrl}`); + set_property("request.url", String.UTF8.encode(newUrl)); + } + + // Add variant header for upstream visibility + stream_context.headers.request.add("X-Experiment", experimentName); + stream_context.headers.request.add("X-Variant", assignedVariant); + + log( + LogLevelValues.info, + `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, + ); + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Recover the assigned variant from the request header set in onRequestHeaders. + // Instance state (this.variant) does not survive the nginx -> core-proxy hop. + const variant = stream_context.headers.request.get("X-Variant"); + if (variant === "") { + return FilterHeadersStatusValues.Continue; + } + + const experimentName = getEnv("EXPERIMENT_NAME"); + const cookieName = "fe_exp_" + experimentName; + + // Set the experiment cookie so subsequent requests stick to the same variant + stream_context.headers.response.add( + "Set-Cookie", + cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", + ); + + // Add variant as response header for observability + stream_context.headers.response.add("X-Variant", variant); + + return FilterHeadersStatusValues.Continue; + } + + private getCookieValue(cookieHeader: string, name: string): string { + if (cookieHeader === "") return ""; + const pairs = cookieHeader.split(";"); + const prefix = name + "="; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].trim(); + if (pair.startsWith(prefix)) { + return pair.substring(prefix.length); + } + } + return ""; + } +} + +registerRootContext((context_id: u32) => { + return new AbTestingRoot(context_id); +}, "abTesting"); +``` + + +### FILE: examples/abTesting/package.json + +```json +{ + "name": "fastedge-as-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/abTesting/README.md + +``` +[← Back to examples](../README.md) + +# A/B Testing + +This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. + +## What it does + +In `onRequestHeaders`, the app: + +1. Checks for an existing experiment cookie (`fe_exp_<EXPERIMENT_NAME>`). +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-cursor/skills/scaffold/reference/cdn/api-key-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-as.md index 218310a..436b145 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -122,6 +122,8 @@ class ApiKeyContext extends Context { } // 3. Strip key before forwarding to upstream + // .remove() sets the header value to "" rather than deleting it entirely — + // the upstream will see X-API-Key: "" rather than a missing header. stream_context.headers.request.remove("X-API-Key"); log(LogLevelValues.info, "API key validated successfully"); diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md index 4c21047..4e444bf 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -212,3 +212,122 @@ target = "wasm32-wasip1" - FastEdge secrets configuration (dashboard secret variable setup) - auth-jwt-rust reference (JWT-based authentication — use when token expiry and claims are needed) - platform-overview reference (CDN vs HTTP app type distinction) + +## Source Material + +### FILE: examples/cdn/api_key/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating API key validation. + +Validates requests using an X-API-Key header checked against a stored +secret. Simpler alternative to JWT when token expiry and claims are +not needed. + +Required configuration: + - Secret: API_KEY +*/ + +use fastedge::proxywasm::secret; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(ApiKeyRoot) }); +}} + +struct ApiKeyRoot; + +impl Context for ApiKeyRoot {} + +impl RootContext for ApiKeyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(ApiKeyContext)) + } +} + +struct ApiKeyContext; + +impl Context for ApiKeyContext {} + +impl HttpContext for ApiKeyContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let expected_key = match secret::get("API_KEY") { + Ok(Some(bytes)) => match String::from_utf8(bytes) { + Ok(s) if !s.is_empty() => s, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }; + + let provided_key = match self.get_http_request_header("X-API-Key") { + Some(k) if !k.is_empty() => k, + _ => { + self.send_http_response( + 401, + vec![("WWW-Authenticate", "API-Key")], + Some(b"Missing X-API-Key header"), + ); + return Action::Pause; + } + }; + + if provided_key != expected_key { + println!("API key validation failed"); + self.send_http_response(403, vec![], Some(b"Invalid API key")); + return Action::Pause; + } + + // Strip the API key header before forwarding to upstream + self.set_http_request_header("X-API-Key", None); + + println!("API key validated successfully"); + Action::Continue + } +} +``` + + +### FILE: examples/cdn/api_key/Cargo.toml + +```toml +[workspace] + +[package] +name = "api_key" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + + +### FILE: examples/cdn/api_key/README.md + +``` +[← Back to examples](../../README.md) + +# API Key (CDN) + +Validates requests using an `X-API-Key` header checked against a stored secret. Returns 401 if missing, 403 if invalid, and strips the header before forwarding to upstream. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md index 6b54958..ff3c689 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -234,6 +234,20 @@ registerRootContext((context_id: u32) => { | Token invalid (any other error) | 403 | `Invalid token` | | Token valid | — | Pass-through (`Continue`) | +## Testing Tokens + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +**Expired token** (returns `403 Forbidden`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, returns `200 OK`): +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + ## Build Output | File | Description | @@ -253,3 +267,206 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint + +## Source Material + +### FILE: examples/jwt/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class AuthRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes + return new Auth(context_id, this); + } +} + +class Auth extends Context { + constructor(context_id: u32, root_context: AuthRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const secret = getSecret("SECRET"); + if (!secret) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const authHeader = stream_context.headers.request.get("Authorization"); + if (!authHeader) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("No Authorization header"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (!authHeader.startsWith("Bearer ")) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Authorization header must use Bearer scheme"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const token = authHeader.slice(7); // strip "Bearer " prefix + if (!token) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Token not found"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Decode the JWT token + const jwtResult = jwtVerify(token, secret); + if (jwtResult !== JwtValidation.Ok) { + if (jwtResult === JwtValidation.Expired) { + log(LogLevelValues.info, "Token Expired"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Expired token"), + [], + ); + } else { + log(LogLevelValues.info, "Bad Token"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid token"), + [], + ); + } + return FilterHeadersStatusValues.StopIteration; + } + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new AuthRoot(context_id); +}, "auth"); +``` + + +### FILE: examples/jwt/package.json + +```json +{ + "name": "fastedge-as-example-jwt", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: JWT validation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/as-jwt": "^1.0.3", + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", + "assemblyscript-json": "^1.1.0" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/jwt/README.md + +``` +[← Back to examples](../README.md) + +# JWT Validation + +This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. +2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. +3. Verifies the token signature and expiry using `jwtVerify()`. +4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. + +## Configuration + +Set the following secret variable on your FastEdge application: + +| Secret | Description | +| -------- | ------------------------------------------------------------------ | +| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | + +## Testing tokens + +**Expired token** (will return `403 Forbidden`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, will return `200 OK`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ---------------------- | -------------------------------------------------- | +| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | +| `build/jwt-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/jwt.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. + +For more on secrets and secret rotation slots, see the [FastEdge secrets documentation](https://gcore.com/docs/fastedge/secrets-manager/slots). +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md index f92ce4c..28c7eda 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -259,142 +259,3 @@ target = "wasm32-wasip1" - FastEdge SDK Rust reference (proxywasm module, secret API) - FastEdge secrets configuration (dashboard secret variable setup) - platform-overview reference (CDN vs HTTP app type distinction) - -## Source Material - -### FILE: examples/cdn/jwt/src/lib.rs - -```rust -use std::time::{SystemTime, UNIX_EPOCH}; -use headers::HeaderValue; -use headers::authorization::{Bearer, Credentials}; - -use fastedge::proxywasm::secret; -use jsonwebtoken::{decode, DecodingKey, Validation}; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde::Deserialize; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); -}} - -struct HttpHeadersRoot; - -impl Context for HttpHeadersRoot {} - -impl RootContext for HttpHeadersRoot { - fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(HttpHeaders {})) - } - - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } -} - -struct HttpHeaders {} - -impl Context for HttpHeaders {} - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -#[derive(Debug, Deserialize, Default)] -struct Claims { - exp: u64, -} - -impl HttpContext for HttpHeaders { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Ok(Some(secret)) = secret::get("secret") else { - println!("'secret' param not set"); - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - let Some(value) = self.get_http_request_header("Authorization") else { - println!("No auth header"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); - return Action::Pause; - }; - - if value.is_empty() { - println!("Auth header is empty"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); - return Action::Pause; - }; - - let Ok(header) = value.parse::<HeaderValue>() else { - println!("Auth header is invalid"); - self.send_http_response(UNAUTHORIZED, vec![], Some(b"Invalid Authorization header")); - return Action::Pause; - }; - - - let Some(bearer) = Bearer::decode(&header) else { - println!("Auth header doesn't contain token"); - self.send_http_response(FORBIDDEN, vec![], Some(b"Token not found")); - return Action::Pause; - }; - - let token = bearer.token(); - - let decoding_key = DecodingKey::from_secret(&secret); - let mut validation = Validation::default(); - validation.set_required_spec_claims(&["exp"]); - // skip validation af aud and nbf claims - validation.validate_aud = false; - validation.validate_nbf = false; - validation.validate_exp = false; // will validate expiration separately - - let token_data = match decode::<Claims>(token, &decoding_key, &validation) { - Ok(token_data) => token_data, - Err(error) => { - println!("Token is invalid"); - self.send_http_response(FORBIDDEN, vec![], Some(format!("Could not decode token {}: {}", token, error).as_bytes())); - return Action::Pause; - } - }; - - let claims = token_data.claims; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - if now > claims.exp { - println!("Token expired"); - self.send_http_response(FORBIDDEN, vec![], Some(b"Token expired")); - return Action::Pause; - } - - println!("Token ok"); - Action::Continue - } -} -``` - - -### FILE: examples/cdn/jwt/Cargo.toml - -```toml -[workspace] - -[package] -name = "jwt" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -jsonwebtoken = "9" -serde = { version = "1", features = ["derive"] } -headers = "0.4" -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md index 0138cee..8b560bd 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -14,8 +14,8 @@ languages: [assemblyscript] template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as -source_ref: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 -updated: 2026-05-20 +source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 +updated: 2026-08-17 --- # Base Skeleton: CDN AssemblyScript @@ -263,112 +263,3 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) - -## Source Material - -### FILE: examples/helloWorld/assembly/index.ts - -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; - -class HelloWorldRoot extends RootContext { - createContext(context_id: u32): Context { - return new HelloWorld(context_id, this); - } -} - -class HelloWorld extends Context { - constructor(context_id: u32, root_context: HelloWorldRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool, - ): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onRequestBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onResponseBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HelloWorldRoot(context_id); -}, "helloWorld"); - - -### FILE: examples/helloWorld/package.json - -{ - "name": "fastedge-as-example-hello-world", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} - - -### FILE: examples/helloWorld/asconfig.json - -{ - "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", - "targets": { - "debug": { - "outFile": "build/helloWorld-debug.wasm", - "textFile": "build/helloWorld-debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/helloWorld.wasm", - "textFile": "build/helloWorld.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "use": "abort=abort_proc_exit" - } -} diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md index 7b47146..d4f417a 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: cdn-base source_repo: https://github.com/G-Core/FastEdge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-07-23 +updated: 2026-08-17 --- # Base Skeleton: CDN Rust @@ -165,29 +165,6 @@ lerna-debug.log* .history/* ``` -## Logging Convention - -FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: - -1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. -2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - -**Do not use:** -- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime -- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer -- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - -If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - -Example pattern for CDN Rust: -```rust -fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - log::info!("incoming request"); // visible in FastEdge logs - // eprintln!("incoming request"); // DO NOT use — dropped - Action::Continue -} -``` - ## Build Configuration ```bash @@ -209,76 +186,25 @@ cargo build --release --target wasm32-wasip1 - **RootContext**: implements `get_type() -> Option<ContextType>` returning `ContextType::HttpContext` and `create_http_context(_: u32) -> Option<Box<dyn HttpContext>>` - **HttpContext**: all 4 hooks return `Action::Continue` in the base skeleton; `on_http_response_headers` adds `x-powered-by: FastEdge` response header via `self.add_http_response_header` -## Source Material - -### FILE: examples/cdn/hello_world/src/lib.rs - -```rust -use log::info; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HelloWorldRoot) }); -}} - -struct HelloWorldRoot; - -impl Context for HelloWorldRoot {} - -impl RootContext for HelloWorldRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(HelloWorld)) - } -} - -struct HelloWorld; +## Logging Convention -impl Context for HelloWorld {} +FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: -impl HttpContext for HelloWorld { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_headers"); - Action::Continue - } +1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. +2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_body"); - Action::Continue - } +**Do not use:** +- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime +- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer +- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - self.add_http_response_header("x-powered-by", "FastEdge"); - info!("Hello from on_http_response_headers"); - Action::Continue - } +If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_response_body"); - Action::Continue - } +Example pattern for CDN Rust: +```rust +fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + log::info!("incoming request"); // visible in FastEdge logs + // eprintln!("incoming request"); // DO NOT use — dropped + Action::Continue } ``` - -### FILE: examples/cdn/hello_world/Cargo.toml - -```toml -[workspace] - -[package] -name = "hello_world" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md index da93373..e3da1c6 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -70,7 +70,7 @@ registerRootContext() → registers the root context factory with a plugin name ```typescript class HttpBodyRoot extends RootContext { createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); + setLogLevel(LogLevelValues.info); // reduce to LogLevelValues.debug for more logging return new HttpBody(context_id, this); } } @@ -97,6 +97,7 @@ Called when request headers arrive. Must remove `content-length` before the body ```typescript onRequestHeaders(headers: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); stream_context.headers.request.remove("content-length"); return FilterHeadersStatusValues.Continue; } @@ -112,6 +113,7 @@ Called (possibly multiple times) as request body chunks arrive. Buffer until `en ```typescript onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } @@ -124,6 +126,7 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); if (bodyStr.includes("Client")) { const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; set_buffer_bytes( @@ -154,10 +157,11 @@ onRequestBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusV ### `onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues` -Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. +Called when response headers arrive. Must remove `content-length` and set chunked transfer encoding before the response body is modified. Captures the `content-type` header into a runtime property for use in `onResponseBody`. ```typescript onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); stream_context.headers.response.remove("content-length"); stream_context.headers.response.replace("transfer-encoding", "Chunked"); @@ -185,15 +189,31 @@ Called (possibly multiple times) as response body chunks arrive. Buffer until `e ```typescript onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + if (!end_of_stream) { return FilterDataStatusValues.StopIterationAndBuffer; } + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + const urlBytes = get_property("request.url"); const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } const contentTypeBytes = get_property("response.content_type"); - const contentType = contentTypeBytes.byteLength === 0 ? "" : String.UTF8.decode(contentTypeBytes); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } const bodyBytes = get_buffer_bytes( BufferTypeValues.HttpResponseBody, @@ -203,7 +223,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus if (bodyBytes.byteLength > 0) { const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "onHttpResponseBody >> bodyStr: " + bodyStr); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); } return FilterDataStatusValues.Continue; } @@ -217,13 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Used for final audit logging. - -```typescript -onLog(): void { - log(LogLevelValues.info, "onLog >> completed (contextId): " + this.context_id.toString()); -} -``` +Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. ## Registration @@ -334,7 +348,7 @@ pnpm run asbuild Build scripts defined in `package.json`: - `asbuild:debug` — `asc assembly/index.ts --target debug` - `asbuild:release` — `asc assembly/index.ts --target release` -- `asbuild` — runs both +- `asbuild` — runs both (`npm run asbuild:debug && npm run asbuild:release`) ## Deploy diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md index 725bf65..e8b9ba2 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -243,3 +243,125 @@ Requires `log = "0.4"` in `Cargo.toml`. Log level is set to `Trace` at startup v - proxy-wasm HttpContext trait reference - host-services-rust reference (property API, logging) - platform-overview reference (CDN app lifecycle) + +## Source Material + +### FILE: examples/cdn/body/src/lib.rs + +```rust +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpBody)) + } +} + +struct HttpBody; + +impl Context for HttpBody {} + +impl HttpContext for HttpBody { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + self.set_http_request_header("content-length", None); + Action::Continue + } + + fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + // Wait -- we'll be called again when the complete body is buffered + // at the host side. + return Action::Pause; + } + + if let Some(body_bytes) = self.get_http_request_body(0, body_size) { + let body_str = String::from_utf8(body_bytes).unwrap(); + if body_str.contains("Client") { + let new_body = + format!("Client's original message body ({body_size} bytes) redacted.\n"); + self.set_http_request_body(0, body_size, &new_body.into_bytes()); + } + } + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // remove content-length as we plan to change the body size + self.set_http_response_header("content-length", None); + // set transfer-encoding to chunked as we don't know body length + self.set_http_response_header("transfer-encoding", Some("Chunked")); + + if let Some(content_type) = self.get_http_response_header("content-type") { + self.set_property(vec!["response.content_type"], Some(content_type.as_bytes())); + } + + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + return Action::Pause; + } + + let url = if let Some(value) = self.get_property(vec!["request.url"]) { + let url = String::from_utf8_lossy(&value); + info!("url={}", url); + url.to_string() + } else { + "".to_string() + }; + + let content_type = + if let Some(content_type) = self.get_property(vec!["response.content_type"]) { + let content_type = String::from_utf8_lossy(&content_type); + info!("content_type={}", content_type); + content_type.to_string() + } else { + "NONE".to_string() + }; + + if let Some(body_bytes) = self.get_http_response_body(0, body_size) { + let body_str = String::from_utf8(body_bytes).unwrap(); + if body_str.contains("Client") { + let new_body = + format!("Original message body ({body_size} bytes) redacted.\nURL: {url}\nContent-Type: {content_type}\n"); + self.set_http_response_body(0, body_size, &new_body.into_bytes()); + } + } + Action::Continue + } +} +``` + +### FILE: examples/cdn/body/Cargo.toml + +```toml +[workspace] + +[package] +name = "body" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md index eff8100..2987a96 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -187,3 +187,191 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=<STATIC_MAX_AGE>, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=<HTML_MAX_AGE>, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=<API_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-cursor/skills/scaffold/reference/cdn/cache-control-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-rust.md index 25517cf..b49ed2a 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -219,144 +219,3 @@ use std::env; - proxy-wasm HttpContext trait reference - FastEdge-sdk-rust CDN examples overview - host-services-rust reference (hostcalls, logging) - -## Source Material - -### FILE: examples/cdn/cache_control/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating content-type-aware cache control. - -Sets Cache-Control response headers based on the content type and -response status, providing fine-grained control over CDN caching. - -Optional configuration: - - Environment variable: STATIC_MAX_AGE (default: 31536000) - - Environment variable: HTML_MAX_AGE (default: 3600) - - Environment variable: API_MAX_AGE (default: 0 = no-cache) -*/ - -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::env; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(CacheControlRoot) }); -}} - -struct CacheControlRoot; - -impl Context for CacheControlRoot {} - -impl RootContext for CacheControlRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(CacheControlContext)) - } -} - -struct CacheControlContext; - -impl Context for CacheControlContext {} - -impl HttpContext for CacheControlContext { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Read response status - let status_code = self - .get_property(vec!["response.status"]) - .and_then(|bytes| { - if bytes.len() == 2 { - Some(u16::from_be_bytes([bytes[0], bytes[1]])) - } else { - None - } - }) - .unwrap_or(200); - - // Error responses should never be cached - if !(200..400).contains(&status_code) { - self.set_http_response_header("Cache-Control", Some("no-store")); - return Action::Continue; - } - - let content_type = self - .get_http_response_header("Content-Type") - .unwrap_or_default(); - - let static_max_age = env::var("STATIC_MAX_AGE").unwrap_or_else(|_| "31536000".to_string()); - let html_max_age = env::var("HTML_MAX_AGE").unwrap_or_else(|_| "3600".to_string()); - let api_max_age = env::var("API_MAX_AGE").unwrap_or_else(|_| "0".to_string()); - - let cache_control = if is_static_asset(&content_type) { - format!("public, max-age={}, immutable", static_max_age) - } else if content_type.contains("text/html") { - self.add_http_response_header("Vary", "Accept-Encoding"); - format!("public, max-age={}, must-revalidate", html_max_age) - } else if content_type.contains("application/json") - || content_type.contains("application/xml") - { - self.add_http_response_header("Vary", "Accept, Authorization"); - if api_max_age == "0" { - "no-cache, no-store, must-revalidate".to_string() - } else { - format!("private, max-age={}, must-revalidate", api_max_age) - } - } else { - "public, max-age=600".to_string() - }; - - self.set_http_response_header("Cache-Control", Some(&cache_control)); - - println!( - "Cache-Control: {} (content-type: {})", - cache_control, content_type - ); - - Action::Continue - } -} - -fn is_static_asset(content_type: &str) -> bool { - content_type.starts_with("image/") - || content_type.starts_with("font/") - || content_type.contains("application/javascript") - || content_type.contains("text/css") - || content_type.contains("text/javascript") - || content_type.contains("application/wasm") -} -``` - -### FILE: examples/cdn/cache_control/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_control" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -``` - -### FILE: examples/cdn/cache_control/README.md - -``` -[← Back to examples](../../README.md) - -# Cache Control (CDN) - -Sets `Cache-Control` response headers based on content type and response status, providing fine-grained control over CDN caching behaviour. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/convert-image-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/convert-image-rust.md index 9ce892f..69d0e83 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/convert-image-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -191,7 +191,7 @@ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Ac ## Helper: `rsp_status` -Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. +Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. Defined as an `impl ConvertImageContext` method. ```rust fn rsp_status(&mut self) -> Option<u16> { @@ -284,3 +284,300 @@ proxy_wasm::main! {{ - scaffold reference for CDN app type - FastEdge SDK Rust host services reference (proxy-wasm ABI, `get_property`, `set_property`) - platform overview (CDN app lifecycle, cache variation with `Vary` headers) + +## Source Material + +### FILE: examples/cdn/convert_image/src/lib.rs + +```rust +use image::*; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::{env, env::VarError, io::Cursor, str::from_utf8}; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(ConvertImageRoot) }); +}} + +struct ConvertImageRoot; + +impl Context for ConvertImageRoot {} + +impl RootContext for ConvertImageRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(ConvertImageContext)) + } +} + +struct ConvertImageContext; + +impl Context for ConvertImageContext {} + +impl HttpContext for ConvertImageContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + // this header is used to select correct image version from cache + self.add_http_request_header("Image-Format", "original"); + + // get extension + let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); + println!("request.path={path:?}"); + let raw_ext = self.get_property(vec!["request.extension"]); + println!("request.extension={raw_ext:?}"); + let Some(ext) = raw_ext else { + println!("No extension in request path, not transforming"); + return Action::Continue; + }; + let Ok(ext) = from_utf8(&ext) else { + println!("Invalid UTF-8 in request extension, not transforming"); + return Action::Continue; + }; + if ext.is_empty() { + println!("No extension in request path, not transforming"); + return Action::Continue; + } + + // FORMATS_TO_TRANSFORM contains list of file extensions to transfor + // note that jpg and jpeg are different extensions + let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { + println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); + return Action::Continue; + }; + if !image_list.split(',').any(|entry| entry == ext) { + println!( + "extension {} is not in the list of formats to transform: {}, not transforming", + ext, image_list + ); + return Action::Continue; + } + + // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed + let Some(ua) = self.get_http_request_header("User-Agent") else { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + }; + if ua.is_empty() { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + } + if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { + if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { + println!("User-Agent is in ignore list, not transforming"); + return Action::Continue; + } + } + + // indicator for on_response_headers and for cache key + self.set_http_request_header("Image-Format", Some("image/avif")); + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // only process 200 responses + if let Some(status) = self.rsp_status() { + if status != 200 { + println!( + "Response status is {} instead of expected 200, not transforming", + status + ); + return Action::Continue; + } + } else { + println!("Response status is not set, not transforming"); + return Action::Continue; + } + + // if "Image-Format" request header is not set, don't convert the image + let Some(content_type) = self.get_http_request_header("Image-Format") else { + return Action::Continue; + }; + // instruct cache to vary by this header so "original" and "image/avif" are cached separately + self.add_http_response_header("Vary", "Image-Format"); + + if content_type == "original" { + return Action::Continue; + }; + + // image to be transformed, set headers accordingly + self.set_http_response_header("Content-Length", None); + self.set_http_response_header("Transfer-Encoding", Some("Chunked")); + self.set_http_response_header("Content-Type", Some(content_type.as_str())); + + // indicate to on_http_response_body that transformation is needed + self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); + + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + // wait till we get complete body + return Action::Pause; + } + + let Some(content_type) = self.get_property(vec!["response.content-type"]) else { + return Action::Continue; + }; + + let Ok(content_type) = from_utf8(&content_type) else { + // should never happen + println!("Invalid UTF-8 in Content-Type"); + self.send_http_response(500, vec![], None); + return Action::Pause; + }; + + if content_type != "image/avif" { + // should never happen + println!( + "Content-Type {} is not supported, not transforming", + content_type + ); + return Action::Continue; + } + + if let Some(body_bytes) = self.get_http_response_body(0, body_size) { + let buf = body_bytes.as_bytes(); + let img = match load_from_memory(buf) { + Ok(i) => i, + Err(e) => { + println!("cannot load image to memory {}, not converting", e); + return Action::Continue; + } + }; + + let mut out = Vec::new(); + let mut c = Cursor::new(&mut out); + let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( + &mut c, + u8_param("AVIF_SPEED", 1, 10, 5), + u8_param("AVIF_QUALITY", 1, 100, 70), + )); + + match res { + Ok(_) => { + println!( + "{} bytes -> {} bytes {}", + body_size, + out.len(), + content_type + ); + self.set_http_response_body(0, body_size, &out) + } + Err(e) => println!("cannot store transformed image {}", e), + } + } else { + println!("No response body to transform"); + } + + Action::Continue + } +} + +impl ConvertImageContext { + fn rsp_status(&mut self) -> Option<u16> { + if let Some(status) = self.get_property(vec!["response.status"]) { + if status.len() != 2 { + println!("HTTP status property is not 2 bytes"); + return None; + } + return Some(u16::from_be_bytes([status[0], status[1]])); + } + None + } +} + +fn str_param(name: &str) -> Result<String, VarError> { + let val = env::var(name)?; + if val.is_empty() { + return Err(VarError::NotPresent); + } + + Ok(val) +} + +fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { + let Ok(val) = env::var(name) else { + println!("Param {} is not set, using default value {}", name, default); + return default; + }; + if val.is_empty() { + println!("Param {} is not set, using default value {}", name, default); + return default; + } + + let val = match val.parse() { + Err(_) => { + println!( + "Param {} is not a valid number, using default value {}", + name, default + ); + return default; + } + Ok(v) => v, + }; + if val < min { + println!( + "Param {} is below minimum {}, using default value {}", + name, min, default + ); + return default; + } + if val > max { + println!( + "Param {} is above maximum {}, using default value {}", + name, max, default + ); + return default; + } + + val +} +``` + + +### FILE: examples/cdn/convert_image/Cargo.toml + +```toml +[workspace] + +[package] +name = "convert_image" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +image = "0.25" +``` + + +### FILE: examples/cdn/convert_image/README.md + +``` +[← Back to examples](../../README.md) + +# Convert Image (CDN) + +Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. + +## Configuration + +- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) +- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip +- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) +- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) + +## How it works + +1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation +2. **on_response_headers** — sets response headers for AVIF content type on 200 responses +3. **on_response_body** — decodes the original image and re-encodes it as AVIF +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-as.md index dd83b67..8c95ff2 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -146,3 +146,157 @@ Build scripts: - proxy-wasm-sdk-as SDK reference - FastEdge CDN application environment variable configuration - FastEdge portal deployment guide + +## Source Material + +### FILE: examples/cors/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CorsRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CorsContext(context_id, this); + } +} + +class CorsContext extends Context { + constructor(context_id: u32, root_context: CorsRoot) { + super(context_id, root_context); + } + + private isOriginAllowed(origin: string, allowedOrigins: string): bool { + if (allowedOrigins === "" || allowedOrigins === "*") return true; + const origins = allowedOrigins.split(","); + for (let i = 0; i < origins.length; i++) { + if (origins[i].trim() == origin) return true; + } + return false; + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + log(LogLevelValues.info, "onRequestHeaders >> origin: " + origin); + + if (origin !== "" && !this.isOriginAllowed(origin, allowedOrigins)) { + log(LogLevelValues.info, "CORS: origin not allowed: " + origin); + } + + // OPTIONS preflights are answered by the FastEdge edge layer before this + // hook fires — don't try to handle them here. + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const allowedOrigins = getEnv("ALLOWED_ORIGINS"); + const origin = stream_context.headers.request.get("Origin"); + + if (origin === "" || !this.isOriginAllowed(origin, allowedOrigins)) { + return FilterHeadersStatusValues.Continue; + } + + const effectiveOrigin = allowedOrigins === "*" ? "*" : origin; + + stream_context.headers.response.add( + "Access-Control-Allow-Origin", + effectiveOrigin, + ); + stream_context.headers.response.add("Vary", "Origin"); + + const exposeHeaders = getEnv("EXPOSE_HEADERS"); + if (exposeHeaders !== "") { + stream_context.headers.response.add( + "Access-Control-Expose-Headers", + exposeHeaders, + ); + } + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new CorsRoot(context_id); +}, "cors"); +``` + +### FILE: examples/cors/package.json + +```json +{ + "name": "fastedge-as-example-cors", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: CORS — preflight handling and response headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/cors/README.md + +``` +[← Back to examples](../README.md) + +# CORS + +This application adds Cross-Origin Resource Sharing (CORS) headers to responses from allowed origins. + +## What it does + +In `onResponseHeaders`, for requests from allowed origins, the app adds `Access-Control-Allow-Origin` and `Vary: Origin` response headers. Optionally exposes additional headers via `Access-Control-Expose-Headers`. Requests from disallowed origins pass through unchanged (no CORS headers added). + +> **Note on OPTIONS preflights:** FastEdge's edge layer answers OPTIONS preflight requests directly — proxy-wasm hooks do not fire for OPTIONS. Configure preflight behaviour (allowed methods, max-age, etc.) in your CDN application settings, not in WASM code. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `ALLOWED_ORIGINS` | `https://example.com,https://app.example.com` | Comma-separated allowed origins, or `*` for any (required) | +| `EXPOSE_HEADERS` | `X-Request-Id, X-Trace-Id` | Response headers to expose to the browser (optional) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cors.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cors-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cors.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `ALLOWED_ORIGINS` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-rust.md index 1c410d6..d8d3907 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -310,3 +310,207 @@ Edit `public/styles.css` directly. No build tools required. Styles are embedded - host-services-rust reference (property API) - platform-overview reference (CDN app lifecycle) - body-rust blueprint (general body manipulation pattern) + +## Source Material + +### FILE: examples/cdn/custom_error_pages/src/lib.rs + +```rust +use handlebars::Handlebars; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde_json::json; +use std::collections::HashMap; +use std::env; + +// Include the generated image map +include!(concat!(env!("OUT_DIR"), "/image_map.rs")); +include!(concat!(env!("OUT_DIR"), "/message_map.rs")); + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpBodyRoot) }); +}} + +struct HttpBodyRoot; + +impl Context for HttpBodyRoot {} + +impl RootContext for HttpBodyRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpBody)) + } +} + +struct HttpBody; + +impl Context for HttpBody {} + +impl HttpContext for HttpBody { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + if let Some(status) = self.get_property(vec!["response.status"]) { + if status.len() == 2 { + let status_code = u16::from_be_bytes([status[0], status[1]]); + if (400..600).contains(&status_code) { + // Remove the Content-Length header if it exists, we are going to change the response body + self.set_http_response_header("Content-Length", None); + self.set_http_response_header("Transfer-Encoding", Some("Chunked")); + self.set_http_response_header("Content-Type", Some("text/html")); + } + } + } + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + // only process 4xx/5xx error responses + let Some(status) = self.get_property(vec!["response.status"]) else { + return Action::Continue; + }; + if status.len() != 2 { + return Action::Continue; + } + let status_code = u16::from_be_bytes([status[0], status[1]]); + if !(400..600).contains(&status_code) { + return Action::Continue; + } + + if !end_of_stream { + // wait for complete body + return Action::Pause; + } + + // Get the image and message maps + let image_map = get_image_map(); + let message_map = get_message_map(); + + // Get the Base64-encoded image for the status code or its fallback + let base64_image = image_map + .get(&status_code) + .or_else(|| { + if (400..500).contains(&status_code) { + image_map.get(&4000) + } else if (500..600).contains(&status_code) { + image_map.get(&5000) + } else { + None + } + }) + .unwrap_or(&""); + + // Get the message and description for the status code or its fallback + let (message, description) = message_map + .get(&status_code) + .or_else(|| { + if (400..500).contains(&status_code) { + message_map.get(&4000) + } else if (500..600).contains(&status_code) { + message_map.get(&5000) + } else { + None + } + }) + .map(|(msg, desc)| (msg.to_string(), desc.to_string())) + .unwrap_or_else(|| { + ( + "Unexpected Error".to_string(), + "The server responded with a {{status}} error.".to_string(), + ) + }); + + let mut handlebars = Handlebars::new(); + // Use handlebars to complete message and description text allowing for usage of {{ status }} variable + handlebars + .register_template_string("message_template", message) + .unwrap(); + handlebars + .register_template_string("description_template", description) + .unwrap(); + + let msg_data = json!({ + "status": status_code.to_string(), + }); + + let complete_message = handlebars.render("message_template", &msg_data).unwrap(); + let complete_description = handlebars + .render("description_template", &msg_data) + .unwrap(); + + // Render the error page using Handlebars + let error_template = include_str!("../templates/error_page.hbs"); + handlebars + .register_template_string("error_template", error_template) + .unwrap(); + + let styles = include_str!("../public/styles.css"); + let page_data = json!({ + "styles": styles, + "status": status_code.to_string(), + "message": complete_message, + "description": complete_description, + "image": base64_image, + }); + + let html_body = handlebars.render("error_template", &page_data).unwrap(); + let body = html_body.as_bytes(); + self.set_http_response_body(0, body_size, body); + + Action::Continue + } +} +``` + +### FILE: examples/cdn/custom_error_pages/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom_error_pages" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +handlebars = "6.3" +serde_json = "1.0" +regex = "1.10" + +[build-dependencies] +base64 = "0.22" +``` + +### FILE: examples/cdn/custom_error_pages/README.md + +``` +[← Back to examples](../../README.md) + +# Custom Error Pages (CDN) + +Intercepts 4xx and 5xx error responses and replaces them with branded HTML error pages using Handlebars templates. + +## How it works + +A [build script](./build.rs) runs at compile time to embed images and messages from the `public/` folder into the WASM binary (since there is no filesystem at runtime). + +At runtime, when an error response is detected: +1. **on_response_headers** — sets `Content-Type` to `text/html` for error responses +2. **on_response_body** — looks up the status code in the embedded image/message maps, falls back to generic `4xx`/`5xx` templates, and renders the error page using Handlebars + +## Adding a custom error page + +1. Add an image: `public/images/<status>.jpg` +2. Add a message file: `public/messages/<status>.hbs` (first line = title, second line = description) +3. Recompile and deploy + +## Styling + +Styles are in [`public/styles.css`](./public/styles.css) — plain CSS, no build tools required. Edit directly. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-rust.md index 99d95cb..de7cb9f 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -260,3 +260,123 @@ Action::Pause - host-services-rust reference (property access, send_http_response ABI details) - sdk-reference-rust reference (proxy-wasm trait hierarchy) - best-practices reference (error handling patterns, Action semantics) + +## Source Material + +### FILE: examples/cdn/custom/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::time::Duration; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(HttpHeadersRoot) }); +}} + +const BAD_REQUEST: u32 = 400; + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(HttpHeaders)) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders; + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Some(path) = self.get_property(vec!["request.path"]) else { + self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); + return Action::Pause; + }; + + let Ok(path) = std::str::from_utf8(&path) else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - not utf8 string"), + ); + return Action::Pause; + }; + + //trim first '/' + let path = if path.starts_with('/') { + &path[1..] + } else { + path + }; + let mut segments = path.split('/'); + + let Some(status_code) = segments.next() else { + return Action::Continue; + }; + + if let Some(delay) = segments.next() { + if let Ok(delay) = delay.parse::<u64>() { + std::thread::sleep(Duration::from_millis(delay)); + } + } + + let Ok(status_code) = status_code.parse::<u32>() else { + self.send_http_response( + BAD_REQUEST, + vec![], + Some(b"Malformed request - invalid status code"), + ); + return Action::Pause; + }; + + match status_code { + 0 | 200 => Action::Continue, + code if code < 600 => { + self.send_http_response(code, vec![], None); + Action::Pause + } + _ => { + self.send_http_response(BAD_REQUEST, vec![], None); + Action::Pause + } + } + } +} +``` + +### FILE: examples/cdn/custom/Cargo.toml + +```toml +[workspace] + +[package] +name = "custom" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/custom/README.md + +``` +[← Back to examples](../../README.md) + +# Custom (CDN) + +Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-as.md index e2d8383..912df27 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -148,9 +148,9 @@ Configure both on the FastEdge application before deployment. The test runner maps environment variable names as follows: -| Env key | Maps to | -| ------------------------------ | ------------------ | -| `FASTEDGE_VAR_ENV_<NAME>` | `getEnv("NAME")` | +| Env key | Maps to | +| ------------------------------ | ------------------- | +| `FASTEDGE_VAR_ENV_<NAME>` | `getEnv("NAME")` | | `FASTEDGE_VAR_SECRET_<NAME>` | `getSecret("NAME")` | Use `"dotenv": {"enabled": true}` in the test fixture to load values from a `.env` file (e.g. `fixtures/.env`): @@ -179,3 +179,138 @@ Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to - cdn-base skeleton (base request/response handling structure) - fastedge-test reference (local WASM test runner and fixture format) - platform-overview reference (FastEdge application configuration and secret management) + +## Source Material + +### FILE: examples/variablesAndSecrets/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class VariablesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new VariablesContext(context_id, this); + } +} + +class VariablesContext extends Context { + constructor(context_id: u32, root_context: VariablesRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const username = getEnv("USERNAME"); + const password = getSecret("PASSWORD"); + + log(LogLevelValues.info, "USERNAME: " + username); + log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); + + stream_context.headers.request.add("x-env-username", username); + stream_context.headers.request.add("x-env-password", password); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new VariablesRoot(context_id); +}, "variablesAndSecrets"); +``` + +### FILE: examples/variablesAndSecrets/package.json + +```json +{ + "name": "fastedge-as-example-variables-and-secrets", + "version": "0.0.1", + "description": "FastEdge AssemblyScript example: Variables and Secrets", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/variablesAndSecrets/README.md + +``` +[← Back to examples](../README.md) + +# Variables and Secrets + +This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the `USERNAME` environment variable using `getEnv`. +2. Reads the `PASSWORD` secret using `getSecret`. +3. Logs that both values were retrieved (without logging the secret value itself). +4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. + +This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. + +> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +| ---------- | -------------------- | -------------------------------------- | +| `USERNAME` | Environment variable | The username value to forward upstream | +| `PASSWORD` | Secret | The password value to forward upstream | + +## Local testing + +The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_<NAME>` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_<NAME>` 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-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md index 598249f..9336819 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -175,3 +175,104 @@ println!("PASSWORD: {}", password); - FastEdge app secrets management (platform docs) - proxy-wasm HttpContext trait reference - fastedge crate proxywasm feature documentation + +## Source Material + +### FILE: examples/cdn/variables_and_secrets/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating environment variables and secrets access. + +Reads USERNAME from environment variables and PASSWORD from secrets, +then forwards both as request headers to the upstream origin. + +Required configuration: + - Environment variable: USERNAME + - Secret: PASSWORD +*/ + +use fastedge::proxywasm::secret; +use std::env; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(VariablesRoot) }); +}} + +struct VariablesRoot; + +impl Context for VariablesRoot {} + +impl RootContext for VariablesRoot { + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(VariablesContext)) + } +} + +struct VariablesContext; + +impl Context for VariablesContext {} + +impl HttpContext for VariablesContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let username = env::var("USERNAME").unwrap_or_default(); + let password = secret::get("PASSWORD") + .ok() + .flatten() + .and_then(|v| String::from_utf8(v).ok()) + .unwrap_or_default(); + + println!("USERNAME: {}", username); + // WARNING: Secrets are stored and retrieved as plaintext. Never log secret values + // in production code — platform logs are visible to operators and may be persisted. + // This line is shown for demonstration only; remove it in any real application. + println!("PASSWORD: {}", password); + + self.add_http_request_header("x-env-username", &username); + // WARNING: Forwarding a secret in a request header exposes it to the upstream origin + // and any intermediary that can inspect headers. Only do this when the upstream + // channel is trusted and the header is required by the destination API. + self.add_http_request_header("x-env-password", &password); + + Action::Continue + } +} +``` + +### FILE: examples/cdn/variables_and_secrets/Cargo.toml + +```toml +[workspace] + +[package] +name = "variables_and_secrets" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + +### FILE: examples/cdn/variables_and_secrets/README.md + +``` +[← Back to examples](../../README.md) + +# Variables and Secrets (CDN) + +Reads `USERNAME` from environment variables and `PASSWORD` from secrets for request forwarding using the proxy-wasm ABI. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md index ea43437..ae208d8 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -132,7 +132,20 @@ const countrySpecificOrigin = getEnv(countryCode); - Returns `""` (empty string) when no matching env var is set. - Do **not** use a null/undefined check; use an empty-string check. -### Step 4 — Read request path +### Step 4 — Read request host (optional) + +```typescript +const hostArrBuf = get_property("request.host"); +if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); +} +``` + +- `get_property("request.host")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. +- Reading the host is optional; absence does not stop iteration. + +### Step 5 — Read request path ```typescript const pathArrBuf = get_property("request.path"); @@ -151,7 +164,7 @@ const path = String.UTF8.decode(pathArrBuf); - `get_property("request.path")` returns `ArrayBuffer`; decode with `String.UTF8.decode`. - Missing path → 500 response, stop iteration. -### Step 5 — Build and set target URL +### Step 6 — Build and set target URL ```typescript const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; @@ -212,7 +225,8 @@ registerRootContext((context_id: u32) => { ## Logging ```typescript -log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${matchedOrigin}`); +log(LogLevelValues.info, "onRequestHeaders >> "); +log(LogLevelValues.info, `Country code: ( ${countryCode} ): ${countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin}`); log(LogLevelValues.info, `Provided Host: ${host}`); log(LogLevelValues.info, `request-url: ${requestUrl}`); ``` @@ -252,3 +266,185 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) + +## Source Material + +### FILE: examples/geoRedirect/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const BAD_GATEWAY: u32 = 502; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class GeoRedirectRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new GeoRedirect(context_id, this); + } +} + +class GeoRedirect extends Context { + constructor(context_id: u32, root_context: GeoRedirectRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> "); + + const defaultOrigin = getEnv("DEFAULT"); + + if (!defaultOrigin) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured - DEFAULT must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const countryArrBuf = get_property("request.country"); + if (countryArrBuf.byteLength === 0) { + send_http_response( + BAD_GATEWAY, + "bad gateway", + String.UTF8.encode("Missing country information"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + const countryCode = String.UTF8.decode(countryArrBuf); + const countrySpecificOrigin = getEnv(countryCode); + + log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, + ); + + const hostArrBuf = get_property("request.host"); + if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); + } + + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Internal server error - no request path"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const path = String.UTF8.decode(pathArrBuf); + const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; + // remove trailing slashes from the origin + const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; + + const requestUrl = `${cleanedOrigin}${path}`; + + log(LogLevelValues.info, `request-url: ${requestUrl}`); + + set_property("request.url", String.UTF8.encode(requestUrl)); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new GeoRedirectRoot(context_id); +}, "geoRedirect"); +``` + + +### FILE: examples/geoRedirect/package.json + +```json +{ + "name": "fastedge-as-example-georedirect", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Geo Redirect", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/geoRedirect/README.md + +``` +[← Back to examples](../README.md) + +# Geo Redirect + +This application redirects requests to different origin URLs based on the client's country code. + +## What it does + +In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. + +- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. +- Otherwise it falls back to the `DEFAULT` origin. +- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. + +> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. + +> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | +| `<COUNTRY_CODE>` | `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-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md index 1fac82b..dab63e7 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -46,7 +46,7 @@ All routing logic executes in `on_http_request_headers`. No body hooks are used. 5. Read `request.path` property; default to `"/"` if absent. 6. Preserve the `Host` header by reading `request.host` and calling `set_http_request_header`. 7. Construct the target URL as `format!("{}{}", origin, path)`. -8. Log the target URL at `Info` level. +8. Log the target URL at `Info` level via `println!`. 9. Write the URL to `request.url` via `set_property` and return `Action::Continue`. ### Country Detection @@ -125,6 +125,8 @@ crate-type = ["cdylib"] proxy-wasm = "0.2" ``` +Note: the workspace-level `[workspace]` key is present in the source Cargo.toml but omitted here as it is not relevant to the library package configuration. + ## Struct Layout | Struct | Traits | Role | @@ -135,6 +137,15 @@ proxy-wasm = "0.2" `GeoRedirectRoot::get_type` returns `Some(ContextType::HttpContext)`. `GeoRedirectRoot::create_http_context` returns `Some(Box::new(GeoRedirectContext))`. +## Entry Point + +```rust +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(GeoRedirectRoot) }); +}} +``` + ## See Also - cdn-base skeleton reference diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md index 54555ac..c0544ac 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -158,8 +158,8 @@ registerRootContext((context_id: u32) => { Both blocked and allowed requests are logged at `INFO` level, providing an audit trail for all traffic decisions. -| Event | Log message | -| ---------------- | ---------------------------------------------- | +| Event | Log message | +| ---------------- | ------------------------------------------------ | | Request blocked | `"geoBlock: blocked request from " + countryStr` | | Request allowed | `"geoBlock: allowed request from " + countryStr` | @@ -169,11 +169,11 @@ Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. ## Status Codes Used -| Constant | Value | Condition | -| ----------------------- | ----- | ------------------------------------------------------- | -| `FORBIDDEN` | 403 | Request's country code is in the blacklist | -| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | -| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | +| Constant | Value | Condition | +| ----------------------- | ----- | ------------------------------------------------------ | +| `FORBIDDEN` | 403 | Request's country code is in the blacklist | +| `BAD_GATEWAY` | 502 | `request.country` property is empty/unavailable | +| `INTERNAL_SERVER_ERROR` | 500 | `BLACKLIST` env var is missing or parses to empty list | --- @@ -256,23 +256,23 @@ Build scripts defined in `package.json`: ## Dependencies -| Package | Role | -| -------------------------------- | ------------------------------------------- | -| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | -| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | -| `assemblyscript` | AssemblyScript compiler (dev) | +| Package | Role | +| ----------------------------- | -------------------------------------- | +| `@gcoredev/proxy-wasm-sdk-as` | Core proxy-wasm SDK for AssemblyScript | +| `@assemblyscript/wasi-shim` | WASI compatibility shim (dev) | +| `assemblyscript` | AssemblyScript compiler (dev) | --- ## Error Conditions -| Condition | Response | Status | -| --------------------------------------------- | ------------------------------------------------ | ------ | -| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | -| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | -| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | -| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | -| Country code not in blacklist | `Continue` | — | +| Condition | Response | Status | +| -------------------------------------------- | ---------------------------------------------------- | ------ | +| `BLACKLIST` env var not set | `StopIteration`, body: "App misconfigured" | 500 | +| `BLACKLIST` parses to empty list | `StopIteration`, body: "App misconfigured" | 500 | +| `request.country` property empty/unavailable | `StopIteration`, body: "Missing country information" | 502 | +| Country code found in blacklist | `StopIteration`, body: "Request blacklisted" | 403 | +| Country code not in blacklist | `Continue` | — | --- diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-rust.md index 031afb2..189abf0 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -155,7 +155,7 @@ Configure these in the FastEdge dashboard under the app's environment variables: | `BLACKLIST_TW_START` | No | Unix timestamp (u64) — start of time window during which blocking applies | | `BLACKLIST_TW_END` | No | Unix timestamp (u64) — end of time window during which blocking applies | -If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. +If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. If only one of the two is set, it is treated as absent and the block is permanent. ## Error Conditions @@ -206,3 +206,112 @@ Output binary: `target/wasm32-wasip1/release/<crate-name>.wasm` - FastEdge SDK Rust reference (proxy-wasm trait definitions and types) - Platform overview reference (request.country property and other injected properties) - deploy skill reference (uploading and registering the compiled WASM binary) + +## Source Material + +### FILE: examples/cdn/geoblock/src/lib.rs + +```rust +use std::env; +use std::time::{SystemTime, UNIX_EPOCH}; + +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(GeoblockRoot) }); +}} + +struct GeoblockRoot; + +impl Context for GeoblockRoot {} + +impl RootContext for GeoblockRoot { + fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> { + Some(Box::new(GeoblockContext {})) + } + + fn get_type(&self) -> Option<ContextType> { + Some(ContextType::HttpContext) + } +} + +struct GeoblockContext {} + +impl Context for GeoblockContext {} + +const BAD_GATEWAY: u32 = 502; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +impl HttpContext for GeoblockContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(blacklist) = env::var("BLACKLIST") else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + + let mut blacklist = blacklist.split(','); + + let Some(country) = self.get_property(vec!["request.country"]) else { + self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - no country field")); + return Action::Pause; + }; + + let Ok(country) = std::str::from_utf8(&country) else { + self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - country not utf8 string")); + return Action::Pause; + }; + + if blacklist.any(|b| country.eq_ignore_ascii_case(b)) { + let tw_start = env::var("BLACKLIST_TW_START").ok(); + let tw_end = env::var("BLACKLIST_TW_END").ok(); + + if let Some((tw_start, tw_end)) = tw_start.zip(tw_end) { + let Ok(tw_start) = tw_start.parse::<u64>() else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + + let Ok(tw_end) = tw_end.parse::<u64>() else { + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if now >= tw_start && now <= tw_end { + self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); + return Action::Pause; + } + } else { + self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); + return Action::Pause; + } + } + + + Action::Continue + } +} +``` + +### FILE: examples/cdn/geoblock/Cargo.toml + +```toml +[workspace] + +[package] +name = "geoblock" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md index 6fc0857..f2b6798 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -246,14 +246,14 @@ let headers = self.get_http_request_headers() .collect::<HashSet<(String, String)>>(); // After add/set operations, expected new entries include: -// ("new-header-01", "") // removed → empty string -// ("new-header-bytes-01", "") // removed → empty string -// ("new-header-02", "new-value-02") // replaced -// ("new-header-bytes-02", "new-value-bytes-02") // replaced -// ("new-header-03", "value-03") // original add -// ("new-header-bytes-03", "value-bytes-03") // original add -// ("new-header-03", "value-03-a") // duplicate add preserved -// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved +// ("new-header-01", "") // removed → empty string +// ("new-header-bytes-01", "") // removed → empty string +// ("new-header-02", "new-value-02") // replaced +// ("new-header-bytes-02", "new-value-bytes-02") // replaced +// ("new-header-03", "value-03") // original add +// ("new-header-bytes-03", "value-bytes-03") // original add +// ("new-header-03", "value-03-a") // duplicate add preserved +// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved let diff = headers .difference(&original_headers) @@ -265,7 +265,7 @@ if !diff.is_empty() { } ``` -The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. +The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. The response phase (`on_http_response_headers`) applies an identical add/set/diff sequence using response header methods. --- diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/http-call-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/http-call-as.md index 2d323d6..590549a 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/http-call-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -91,12 +91,12 @@ function handleHttpCallResponse( **Parameters:** -| Parameter | Type | Description | -|------------|-------------|-------------| +| Parameter | Type | Description | +|------------|---------------|-------------| | `ctx` | `BaseContext` | Originating context | -| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | -| `bodySize` | `usize` | Size of the response body in bytes | -| `trls` | `u32` | Number of response trailers | +| `hdrs` | `u32` | Number of response headers; `0` indicates failure (timeout, DNS error, etc.) | +| `bodySize` | `usize` | Size of the response body in bytes | +| `trls` | `u32` | Number of response trailers | --- diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-as.md index 4377eda..717fbc3 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -205,6 +205,14 @@ The `validateQueryParams` function: - Validates `action` against `ALL_ACTIONS = ["get", "scan", "zscan", "zrange", "bfExists"]` - Checks all required parameters for the resolved action are present and non-empty +**Required parameters by action** (as defined in `validateQueryParams`): +- `store` — required for all actions +- `key` — required for `get`, `zrange`, `zscan`, `bfExists` +- `match` — required for `scan`, `zscan` +- `min` — required for `zrange` +- `max` — required for `zrange` +- `item` — required for `bfExists` + --- ## Error Handling @@ -248,6 +256,18 @@ set_buffer_bytes( The response body is a JSON object built from a `Map<string, string>` using `stringifyMap`. It includes fields such as `Store`, `Action`, `Key`, `Response`, and action-specific fields (`Match`, `Min`, `Max`, `Item`). +**`stringifyMap` format:** +```typescript +// Output: {"key1": "value1", "key2": "value2"} +function stringifyMap(map: Map<string, string>): string +``` + +**`stringifyValueScoreTuples` format:** +```typescript +// Output: "{ value: <val>, score: <score> }, ..." +function stringifyValueScoreTuples(arr: Array<ValueScoreTuple>): string +``` + --- ## Hook: `onResponseBody` @@ -295,6 +315,11 @@ registerRootContext → KvStoreRoot.createContext (sets log level to info) The KV Store must be configured and linked to the FastEdge application before use. `KvStore.open` will return `null` if the named store is not attached. The `store` query parameter at runtime must exactly match the binding name configured on the application. +KV Store setup steps: +1. Create a KV Store in the FastEdge portal under the Key-Value storage section. +2. Populate it with the keys and values you want to query. +3. Link the store to the app — when configuring the FastEdge application, add the store under the app's KV store bindings. The name given to the binding is what the `store` query parameter must match at runtime. + --- ## See Also diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md index d9a6ea5..8d280e2 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -76,14 +76,10 @@ class LargeDictionaryContext extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. const config = getDictionary("LARGE_CONFIG"); - // getDictionary returns "" (never null) when variable is not set - if (config.length === 0) { - log(LogLevelValues.info, "LARGE_CONFIG is not set"); - return FilterHeadersStatusValues.Continue; - } - const size = config.length; log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md index 925690d..a8334a3 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -153,111 +153,3 @@ println!("LARGE_CONFIG size: {} bytes", size); - proxy-wasm HttpContext trait reference - FastEdge environment variable configuration docs - `std::env::var` (standard Rust env access for values under 64KB) - -## Source Material - -### FILE: examples/cdn/large_env_variable/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating access to large environment variables. - -Uses `fastedge::proxywasm::dictionary` to read environment variables that -may exceed the 64KB WASI environment variable size limit. - -For normal-sized environment variables (< 64KB), prefer `std::env::var()` -instead. The dictionary API is only required when your variable value -may be larger than 64KB. - -Required configuration: - - Environment variable: LARGE_CONFIG (a large configuration payload, e.g. JSON) -*/ - -use fastedge::proxywasm::dictionary; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box<dyn RootContext> { Box::new(LargeEnvRoot) }); -}} - -struct LargeEnvRoot; - -impl Context for LargeEnvRoot {} - -impl RootContext for LargeEnvRoot { - fn get_type(&self) -> Option<ContextType> { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option<Box<dyn HttpContext>> { - Some(Box::new(LargeEnvContext)) - } -} - -struct LargeEnvContext; - -impl Context for LargeEnvContext {} - -impl HttpContext for LargeEnvContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // Use dictionary::get for environment variables that may exceed 64KB. - // For normal-sized env vars, use std::env::var() instead. - let config = dictionary::get("LARGE_CONFIG").unwrap_or_default(); - - let size = config.len(); - println!("LARGE_CONFIG size: {} bytes", size); - - self.add_http_request_header("x-config-size", &size.to_string()); - - Action::Continue - } -} -``` - -### FILE: examples/cdn/large_env_variable/Cargo.toml - -```toml -[workspace] - -[package] -name = "large_env_variable" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -``` - -### FILE: examples/cdn/large_env_variable/README.md - -``` -[← Back to examples](../../README.md) - -# Large Environment Variable (CDN) - -Demonstrates how to read **large environment variables** (> 64KB) using `fastedge::proxywasm::dictionary`. - -## When to use `dictionary` vs `std::env` - -| Method | Use when | -|--------|----------| -| `std::env::var("KEY")` | Variable value is under 64KB (most cases) | -| `fastedge::proxywasm::dictionary::get("KEY")` | Variable value may exceed the 64KB WASI env var size limit | - -The WASI environment variable interface has a **64KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, certificates, policy documents), use the `dictionary` API which bypasses this limit. - -For all other environment variable access, prefer `std::env::var()` as it is the standard, idiomatic Rust approach. - -## Required configuration - -- **Environment variable**: `LARGE_CONFIG` - a large configuration payload (e.g. JSON, PEM certificate) -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md index 14d1bd8..f6472f8 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-17 --> --- @@ -130,7 +130,7 @@ set_property("request.path", String.UTF8.encode(newPath)); Parse the raw query string and rewrite properties based on matching parameter keys. ```typescript -const query = get_property("request.query"); +const query = get_property(REQUEST_QUERY); if (query.byteLength !== 0) { const queryString = String.UTF8.decode(query); const params = queryString @@ -145,11 +145,11 @@ if (query.byteLength !== 0) { const key = param[0]; const value = param[1]; if (key.toLowerCase() === "url") { - set_property("request.url", String.UTF8.encode(value)); + set_property(REQUEST_URI, String.UTF8.encode(value)); } else if (key.toLowerCase() === "host") { - set_property("request.host", String.UTF8.encode(value)); + set_property(REQUEST_HOST, String.UTF8.encode(value)); } else if (key.toLowerCase() === "path") { - set_property("request.path", String.UTF8.encode(value)); + set_property(REQUEST_PATH, String.UTF8.encode(value)); } } } diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-ts.md index 1e1f8f7..71565f9 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -252,3 +252,121 @@ Output: Headers - fastedge-sdk-js SDK reference - http-base skeleton - FastEdge build CLI reference + +## Source Material + +### FILE: examples/ab-testing/src/index.js + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(request); + + const headers = createAbTestHeaders(slicedHeaders, testConfig, xid); + + // This is the URL of the outbound service - i.e. could be a url to your origin + // e.g. https://template-invoice-ab-test-123456.fastedge.cdn.gc.onl/ + const outboundUrl = getEnv('OUTBOUND_URL'); + if (!outboundUrl || !String(outboundUrl).trim()) { + return new Response('OUTBOUND_URL environment variable is not configured', { + status: 500, + }); + } + + const response = await fetch(outboundUrl, { headers }); + + // Request/Response Headers are immutable, so we need to create a new Headers object + const resHeaders = new Headers(response.headers); + resHeaders.set( + 'set-cookie', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); + +const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { + // Request/Response Headers are immutable, so we need to create a new Headers object + const headers = new Headers(reqHeaders); + const cookie = headers.get('cookie') || ''; + // Read the existing `xid` cookie value. + const xid = (cookie.match(/(?:^|;) *x-fastedge-abid=((0|1|)\.\d+) *(?:;|$)/u) || [])[1]; + if (xid) { + // Request contains A/B cookie, hide it from the origin + const newCookie = cookie.replace(/x-fastedge-abid=[^;]+;?\s*/gu, ''); + if (newCookie) { + headers.set('cookie', newCookie); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + return headers; +}; +``` + +### FILE: examples/ab-testing/package.json + +```json +{ + "name": "fastedge-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge JS example: cookie-based A/B testing", + "type": "module", + "main": "src/index.js", + "scripts": { + "build": "fastedge-build src/index.js dist/ab-testing.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-wasi-rust.md index 49f2ccf..9f1a6a0 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -217,214 +217,3 @@ Response::builder() - fastedge-sdk-rust platform overview - host-services-rust reference (outbound HTTP, environment variables) - best-practices reference (cookie security, entropy sources) - -## Source Material - -### FILE: examples/http/wasi/ab_testing/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Cookie-based A/B testing example. - -Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically -assign the visitor to weighted variants of each configured test, then proxies the -request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-<name>` -headers. The origin response is returned verbatim with a `set-cookie` header so -returning visitors receive the same variants on subsequent visits. - -Required configuration: - - Environment variable: OUTBOUND_URL (downstream origin to proxy to) - -Mirror of the FastEdge-sdk-js `ab-testing` example. -*/ - -use std::env; -use std::time::{SystemTime, UNIX_EPOCH}; - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -struct VariantWeight { - variant: &'static str, - weight: f64, -} - -struct AbTest { - name: &'static str, - variants: &'static [VariantWeight], -} - -static TESTS: &[AbTest] = &[ - AbTest { - name: "logo", - variants: &[ - VariantWeight { variant: "hops", weight: 50.0 }, - VariantWeight { variant: "bottle", weight: 50.0 }, - ], - }, - AbTest { - name: "font", - variants: &[ - VariantWeight { variant: "exo2", weight: 40.0 }, - VariantWeight { variant: "gloria", weight: 65.0 }, - VariantWeight { variant: "standard", weight: 45.0 }, - ], - }, -]; - -const AB_COOKIE: &str = "x-fastedge-abid"; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let outbound_url = match env::var("OUTBOUND_URL") { - Ok(u) if !u.trim().is_empty() => u, - _ => { - return Ok(Response::builder() - .status(500) - .body(Body::from( - "OUTBOUND_URL environment variable is not configured", - ))?); - } - }; - - let raw_cookie = req - .headers() - .get("cookie") - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .to_string(); - - let (xid, cleaned_cookie) = match extract_abid(&raw_cookie) { - Some(existing) if is_valid_xid(existing) => { - (existing.to_string(), strip_abid(&raw_cookie)) - } - _ => (generate_xid(), raw_cookie.clone()), - }; - - // Build the outbound request: copy incoming headers except `host` and - // `cookie` (which we handle specially), replace the cookie with a version - // that has the abid stripped (so the origin never sees it), and attach an - // `ab-test-<name>` header for every configured test. - let mut builder = Request::get(&outbound_url); - for (name, value) in req.headers() { - let n = name.as_str(); - if n == "host" || n == "cookie" { - continue; - } - if let Ok(v) = value.to_str() { - builder = builder.header(n, v); - } - } - if !cleaned_cookie.trim().is_empty() { - builder = builder.header("cookie", cleaned_cookie); - } - for test in TESTS { - if let Some(variant) = assign_variant(&xid, test) { - builder = builder.header(format!("ab-test-{}", test.name), variant); - } - } - - let outbound_req = builder - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build outbound request: {e}"))?; - - let outbound_resp = Client::new() - .send(outbound_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - let (parts, mut body) = outbound_resp.into_parts(); - let body_bytes = body.contents().await?; - - let content_type = parts - .headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream") - .to_string(); - - let xid_cookie = - format!("{AB_COOKIE}={xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"); - - Ok(Response::builder() - .status(parts.status) - .header("content-type", content_type) - .header("set-cookie", xid_cookie) - .body(Body::from(body_bytes))?) -} - -fn extract_abid(cookie_header: &str) -> Option<&str> { - let needle = format!("{AB_COOKIE}="); - cookie_header - .split(';') - .map(str::trim) - .find_map(|p| p.strip_prefix(needle.as_str())) -} - -fn strip_abid(cookie_header: &str) -> String { - let needle = format!("{AB_COOKIE}="); - cookie_header - .split(';') - .map(str::trim) - .filter(|p| !p.is_empty()) - .filter(|p| !p.starts_with(needle.as_str())) - .collect::<Vec<_>>() - .join("; ") -} - -fn is_valid_xid(xid: &str) -> bool { - matches!(xid.parse::<f64>(), Ok(v) if (0.0..1.0).contains(&v)) -} - -/// Generate a pseudo-random A/B id of the form `"0.NNNN"`. -/// -/// Uses request-time nanoseconds as a weak entropy source. For production, -/// prefer a cryptographic RNG (e.g. `rand` wired to `wasi-random`). -fn generate_xid() -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - format!("0.{:04}", now.subsec_nanos() % 10000) -} - -fn assign_variant(xid: &str, test: &AbTest) -> Option<&'static str> { - let xid_value: f64 = xid.parse().ok()?; - let xid_percentage = xid_value * 100.0; - let total: f64 = test.variants.iter().map(|v| v.weight).sum(); - if total == 0.0 { - return None; - } - let mut start = 0.0; - for vw in test.variants { - let percentage = (vw.weight / total) * 100.0; - let end = start + percentage; - if xid_percentage >= start && xid_percentage < end { - return Some(vw.variant); - } - start = end; - } - None -} -``` - -### FILE: examples/http/wasi/ab_testing/Cargo.toml - -```toml -[workspace] - -[package] -name = "ab_testing_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/base-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/base-rust.md index a8d1e14..f2b3419 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/base-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-07-23 +updated: 2026-08-17 --- @@ -174,3 +174,45 @@ cargo build --release --target wasm32-wasip2 - **Handler signature**: `async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>>` - **Crate type**: `cdylib` (required for WASM output) - **Workspace**: Single-project workspace pattern (`[workspace]` declared in Cargo.toml) + +## Source Material + +### FILE: examples/http/wasi/hello_world/src/lib.rs + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { + let url = request.uri().to_string(); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain;charset=UTF-8") + .body(Body::from(format!( + "Hello, you made a wasi request to {url}" + )))?) +} + +``` + + +### FILE: examples/http/wasi/hello_world/Cargo.toml + +```toml +[workspace] + +[package] +name = "hello_world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" + +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-ts.md index 6b889c9..f81acb2 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -176,3 +176,67 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide + +## Source Material + +### FILE: examples/bloom-filter-denylist/src/index.js + +```js +import { getEnv } from 'fastedge::env'; +import { KvStore } from 'fastedge::kv'; + +const BLOOM_KEY = 'blocked-ips'; + +function app(event) { + const storeName = getEnv('DENYLIST_STORE'); + if (!storeName) { + return Response.json( + { error: 'DENYLIST_STORE environment variable is not configured' }, + { status: 500 }, + ); + } + + const ip = event.client.address; + if (!ip) { + return Response.json({ error: 'client address unavailable' }, { status: 500 }); + } + + let blocked; + try { + const store = KvStore.open(storeName); + blocked = store.bfExists(BLOOM_KEY, ip); + } catch (error) { + return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); + } + + if (blocked) { + // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. + // Acceptable for a denylist (you over-block some legitimate users); not acceptable for + // allowlists or anything requiring exact membership — use KvStore.get() for that. + return Response.json({ allowed: false, ip }, { status: 403 }); + } + + return Response.json({ allowed: true, ip }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/bloom-filter-denylist/package.json + +```json +{ + "name": "fastedge-example-bloom-filter-denylist", + "version": "1.0.0", + "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md index 07dd858..ad66102 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,117 @@ fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result<Respon - host-services-rust reference (KV store host service documentation) - http-base skeleton (entry point, request/response types, wstd::http_server macro) - examples-kv-rust (general KV store usage patterns) + +## Source Material + +### FILE: examples/http/wasi/bloom_filter_denylist/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Bloom-filter IP denylist example. + +Checks the client IP (from the `x-real-ip` request header, falling back to +`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 +on a hit, 200 otherwise. + +Required configuration: + - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) + +The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; +populate the filter out of band. + +Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::key_value::{Error as StoreError, Store}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +const BLOOM_KEY: &str = "blocked-ips"; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let store_name = match env::var("DENYLIST_STORE") { + Ok(s) if !s.trim().is_empty() => s, + _ => { + return json_response( + 500, + json!({ "error": "DENYLIST_STORE environment variable is not configured" }), + ); + } + }; + + let headers = req.headers(); + let client_ip = headers + .get("x-real-ip") + .or_else(|| headers.get("x-forwarded-for")) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()); + + let Some(ip) = client_ip else { + return json_response(500, json!({ "error": "client IP not available" })); + }; + + let store = match Store::open(&store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return json_response( + 403, + json!({ "error": "access denied opening denylist store" }), + ); + } + Err(e) => { + return json_response(500, json!({ "error": format!("store open error: {e}") })); + } + }; + + let blocked = store + .bf_exists(BLOOM_KEY, ip) + .map_err(|e| anyhow!("bf_exists error: {e}"))?; + + if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. + return json_response(403, json!({ "allowed": false, "ip": ip })); + } + + json_response(200, json!({ "allowed": true, "ip": ip })) +} + +fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result<Response<Body>> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(value.to_string()))?) +} +``` + +### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml + +```toml +[workspace] + +[package] +name = "bloom_filter_denylist_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +serde_json = "1" +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-basic-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-basic-ts.md index ec77b72..8e33d7f 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-basic-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-basic-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -231,3 +231,117 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` - `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface - http-base skeleton — base HTTP handler structure this feature extends - deploy skill reference — building and uploading the compiled WASM binary + +## Source Material + +### FILE: examples/cache-basic/src/index.js + +```js +// FastEdge Cache — basic operations +// +// The `fastedge::cache` module gives you a fast, data-center-scoped +// key/value store. Values written here are stored in the same point of +// presence (POP) that runs the worker, so reads and writes are very fast, +// and writes from one POP are not visible to others. +// +// Use this for transient, request-time state — short-lived caches, hit +// counters, rate limit windows, deduplicated work. For globally +// replicated storage, use the `fastedge::kv` module instead. +// +// This example demonstrates the four most common operations: +// +// GET /?action=set&key=foo&value=bar -> Cache.set +// GET /?action=get&key=foo -> Cache.get +// GET /?action=exists&key=foo -> Cache.exists +// GET /?action=delete&key=foo -> Cache.delete + +import { Cache } from 'fastedge::cache'; + +async function eventHandler(event) { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + const key = url.searchParams.get('key'); + + if (!key) { + throw new Error('Missing required query parameter: "key"'); + } + + switch (action) { + case 'set': { + // Cache.set writes a value under `key`. Accepts strings, + // ArrayBuffers, ArrayBufferViews, ReadableStreams, and Response + // objects (the body is consumed; status and headers are not stored). + // + // The `{ ttl: 60 }` option means "expire 60 seconds from now". You + // can also use `ttlMs` for sub-second precision, or `expiresAt` for + // a fixed Unix-epoch deadline. Omit options entirely for no expiry. + const value = url.searchParams.get('value') ?? ''; + await Cache.set(key, value, { ttl: 60 }); + return Response.json({ action, key, value, ttl: 60 }); + } + + case 'get': { + // Cache.get returns a CacheEntry on a hit, or `null` on a miss + // (key absent or expired). The cache stores raw bytes, so on read + // you choose how to decode using one of: + // entry.text() -> Promise<string> (UTF-8) + // entry.json() -> Promise<unknown> (parsed JSON) + // entry.arrayBuffer() -> Promise<ArrayBuffer> + const entry = await Cache.get(key); + if (entry === null) { + return Response.json({ action, key, hit: false }); + } + const value = await entry.text(); + return Response.json({ action, key, hit: true, value }); + } + + case 'exists': { + // Cache.exists is a cheap presence check — useful when you only + // need to know whether a key is set without transferring its value + // (e.g. idempotency-key checks, "have we seen this token?"). + const present = await Cache.exists(key); + return Response.json({ action, key, present }); + } + + case 'delete': { + // Cache.delete removes the entry. It is a no-op if the key is + // already absent — no error is thrown. + await Cache.delete(key); + return Response.json({ action, key, deleted: true }); + } + + default: + throw new Error( + `Unknown action: "${action}". Use one of: set, get, exists, delete.`, + ); + } + } catch (error) { + // Validation errors (e.g. wrong types, conflicting WriteOptions fields) + // are thrown synchronously; host errors (access denied, internal error) + // arrive as Promise rejections. Both are caught by this single handler. + return Response.json({ error: error.message }, { status: 500 }); + } +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); +``` + +### FILE: examples/cache-basic/package.json + +```json +{ + "name": "fastedge-example-cache-basic", + "version": "1.0.0", + "description": "FastEdge JS example: simple Cache set/get/exists/delete operations", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/cache-basic.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-ts.md index c8c4e8a..5579117 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -315,3 +315,281 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise<Response> { + // `event.client.address` is the trusted-edge client IP. Sourced from + // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by + // the FastEdge POP, not the client, so they're safe to key on. + const ip = event.client.address || 'unknown'; + + const key = `rl:${ip}`; + + const count = await Cache.incr(key); + + // Only set the expiry on the first hit of a new window. If we set it + // on every request, the window would never close — each new request + // would push the deadline another 60 seconds out. + if (count === 1) { + await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); + } + + if (count > RATE_LIMIT_MAX) { + return Response.json( + { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, + { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, + ); + } + + return Response.json({ + pattern: 'rate-limit', + ip, + count, + remaining: RATE_LIMIT_MAX - count, + windowSeconds: RATE_LIMIT_WINDOW_S, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 2 — Origin-cache proxy with conditional caching +// --------------------------------------------------------------------------- +// +// Cache successful upstream responses for PROXY_TTL_S seconds; pass +// non-2xx and redirects through *without* caching, so a transient 404 +// or 500 doesn't get pinned for the rest of the window. The cache is +// a byte cache (no status/headers), so we only cache when "200 OK with +// application/octet-stream" is a faithful replay of the upstream. +// +// `getOrSet` is not used here because its populator can't signal +// "fetched, but don't cache" — we need that distinction to handle +// error responses safely. See Pattern 3 for `getOrSet` in a context +// where every populator output is cacheable. + +const PROXY_TTL_S = 30; + +async function proxy(url: string): Promise<Response> { + // Validate the URL before we use it as a cache key. + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); + } + + // Strip the fragment: fetch() never sends it to the origin, so + // `https://example.com/#a` and `#b` are the same upstream resource + // and must share one cache entry. + parsed.hash = ''; + + const key = `proxy:${parsed.toString()}`; + + // Cache hit — replay the bytes as 200 OK. Status/headers from the + // original response are not preserved by the byte cache. + const cached = await Cache.get(key); + if (cached !== null) { + return new Response(await cached.arrayBuffer(), { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'hit', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); + } + + // Cache miss — fetch upstream and only cache successful responses. + // Non-2xx and redirects flow through unchanged so callers see the + // real status code instead of a synthetic 200. + const upstream = await fetch(parsed.toString()); + if (!upstream.ok) { + return upstream; + } + + const bytes = await upstream.arrayBuffer(); + await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); + return new Response(bytes, { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'miss', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 3 — JSON memoisation via getOrSet with a computed populator +// --------------------------------------------------------------------------- +// +// Same shape as the proxy pattern, but the populator does CPU work +// instead of network I/O. Use this whenever you compute the same +// expensive answer many times in a row — search index lookups, +// signed-token verification, derived report rollups, JSON +// transformations of slow-changing source data. +// +// We embed `generatedAt` in the result so a client refreshing the +// page can see the timestamp stay constant within the cache window +// and update once it expires. + +const MEMO_TTL_S = 60; + +async function memo(): Promise<Response> { + const entry = await Cache.getOrSet( + 'memo:report', + () => { + // Stand-in for "expensive computation". The populator can be + // synchronous or async — both are accepted. + const report = { + generatedAt: new Date().toISOString(), + topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ + name, + score: Math.round(Math.random() * 1000) / 10, + rank: i + 1, + })), + }; + // The populator returns the value to store. Because we want + // structured JSON back later, we serialise here and re-parse + // via `entry.json()` on read. + return JSON.stringify(report); + }, + { ttl: MEMO_TTL_S }, + ); + + // `entry.json()` parses the cached UTF-8 bytes as JSON. Use + // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. + const report = await entry.json(); + + return Response.json({ + pattern: 'memo', + note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, + report, + }); +} + +// --------------------------------------------------------------------------- +// Default landing — usage menu when no action is supplied +// --------------------------------------------------------------------------- + +function landing(): Response { + return Response.json({ + name: 'FastEdge Cache patterns', + actions: { + 'rate-limit': '/?action=rate-limit', + proxy: '/?action=proxy&url=https://www.example.com', + memo: '/?action=memo', + }, + }); +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +async function eventHandler(event: FetchEvent): Promise<Response> { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'rate-limit': + return await rateLimit(event); + case 'proxy': + return await proxy(url.searchParams.get('url') ?? ''); + case 'memo': + return await memo(); + case null: + return landing(); + default: + return Response.json( + { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, + { status: 400 }, + ); + } + } catch (error: unknown) { + // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions + // fields) are synchronous; host errors arrive as Promise rejections. + // Both are caught by this single handler. + return Response.json({ error: (error as Error).message }, { status: 500 }); + } +} + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(eventHandler(event)); +}); +``` + + +### FILE: examples/cache/package.json + +```json +{ + "name": "fastedge-example-cache", + "version": "1.0.0", + "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", + "type": "module", + "scripts": { + "build": "fastedge-build -c" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` + + +### FILE: examples/cache/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-wasi-rust.md index f08fffb..20e88de 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -273,201 +273,3 @@ cargo build --release - http-base skeleton - outbound-http feature blueprint - platform-overview (environment variable configuration) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/<path_and_query> — purge keys whose cache key starts with cache:/<path_and_query> - -Environment variables: - ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com - CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) - -Build: - cargo build --release -*/ - -use std::env; - -use anyhow::anyhow; -use fastedge::cache; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let origin = env::var("ORIGIN_HOST") - .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; - - let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse().ok()) - .unwrap_or_else(|| { - env::var("CACHE_TTL_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(60_000) - }); - - // Build cache key from the request path (and query string if present) - let path_and_query = req - .uri() - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - - - // Handle purge requests before any cache/origin logic - if path_and_query == "/purge" { - let deleted = cache::purge()?; - println!("purge all: {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - if let Some(prefix) = path_and_query.strip_prefix("/purge/") { - let prefix = format!("cache:/{prefix}"); - let deleted = cache::purge_prefix(&prefix)?; - println!("purge prefix '{prefix}': {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - if let Some(prefix) = path_and_query.strip_prefix("/delete/") { - let prefix = format!("cache:/{prefix}"); - cache::delete(&prefix)?; - println!("prefix '{prefix}': removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - let cache_key = format!("cache:{path_and_query}"); - - // Return cached response if available - if let Some(cached) = cache::get(&cache_key)? { - println!("cache hit: {cache_key}"); - return Ok(Response::builder() - .status(200) - .header("content-type", "application/octet-stream") - .header("x-cache", "hit") - .body(Body::from(cached))?); - } - - // Cache miss — forward request to origin - let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); - println!("cache miss: {cache_key} → {upstream_url}"); - - let upstream_req = Request::get(&upstream_url) - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("upstream request failed: {e}"))?; - - let status = upstream_resp.status(); - let headers: Vec<(String, String)> = upstream_resp - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - // Read body bytes - let mut body = upstream_resp.into_body(); - let body_bytes = body.contents().await?.to_vec(); - - // Only cache successful responses - if status.is_success() { - cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; - } - - // Replay original response - let mut builder = Response::builder() - .status(status) - .header("x-cache", "miss"); - for (k, v) in &headers { - builder = builder.header(k, v); - } - Ok(builder.body(Body::from(body_bytes))?) -} -``` - - -### FILE: examples/http/wasi/cache/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/cache/README.md - -``` -[← Back to examples](../../../README.md) - -# Cache (WASI) - -Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | -| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | - -## How it works - -``` -GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) -GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) -``` - -Cache key is `cache:<path>?<query>`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/cache_wasi.wasm -``` - -## APIs used - -- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option<Vec<u8>>)` -- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry -- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md index 6d6aa86..3331544 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -40,7 +40,7 @@ Build script: `fastedge-build src/index.js dist/crypto-hmac-jwt.wasm` import { getSecret } from 'fastedge::secret'; ``` -`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons: +`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons reused across requests: ```js const encoder = new TextEncoder(); diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md index 0710498..64f2d27 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,103 @@ async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - http-base skeleton (base handler structure, entry point macro) - platform-overview (log viewer, per-request diagnostics context) - fastedge SDK Rust reference (full `fastedge::utils` API surface) + +## Source Material + +### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Diagnostic logging example. + +Tiny pass-through proxy that writes a single `set_user_diag` tag per request +summarising the outcome (config_error, origin_unreachable, or proxied). The +tag appears in the FastEdge platform's per-request log viewer and is distinct +from stdout — it's intended for filterable outcome labels, not verbose +traces. + +Required configuration: + - Environment variable: ORIGIN_URL (origin to proxy to) + +Uses `logfmt`-ish formatting (`outcome=<verb> key=value ...`) so the tag is +easy to slice in log search tooling. +*/ + +use std::env; + +use fastedge::utils::set_user_diag; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { + let method = req.method().as_str().to_string(); + let path = req.uri().path().to_string(); + + let origin = match env::var("ORIGIN_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + set_user_diag("outcome=config_error reason=origin_missing"); + return Ok(Response::builder() + .status(500) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("ORIGIN_URL is not configured"))?); + } + }; + + let outbound = Request::get(&origin).body(Body::empty())?; + let resp = match Client::new().send(outbound).await { + Ok(r) => r, + Err(e) => { + set_user_diag(&format!( + "outcome=origin_unreachable method={method} path={path} err={e}" + )); + return Ok(Response::builder() + .status(502) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("origin unreachable"))?); + } + }; + + let status = resp.status().as_u16(); + set_user_diag(&format!( + "outcome=proxied method={method} path={path} status={status}" + )); + + let (parts, mut body) = resp.into_parts(); + let bytes = body.contents().await?; + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .body(Body::from(bytes))?) +} +``` + +### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml + +```toml +[workspace] + +[package] +name = "diagnostic_logging_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-wasi-rust.md index d8d4004..85ad7d9 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -154,119 +154,3 @@ cargo build --release - http-base skeleton reference - FastEdge platform overview (geoip-country-code header injection) - deploy skill reference (uploading WASM binary and setting environment variables) - -## Source Material - -### FILE: examples/http/wasi/geo_redirect/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example WASI-HTTP app demonstrating geo-based redirects. - -Reads the country code from the geoip-country-code request header -and redirects to a country-specific origin URL. Falls back to -BASE_ORIGIN when no country-specific mapping is configured. - -Required configuration: - - Environment variable: BASE_ORIGIN (fallback origin URL) - - Environment variable: <COUNTRY_CODE> (optional per-country origin URLs, e.g. US, DE, GB) -*/ - -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(req: Request<Body>) -> anyhow::Result<Response<Body>> { - let base_origin = match env::var("BASE_ORIGIN") { - Ok(origin) => origin, - Err(_) => { - return Ok(Response::builder() - .status(500) - .body(Body::from("BASE_ORIGIN is not set"))?); - } - }; - - let country_code = req - .headers() - .get("geoip-country-code") - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .to_string(); - - let redirect_origin = if !country_code.is_empty() { - env::var(&country_code).unwrap_or(base_origin) - } else { - base_origin - }; - - Ok(Response::builder() - .status(302) - .header("location", &redirect_origin) - .body(Body::empty())?) -} -``` - - -### FILE: examples/http/wasi/geo_redirect/Cargo.toml - -```toml -[workspace] - -[package] -name = "geo_redirect_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/geo_redirect/README.md - -``` -[← Back to examples](../../../README.md) - -# Geo Redirect (WASI) - -Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. - -Demonstrates reading request headers, reading environment variables, and returning redirect responses. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | -| `<COUNTRY_CODE>` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | - -## How it works - -``` -geoip-country-code: DE → env var DE is set → 302 to DE value -geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN -(no header) → 302 to BASE_ORIGIN -BASE_ORIGIN not set → 500 -``` - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm -``` - -## APIs used - -- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge -- `std::env::var(country_code)` — dynamic env var lookup by country code -- `Response::builder().status(302).header("location", url)` — redirect response -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-ts.md index b14c60e..ea38fe3 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -154,7 +154,6 @@ addEventListener('fetch', (event) => { }); ``` - ### FILE: examples/headers/package.json ```json diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-wasi-rust.md index 8c5712a..4cc23df 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/headers-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -164,7 +164,6 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { } ``` - ### FILE: examples/http/wasi/headers/Cargo.toml ```toml @@ -183,7 +182,6 @@ wstd = "0.6" anyhow = "1" ``` - ### FILE: examples/http/wasi/headers/README.md ``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-ts.md index f11cd59..bd5f092 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -286,186 +286,3 @@ Missing required parameters return HTTP 500 with JSON `{ "error": "..." }`. - The KV store must be pre-created in the Gcore dashboard or API before the app can use it. The store name is passed at runtime (e.g., as a query parameter). - Available KV operations: `get`, `scan`, `zrangeByScore`, `zscan`, `bfExists`. - `tsconfig.json` uses `"moduleResolution": "Bundler"` and `"target": "ES2023"`. Do not use `"moduleResolution": "Node"` or older ES targets with this SDK version. - -## Source Material - -### FILE: examples/kv-store/src/index.ts - -```ts -import { KvStore } from 'fastedge::kv'; - -import { Action, decodeValueArray, stringifyValueScoreTuples, validateQueryParams } from './utils'; - -async function eventHandler(event: FetchEvent): Promise<Response> { - try { - const { request: req } = event; - const url = new URL(req.url); - - const params = validateQueryParams(url.searchParams); - if (params.error) { - throw new Error(params.error); - } - - const myStore = KvStore.open(params.store); - const action = params.action as Action; - - const responseObj: Record<string, string> = { - Store: params.store, - Action: action, - }; - - switch (action) { - case 'get': { - const response = myStore.get(params.key); - responseObj.Key = params.key; - responseObj.Response = decodeValueArray(response); - break; - } - case 'scan': { - const response = myStore.scan(params.match); - responseObj.Match = params.match; - responseObj.Response = response.join(', '); - break; - } - case 'zrange': { - const { key, min, max } = params; - const response = myStore.zrangeByScore(key, Number.parseFloat(min), Number.parseFloat(max)); - responseObj.Key = key; - responseObj.Min = min; - responseObj.Max = max; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'zscan': { - const { key, match } = params; - const response = myStore.zscan(key, match); - responseObj.Key = key; - responseObj.Match = match; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'bfExists': { - const { key, item } = params; - const exists = myStore.bfExists(key, item); - responseObj.Key = key; - responseObj.Item = item; - responseObj.Response = exists ? 'true' : 'false'; - break; - } - default: - break; - } - - return Response.json(responseObj); - } catch (error: Error | unknown) { - return Response.json({ error: `${(error as Error).message}` }, { status: 500 }); - } -} - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store/src/utils.ts - -```ts -const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; - -export type Action = (typeof ALL_ACTIONS)[number]; - -type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; - -type Params = { [key in ParamKey]: string }; - -export function validateQueryParams(queryParams: URLSearchParams): Params { - const validParams = {} as Params; - - // Validate 'action' parameter - const action = queryParams.get('action') ?? 'get'; - if (ALL_ACTIONS.includes(action as Action)) { - validParams.action = action; - } else { - validParams.error = `Invalid action '${action}'. Supported actions are: ${ALL_ACTIONS.join( - ', ', - )}`; - return validParams; - } - - const requiredParameters = { - store: [...ALL_ACTIONS], - key: ['get', 'zrange', 'zscan', 'bfExists'], - match: ['scan', 'zscan'], - min: ['zrange'], - max: ['zrange'], - item: ['bfExists'], - } as Record<ParamKey, Array<string>>; - - for (const [key, actions] of Object.entries(requiredParameters)) { - if (actions.includes(action)) { - const value = queryParams.get(key); - if (value && value !== '') { - validParams[key as ParamKey] = value; - } else { - validParams.error = `Query parameters must provide '${key}' for a '${action}' action.`; - return validParams; - } - } - } - - return validParams; -} - -export const decodeValueArray = (arrVal: ArrayBuffer | null) => { - if (arrVal) { - const decoder = new TextDecoder(); - return decoder.decode(arrVal); - } - return ''; -}; - -export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { - let strResponse = '['; - for (const tuple of tupleList) { - strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; - } - strResponse += ']'; - return strResponse; -}; -``` - -### FILE: examples/kv-store/package.json - -```json -{ - "name": "fastedge-example-kv-store", - "version": "1.0.0", - "description": "FastEdge JS example: KV Store operations via query params", - "type": "module", - "scripts": { - "build": "fastedge-build -c" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` - -### FILE: examples/kv-store/tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2023", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "lib": ["ES2023"], - "types": ["@gcoredev/fastedge-sdk-js"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/mcp-server-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/mcp-server-ts.md index 46c119b..54b0eef 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/mcp-server-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/mcp-server-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -400,3 +400,282 @@ The MCP server listens at `/mcp` on whatever domain the FastEdge app is deployed - Model Context Protocol specification (MCP HTTP transport, tool registration) - FastEdge build CLI reference (fastedge-build) - http-base blueprint (base HTTP worker skeleton) + +## Source Material + +### FILE: examples/mcp-server/src/index.ts + +```ts +import { StreamableHTTPTransport } from '@hono/mcp'; +import { Hono } from 'hono'; + +import server from './server.js'; + +const router = new Hono(); + +router.all('/mcp', async (c) => { + const transport = new StreamableHTTPTransport(); + await server.connect(transport); + return transport.handleRequest(c); +}); + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(router.fetch(event.request)); +}); +``` + + +### FILE: examples/mcp-server/src/server.ts + +```ts +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +import type { + AlertFeature, + AlertsResponse, + ForecastPeriod, + ForecastResponse, + PointsResponse, +} from './types.js'; + +const NWS_API_BASE = 'https://api.weather.gov'; +const USER_AGENT = 'weather-app/1.0'; + +// Helper function for making NWS API requests +async function makeNWSRequest<T>(url: string): Promise<T | null> { + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/geo+json', + }; + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return (await response.json()) as T; + } catch (error) { + console.error('Error making NWS request:', error); + return null; + } +} + +// Format alert data +function formatAlert(feature: AlertFeature): string { + const props = feature.properties; + return [ + `Event: ${props.event || 'Unknown'}`, + `Area: ${props.areaDesc || 'Unknown'}`, + `Severity: ${props.severity || 'Unknown'}`, + `Status: ${props.status || 'Unknown'}`, + `Headline: ${props.headline || 'No headline'}`, + '---', + ].join('\n'); +} + +// Create server instance +const server = new McpServer({ + name: 'weather', + version: '1.0.0', +}); + +// Register weather tools +server.registerTool( + 'get-alerts', + { + title: 'Get Weather Alerts', + description: 'Get weather alerts for a US state', + inputSchema: z.object({ + state: z.string().length(2).describe('Two-letter state code (e.g. CA, NY)'), + }), + }, + async ({ state }) => { + const stateCode = state.toUpperCase(); + + const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; + const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl); + + if (!alertsData) { + return { + content: [ + { + type: 'text', + text: 'Failed to retrieve alerts data', + }, + ], + }; + } + + const features = alertsData.features || []; + if (features.length === 0) { + return { + content: [ + { + type: 'text', + text: `No active alerts for ${stateCode}`, + }, + ], + }; + } + + const formattedAlerts = features.map(formatAlert); + const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`; + + return { + content: [ + { + type: 'text', + text: alertsText, + }, + ], + }; + }, +); + +server.registerTool( + 'get-forecast', + { + title: 'Get Weather Forecast', + description: 'Get weather forecast for a location', + inputSchema: z.object({ + latitude: z.number().min(-90).max(90).describe('Latitude of the location'), + longitude: z.number().min(-180).max(180).describe('Longitude of the location'), + }), + }, + async ({ latitude, longitude }) => { + // Get grid point data + const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; + const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl); + + if (!pointsData) { + return { + content: [ + { + type: 'text', + text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, + }, + ], + }; + } + + const forecastUrl = pointsData.properties?.forecast; + if (!forecastUrl) { + return { + content: [ + { + type: 'text', + text: 'Failed to get forecast URL from grid point data', + }, + ], + }; + } + + // Get forecast data + const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl); + if (!forecastData) { + return { + content: [ + { + type: 'text', + text: 'Failed to retrieve forecast data', + }, + ], + }; + } + + const periods = forecastData.properties?.periods || []; + if (periods.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No forecast periods available', + }, + ], + }; + } + + // Format forecast periods + const formattedForecast = periods.map((period: ForecastPeriod) => + [ + `${period.name || 'Unknown'}:`, + `Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`, + `Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`, + `${period.shortForecast || 'No forecast available'}`, + '---', + ].join('\n'), + ); + + const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( + '\n', + )}`; + + return { + content: [ + { + type: 'text', + text: forecastText, + }, + ], + }; + }, +); + +export default server; +``` + + +### FILE: examples/mcp-server/package.json + +```json +{ + "name": "fastedge-example-mcp-server", + "version": "1.0.0", + "description": "Basic Example of running MCP Server on FastEdge", + "main": "src/index.ts", + "type": "module", + "bin": { + "weather": "./build/index.js" + }, + "scripts": { + "build": "npm run transpile && npm run build-wasm", + "build-wasm": "npx fastedge-build --input build/index.js --output build/weather.wasm --tsconfig tsconfig.json", + "transpile": "tsc" + }, + "files": [ + "build" + ], + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0", + "@hono/mcp": "^0.2.5", + "@modelcontextprotocol/sdk": "^1.29.0", + "hono": "^4.12.25", + "zod": "^4.3.6" + }, + "devDependencies": { + "typescript": "^5.9.2" + } +} +``` + + +### FILE: examples/mcp-server/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md index 871a96b..c1a13e7 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -127,90 +127,3 @@ Both errors propagate via `?` and result in a 500-class response from the FastEd - `outbound-modify-response` (WASI, Rust) — fetches upstream and reshapes the body into a new JSON response - `streaming` (WASI, Rust) — handler that generates its own streaming response without an upstream fetch - `outbound-fetch` (JavaScript) — mirror of this example in the FastEdge SDK JS - -## Source Material - -### FILE: examples/http/wasi/outbound_fetch/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Minimal outbound fetch example. - -Makes a GET request to an upstream HTTP origin and returns the upstream -response verbatim — status, headers, and body pass through unchanged. - -For a variant that reads and transforms the upstream body, see -`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. - -Mirror of the FastEdge-sdk-js `outbound-fetch` example. -*/ - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - // Return the upstream response verbatim. The body is passed through - // without calling `.contents()`, so it streams to the client as upstream - // produces it. - let (parts, body) = upstream_resp.into_parts(); - let mut response = Response::new(body); - *response.status_mut() = parts.status; - *response.headers_mut() = parts.headers; - Ok(response) -} -``` - - -### FILE: examples/http/wasi/outbound_fetch/Cargo.toml - -```toml -[workspace] - -[package] -name = "outbound_fetch" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/outbound_fetch/README.md - -``` -[← Back to examples](../../../README.md) - -# Outbound Fetch (WASI) - -Fetch data from an outbound HTTP origin and return the response directly — status, headers, -and body pass through unchanged. - -The body is never buffered (no `.contents().await`), so upstream chunks stream to the client -as they arrive. - -## Related - -- [outbound_modify_response](../outbound_modify_response/) — same fetch, but reads the body - and reshapes it into a new JSON response. -- [streaming](../streaming/) — a handler that generates its own streaming response body. -- Mirror of `FastEdge-sdk-js/examples/outbound-fetch/`. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-ts.md index 7fd769f..f802a94 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,49 @@ From `package.json`: - sdk-reference-js (fetch API, Response constructor, addEventListener) - deploy skill reference (uploading and registering the compiled WASM binary) - outbound-fetch feature blueprint (fetch without body transformation) + +## Source Material + +### FILE: examples/outbound-modify-response/src/index.js + +```js +async function app(event) { + const outboundResponse = await fetch('http://jsonplaceholder.typicode.com/users'); + const users = await outboundResponse.json(); + return new Response( + JSON.stringify({ + users: users.slice(0, 5), + total: 5, + skip: 0, + limit: 30, + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/outbound-modify-response/package.json + +```json +{ + "name": "fastedge-example-outbound-modify-response", + "version": "1.0.0", + "description": "FastEdge JS example: fetch and modify outbound response", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/outbound-modify-response.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md index fb8121f..9d4a66e 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -216,82 +216,3 @@ async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - outbound-fetch-wasi-rust (simpler variant — passes upstream response through unchanged) - http-base skeleton (base handler structure, `#[wstd::http_server]`, `Body`, `Request`, `Response`) - sdk-reference-rust (full `wstd` API surface) - -## Source Material - -### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Outbound fetch with response transformation. - -Fetches JSON from an upstream origin, reads and parses the body, reshapes it -into a new JSON object (first 5 users with pagination metadata), and returns -it with a fresh `content-type: application/json` header. - -This is the stepping-stone beyond `outbound_fetch/` which just passes the -upstream response through unchanged. - -Mirror of the FastEdge-sdk-js `outbound-modify-response` example. -*/ - -use anyhow::anyhow; -use serde_json::{Value, json}; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("outbound request failed: {e}"))?; - - let (_, mut body) = upstream_resp.into_parts(); - let body_bytes = body.contents().await?; - let users: Value = serde_json::from_slice(body_bytes)?; - - let sliced_users = match users.as_array() { - Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), - None => Value::Array(vec![]), - }; - - let result = json!({ - "users": sliced_users, - "total": 5, - "skip": 0, - "limit": 30, - }); - - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(Body::from(result.to_string()))?) -} -``` - -### FILE: examples/http/wasi/outbound_modify_response/Cargo.toml - -```toml -[workspace] - -[package] -name = "outbound_modify_response_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -serde_json = "1" -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rollover-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rollover-wasi-rust.md index c5cb2b8..7bbfb60 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -42,6 +42,15 @@ get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" Slot `0` serves as the baseline — always matched when no higher slot qualifies. +Usage as indices: +``` +get_effective_at("token-secret", 0) -> "original_password" +get_effective_at("token-secret", 3) -> "original_password" +get_effective_at("token-secret", 5) -> slot 5's value (if exists) +``` + +Usage as timestamps: a token's `iat` claim determines which password to validate against. `get_effective_at("token-secret", claims.iat)` returns the password that was effective when the token was issued. + ## API Reference ### `secret::get` @@ -158,6 +167,12 @@ let slot: u32 = request .as_secs() as u32 }); +let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + // Current (latest) value let current = secret::get(secret_name) .map_err(|e| anyhow!("secret::get failed: {e}"))?; @@ -165,59 +180,19 @@ let current = secret::get(secret_name) // Value effective at the given slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; -``` -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/secret_rollover.wasm +let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, +}); ``` -## See Also - -- fastedge::secret module reference (Rust SDK reference) -- http-base skeleton (base HTTP app structure) -- platform-overview (secret management configuration) -- best-practices (secret rotation strategies) - -## Source Material - -### FILE: examples/http/wasi/secret_rollover/src/lib.rs +## Full Source ```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Secret rollover example using slot-based secret retrieval. - -Demonstrates how to use `secret::get_effective_at()` with slots to support -secret rotation. Slots use a greatest matching rule: the slot with the highest -value that is <= the requested `effective_at` is returned. - -Example secret configuration: -{ - "secret": { - "name": "token-secret", - "secret_slots": [ - { "slot": 0, "value": "original_password" }, - { "slot": 1741790697, "value": "new_password" } - ] - } -} - -Usage as indices: - get_effective_at("token-secret", 0) -> "original_password" - get_effective_at("token-secret", 3) -> "original_password" - get_effective_at("token-secret", 5) -> slot 5's value (if exists) - -Usage as timestamps: - A token's `iat` claim determines which password to validate against. - get_effective_at("token-secret", claims.iat) returns the password - that was effective when the token was issued. -*/ - use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::anyhow; @@ -228,7 +203,6 @@ use wstd::http::{Request, Response}; #[wstd::http_server] async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { - // Read the slot from the x-slot header, defaulting to current timestamp let slot: u32 = request .headers() .get("x-slot") @@ -247,10 +221,8 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { .and_then(|v| v.to_str().ok()) .unwrap_or("TOKEN_SECRET"); - // Get the current secret value (latest slot) let current = secret::get(secret_name).map_err(|e| anyhow!("secret::get failed: {e}"))?; - // Get the secret effective at the requested slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; @@ -269,73 +241,6 @@ async fn main(request: Request<Body>) -> anyhow::Result<Response<Body>> { } ``` - -### FILE: examples/http/wasi/secret_rollover/Cargo.toml - -```toml -[workspace] - -[package] -name = "secret_rollover" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -serde_json = "1" -``` - - -### FILE: examples/http/wasi/secret_rollover/README.md - -``` -[← Back to examples](../../../README.md) - -# Secret Rollover (WASI) - -Demonstrates slot-based secret retrieval for secret rotation scenarios using `secret::get_effective_at()`. - -Compares the current secret value with the value effective at a given slot, returning both as JSON. This lets you validate that rotation is working correctly before removing the old slot. - -## Usage - -| Request header | Default | Description | -|---|---|---| -| `x-secret-name` | `TOKEN_SECRET` | Name of the secret to query | -| `x-slot` | current Unix timestamp | Slot value passed to `get_effective_at` | - -## How slots work - -Slots use a **greatest-match rule**: the slot with the highest value that is `<= effective_at` is returned. - -``` -Secret slots: { 0: "old-password", 1741790697: "new-password" } - -get_effective_at("TOKEN_SECRET", 0) → "old-password" -get_effective_at("TOKEN_SECRET", 100) → "old-password" -get_effective_at("TOKEN_SECRET", 1741790697) → "new-password" -get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" -``` - -When used with token `iat` (issued-at) timestamps, `get_effective_at(name, claims.iat)` returns the password that was active when the token was issued — enabling zero-downtime rotation without invalidating existing tokens. - -## What it returns - -```json -{ - "secret_name": "TOKEN_SECRET", - "slot": 0, - "current": "new-password", - "effective_at_slot": "old-password", - "is_same": false -} -``` - ## Build ```sh @@ -343,8 +248,9 @@ cargo build --release # Output: target/wasm32-wasip2/release/secret_rollover.wasm ``` -## APIs used +## See Also -- `fastedge::secret::get(name)` — current (latest-slot) secret value; `Ok(Option<String>)` -- `fastedge::secret::get_effective_at(name, slot)` — secret value at a given slot; `Ok(Option<String>)` -``` +- fastedge::secret module reference (Rust SDK reference) +- http-base skeleton (base HTTP app structure) +- platform-overview (secret management configuration) +- best-practices (secret rotation strategies) diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-ts.md index 5fe2b27..8155e9b 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -73,6 +73,7 @@ function app(event) { const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 5; i++) { + // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, 200); }); controller.enqueue(encoder.encode(`chunk ${i}\n`)); } @@ -114,51 +115,3 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference - -## Source Material - -### FILE: examples/streaming/src/index.js - -```js -function app(event) { - const encoder = new TextEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { setTimeout(resolve, 200); }); - controller.enqueue(encoder.encode(`chunk ${i}\n`)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - - -### FILE: examples/streaming/package.json - -```json -{ - "name": "fastedge-example-streaming", - "version": "1.0.0", - "description": "FastEdge JS example: streaming response with ReadableStream", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/streaming.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md index 765e332..e011ba1 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -141,67 +141,3 @@ async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - FastEdge-sdk-rust HTTP examples (other HTTP feature blueprints) - wstd crate documentation (Runtime, Body, Timer APIs) - futures-lite crate documentation (stream combinators, unfold) - -## Source Material - -### FILE: examples/http/wasi/streaming/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Streaming response example. - -Generates a response body on the fly — five text chunks, one every 200ms — -using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime -polls the stream as the body is sent, so chunks flow to the client as they -are produced instead of all at once at the end. - -Watch it stream with `curl -N https://<app-url>/` (`-N` disables client-side -buffering). - -Mirror of the FastEdge-sdk-js `streaming` example. -*/ - -use futures_lite::stream; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; -use wstd::time::{Duration, Timer}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let chunk_stream = stream::unfold(0u32, |i| async move { - if i >= 5 { - return None; - } - Timer::after(Duration::from_millis(200)).wait().await; - Some((format!("chunk {i}\n"), i + 1)) - }); - - Ok(Response::builder() - .status(200) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from_stream(chunk_stream))?) -} -``` - - -### FILE: examples/http/wasi/streaming/Cargo.toml - -```toml -[workspace] - -[package] -name = "streaming_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -futures-lite = "1" -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ts.md index 43e1a3d..5d7002e 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -202,87 +202,3 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference - -## Source Material - -### FILE: examples/template-invoice/src/index.js - -```js -import Handlebars from 'handlebars'; - -import { cssStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { logoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler() { - const rawHtmlTemplate = htmlTemplate(); - - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logoBrand, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler()); -}); -``` - - -### FILE: examples/template-invoice/package.json - -```json -{ - "name": "fastedge-example-template-invoice", - "version": "1.0.0", - "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md index c06bc83..c23693c 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-17 --> --- @@ -135,89 +135,3 @@ cargo build --release - deploy skill reference (uploading the compiled `.wasm` binary) - manage skill reference (setting environment variables and secrets on an app) - FastEdge platform overview (secret storage model) - -## Source Material - -### FILE: examples/http/wasi/variables_and_secrets/src/lib.rs - -```rust -use fastedge::secret; -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> { - let username = env::var("USERNAME").unwrap_or_default(); - let password = match secret::get("PASSWORD") { - Ok(Some(value)) => value, - _ => String::new(), - }; - - Ok(Response::builder() - .status(200) - .body(Body::from(format!( - "Username: {username}, Password: {password}" - )))?) -} -``` - - -### FILE: examples/http/wasi/variables_and_secrets/Cargo.toml - -```toml -[workspace] - -[package] -name = "variables_and_secrets" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/variables_and_secrets/README.md - -``` -[← Back to examples](../../../README.md) - -# Variables and Secrets (WASI) - -Demonstrates reading an environment variable (`USERNAME`) and a secret (`PASSWORD`), returning both in the response body. - -Environment variables are set via the FastEdge app configuration and accessed with `std::env::var`. Secrets are stored encrypted and accessed with `fastedge::secret::get` — they are never exposed in platform logs or configuration UIs. - -## Configuration - -| Key | Type | Required | Description | -|---|---|---|---| -| `USERNAME` | Environment variable | No | Username to include in response. Empty string if unset. | -| `PASSWORD` | Secret | No | Password to include in response. Empty string if unset or unavailable. | - -## What it returns - -``` -HTTP/1.1 200 OK - -Username: <USERNAME value>, Password: <PASSWORD value> -``` - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/variables_and_secrets.wasm -``` - -## APIs used - -- `std::env::var("USERNAME").unwrap_or_default()` — read env var with fallback -- `fastedge::secret::get("PASSWORD")` — read secret by name; returns `Ok(Some(String))` on success -``` diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/dotenv.md b/plugins/gcore-fastedge-cursor/skills/test/reference/dotenv.md index 9088970..30b38d6 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/dotenv.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/dotenv.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Dotenv — Runtime Secrets and Environment Variables diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/runner-internals.md b/plugins/gcore-fastedge-cursor/skills/test/reference/runner-internals.md index b75c2eb..5501551 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/runner-internals.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/runner-internals.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Runner Internals — Low-Level Runner API diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/server-api.md b/plugins/gcore-fastedge-cursor/skills/test/reference/server-api.md index 1e7af77..057aa7a 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/server-api.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/server-api.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # Server API — REST and WebSocket Endpoints diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/test-config.md b/plugins/gcore-fastedge-cursor/skills/test/reference/test-config.md index 164f73f..af1c4cb 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/test-config.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/test-config.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # test-config Reference diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/test-framework.md b/plugins/gcore-fastedge-cursor/skills/test/reference/test-framework.md index 3c928e7..b65dfde 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/test-framework.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/test-framework.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # FastEdge Test Framework API diff --git a/plugins/gcore-fastedge-cursor/skills/test/reference/vscode-debugger.md b/plugins/gcore-fastedge-cursor/skills/test/reference/vscode-debugger.md index 457d95d..28df592 100644 --- a/plugins/gcore-fastedge-cursor/skills/test/reference/vscode-debugger.md +++ b/plugins/gcore-fastedge-cursor/skills/test/reference/vscode-debugger.md @@ -2,9 +2,9 @@ auto-updated: true sources: - id: fastedge-test - ref: v0.2.5 + ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f - updated: 2026-07-23 + updated: 2026-08-17 --> # FastEdge Visual Debugger diff --git a/plugins/gcore-fastedge/.claude-plugin/plugin.json b/plugins/gcore-fastedge/.claude-plugin/plugin.json index 59baf0f..eeb19f1 100644 --- a/plugins/gcore-fastedge/.claude-plugin/plugin.json +++ b/plugins/gcore-fastedge/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "gcore-fastedge", - "version": "0.2.5", + "version": "0.2.6", "description": "Build, deploy, and manage serverless edge applications on Gcore FastEdge — Wasm-powered compute on 210+ global PoPs", "author": { "name": "Gcore", diff --git a/plugins/gcore-fastedge/docs-index.json b/plugins/gcore-fastedge/docs-index.json index 84e2ad5..90eeb93 100644 --- a/plugins/gcore-fastedge/docs-index.json +++ b/plugins/gcore-fastedge/docs-index.json @@ -1,16 +1,16 @@ { "schema_version": "1.0.0", - "generated_at": "2026-07-24T07:50:35.090Z", + "generated_at": "2026-08-18T07:33:54.938Z", "source": { "repo": "fastedge-plugin", - "commit": "05c2493", + "commit": "067d44e", "pipeline": "sync-reference-docs" }, "topics": [ { "id": "cdn-apps-rust", "title": "FastEdge Rust SDK — CDN Apps (Proxy-Wasm)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -31,7 +31,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "24a86619b212c904d2bae26e563353bfed0d28ff4743c99c0eb0d1908ee0d1fb", + "content_sha256": "884a6d522e8ab8459d1662eba50c58a0d1d6c02c56f2dd0bb413c6eb782166ea", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -60,7 +60,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "rust", "sdk", @@ -532,7 +532,7 @@ { "id": "cdn-examples-ab-testing-as", "title": "A/B Testing — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md", "skill": "fastedge-docs", "category": "examples", @@ -554,9 +554,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "cookie-based", "traffic", @@ -566,7 +566,7 @@ "requests", "different" ], - "content_sha256": "419863edee5fea328eaa0d29bec26e3e2baab36e05c21d29fe5d8837fb0a4c9b", + "content_sha256": "9546c33b6d3c8657538a75f442e7738541cdad753fc7b88a1cd759cc036629f0", "sections": [ { "id": "cdn-examples-ab-testing-as#introduction", @@ -583,9 +583,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "testing", "assemblyscript", "cdn", @@ -694,7 +694,7 @@ "level": 2, "anchor": "request-flow-—-onrequestheaders", "line_start": 44, - "line_end": 118, + "line_end": 120, "keywords": [ "request", "flow", @@ -729,8 +729,8 @@ "heading": "Response Flow — `onResponseHeaders`", "level": 2, "anchor": "response-flow-—-onresponseheaders", - "line_start": 119, - "line_end": 151, + "line_start": 121, + "line_end": 153, "keywords": [ "response", "flow", @@ -773,8 +773,8 @@ "heading": "API Surface", "level": 2, "anchor": "api-surface", - "line_start": 152, - "line_end": 169, + "line_start": 154, + "line_end": 171, "keywords": [ "api", "surface", @@ -822,8 +822,8 @@ "heading": "Cookie Convention", "level": 2, "anchor": "cookie-convention", - "line_start": 170, - "line_end": 181, + "line_start": 172, + "line_end": 183, "keywords": [ "cookie", "convention", @@ -846,8 +846,8 @@ "heading": "Error Conditions", "level": 2, "anchor": "error-conditions", - "line_start": 182, - "line_end": 192, + "line_start": 184, + "line_end": 194, "keywords": [ "error", "conditions", @@ -884,8 +884,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 193, - "line_end": 208, + "line_start": 195, + "line_end": 210, "keywords": [ "build", "pnpm", @@ -917,8 +917,8 @@ "heading": "AssemblyScript-Specific Constraints", "level": 2, "anchor": "assemblyscript-specific-constraints", - "line_start": 209, - "line_end": 218, + "line_start": 211, + "line_end": 220, "keywords": [ "assemblyscript-specific", "constraints", @@ -967,8 +967,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 219, - "line_end": 225, + "line_start": 221, + "line_end": 227, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -1000,7 +1000,7 @@ { "id": "cdn-examples-ab-testing-rust", "title": "A/B Testing — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1024,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1034,7 +1034,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "e0967ff8a5a76ceb6efaaf12499f28d2197799098608bbdbe5e3637b1b9e8495", + "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1053,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -1650,13 +1650,63 @@ "structure", "examples-a" ] + }, + { + "id": "cdn-examples-ab-testing-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 252, + "line_end": 439, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "ab_testing", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "traffic", + "splitting", + "layer.", + "uses", + "cookie", + "assign", + "users", + "variant", + "rewrites", + "request", + "path", + "route", + "different", + "parts", + "origin", + "server.", + "required", + "configuration", + "environment", + "variable", + "experiment_name", + "variant_a_path", + "prefix" + ] } ] }, { "id": "cdn-examples-api-key-as", "title": "CDN Example: API Key Authentication (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1678,9 +1728,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -1690,7 +1740,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "144feff24d4893ee721a26628b201437fd48d6c34b14d465c821cab5cff05f07", + "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1707,9 +1757,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -2064,13 +2114,183 @@ "deployment", "guide" ] + }, + { + "id": "cdn-examples-api-key-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 203, + "line_end": 326, + "keywords": [ + "source", + "material", + "file", + "examples", + "apikey", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "getsecret", + "setloglevel", + "fastedge", + "const", + "unauthorized", + "u32", + "401", + "forbidd" + ] + }, + { + "id": "cdn-examples-api-key-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 327, + "line_end": 340, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "expected", + "api", + "key", + "api_key", + "secret.", + "checks", + "x-api-key", + "request", + "header.", + "returns", + "401", + "unauthorized", + "header", + "missing.", + "403", + "forbidden", + "match.", + "success", + "clears", + "forwarding", + "upstream", + "origin", + "proxy-wasm", + ".remove", + "sets", + "value", + "empty", + "string", + "rather", + "deleting", + "simpler", + "alternative", + "jwt", + "validation", + "you" + ] + }, + { + "id": "cdn-examples-api-key-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 341, + "line_end": 348, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "api_key", + "secret", + "expected", + "api", + "key", + "value" + ] + }, + { + "id": "cdn-examples-api-key-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 349, + "line_end": 362, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "apikey.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "apikey-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-api-key-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 363, + "line_end": 367, + "keywords": [ + "deploy", + "upload", + "build", + "apikey.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "api_key", + "secret", + "application", + "settings." + ] } ] }, { "id": "cdn-examples-api-key-rust", "title": "API Key Validation — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2094,7 +2314,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "api", "key", "validation", @@ -2104,7 +2324,7 @@ "requests", "against" ], - "content_sha256": "dcffdcc456556e41a80a866fcd1ccb02dcc074a7882e5357bed515977ec38748", + "content_sha256": "bbaa97e8bc532d48176cd6b5f6c44e90f38200072425c6f32426bada8a682099", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2123,7 +2343,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "api", "key", "validation", @@ -2374,7 +2594,7 @@ { "id": "cdn-examples-auth-jwt-as", "title": "JWT Validation — CDN App (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-as.md", "skill": "fastedge-docs", "category": "examples", @@ -2398,9 +2618,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2411,7 +2631,7 @@ "example", "app_type" ], - "content_sha256": "6facd62118f8f4e03232cb0574acfa59cfdf3cd113a070d7d46847c6e62084dd", + "content_sha256": "c9792a91f6b66ee092a92fc63a8c0a961358dc9ee9cd2b42f8e0d5a9791869d5", "sections": [ { "id": "cdn-examples-auth-jwt-as#introduction", @@ -2428,9 +2648,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "capabilities", "jwt-auth", @@ -2520,7 +2740,7 @@ "level": 2, "anchor": "key-apis", "line_start": 57, - "line_end": 115, + "line_end": 116, "keywords": [ "key", "apis", @@ -2546,6 +2766,8 @@ "pass", "directly", "jwtverify", + "without", + "encoding", "typescript", "const", "send_http_response", @@ -2553,7 +2775,7 @@ "internal", "server", "error", - "string.utf8.encode" + "strin" ] }, { @@ -2561,8 +2783,8 @@ "heading": "Validation Flow", "level": 2, "anchor": "validation-flow", - "line_start": 116, - "line_end": 131, + "line_start": 117, + "line_end": 132, "keywords": [ "validation", "flow", @@ -2602,8 +2824,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 132, - "line_end": 146, + "line_start": 133, + "line_end": 147, "keywords": [ "error", "responses", @@ -2640,8 +2862,8 @@ "heading": "Required Secret", "level": 2, "anchor": "required-secret", - "line_start": 147, - "line_end": 156, + "line_start": 148, + "line_end": 157, "keywords": [ "required", "secret", @@ -2663,19 +2885,48 @@ "application", "deployment.", "rotation", - "see", + "use", "getsecreteffectiveat", + "instead", + "getsecret", + "see", "secrets", "reference." ] }, + { + "id": "cdn-examples-auth-jwt-as#testing-tokens", + "heading": "Testing Tokens", + "level": 2, + "anchor": "testing-tokens", + "line_start": 158, + "line_end": 173, + "keywords": [ + "testing", + "tokens", + "use", + "secret", + "a-string-secret-at-least-256-bits-long-thats-hard-to-break", + "expired", + "token", + "returns", + "403", + "forbidden", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiylcjlehaiojk3odmxmdg2mx0.egssdoddahz8kqee7be9n168cdewoioej96idm2c1yq", + "valid", + "expiry", + "2035-01-01", + "200", + "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte" + ] + }, { "id": "cdn-examples-auth-jwt-as#dependencies", "heading": "Dependencies", "level": 2, "anchor": "dependencies", - "line_start": 157, - "line_end": 171, + "line_start": 174, + "line_end": 196, "keywords": [ "dependencies", "json", @@ -2695,6 +2946,11 @@ "directly", "used", "example.", + "dev", + "assemblyscript", + "wasi-shim", + "0.1.0", + "0.28.9", "---" ] }, @@ -2703,8 +2959,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 172, - "line_end": 190, + "line_start": 197, + "line_end": 215, "keywords": [ "build", "pnpm", @@ -2738,8 +2994,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 191, - "line_end": 202, + "line_start": 216, + "line_end": 227, "keywords": [ "registration", "typescript", @@ -2761,8 +3017,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 203, - "line_end": 214, + "line_start": 228, + "line_end": 240, "keywords": [ "gotchas", "secret", @@ -2811,8 +3067,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 215, - "line_end": 221, + "line_start": 241, + "line_end": 247, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -2840,7 +3096,7 @@ { "id": "cdn-examples-auth-jwt-rust", "title": "JWT Authentication — CDN App (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2864,7 +3120,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -2874,7 +3130,7 @@ "auth", "jwt" ], - "content_sha256": "8f4d5706f48a9e06741d84231751b9b75f334ae1abf22a26a3f5b7ca90ff3603", + "content_sha256": "35c20c709fe8bd2a6ae069467c5a1da66bf3f28d8e251c6ce2b1c692532d625a", "sections": [ { "id": "cdn-examples-auth-jwt-rust#introduction", @@ -2893,7 +3149,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -3396,7 +3652,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3418,9 +3674,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3430,7 +3686,7 @@ "app_type", "languages" ], - "content_sha256": "131d3cb75bfb089b33860c1da8965a6ff2dbf7ba478f8e12cdb37384d5412f3e", + "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3447,9 +3703,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "title", "body", @@ -3876,13 +4132,146 @@ "body", "buffering" ] + }, + { + "id": "cdn-examples-body-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 341, + "line_end": 519, + "keywords": [ + "source", + "material", + "file", + "examples", + "body", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "buffertypevalues", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_buffer_bytes", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "set_property", + "stream_context" + ] + }, + { + "id": "cdn-examples-body-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 520, + "line_end": 534, + "keywords": [ + "onrequestheaders", + "removes", + "content-length", + "header", + "required", + "because", + "body", + "content", + "altered.", + "onrequestbody", + "buffers", + "full", + "request", + "checks", + "whether", + "contains", + "word", + "client", + "found", + "replaced", + "redaction", + "notice.", + "onresponseheaders", + "sets", + "transfer-encoding", + "chunked", + "captures", + "content-type", + "runtime", + "property.", + "onresponsebody", + "logs", + "url" + ] + }, + { + "id": "cdn-examples-body-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 535, + "line_end": 548, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "-----------------------", + "--------------------------------------------------", + "body.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "body-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-body-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 549, + "line_end": 553, + "keywords": [ + "deploy", + "upload", + "build", + "body.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application." + ] } ] }, { "id": "cdn-examples-body-rust", "title": "CDN Body Inspection and Modification — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -3906,7 +4295,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -3916,7 +4305,7 @@ "body-inspection", "body-modification" ], - "content_sha256": "bc2329c7aa193d958a996c6de7e4f860ec1225ccf9940a037e3dbd6ef53cd22c", + "content_sha256": "24fb25e4211d794f9ead55478f050a7b3f88d2a46128c145112ab22b167617c0", "sections": [ { "id": "cdn-examples-body-rust#introduction", @@ -3935,7 +4324,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -4338,7 +4727,7 @@ { "id": "cdn-examples-cache-control-as", "title": "Cache Control — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md", "skill": "fastedge-docs", "category": "examples", @@ -4362,9 +4751,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4374,7 +4763,7 @@ "cache-control", "response-headers" ], - "content_sha256": "a088ac66592e155e209debe7d4c503dbc1cd638a3570ff06890f35c6258c1fa8", + "content_sha256": "1addef442e8b046f2689030c6ded25b7bd7ce8c303a7d46d5249db12d9955e1f", "sections": [ { "id": "cdn-examples-cache-control-as#introduction", @@ -4391,9 +4780,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -4784,13 +5173,191 @@ "variable", "patterns" ] + }, + { + "id": "cdn-examples-cache-control-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 232, + "line_end": 376, + "keywords": [ + "source", + "material", + "file", + "examples", + "cachecontrol", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "cachecontrolroot", + "extends", + "createcontext", + "context_id", + "u32" + ] + }, + { + "id": "cdn-examples-cache-control-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 377, + "line_end": 390, + "keywords": [ + "onresponseheaders", + "app", + "inspects", + "content-type", + "response.status", + "apply", + "appropriate", + "caching", + "policy", + "content", + "type", + "cache", + "default", + "max-age", + "---", + "images", + "fonts", + "css", + "wasm", + "public", + "static_max_age", + "immutable", + "year", + "31536000s", + "text", + "html", + "html_max_age", + "must-revalidate", + "hour", + "3600s", + "application", + "json", + "xml", + "private", + "api_max_age", + "no-cache", + "no-" + ] + }, + { + "id": "cdn-examples-cache-control-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 391, + "line_end": 400, + "keywords": [ + "configuration", + "environment", + "variables", + "optional", + "sensible", + "defaults", + "applied", + "unset.", + "variable", + "default", + "description", + "----------", + "---------", + "-------------", + "static_max_age", + "31536000", + "max-age", + "static", + "assets", + "seconds", + "html_max_age", + "3600", + "html", + "responses", + "api_max_age", + "api", + "no-cache" + ] + }, + { + "id": "cdn-examples-cache-control-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 401, + "line_end": 414, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cachecontrol.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cachecontrol-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cache-control-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 415, + "line_end": 419, + "keywords": [ + "deploy", + "upload", + "build", + "cachecontrol.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "optionally", + "configure", + "max-age", + "environment", + "variables." + ] } ] }, { "id": "cdn-examples-cache-control-rust", "title": "Cache Control — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -4814,7 +5381,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -4824,7 +5391,7 @@ "cache-control", "content-type-aware-caching" ], - "content_sha256": "48bc72c7f9d29455ba72ca82d6ee3a23c91daefe2c4c8eb4e2f825e87771d980", + "content_sha256": "52b2a31bb8aad5cdca314ece4bd67e6404091a413f20cd8198f3449d3d5b6d0d", "sections": [ { "id": "cdn-examples-cache-control-rust#introduction", @@ -4843,7 +5410,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -5376,7 +5943,7 @@ { "id": "cdn-examples-convert-image-rust", "title": "CDN Example: Convert Image (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5400,7 +5967,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "example", "convert", "image", @@ -5410,7 +5977,7 @@ "avif", "format" ], - "content_sha256": "b2e6e63bcb77be6336941490ac3a834292ab5f939ac981b1d4d2e13942ece297", + "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5429,7 +5996,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "cdn", "example", "convert", @@ -5894,129 +6461,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-convert-image-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 206, - "line_end": 488, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "convert_image", - "src", - "lib.rs", - "rust", - "use", - "image", - "proxy_wasm", - "traits", - "types", - "std", - "env", - "varerror", - "cursor", - "str", - "from_utf8", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "convertimageroot", - "struct", - "impl", - "context", - "get_type", - "self" - ] - }, - { - "id": "cdn-examples-convert-image-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 489, - "line_end": 495, - "keywords": [ - "configuration", - "environment", - "variable", - "formats_to_transform", - "comma-separated", - "list", - "file", - "extensions", - "convert", - "e.g.", - "jpg", - "jpeg", - "png", - "ignored_ua_list", - "optional", - "user-agent", - "substrings", - "skip", - "avif_speed", - "avif", - "encoding", - "speed", - "1-10", - "default", - "avif_quality", - "quality", - "1-100" - ] - }, - { - "id": "cdn-examples-convert-image-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 496, - "line_end": 502, - "keywords": [ - "works", - "on_request_headers", - "checks", - "file", - "extension", - "user-agent", - "sets", - "image-format", - "header", - "cache", - "variation", - "on_response_headers", - "response", - "headers", - "avif", - "content", - "type", - "200", - "responses", - "on_response_body", - "decodes", - "original", - "image", - "re-encodes" - ] } ] }, { "id": "cdn-examples-cors-as", "title": "CORS — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6038,9 +6489,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "response-header", "injection", @@ -6050,7 +6501,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "9d28e66e98ae483455490d57761e0994771a24b3e869097c66a3190591c02e2e", + "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6067,9 +6518,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cors", "assemblyscript", "cdn", @@ -6449,13 +6900,196 @@ "vary", "usage" ] + }, + { + "id": "cdn-examples-cors-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 197, + "line_end": 318, + "keywords": [ + "source", + "material", + "file", + "examples", + "cors", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "setloglevel", + "fastedge", + "class", + "corsroot", + "extends", + "createcontext", + "context_id", + "u32", + "loglev" + ] + }, + { + "id": "cdn-examples-cors-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 319, + "line_end": 324, + "keywords": [ + "onresponseheaders", + "requests", + "allowed", + "origins", + "app", + "adds", + "access-control-allow-origin", + "vary", + "origin", + "response", + "headers.", + "optionally", + "exposes", + "additional", + "headers", + "via", + "access-control-expose-headers", + "disallowed", + "pass", + "unchanged", + "cors", + "added", + "note", + "options", + "preflights", + "fastedge", + "edge", + "layer", + "answers", + "preflight", + "directly", + "proxy-wasm", + "hooks", + "fire", + "options.", + "configure", + "behaviour", + "meth" + ] + }, + { + "id": "cdn-examples-cors-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 325, + "line_end": 333, + "keywords": [ + "configuration", + "set", + "following", + "environment", + "variables", + "your", + "fastedge", + "application", + "variable", + "example", + "description", + "----------", + "---------", + "-------------", + "allowed_origins", + "https", + "example.com", + "app.example.com", + "comma-separated", + "allowed", + "origins", + "required", + "expose_headers", + "x-request-id", + "x-trace-id", + "response", + "headers", + "expose", + "browser", + "optional" + ] + }, + { + "id": "cdn-examples-cors-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 334, + "line_end": 347, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "cors.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "cors-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-cors-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 348, + "line_end": 352, + "keywords": [ + "deploy", + "upload", + "build", + "cors.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "allowed_origins", + "environment", + "variable", + "application", + "settings." + ] } ] }, { "id": "cdn-examples-cors-rust", "title": "CORS — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -6479,7 +7113,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -6489,7 +7123,7 @@ "cors", "preflight" ], - "content_sha256": "244be54d1f60bd8e4359d25fe991c1e492dff39f624099615db4cbccdf58df60", + "content_sha256": "a47598b96dd1c4106627c991a847554a508c930b05c892327d99ac7d7b11620d", "sections": [ { "id": "cdn-examples-cors-rust#introduction", @@ -6508,7 +7142,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -7009,7 +7643,7 @@ { "id": "cdn-examples-custom-error-pages-as", "title": "Custom Error Pages — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7031,9 +7665,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7043,7 +7677,7 @@ "response-headers", "response-body" ], - "content_sha256": "bc71a12495f399f47434a5c8529feafb96e975dc762a1cd99a49ea8b8df2465f", + "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7060,9 +7694,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -7248,7 +7882,7 @@ "level": 2, "anchor": "error-code-coverage", "line_start": 154, - "line_end": 182, + "line_end": 200, "keywords": [ "error", "code", @@ -7283,13 +7917,10 @@ "unavailable", "504", "5xx", - "categories", - "range", - "category", - "label", - "499", - "client", - "599" + "descriptions", + "description", + "understand", + "due" ] }, { @@ -7297,8 +7928,8 @@ "heading": "HTML Output Structure", "level": 2, "anchor": "html-output-structure", - "line_start": 183, - "line_end": 210, + "line_start": 201, + "line_end": 228, "keywords": [ "html", "output", @@ -7339,8 +7970,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 211, - "line_end": 229, + "line_start": 229, + "line_end": 247, "keywords": [ "build", "pnpm", @@ -7373,8 +8004,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 230, - "line_end": 235, + "line_start": 248, + "line_end": 253, "keywords": [ "deploy", "upload", @@ -7397,8 +8028,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 236, - "line_end": 248, + "line_start": 254, + "line_end": 266, "keywords": [ "constraints", "gotchas", @@ -7442,8 +8073,8 @@ "heading": "Imports", "level": 2, "anchor": "imports", - "line_start": 249, - "line_end": 270, + "line_start": 267, + "line_end": 288, "keywords": [ "imports", "import", @@ -7473,8 +8104,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 271, - "line_end": 277, + "line_start": 289, + "line_end": 295, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -7501,7 +8132,7 @@ { "id": "cdn-examples-custom-error-pages-rust", "title": "cdn-examples-custom-error-pages-rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7525,7 +8156,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "custom", "error", "pages", @@ -7535,7 +8166,7 @@ "5xx", "http" ], - "content_sha256": "e2e516ccf39ab64b58e9c2111f315294d4ae02c35a28c5ae42705b56fbce70e1", + "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", "sections": [ { "id": "cdn-examples-custom-error-pages-rust#introduction", @@ -7554,7 +8185,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -7563,7 +8194,7 @@ "level": 2, "anchor": "custom-error-pages-—-cdn-rust", "line_start": 10, - "line_end": 242, + "line_end": 297, "keywords": [ "custom", "error", @@ -7595,16 +8226,16 @@ "regardless", "origin", "returns.", - "---", - "api", - "patterns", - "logic", + "build", + "script", + "build.rs", "runs", - "inside", - "proxy_wasm", - "traits", - "httpcontext", - "implementation." + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] @@ -7612,7 +8243,7 @@ { "id": "cdn-examples-custom-rust", "title": "CDN Example: Custom — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7636,7 +8267,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -7646,7 +8277,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "45a2197a3910b5dbb1f0873d4726f360fe12ced198feef50a806e0d3c08505d9", + "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -7665,7 +8296,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -8047,13 +8678,59 @@ "overview", "platform-overview" ] + }, + { + "id": "cdn-examples-custom-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 373, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "custom", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "const", + "bad_request", + "u32", + "400", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id" + ] } ] }, { "id": "cdn-examples-env-secrets-as", "title": "Environment Variables and Secrets — CDN (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-as.md", "skill": "fastedge-docs", "category": "examples", @@ -8075,9 +8752,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8088,7 +8765,7 @@ "secrets", "environment" ], - "content_sha256": "34d6cb6344af07831efe048bd621e270ce2ad9377a1645d26b883ede493e6cb5", + "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8105,9 +8782,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -8518,13 +9195,228 @@ "handling", "guidelines" ] + }, + { + "id": "cdn-examples-env-secrets-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 229, + "line_end": 313, + "keywords": [ + "source", + "material", + "file", + "examples", + "variablesandsecrets", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getenv", + "getsecret", + "setloglevel", + "fastedge" + ] + }, + { + "id": "cdn-examples-env-secrets-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 314, + "line_end": 326, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "username", + "environment", + "variable", + "using", + "getenv", + "password", + "secret", + "getsecret", + "logs", + "values", + "retrieved", + "without", + "logging", + "value", + "itself", + "injects", + "them", + "x-env-username", + "x-env-password", + "request", + "headers", + "upstream", + "receives", + "them.", + "useful", + "reference", + "understanding", + "access", + "variables", + "secrets", + "within", + "fastedge", + "plugin.", + "security", + "warning", + "neve" + ] + }, + { + "id": "cdn-examples-env-secrets-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 327, + "line_end": 335, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "----------", + "--------------------", + "--------------------------------------", + "username", + "environment", + "variable", + "value", + "forward", + "upstream", + "password", + "secret" + ] + }, + { + "id": "cdn-examples-env-secrets-as#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 336, + "line_end": 346, + "keywords": [ + "local", + "testing", + "fixture", + "fixtures", + "happy-path.test.json", + "uses", + "dotenv", + "enabled", + "true", + "load", + "values", + ".env", + "runner", + "maps", + "fastedge_var_env_", + "name", + "getenv", + "fastedge_var_secret_", + "getsecret", + "test", + "locally", + "visual", + "debugger", + "create", + "fastedge_var_env_username", + "my-username", + "fastedge_var_secret_password", + "my-password" + ] + }, + { + "id": "cdn-examples-env-secrets-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 347, + "line_end": 360, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "--------------------------------------", + "--------------------------------------------------", + "variablesandsecrets.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "variablesandsecrets-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-env-secrets-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 361, + "line_end": 365, + "keywords": [ + "deploy", + "upload", + "build", + "variablesandsecrets.wasm", + "fastedge", + "portal", + "https", + "portal.gcore.com", + "attach", + "your", + "cdn", + "application.", + "configure", + "username", + "environment", + "variable", + "password", + "secret", + "application", + "settings." + ] } ] }, { "id": "cdn-examples-env-secrets-rust", "title": "Environment Variables and Secrets — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 --> --- type: example app_type: cdn languages: [rust] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 --> --- type: example app_type: cdn languages: [rust] capabilities: [env-vars, secrets] ---", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -8548,7 +9440,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -8558,7 +9450,7 @@ "env-vars", "secrets" ], - "content_sha256": "53751d3f9c227999fe37886133d8500ccf9bf7b8451181faadc16fc810e2f7a6", + "content_sha256": "626204a284720f5610349657a81d03da8d6fd6ae3440ed314183318d50bf3e8e", "sections": [ { "id": "cdn-examples-env-secrets-rust#introduction", @@ -8577,7 +9469,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -8980,7 +9872,7 @@ { "id": "cdn-examples-geo-redirect-as", "title": "Geo Redirect — CDN (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md", "skill": "fastedge-docs", "category": "examples", @@ -9002,9 +9894,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -9014,7 +9906,7 @@ "geo-routing", "header-manipulation" ], - "content_sha256": "d809b1aeaf9eef762fe336d6a7b4e21ad50201bb525e0716c678d58114a67153", + "content_sha256": "067cd6a598690aec364ccea7550edc1cf38e963244f81bc53a1f9e51521a20e5", "sections": [ { "id": "cdn-examples-geo-redirect-as#introduction", @@ -9031,9 +9923,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -9595,7 +10487,7 @@ { "id": "cdn-examples-geo-redirect-rust", "title": "Geo Redirect — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -9619,7 +10511,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -9629,7 +10521,7 @@ "geo-routing", "origin-rewrite" ], - "content_sha256": "b3c1628b50321514d90b611d3720246dfd2a0db2d07b82a304a0dfb0b9adf975", + "content_sha256": "310cec0f54b004867a013a8705658059764759351d317237fa99619f4369db8d", "sections": [ { "id": "cdn-examples-geo-redirect-rust#introduction", @@ -9648,7 +10540,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -10151,7 +11043,7 @@ { "id": "cdn-examples-geoblock-as", "title": "Geoblock — CDN App Example (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md", "skill": "fastedge-docs", "category": "examples", @@ -10173,9 +11065,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -10185,7 +11077,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "ba56ec73855e44c6588d02ada1120283ed42ea283957a420794ba60c18068e88", + "content_sha256": "45f4aed05034630ae0428c51417368006589a428d93125905ff42c54a819deda", "sections": [ { "id": "cdn-examples-geoblock-as#introduction", @@ -10202,9 +11094,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -10680,7 +11572,7 @@ { "id": "cdn-examples-geoblock-rust", "title": "Geoblock — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -10704,7 +11596,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -10714,7 +11606,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "d972e44932dd9f3853b33e90f27a5b46e80357f8a89911e9e4a156047d02f66a", + "content_sha256": "9284d14217f3925fd3de6aa8a69a6e0ab2de9bf4abdbe7a4a4ca23298e1bbe66", "sections": [ { "id": "cdn-examples-geoblock-rust#introduction", @@ -10733,7 +11625,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -11210,7 +12102,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11234,9 +12126,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11246,7 +12138,7 @@ "headers", "request-headers" ], - "content_sha256": "ce0dbc8a8e9aa6790dfde6e27b5a069105063a9725c0593334e625084387b9f7", + "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11263,9 +12155,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -11572,13 +12464,82 @@ "hea" ] }, + { + "id": "cdn-examples-headers-as#multi-value-headers", + "heading": "Multi-Value Headers", + "level": 2, + "anchor": "multi-value-headers", + "line_start": 202, + "line_end": 205, + "keywords": [ + "multi-value", + "headers", + "new-header-03", + "deliberately", + "added", + "twice", + "add", + "called", + "same", + "name", + "twice.", + "produces", + "header", + "two", + "separate", + "entries.", + "validation", + "pattern", + "uses", + "set", + "string", + "value", + "pairs", + "assert", + "values", + "present." + ] + }, + { + "id": "cdn-examples-headers-as#cross-phase-response-header-writes", + "heading": "Cross-Phase Response Header Writes", + "level": 2, + "anchor": "cross-phase-response-header-writes", + "line_start": 206, + "line_end": 212, + "keywords": [ + "cross-phase", + "response", + "header", + "writes", + "stream_context.headers.response.add", + "...", + "called", + "onrequestheaders", + "headers", + "written", + "request", + "phase", + "appear", + "final", + "alongside", + "set", + "onresponseheaders", + "distinction", + "new-response-header", + "value-01", + "safe", + "stream_context.headers.response.get", + "panics" + ] + }, { "id": "cdn-examples-headers-as#error-response-codes-used", "heading": "Error Response Codes Used", "level": 2, "anchor": "error-response-codes-used", - "line_start": 202, - "line_end": 211, + "line_start": 213, + "line_end": 222, "keywords": [ "error", "response", @@ -11611,8 +12572,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 212, - "line_end": 235, + "line_start": 223, + "line_end": 246, "keywords": [ "logging", "log", @@ -11651,8 +12612,8 @@ "heading": "Known Issues", "level": 2, "anchor": "known-issues", - "line_start": 236, - "line_end": 245, + "line_start": 247, + "line_end": 254, "keywords": [ "known", "issues", @@ -11694,8 +12655,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 246, - "line_end": 265, + "line_start": 255, + "line_end": 274, "keywords": [ "build", "pnpm", @@ -11730,8 +12691,8 @@ "heading": "Deployment", "level": 2, "anchor": "deployment", - "line_start": 266, - "line_end": 269, + "line_start": 275, + "line_end": 278, "keywords": [ "deployment", "upload", @@ -11752,8 +12713,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 270, - "line_end": 276, + "line_start": 279, + "line_end": 285, "keywords": [ "see", "proxy-wasm-sdk-as", @@ -11774,7 +12735,7 @@ { "id": "cdn-examples-headers-rust", "title": "Headers — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -11798,7 +12759,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -11808,7 +12769,7 @@ "headers", "request-headers" ], - "content_sha256": "9b8fa82906d3a7e8498f68496bb247ca4f942000fddfef9a2e4bf8b65faf16cc", + "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -11827,7 +12788,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -12167,13 +13128,58 @@ "abi", "surface" ] + }, + { + "id": "cdn-examples-headers-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 234, + "line_end": 586, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "headers", + "src", + "lib.rs", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "collections", + "hashset", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "context_id", + "u32", + "option", + "httpcont" + ] } ] }, { "id": "cdn-examples-http-call-as", "title": "cdn-examples-http-call-as", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12197,9 +13203,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "overview", "httpcall", "dispatches", @@ -12210,7 +13216,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "11691976691add5d7914e80eff68a71b36c33bc8d12beac13c0bbacdb1576e55", + "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12227,9 +13233,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20" + "2026-08-17" ] }, { @@ -12328,7 +13334,7 @@ "level": 2, "anchor": "common-patterns", "line_start": 102, - "line_end": 262, + "line_end": 286, "keywords": [ "common", "patterns", @@ -12360,13 +13366,60 @@ "proxy-wasm-sdk" ] }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 287, + "line_end": 306, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps", + "dependencies", + "package.json", + "package", + "role", + "---------", + "gcoredev", + "proxy-wasm-sdk-as", + "sdk", + "required", + "runtime", + "dep", + "1.2.3", + "assemblyscript", + "compiler", + "0.28.9", + "wasi-shim", + "wasi", + "compatibility" + ] + }, { "id": "cdn-examples-http-call-as#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 263, - "line_end": 273, + "line_start": 307, + "line_end": 318, "keywords": [ "gotchas", "closures", @@ -12410,8 +13463,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 274, - "line_end": 279, + "line_start": 319, + "line_end": 324, "keywords": [ "related", "sdk", @@ -12460,7 +13513,7 @@ { "id": "cdn-examples-http-call-rust", "title": "HTTP Call — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12484,7 +13537,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "makes", @@ -12494,7 +13547,7 @@ "services", "timeout" ], - "content_sha256": "a877fd720fb921fd1b8c38d69e81717621c2c7ffa28ee41f98bbded108fea44c", + "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -12513,7 +13566,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "call", "cdn", @@ -12909,13 +13962,54 @@ "resume_http_request" ] }, + { + "id": "cdn-examples-http-call-rust#complete-example", + "heading": "Complete Example", + "level": 2, + "anchor": "complete-example", + "line_start": 247, + "line_end": 359, + "keywords": [ + "complete", + "example", + "rust", + "use", + "proxy_wasm", + "traits", + "types", + "std", + "time", + "duration", + "main", + "set_log_level", + "loglevel", + "trace", + "set_root_context", + "box", + "dyn", + "rootcontext", + "new", + "httpheadersroot", + "struct", + "impl", + "context", + "create_http_context", + "self", + "_context_id", + "u32", + "option", + "httpcontext", + "httpheaders", + "sta" + ] + }, { "id": "cdn-examples-http-call-rust#gotchas", "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 247, - "line_end": 257, + "line_start": 360, + "line_end": 370, "keywords": [ "gotchas", "action", @@ -12964,8 +14058,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 258, - "line_end": 264, + "line_start": 371, + "line_end": 377, "keywords": [ "see", "proxy-wasm", @@ -12986,58 +14080,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-http-call-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 265, - "line_end": 408, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "http_call", - "src", - "lib.rs", - "rust", - "use", - "proxy_wasm", - "traits", - "types", - "std", - "time", - "duration", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "httpheadersroot", - "struct", - "impl", - "context", - "create_http_context", - "self", - "_context_id", - "u32", - "option", - "httpcontext" - ] } ] }, { "id": "cdn-examples-kv-store-as", "title": "KV Store — AssemblyScript CDN Example", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13061,9 +14110,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13073,7 +14122,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "5818392d7f01341518fb38b62a6267eece48af77d2334793bc92cda935fb596a", + "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -13090,9 +14139,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -13631,7 +14680,7 @@ "level": 2, "anchor": "gotchas", "line_start": 316, - "line_end": 326, + "line_end": 328, "keywords": [ "gotchas", "kvstore.get", @@ -13675,8 +14724,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 327, - "line_end": 332, + "line_start": 329, + "line_end": 334, "keywords": [ "see", "kvstore", @@ -13709,7 +14758,7 @@ { "id": "cdn-examples-kv-store-rust", "title": "KV Store — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13733,7 +14782,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -13744,7 +14793,7 @@ "example", "app_type" ], - "content_sha256": "3e66a0f9ea746ad8415d020dfa703809ee9c371e9bde63f98aae6872310a47ad", + "content_sha256": "b56c8120fcb573549cbddbd0fdf3a443307eed1688172532a2abc64f3e513366", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -13763,7 +14812,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -14060,7 +15109,7 @@ { "id": "cdn-examples-large-dictionary-as", "title": "Large Dictionary — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14082,9 +15131,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "large", "dictionary", "overview", @@ -14093,7 +15142,7 @@ "environment", "variables" ], - "content_sha256": "6d6681e4b50286f8686c5abb48674711db1f35f9090ae69be2f77d3fbce085f7", + "content_sha256": "a936577a2b3efa9cfcff9339d604f7a0272e309e4918a08568fdae87213a259a", "sections": [ { "id": "cdn-examples-large-dictionary-as#introduction", @@ -14110,9 +15159,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "large", "dictionary", "assemblyscript", @@ -14422,7 +15471,7 @@ { "id": "cdn-examples-large-dictionary-rust", "title": "cdn-examples-large-dictionary-rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14446,7 +15495,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "large-env-var", @@ -14456,7 +15505,7 @@ "app_type", "languages" ], - "content_sha256": "9db2e5f6717c00596362ad01e92312b37abe71da5c4d70fdb67e16b2019cd99b", + "content_sha256": "9f6288861599a4bb46e1693f7e6f6e2d598bd3a0228c090806a292f2bbec154b", "sections": [ { "id": "cdn-examples-large-dictionary-rust#introduction", @@ -14475,7 +15524,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "large-env-var", @@ -14542,7 +15591,7 @@ { "id": "cdn-examples-log-time-as", "title": "Log Time — CDN App (AssemblyScript)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14566,9 +15615,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -14578,7 +15627,7 @@ "logging", "time" ], - "content_sha256": "99b3d34628f43d8bd6051059e53f5bf0220892647873991ce0432f8bfe1e1b0b", + "content_sha256": "fe63b52ab054457ceacd13f3722b33ea5b1f0aceb1f6b55d2c1801be3cd946da", "sections": [ { "id": "cdn-examples-log-time-as#introduction", @@ -14595,9 +15644,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "---", "type", "example", @@ -14941,7 +15990,7 @@ { "id": "cdn-examples-log-time-rust", "title": "Log Time — CDN (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14965,7 +16014,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -14975,7 +16024,7 @@ "logging", "timing" ], - "content_sha256": "708026a43aeebb0a17df0a1ff2450b8b4971a1053cceb343e4a2b9ffed0a455c", + "content_sha256": "8dcf8bf7448143db11991ddddaeb01b7a161fe81224189a2b4631a49d9060f4d", "sections": [ { "id": "cdn-examples-log-time-rust#introduction", @@ -14994,7 +16043,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15332,7 +16381,7 @@ { "id": "cdn-examples-md2html-rust", "title": "CDN Example: Markdown to HTML (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15356,7 +16405,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15366,7 +16415,7 @@ "response-body-transform", "request-path-rewrite" ], - "content_sha256": "e22ac7622cc26345ca2016244b7a82727792c8c2294d27f9cfa4c87008fafd64", + "content_sha256": "796fc091efb4397c28b107d9b27eda72793c58776c4ecb1f23300362f6b68f86", "sections": [ { "id": "cdn-examples-md2html-rust#introduction", @@ -15385,7 +16434,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -15814,7 +16863,7 @@ { "id": "cdn-examples-properties-as", "title": "CDN Runtime Properties — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15836,9 +16885,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "runtime", "properties", "language", @@ -15848,7 +16897,7 @@ "type", "overview" ], - "content_sha256": "92e45ac0e88b8037fef119fc2a3a3010b2409de712b9b40a8a1db75a1b75b5a4", + "content_sha256": "487273a2f72dca3787758f1b60af9110a2c173879ba02842d23c21af59930534", "sections": [ { "id": "cdn-examples-properties-as#introduction", @@ -15865,9 +16914,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "cdn", "runtime", "properties", @@ -16051,7 +17100,7 @@ "level": 2, "anchor": "usage-patterns", "line_start": 86, - "line_end": 138, + "line_end": 147, "keywords": [ "usage", "patterns", @@ -16088,8 +17137,8 @@ "heading": "Complete Example Structure", "level": 2, "anchor": "complete-example-structure", - "line_start": 139, - "line_end": 236, + "line_start": 148, + "line_end": 314, "keywords": [ "complete", "example", @@ -16127,8 +17176,8 @@ "heading": "Error Handling Pattern", "level": 2, "anchor": "error-handling-pattern", - "line_start": 237, - "line_end": 268, + "line_start": 315, + "line_end": 346, "keywords": [ "error", "handling", @@ -16173,8 +17222,8 @@ "heading": "Expected Output", "level": 2, "anchor": "expected-output", - "line_start": 269, - "line_end": 288, + "line_start": 347, + "line_end": 366, "keywords": [ "expected", "output", @@ -16211,8 +17260,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 312, + "line_start": 367, + "line_end": 390, "keywords": [ "build", "pnpm", @@ -16254,8 +17303,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 313, - "line_end": 318, + "line_start": 391, + "line_end": 396, "keywords": [ "deploy", "upload", @@ -16277,8 +17326,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 319, - "line_end": 331, + "line_start": 397, + "line_end": 410, "keywords": [ "gotchas", "get_property", @@ -16327,8 +17376,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 332, - "line_end": 338, + "line_start": 411, + "line_end": 417, "keywords": [ "see", "cdn", @@ -16365,7 +17414,7 @@ { "id": "cdn-examples-properties-rust", "title": "Request Properties — CDN App Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -16389,7 +17438,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -16399,7 +17448,7 @@ "request-properties", "geo-data" ], - "content_sha256": "6670dc3c530b8dc5ace8e0d34eeb6e594881614288d0f95323fa06c13b909b1e", + "content_sha256": "82d09fea9ce746cfca7d504e9c7904e0154f9c4ee7f47da800421a3be9b5066d", "sections": [ { "id": "cdn-examples-properties-rust#introduction", @@ -16418,7 +17467,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -17091,7 +18140,7 @@ { "id": "dotenv", "title": "Dotenv — Runtime Secrets and Environment Variables", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/dotenv.md", "skill": "test", "category": "testing", @@ -17108,11 +18157,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17123,7 +18172,7 @@ "debugger", "inject" ], - "content_sha256": "cae196f2afaa09f7a84db329df3c77208afb1e4e206ad0e023cd22fef12f6491", + "content_sha256": "c7c39cf06d60e6c2cfb502b636db8a85f3ad05d79ed5acbcc9ded2e54390e567", "sections": [ { "id": "dotenv#introduction", @@ -17138,11 +18187,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "dotenv", "runtime", "secrets", @@ -17387,7 +18436,7 @@ { "id": "host-services-rust", "title": "Rust Host Services Reference", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -17408,7 +18457,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "host", "services", "reference", @@ -17419,7 +18468,7 @@ "storage", "secret" ], - "content_sha256": "702508848c5aadcd4ee98b9fa6c0ead4ed2085d84e0c321143f5bc626f376ece", + "content_sha256": "8f3541160c886153298e2cd2f0fd7c056bf959ecef2da28e70bd11b9788400c3", "sections": [ { "id": "host-services-rust#introduction", @@ -17438,7 +18487,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "rust", "host", "services", @@ -17708,7 +18757,7 @@ { "id": "http-examples-ab-testing-js", "title": "A/B Testing — FastEdge Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -17732,7 +18781,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example", @@ -17743,7 +18792,7 @@ "visitors", "test" ], - "content_sha256": "e840de6f715610ad3709f719b391b7dab00230196769178c24e05dd501f87a85", + "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -17762,7 +18811,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "testing", "fastedge", "example" @@ -18227,13 +19276,54 @@ "keys", "vary" ] + }, + { + "id": "http-examples-ab-testing-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 211, + "line_end": 328, + "keywords": [ + "source", + "material", + "file", + "examples", + "ab-testing", + "src", + "index.js", + "import", + "getenv", + "fastedge", + "env", + "const", + "testconfig", + "logo", + "variant", + "hops", + "weight", + "bottle", + "font", + "exo2", + "gloria", + "standard", + "async", + "function", + "eventhandler", + "request", + "xid", + "slicedheaders", + "sliceabtestidfromcookie", + "headers", + "createabtesthe" + ] } ] }, { "id": "http-examples-ab-testing-wasi-rust", "title": "Example: A/B Testing (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -18257,7 +19347,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -18267,7 +19357,7 @@ "ab-testing", "cookie-parsing" ], - "content_sha256": "c7e2fae1735c3993d6c974df6cda8e89c7a925e210a92a5e562755df2e6d4383", + "content_sha256": "b245afb81d6b40b72fe551491d375c288dc9d18b10c65ce15d85b513144abdcb", "sections": [ { "id": "http-examples-ab-testing-wasi-rust#introduction", @@ -18286,7 +19376,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -18761,7 +19851,7 @@ { "id": "http-examples-api-wrapper-basic-rust", "title": "examples-api-wrapper-basic-rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -18785,7 +19875,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "examples-api-wrapper-basic-rust", "type", "example", @@ -18795,7 +19885,7 @@ "language", "app" ], - "content_sha256": "43f36349cca38de74908848a0c7ea877fb64db4e6553287a974e3be2d68e7b06", + "content_sha256": "74f225e315847ffb3cd2a02a6b9ec467cff01874a624faa9a71908d68c9948f1", "sections": [ { "id": "http-examples-api-wrapper-basic-rust#introduction", @@ -18814,7 +19904,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "examples-api-wrapper-basic-rust", "type", "http", @@ -19344,7 +20434,7 @@ { "id": "http-examples-auth-js", "title": "Authentication Patterns (JavaScript)", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md", "skill": "fastedge-docs", "category": "examples", @@ -19368,7 +20458,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "authentication", "patterns", "fastedge", @@ -19379,7 +20469,7 @@ "shared", "secrets" ], - "content_sha256": "6fdf5624783e213986609200f74686bb8603e0b17f91c53a80cf693cc87250d6", + "content_sha256": "55de1130ba0160fafee05133cfcbf66aa1309fff2174117f714f31a5ddb01782", "sections": [ { "id": "http-examples-auth-js#introduction", @@ -19398,7 +20488,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "authentication", "patterns", "javascript", @@ -19695,7 +20785,7 @@ { "id": "http-examples-backend-basic-rust", "title": "Example: Backend (URL Proxy) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19719,7 +20809,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -19729,7 +20819,7 @@ "outbound-http", "query-params" ], - "content_sha256": "a2b700d31b619ab571bf016b276a2063db60d54b0a63308088097e1a1b5f4446", + "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -19748,7 +20838,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20170,207 +21260,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20394,7 +21290,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20404,7 +21300,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "19e3d3e017e89bd5e2f2d3cc57067a1b5b2d83502c762e0ada31536984710a19", + "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -20423,7 +21319,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20876,13 +21772,195 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 281, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "bloom_filter_denylist", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "bloom-filter", + "denylist", + "example.", + "checks", + "client", + "x-real-ip", + "request", + "header", + "falling", + "back", + "x-forwarded-for", + "against", + "bloom", + "filter", + "stored", + "fastedge", + "kv.", + "returns", + "403", + "hit", + "200", + "otherwise.", + "required", + "configuration", + "environment" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 282, + "line_end": 287, + "keywords": [ + "configuration", + "environment", + "variable", + "denylist_store", + "name", + "store", + "holds", + "bloom", + "filter.", + "bloom-filter", + "key", + "hardcoded", + "blocked-ips", + "change", + "bloom_key", + "src", + "lib.rs", + "your", + "different." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#behaviour", + "heading": "Behaviour", + "level": 2, + "anchor": "behaviour", + "line_start": 288, + "line_end": 294, + "keywords": [ + "behaviour", + "bf_exists", + "blocked-ips", + "response", + "---", + "true", + "403", + "allowed", + "false", + "...", + "200" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#tradeoff-false-positives", + "heading": "Tradeoff: false positives", + "level": 2, + "anchor": "tradeoff-false-positives", + "line_start": 295, + "line_end": 302, + "keywords": [ + "tradeoff", + "false", + "positives", + "bloom", + "filters", + "answer", + "definitely", + "set", + "maybe", + "bf_exists", + "returns", + "true", + "probably", + "added", + "small", + "fraction", + "hits", + "meaning", + "legitimate", + "ips", + "over-blocked.", + "acceptable", + "denylist", + "allowlists", + "anything", + "requiring", + "exact", + "membership", + "use", + "store.get", + "against", + "regular", + "key", + "instead." + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#populating-the-filter", + "heading": "Populating the filter", + "level": 2, + "anchor": "populating-the-filter", + "line_start": 303, + "line_end": 307, + "keywords": [ + "populating", + "filter", + "edge", + "handler", + "read-only.", + "populate", + "blocked-ips", + "out", + "band", + "example", + "via", + "fastedge", + "api" + ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 308, + "line_end": 312, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "bloom-filter-denylist" + ] } ] }, { "id": "http-examples-cache-basic-rust", "title": "Cache Basic (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20906,7 +21984,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -20916,7 +21994,7 @@ "cache", "basic" ], - "content_sha256": "fcc0c42a053e0104b03a420b039b53f6e58b97cb14c7f0f77c1dfb5fad31d840", + "content_sha256": "d9497b9de89acf7daf64068b5f9abe4e6118b9e5de4bd8b62f77282b88d34b3f", "sections": [ { "id": "http-examples-cache-basic-rust#introduction", @@ -20935,7 +22013,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -21334,7 +22412,7 @@ { "id": "http-examples-cache-js", "title": "FastEdge Cache — JavaScript Examples", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -21360,7 +22438,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "cache", "module", @@ -21370,7 +22448,7 @@ "2.3.0", "app" ], - "content_sha256": "137b45736825a3dd07f53e14f0a413bbef13b403e482856fabf51695c93a1433", + "content_sha256": "cf67b907345117e9955805dd7a38a16f9e8fed0fe9600d3df6a5b9713e2577a2", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -21389,7 +22467,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "cache", "javascript", @@ -21724,7 +22802,7 @@ { "id": "http-examples-cache-wasi-rust", "title": "Cache (WASI) — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21748,7 +22826,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -21758,7 +22836,7 @@ "cache", "outbound-http" ], - "content_sha256": "e0929deee35dbe662b0a26183b2655f95b7796526be190276c8e4ca0bce0eddf", + "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -21777,7 +22855,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22126,13 +23204,201 @@ "workflow", "skill" ] + }, + { + "id": "http-examples-cache-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 192, + "line_end": 359, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "response", + "caching", + "purging", + "via", + "interface.", + "reads", + "origin_host", + "environment", + "forwards", + "incoming", + "request", + "origin", + "caches", + "body", + "keyed", + "path.", + "subsequent", + "requests", + "same", + "path", + "cached", + "returned" + ] + }, + { + "id": "http-examples-cache-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 360, + "line_end": 366, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "origin_host", + "yes", + "base", + "url", + "upstream", + "origin", + "e.g.", + "https", + "api.example.com", + "returns", + "500", + "unset.", + "cache_ttl_ms", + "long", + "cache", + "responses", + "milliseconds.", + "default", + "60000" + ] + }, + { + "id": "http-examples-cache-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 367, + "line_end": 373, + "keywords": [ + "works", + "get", + "data", + "cache", + "miss", + "forward", + "origin_host", + "2xx", + "body", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "path", + "query", + "only", + "responses", + "origin", + "error", + "pass", + "without", + "stored.", + "response", + "headers", + "replayed", + "cache-hit", + "use", + "content-type", + "application", + "octet-stream", + "sinc" + ] + }, + { + "id": "http-examples-cache-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 374, + "line_end": 378, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "cache_wasi.wasm" + ] + }, + { + "id": "http-examples-cache-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 379, + "line_end": 385, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry", + "wstd", + "http", + "client", + "new", + ".send", + "req", + ".await", + "async", + "outbound", + "request", + "origin" + ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22156,7 +23422,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22166,7 +23432,7 @@ "languages", "diagnostic" ], - "content_sha256": "07465c714cb1fa963a3f6a8ff6759449d473393481ff4aac12103dbef85e899f", + "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -22185,7 +23451,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "diagnostic-logging", @@ -22686,13 +23952,230 @@ "store", "usage" ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 194, + "line_end": 307, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "diagnostic_logging", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "diagnostic", + "logging", + "example.", + "tiny", + "pass-through", + "proxy", + "writes", + "single", + "set_user_diag", + "tag", + "per", + "request", + "summarising", + "outcome", + "config_error", + "origin_unreachable", + "proxied", + "appears", + "fastedge", + "platform", + "per-request", + "log", + "viewer", + "distinct", + "stdout" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 308, + "line_end": 311, + "keywords": [ + "configuration", + "origin_url", + "environment", + "variable", + "origin", + "requests", + "proxied", + "to." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", + "heading": "Outcomes", + "level": 2, + "anchor": "outcomes", + "line_start": 312, + "line_end": 321, + "keywords": [ + "outcomes", + "request", + "writes", + "exactly", + "one", + "condition", + "tag", + "---", + "origin_url", + "missing", + "outcome", + "config_error", + "reason", + "origin_missing", + "origin", + "unreachable", + "origin_unreachable", + "method", + "path", + "err", + "proxied", + "status" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", + "heading": "`set_user_diag` vs `println!`", + "level": 2, + "anchor": "set_user_diag-vs-println", + "line_start": 322, + "line_end": 330, + "keywords": [ + "set_user_diag", + "println", + "---", + "channel", + "stdout", + "general", + "application", + "logs", + "per-request", + "structured", + "tag", + "platform", + "log", + "viewer", + "cardinality", + "many", + "per", + "request", + "one", + "multiple", + "calls", + "leave", + "only", + "last", + "concatenated", + "undefined", + "best", + "verbose", + "traces", + "debug", + "details", + "single", + "filterable", + "outcome", + "label", + "forbidden", + "secrets", + "pii", + "tags", + "appear" + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#convention", + "heading": "Convention", + "level": 2, + "anchor": "convention", + "line_start": 331, + "line_end": 336, + "keywords": [ + "convention", + "call", + "set_user_diag", + "once", + "branch", + "late", + "enough", + "handler", + "know", + "outcome.", + "logfmt", + "-ish", + "format", + "outcome", + "verb", + "key", + "value", + "easy", + "slice", + "log", + "search", + "tooling", + "keep", + "keys", + "short", + "stable", + "they", + "make", + "good", + "filter", + "terms." + ] + }, + { + "id": "http-examples-diagnostic-logging-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 337, + "line_end": 342, + "keywords": [ + "related", + "cdn", + "proxy-wasm", + "variant", + "fastedge", + "proxywasm", + "utils", + "set_user_diag", + "same", + "semantics", + "different", + "module", + "path.", + "see", + "docs", + "cdn_apps.md" + ] } ] }, { "id": "http-examples-fetch-js", "title": "http-examples-fetch-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22716,7 +24199,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fetch", "outbound", "requests", @@ -22727,7 +24210,7 @@ "standard", "url" ], - "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", + "content_sha256": "49ef6239614d42258573902b5711bde7807b8c31eea93cf6df351d25de5702f2", "sections": [ { "id": "http-examples-fetch-js#introduction", @@ -22746,7 +24229,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -22803,7 +24286,7 @@ { "id": "http-examples-geo-redirect-js", "title": "http-examples-geo-redirect-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22827,7 +24310,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "example", "geo-redirect", "redirects", @@ -22839,7 +24322,7 @@ "visitor", "country" ], - "content_sha256": "85dd6fb3eebc38fb91b2170277d959369cf5de6c28fb67cff6e68263487d08f8", + "content_sha256": "0bfad27c4682481b14340d4a70e53c0738d79b48c729219df0e5b2f92fec8c9c", "sections": [ { "id": "http-examples-geo-redirect-js#introduction", @@ -22858,7 +24341,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -22916,7 +24399,7 @@ { "id": "http-examples-geo-redirect-wasi-rust", "title": "Geo Redirect — WASI HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22940,7 +24423,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -22950,7 +24433,7 @@ "geo-redirect", "headers" ], - "content_sha256": "77ed57c49c605d2b3301511ccb2591fd39dc6726111761bc541c9229ec53ca23", + "content_sha256": "5c4595ed117a31ae93362494563368610e2f957a6ae0f6fcdd2e9d9ca9570e2a", "sections": [ { "id": "http-examples-geo-redirect-wasi-rust#introduction", @@ -22969,7 +24452,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -23375,13 +24858,174 @@ "examples-env-vars-wasi-rust", "access" ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 149, + "line_end": 232, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "geo_redirect", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "wasi-http", + "app", + "demonstrating", + "geo-based", + "redirects.", + "reads", + "country", + "code", + "geoip-country-code", + "request", + "header", + "redirects", + "country-specific", + "origin", + "url.", + "falls", + "back", + "base_origin", + "mapping", + "configured.", + "required", + "configuration", + "environment", + "variable" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 233, + "line_end": 239, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "base_origin", + "yes", + "fallback", + "redirect", + "url", + "e.g.", + "https", + "example.com", + "returns", + "500", + "unset.", + "country_code", + "per-country", + "keyed", + "2-letter", + "country", + "code", + "falls", + "back", + "set." + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 240, + "line_end": 248, + "keywords": [ + "works", + "geoip-country-code", + "env", + "var", + "set", + "302", + "value", + "base_origin", + "header", + "500" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 249, + "line_end": 255, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "geo_redirect_wasi.wasm" + ] + }, + { + "id": "http-examples-geo-redirect-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 256, + "line_end": 262, + "keywords": [ + "apis", + "used", + "request.headers", + ".get", + "geoip-country-code", + "read", + "geo", + "header", + "injected", + "fastedge", + "edge", + "std", + "env", + "var", + "country_code", + "dynamic", + "lookup", + "country", + "code", + "response", + "builder", + ".status", + "302", + ".header", + "location", + "url", + "redirect" + ] } ] }, { "id": "http-examples-headers-js", "title": "http-examples-headers-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md", "skill": "fastedge-docs", "category": "examples", @@ -23405,7 +25049,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "headers", "example", "fastedge", @@ -23417,7 +25061,7 @@ "environment", "variable" ], - "content_sha256": "4ee6a68dbcc81911e65fa23dd8ebf5df915e09e0fea2b3ae18d2134255f7da56", + "content_sha256": "632e383ad4e7a30633f7e55a82768bfe1de9f44f53b8918113de23c41a1748db", "sections": [ { "id": "http-examples-headers-js#introduction", @@ -23436,7 +25080,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -23488,7 +25132,7 @@ { "id": "http-examples-headers-wasi-rust", "title": "HTTP Headers Example — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -23512,7 +25156,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -23522,7 +25166,7 @@ "headers", "env-vars" ], - "content_sha256": "6eb14c6f398b0dc055ae8a69e88eba6e16ecc5c501ed44c7f031879dbb9c7689", + "content_sha256": "8e03eeb304c1cd911ea0f30ecb0d686d6e6af33163b0a7b665d41162785fc059", "sections": [ { "id": "http-examples-headers-wasi-rust#introduction", @@ -23541,7 +25185,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24137,7 +25781,7 @@ { "id": "http-examples-hello-world-basic-rust", "title": "Hello World — Basic HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24161,7 +25805,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24171,7 +25815,7 @@ "hello-world", "basic-handler" ], - "content_sha256": "25a81f9b61367a69785d9d14cda8d646224cafc25bb549db813138aecac1db3f", + "content_sha256": "8a3f946fbee1451f81cbcef5859a2efd998d661f3f6af16fca2ec48839ea7fee", "sections": [ { "id": "http-examples-hello-world-basic-rust#introduction", @@ -24190,7 +25834,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24569,7 +26213,7 @@ { "id": "http-examples-hello-world-wasi-rust", "title": "Example: Hello World (WASI) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24593,7 +26237,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -24603,7 +26247,7 @@ "hello-world", "wasi" ], - "content_sha256": "1777edec022e50301971e706c675717cea56db7b51e3e487b9379ceb00d95d08", + "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -24622,7 +26266,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -25015,13 +26659,134 @@ "base", "rust" ] + }, + { + "id": "http-examples-hello-world-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 186, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "hello_world", + "src", + "lib.rs", + "rust", + "use", + "wstd", + "body", + "request", + "response", + "http_server", + "async", + "main", + "anyhow", + "result", + "let", + "url", + "request.uri", + ".to_string", + "builder", + ".status", + "200", + ".header", + "content-type", + "text", + "plain", + "charset", + "utf-8", + ".body", + "format", + "hello", + "you", + "made" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 187, + "line_end": 195, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "plain", + "charset", + "utf-8", + "hello", + "you", + "made", + "wasi", + "request", + "host", + "path", + "query" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 196, + "line_end": 202, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "hello_world.wasm" + ] + }, + { + "id": "http-examples-hello-world-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 203, + "line_end": 210, + "keywords": [ + "apis", + "used", + "wstd", + "http_server", + "wasi", + "http", + "entry-point", + "macro", + "request", + "response", + "types", + "body", + "construction", + "request.uri", + ".to_string", + "full", + "absolute", + "uri" + ] } ] }, { "id": "http-examples-hono-js", "title": "Hono Patterns on FastEdge (JavaScript/TypeScript)", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md", "skill": "fastedge-docs", "category": "examples", @@ -25047,7 +26812,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "hono", "patterns", "fastedge", @@ -25056,7 +26821,7 @@ "apps.", "small" ], - "content_sha256": "2c31a7ba6edf3f3f764264b6e70b804caeaaf568b6d25d26f51874af328f56bd", + "content_sha256": "7e3e27ca00488b97dfe98e96516cb4dd19a222664008cfa50d17898b59b5cf30", "sections": [ { "id": "http-examples-hono-js#introduction", @@ -25075,7 +26840,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "hono", "patterns", "fastedge", @@ -25302,7 +27067,7 @@ { "id": "http-examples-kv-store-js", "title": "http-examples-kv-store-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md", "skill": "fastedge-docs", "category": "examples", @@ -25326,7 +27091,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "store", "example", "reference", @@ -25338,7 +27103,7 @@ "fastedge", "edge" ], - "content_sha256": "1ffcb44a8617f30c59fe878b41487d54f63062a0862ae4578a2ad41f1016b76d", + "content_sha256": "d3c4dbab0c624edfd7e3590c6413091cc12881c8a06e43b9a74fe7008ce7e515", "sections": [ { "id": "http-examples-kv-store-js#introduction", @@ -25357,7 +27122,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23" + "2026-08-17" ] }, { @@ -25366,7 +27131,7 @@ "level": 2, "anchor": "kv-store-—-example-reference", "line_start": 10, - "line_end": 302, + "line_end": 332, "keywords": [ "store", "example", @@ -25413,7 +27178,7 @@ { "id": "http-examples-kv-store-wasi-rust", "title": "KV Store — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25437,7 +27202,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25447,7 +27212,7 @@ "languages", "store" ], - "content_sha256": "b6fb89540d2ef2384d671a94a18bf815537190adacbc1f2e03b861eae1e5ff83", + "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -25466,7 +27231,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "capabilities", "kv-store", @@ -25893,13 +27658,229 @@ "javascript", "equivalent" ] + }, + { + "id": "http-examples-kv-store-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 382, + "line_end": 605, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "key_value", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "store", + "operations", + "via", + "wasi-http", + "interface.", + "supports", + "query", + "parameters", + "name", + "action", + "get", + "key", + "scan", + "match", + "pattern", + "zrange", + "min", + "f64", + "max", + "zscan", + "bfexists" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 606, + "line_end": 619, + "keywords": [ + "usage", + "operations", + "require", + "store", + "name", + "action", + "query", + "parameters", + "additional", + "params", + "description", + "---", + "get", + "key", + "fetch", + "single", + "value", + "scan", + "match", + "pattern", + "list", + "keys", + "matching", + "glob", + "zrange", + "min", + "f64", + "max", + "sorted-set", + "members", + "score", + "range", + "zscan", + "bfexists", + "item", + "check", + "bloom", + "filt" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 620, + "line_end": 629, + "keywords": [ + "example", + "get", + "store", + "my-store", + "action", + "key", + "hello", + "200", + "response", + "world", + "scan", + "match", + "user" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#error-responses", + "heading": "Error responses", + "level": 2, + "anchor": "error-responses", + "line_start": 630, + "line_end": 638, + "keywords": [ + "error", + "responses", + "condition", + "status", + "body", + "---", + "store", + "found", + "access", + "denied", + "403", + "missing", + "required", + "params", + "530", + "runtime", + "open", + "500", + "...", + "invalid", + "action", + "400", + "supported" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 639, + "line_end": 645, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "key_value_wasi.wasm" + ] + }, + { + "id": "http-examples-kv-store-wasi-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 646, + "line_end": 655, + "keywords": [ + "apis", + "used", + "fastedge", + "key_value", + "store", + "open", + "name", + "named", + "store.get", + "key", + "fetch", + "value", + "returns", + "option", + "vec", + "store.scan", + "pattern", + "list", + "keys", + "glob", + "store.zrange_by_score", + "min", + "max", + "range", + "query", + "sorted", + "set", + "store.zscan", + "scan", + "store.bf_exists", + "item", + "bloom", + "filter", + "membership", + "test" + ] } ] }, { "id": "http-examples-large-env-variable-wasi-rust", "title": "Large Environment Variable (WASI) — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25923,7 +27904,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "large", "environment", "variable", @@ -25933,7 +27914,7 @@ "demonstrates", "read" ], - "content_sha256": "eb44bc89da5ff979959ba8aeaf750453f77370bdddff67dcebec7af5cf00a3a0", + "content_sha256": "37b6892618f7fddf856bd9cf38f8d86dab1740c449c61cb78869f309c9c94d9e", "sections": [ { "id": "http-examples-large-env-variable-wasi-rust#introduction", @@ -25952,7 +27933,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "large", "environment", "variable", @@ -26288,7 +28269,7 @@ { "id": "http-examples-markdown-render-basic-rust", "title": "HTTP Example: Markdown Render (Rust, Basic)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26312,7 +28293,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26322,7 +28303,7 @@ "outbound-fetch", "env-vars" ], - "content_sha256": "f72f4be89063520134023c34d38ba6d901f66977bbeab889a4f8640e36fa288b", + "content_sha256": "2c31f5e301b8fd9c5eede29a285d26452eeb1606d1d009ebb4bbe5d5bc1fafc9", "sections": [ { "id": "http-examples-markdown-render-basic-rust#introduction", @@ -26341,7 +28322,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26472,6 +28453,8 @@ "var", "strip", "trailing", + "via", + "base.trim_end_matches", "validate", "request", "path", @@ -26483,27 +28466,25 @@ "fastedge", "call", "send_request", - "via", "internal", "helper.", "follow", "redirects", "max_redirects", - "hops.", + "hops", + "request_inner", + "req", + "depth", "decode", "response", "body", "utf-8", + "string", + "from_utf8", + "rsp.body", + ".to_vec", "failure", - "500", - "parse", - "markdown", - "parser", - "new_ext", - "options", - "enable_tables", - "enable_footnotes", - "render" + "500" ] }, { @@ -26728,7 +28709,7 @@ "level": 2, "anchor": "gotchas", "line_start": 189, - "line_end": 198, + "line_end": 199, "keywords": [ "gotchas", "base.trim_end_matches", @@ -26777,8 +28758,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 199, - "line_end": 206, + "line_start": 200, + "line_end": 207, "keywords": [ "see", "fastedge-sdk-rust", @@ -26814,7 +28795,7 @@ { "id": "http-examples-outbound-fetch-basic-rust", "title": "Example: Outbound Fetch — Basic HTTP (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26838,7 +28819,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -26848,7 +28829,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "700a80efb4a443683f2d328b4b720fcbb8bdcfacaa14c271fd4afa30db17ae2d", + "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -26867,7 +28848,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27235,13 +29216,178 @@ "target", "context" ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 190, + "line_end": 262, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "outbound_fetch", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "fastedge", + "body", + "request", + "response", + "statuscode", + "serde_json", + "json", + "value", + "main", + "_req", + "let", + "upstream_req", + "builder", + ".uri", + "jsonplaceholder.typicode.com", + "users", + ".body", + "empty", + "upstream_resp", + "send_request", + ".map_err" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 263, + "line_end": 282, + "keywords": [ + "incoming", + "request", + "makes", + "get", + "http", + "jsonplaceholder.typicode.com", + "users", + "using", + "fastedge", + "send_request", + "parses", + "json", + "response", + "body.", + "takes", + "first", + "array.", + "returns", + "envelope", + "pagination", + "metadata.", + "example", + "body", + "name", + "leanne", + "graham", + "...", + "total", + "skip", + "limit" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 283, + "line_end": 292, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "send_request", + "outbound", + "request", + "upstream", + "response", + "statuscode", + "types", + "body", + "bodies", + "serde_json", + "json", + "parsing", + "serialisation" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 293, + "line_end": 299, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "outbound_fetch.wasm" + ] + }, + { + "id": "http-examples-outbound-fetch-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 300, + "line_end": 306, + "keywords": [ + "expected", + "behavior", + "request", + "response", + "status", + "content-type", + "body", + "---", + "get", + "200", + "application", + "json", + "object", + "users", + "array", + "total", + "skip", + "limit" + ] } ] }, { "id": "http-examples-outbound-fetch-wasi-rust", "title": "Outbound Fetch — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27265,7 +29411,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "outbound", "fetch", "wasi", @@ -27275,7 +29421,7 @@ "upstream", "origin" ], - "content_sha256": "14578831717edef6fee0239d64d098fe13deeebbe6cff6464186f3dfa9414369", + "content_sha256": "924c9269ecb0377ada1ee5ef156d290297e9b92ca1a918b15c0ed328c38b2e31", "sections": [ { "id": "http-examples-outbound-fetch-wasi-rust#introduction", @@ -27294,7 +29440,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "outbound", "fetch", "wasi", @@ -27691,7 +29837,7 @@ { "id": "http-examples-outbound-modify-response-wasi-rust", "title": "Example: Outbound Modify Response (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27715,7 +29861,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -27725,7 +29871,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "17e973e59daaef6de41a976cc9c3cbb1c037242bd84aee527c2d6a840100df18", + "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -27744,7 +29890,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -28060,13 +30206,85 @@ "serde_json", "crate" ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 277, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "outbound_modify_response", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "outbound", + "fetch", + "response", + "transformation.", + "fetches", + "json", + "upstream", + "origin", + "reads", + "parses", + "body", + "reshapes", + "new", + "object", + "first", + "users", + "pagination", + "metadata", + "returns", + "fresh", + "content-type", + "application", + "header.", + "stepping-stone", + "beyond" + ] + }, + { + "id": "http-examples-outbound-modify-response-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 278, + "line_end": 284, + "keywords": [ + "related", + "outbound_fetch", + "simpler", + "variant", + "passes", + "upstream", + "response", + "unchanged.", + "mirror", + "fastedge-sdk-js", + "examples", + "outbound-modify-response" + ] } ] }, { "id": "http-examples-print-basic-rust", "title": "Example: Print (HTTP Request Echo) — Rust", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28090,7 +30308,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -28100,7 +30318,7 @@ "request-inspection", "header-iteration" ], - "content_sha256": "2301b014adb12b2ba4442aef724edf0a892cda4df9240c2959f515a28095883b", + "content_sha256": "b2851a2b3a8f9ff251ca478362e8120186a3863d09a4570781c3c28bd518e650", "sections": [ { "id": "http-examples-print-basic-rust#introduction", @@ -28119,7 +30337,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -28718,7 +30936,7 @@ { "id": "http-examples-proxy-js", "title": "Proxy and Response Transform Patterns (JavaScript/TypeScript)", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -28744,7 +30962,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "proxy", "response", "transform", @@ -28753,7 +30971,7 @@ "fastedge", "apps" ], - "content_sha256": "1199a5c5c761e3723a541167ff50b4e4009071ddc549726456bb8e300d9b9c94", + "content_sha256": "4640d7da83d11810dadbfff726f51712a64847b66562b908cd5b1d0f1beec95a", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -28772,7 +30990,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "proxy", "response", "transform", @@ -29212,7 +31430,7 @@ { "id": "http-examples-s3upload-basic-rust", "title": "S3 Upload — HTTP Example (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29236,7 +31454,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29246,7 +31464,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "16c81799dc962bed122dbb754b1370cc70719e1fb425adbb9b343a066ddea4fc", + "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -29265,7 +31483,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -29630,7 +31848,7 @@ "level": 2, "anchor": "key-constraints-and-gotchas", "line_start": 166, - "line_end": 176, + "line_end": 177, "keywords": [ "key", "constraints", @@ -29679,8 +31897,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 177, - "line_end": 184, + "line_start": 178, + "line_end": 185, "keywords": [ "see", "fastedge-docs", @@ -29710,7 +31928,7 @@ { "id": "http-examples-secret-basic-rust", "title": "Secret Access — Rust HTTP Example", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29734,7 +31952,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "example", @@ -29744,7 +31962,7 @@ "fastedge", "platform" ], - "content_sha256": "3036554dd9530db71002d821ac1c653a53831fbcaf9598cd6ac93742ef7edba8", + "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -29763,7 +31981,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "access", "rust", @@ -30153,13 +32371,284 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 370, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "secret", + "src", + "lib.rs", + "rust", + "use", + "anyhow", + "error", + "result", + "std", + "time", + "systemtime", + "fastedge", + "body", + "request", + "response", + "statuscode", + "allow", + "dead_code", + "main", + "_req", + "let", + "value", + "match", + "get", + "err", + "accessdenied", + "return", + "builder" + ] + }, + { + "id": "http-examples-secret-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 371, + "line_end": 380, + "keywords": [ + "request", + "handler", + "calls", + "secret", + "get", + "retrieves", + "current", + "value", + "named", + "missing", + "returned", + "none", + "returns", + "404", + "get_effective_at", + "unix_ts", + "effective", + "unix", + "timestamp", + "demonstrating", + "rotation", + "versioning", + "api.", + "success", + "200", + "values", + "body", + "debug" + ] + }, + { + "id": "http-examples-secret-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 381, + "line_end": 391, + "keywords": [ + "apis", + "used", + "api", + "purpose", + "---", + "fastedge", + "http", + "sync", + "request-response", + "handler", + "macro", + "secret", + "get", + "key", + "retrieve", + "current", + "value", + "named", + "get_effective_at", + "timestamp", + "effective", + "specific", + "unix", + "error", + "variants", + "accessdenied", + "msg", + "decrypterror", + "request", + "response", + "statuscode", + "types", + "body", + "respo" + ] + }, + { + "id": "http-examples-secret-basic-rust#secret-error-variants", + "heading": "Secret error variants", + "level": 2, + "anchor": "secret-error-variants", + "line_start": 392, + "line_end": 401, + "keywords": [ + "secret", + "error", + "variants", + "variant", + "http", + "response", + "meaning", + "---", + "value", + "200", + "body", + "found", + "none", + "404", + "empty", + "name", + "valid", + "set", + "err", + "accessdenied", + "403", + "app", + "permitted", + "read", + "msg", + "denial", + "human-readable", + "message", + "decrypterror", + "500", + "exists", + "decrypted" + ] + }, + { + "id": "http-examples-secret-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 402, + "line_end": 408, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] + }, + { + "id": "http-examples-secret-basic-rust#expected-behavior", + "heading": "Expected behavior", + "level": 2, + "anchor": "expected-behavior", + "line_start": 409, + "line_end": 418, + "keywords": [ + "expected", + "behavior", + "scenario", + "secret", + "response", + "status", + "body", + "---", + "happy", + "path", + "my-value", + "200", + "get", + "nget_efective_at", + "set", + "absent", + "404", + "empty", + "access", + "denied", + "403", + "note", + "contains", + "typo", + "field", + "name", + "get_efective_at", + "instead", + "get_effective_at", + "known", + "cosmetic", + "issue", + "source." + ] + }, + { + "id": "http-examples-secret-basic-rust#local-testing", + "heading": "Local testing", + "level": 2, + "anchor": "local-testing", + "line_start": 419, + "line_end": 436, + "keywords": [ + "local", + "testing", + "inject", + "secret", + "via", + ".env", + "file", + "your", + "fixtures", + "directory", + "using", + "fastedge_var_secret_", + "name", + "prefix", + "fastedge_var_secret_secret", + "my-test-value", + "run", + "fixture", + "validator", + "node", + "tools", + "fixture-validator", + "index.mjs", + "fastedge-sdk-rust", + "examples", + "http", + "basic", + "--wasm", + "target", + "wasm32-wasip1", + "release", + "secret.wasm" + ] } ] }, { "id": "http-examples-secret-rollover-wasi-rust", "title": "Secret Rollover (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -30183,7 +32672,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "rollover", "wasi", @@ -30194,7 +32683,7 @@ "rotation", "using" ], - "content_sha256": "0ca24f700d200210c2587774b740319ee8b982f0044d22467b92fc8536a433c1", + "content_sha256": "b1e9ad960518a8d3660beb3cbce5070ea6eb16c44ced1266fcc6af599c5eb272", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -30213,7 +32702,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "secret", "rollover", "wasi", @@ -30616,7 +33105,7 @@ { "id": "http-examples-simple-fetch-wasi-rust", "title": "Example: Simple Fetch (WASI HTTP, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -30640,7 +33129,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -30650,7 +33139,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "59940cbcac813462e867ba44395edd99653c63a1b5dad9fd847815d05a4a3ce8", + "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -30669,7 +33158,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31034,13 +33523,193 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 151, + "line_end": 235, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "simple_fetch", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "wasi-http", + "interface", + "via", + "wstd", + "crate.", + "receives", + "incoming", + "request", + "makes", + "outbound", + "url", + "specified", + "x-fetch-url", + "header", + "defaults", + "https", + "httpbin.org", + "get", + "build", + "cargo-component", + "cargo", + "component" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 236, + "line_end": 241, + "keywords": [ + "works", + "app", + "receives", + "incoming", + "request", + "reads", + "target", + "url", + "x-fetch-url", + "header", + "makes", + "outbound", + "get", + "streams", + "response", + "back", + "caller.", + "absent", + "defaults", + "https", + "httpbin.org" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#request-headers", + "heading": "Request headers", + "level": 2, + "anchor": "request-headers", + "line_start": 242, + "line_end": 247, + "keywords": [ + "request", + "headers", + "header", + "required", + "description", + "--------", + "----------", + "-------------", + "x-fetch-url", + "url", + "fetch.", + "defaults", + "https", + "httpbin.org", + "get" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#example", + "heading": "Example", + "level": 2, + "anchor": "example", + "line_start": 248, + "line_end": 253, + "keywords": [ + "example", + "bash", + "curl", + "x-fetch-url", + "https", + "httpbin.org", + "uuid", + "your-app-domain" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 254, + "line_end": 260, + "keywords": [ + "build", + "bash", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip2", + "release", + "simple_fetch.wasm" + ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#key-differences-from-basic-http-examples", + "heading": "Key differences from basic HTTP examples", + "level": 2, + "anchor": "key-differences-from-basic-http-examples", + "line_start": 261, + "line_end": 270, + "keywords": [ + "key", + "differences", + "basic", + "http", + "examples", + "fastedge", + "crate", + "wasi", + "wstd", + "---", + "handler", + "main", + "req", + "sync", + "async", + "macro", + "http_server", + "outbound", + "send_request", + "client", + "new", + ".send", + ".await", + "build", + "target", + "wasm32-wasip1", + "wasm32-wasip2" + ] } ] }, { "id": "http-examples-smart-switch-basic-rust", "title": "Example: Smart Switch (Basic HTTP, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31064,7 +33733,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31074,7 +33743,7 @@ "outbound-http", "env-vars" ], - "content_sha256": "0fba2c9ea0ce15a73c795ca6e7351997b10aad648632c67f484f418df2976d8e", + "content_sha256": "3b6d38e23605762f4da281035dedd49a989d71608c1a70e4258efce6a57dc746", "sections": [ { "id": "http-examples-smart-switch-basic-rust#introduction", @@ -31093,7 +33762,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31507,7 +34176,7 @@ { "id": "http-examples-static-assets-wasi-rust", "title": "Static Assets — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31533,7 +34202,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -31543,7 +34212,7 @@ "static-assets", "routing" ], - "content_sha256": "c338e77e5b5ba53c5eb1f471a62a344a01822a2f0db4955e6065abf8e549d1ee", + "content_sha256": "3bd6b8863a1735b55457f71936fd446400c805558a41a5f1afc836c80da1d69d", "sections": [ { "id": "http-examples-static-assets-wasi-rust#introduction", @@ -31562,7 +34231,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32109,7 +34778,7 @@ { "id": "http-examples-streaming-wasi-rust", "title": "Streaming Response — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32133,7 +34802,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32143,7 +34812,7 @@ "streaming", "async" ], - "content_sha256": "d43182bd8af1e158b7f960a742038ab49a7da9f93a10b9cd8a9163b91e79cb1d", + "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -32162,7 +34831,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32491,13 +35160,151 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, { "id": "http-examples-variables-and-secrets-wasi-rust", "title": "Variables and Secrets — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32521,7 +35328,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -32531,7 +35338,7 @@ "env-vars", "secrets" ], - "content_sha256": "687a28813c1fa891094d63568c1270e6b9fd5047f591831380c3ed66cb338d4d", + "content_sha256": "ae6dbe416dacde5f3693ca7811601acccf63b3e59ee8dd549a1151f03782078f", "sections": [ { "id": "http-examples-variables-and-secrets-wasi-rust#introduction", @@ -32550,7 +35357,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "---", "type", "example", @@ -33007,7 +35814,7 @@ { "id": "http-examples-watermark-basic-rust", "title": "HTTP Example: Watermark (Basic, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33031,7 +35838,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "example", "watermark", "basic", @@ -33041,7 +35848,7 @@ "handler", "fastedge" ], - "content_sha256": "467cabb7e5df4f34c39dc1877cd3b4e3e4750f6870e8c2d5963718467907352e", + "content_sha256": "df0d2b23a77c3575ea0b55b0217dfb97f259ea286c4c78126390ddab63db305f", "sections": [ { "id": "http-examples-watermark-basic-rust#introduction", @@ -33060,7 +35867,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "http", "example", "watermark", @@ -33582,7 +36389,7 @@ { "id": "js-runtime", "title": "FastEdge JS Runtime — Constraints & Compatibility", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md", "skill": "fastedge-docs", "category": "platform", @@ -33600,7 +36407,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "runtime", "constraints", @@ -33612,7 +36419,7 @@ "particularly", "affecting" ], - "content_sha256": "19c3ea4ba051feb743659045188ccd07e01d5fd740aa6637084d539b6e7daf9a", + "content_sha256": "a06ba95b1d679971a8bbccad222c8e3892781a57fe0dd89318488e4f45142132", "sections": [ { "id": "js-runtime#introduction", @@ -33631,7 +36438,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "runtime", "constraints", @@ -36775,7 +39582,7 @@ { "id": "quickstart-as", "title": "Quickstart: AssemblyScript CDN Apps on FastEdge", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md", "skill": "fastedge-docs", "category": "reference", @@ -36796,9 +39603,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "cdn", "apps", @@ -36809,7 +39616,7 @@ "compile", "webassembly" ], - "content_sha256": "5fc2440dbcf9811f874674b3390cc4c3227b2c2d095903c73343095e2f218549", + "content_sha256": "41ad91b4703ac00f166601df66080f0e3bd82de89c6e9fd8abfe5f3cf36abc80", "sections": [ { "id": "quickstart-as#introduction", @@ -36826,9 +39633,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "quickstart", "assemblyscript", "cdn", @@ -37209,7 +40016,7 @@ { "id": "quickstart-js", "title": "FastEdge JavaScript Quickstart", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md", "skill": "fastedge-docs", "category": "reference", @@ -37232,7 +40039,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "quickstart", "prerequisites", @@ -37243,7 +40050,7 @@ "install", "bash" ], - "content_sha256": "9861e7848665e985d4bb0324ccf860a5aca5b5c130fe34ee4673e6a8333d7b86", + "content_sha256": "8a824dda2cf16aed9d754c8e1f6e56580ad28f51b38337244715900bcd08442b", "sections": [ { "id": "quickstart-js#introduction", @@ -37262,7 +40069,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "javascript", "quickstart" @@ -37550,7 +40357,7 @@ { "id": "quickstart-rust", "title": "FastEdge Rust SDK — Quickstart", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -37571,7 +40378,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "sdk", "quickstart", @@ -37581,7 +40388,7 @@ "wasm32-wasip1", "target" ], - "content_sha256": "6aef250aeba4dbebf61afe4663af53f016653defea1b5af58969680829476382", + "content_sha256": "d2adca4401b83156057de1dc4163c81eab082a24bf4063bc2f2b8ba4ef2cbfbd", "sections": [ { "id": "quickstart-rust#introduction", @@ -37600,7 +40407,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "rust", "sdk", @@ -37900,7 +40707,7 @@ { "id": "runner-internals", "title": "Runner Internals — Low-Level Runner API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/runner-internals.md", "skill": "test", "category": "testing", @@ -37914,11 +40721,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -37930,7 +40737,7 @@ "use", "you" ], - "content_sha256": "2281ea5aaf755391320fbb293d097ebbb9ca95c272b510df303c43541b5c4806", + "content_sha256": "3ece5464164e143468e6f52883fffde960a434dddb1352dc3f9b65f188f0a4f8", "sections": [ { "id": "runner-internals#introduction", @@ -37945,11 +40752,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "runner", "internals", "low-level", @@ -38295,7 +41102,7 @@ { "id": "sdk-reference-as", "title": "AssemblyScript Proxy-Wasm SDK Reference", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md", "skill": "fastedge-docs", "category": "sdk", @@ -38316,9 +41123,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "proxy-wasm", "reference", "gcoredev", @@ -38328,7 +41135,7 @@ "writing", "cdn" ], - "content_sha256": "41b8129100103bce01866f20179662cdbe02e850593df1728abec65ffa86b79a", + "content_sha256": "353effa70cde17d46a928071309fca138aa211b6c6768542059770adcf6cc021", "sections": [ { "id": "sdk-reference-as#introduction", @@ -38345,9 +41152,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-17", "assemblyscript", "proxy-wasm", "sdk", @@ -38383,7 +41190,7 @@ "level": 2, "anchor": "entry-point-pattern", "line_start": 20, - "line_end": 69, + "line_end": 71, "keywords": [ "entry", "point", @@ -38429,8 +41236,8 @@ "heading": "Build Configuration", "level": 2, "anchor": "build-configuration", - "line_start": 70, - "line_end": 111, + "line_start": 72, + "line_end": 113, "keywords": [ "build", "configuration", @@ -38471,8 +41278,8 @@ "heading": "Proxy-Wasm Lifecycle", "level": 2, "anchor": "proxy-wasm-lifecycle", - "line_start": 112, - "line_end": 317, + "line_start": 114, + "line_end": 319, "keywords": [ "proxy-wasm", "lifecycle", @@ -38518,8 +41325,8 @@ "heading": "Return Value Enums", "level": 2, "anchor": "return-value-enums", - "line_start": 318, - "line_end": 397, + "line_start": 320, + "line_end": 399, "keywords": [ "return", "value", @@ -38551,8 +41358,8 @@ "heading": "Request and Response Manipulation", "level": 2, "anchor": "request-and-response-manipulation", - "line_start": 398, - "line_end": 668, + "line_start": 400, + "line_end": 670, "keywords": [ "request", "response", @@ -38587,8 +41394,8 @@ "heading": "Outbound HTTP (httpCall)", "level": 2, "anchor": "outbound-http-httpcall", - "line_start": 669, - "line_end": 814, + "line_start": 671, + "line_end": 816, "keywords": [ "outbound", "http", @@ -38632,8 +41439,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 815, - "line_end": 837, + "line_start": 817, + "line_end": 839, "keywords": [ "logging", "import", @@ -38675,8 +41482,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 838, - "line_end": 859, + "line_start": 840, + "line_end": 861, "keywords": [ "registration", "import", @@ -38723,8 +41530,8 @@ "heading": "FastEdge Host APIs", "level": 2, "anchor": "fastedge-host-apis", - "line_start": 860, - "line_end": 1067, + "line_start": 862, + "line_end": 1079, "keywords": [ "fastedge", "host", @@ -38771,8 +41578,8 @@ "heading": "Deprecated Functions", "level": 2, "anchor": "deprecated-functions", - "line_start": 1068, - "line_end": 1081, + "line_start": 1080, + "line_end": 1093, "keywords": [ "deprecated", "functions", @@ -38804,8 +41611,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 1082, - "line_end": 1088, + "line_start": 1094, + "line_end": 1100, "keywords": [ "see", "fastedge", @@ -38842,7 +41649,7 @@ { "id": "sdk-reference-js", "title": "JavaScript SDK Reference (`@gcoredev/fastedge-sdk-js`)", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md", "skill": "fastedge-docs", "category": "sdk", @@ -38863,7 +41670,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "reference", "gcoredev", "source", @@ -38873,7 +41680,7 @@ "authoritative", "api" ], - "content_sha256": "9c43ebcd420d4cab2b3328e46d9cf49548b21743746ac692d641b476949deee3", + "content_sha256": "1875e18221368a4e9d95a8215be2efa52d35634b23916b74e51d0b51dbeef789", "sections": [ { "id": "sdk-reference-js#introduction", @@ -38892,7 +41699,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-17", "javascript", "sdk", "reference", @@ -39437,7 +42244,7 @@ { "id": "sdk-reference-rust", "title": "Rust SDK Reference (`fastedge` crate + `fastedge-derive`)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -39458,7 +42265,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "reference", "fastedge", "crate", @@ -39468,7 +42275,7 @@ "start", "cargo.toml" ], - "content_sha256": "db0a020e14f32b7e906a49270a31ac17c794d3bdcaf6f8dd73165aff8c068a9d", + "content_sha256": "1aae120746bea8e110025874bbf4432f20b571af4f8a713875fec01c7e9ad8de", "sections": [ { "id": "sdk-reference-rust#introduction", @@ -39487,7 +42294,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-17", "rust", "sdk", "reference", @@ -39844,7 +42651,7 @@ { "id": "server-api", "title": "Server API — REST and WebSocket Endpoints", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/server-api.md", "skill": "test", "category": "testing", @@ -39858,11 +42665,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -39874,7 +42681,7 @@ "interfaces", "loading" ], - "content_sha256": "d03fdff0b2a4179f0a7e45bbd2519f4edb1ac604a36369aba6b661c45a82d201", + "content_sha256": "96fce5e89649293442cc67e66685fffc2282f052c1d92ac935c72b815ae71822", "sections": [ { "id": "server-api#introduction", @@ -39889,11 +42696,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "server", "api", "rest", @@ -40096,10 +42903,1055 @@ } ] }, + { + "id": "templates-catalog", + "title": "FastEdge Bolt-On Templates", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/catalog.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check" + ], + "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "sections": [ + { + "id": "templates-catalog#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 13, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "fastedge", + "bolt-on", + "templates", + "writing", + "authentication", + "cookie-hardening", + "content-transform", + "logic", + "scratch", + "check", + "catalog.", + "entry", + "maintained", + "tested", + "application", + "handles", + "security-sensitive", + "parts", + "hand-rolled", + "implementation", + "get", + "right", + "independently.", + "use", + "adapt", + "templat" + ] + }, + { + "id": "templates-catalog#html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "heading": "html2md — HTML-to-Markdown conversion / content transformation / Accept header negotiation", + "level": 2, + "anchor": "html2md-—-html-to-markdown-conversion-content-transformation-accept-header-negotiation", + "line_start": 14, + "line_end": 31, + "keywords": [ + "html2md", + "html-to-markdown", + "conversion", + "content", + "transformation", + "accept", + "header", + "negotiation", + "solves", + "cdn", + "filter", + "transparently", + "converts", + "html", + "origin", + "responses", + "markdown", + "client", + "sends", + "text", + "handles", + "encoding", + "caching", + "concerns", + "hand-rolled", + "get", + "right", + "independently.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "110", + "changes" + ] + }, + { + "id": "templates-catalog#harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "heading": "harden-cookies — Cookie security hardening / Secure HttpOnly SameSite attributes / Set-Cookie rewriting", + "level": 2, + "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", + "line_start": 32, + "line_end": 47, + "keywords": [ + "harden-cookies", + "cookie", + "security", + "hardening", + "secure", + "httponly", + "samesite", + "attributes", + "set-cookie", + "rewriting", + "solves", + "cdn", + "filter", + "adds", + "strict", + "targeted", + "response", + "headers.", + "eliminates", + "modify", + "backend", + "code", + "enforce", + "policy", + "edge.", + "deploys", + "single", + "proxy-wasm", + "app.", + "deployed", + "via", + "gcore", + "portal", + "template", + "gallery", + "184", + "origin", + "changes", + "required.", + "runtime" + ] + }, + { + "id": "templates-catalog#edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "heading": "edge-sso — SSO / login / identity federation / Identity-Aware Proxy (Google, GitHub, Microsoft, Facebook, SAML)", + "level": 2, + "anchor": "edge-sso-—-sso-login-identity-federation-identity-aware-proxy-google-github-microsoft-facebook-saml", + "line_start": 48, + "line_end": 71, + "keywords": [ + "edge-sso", + "sso", + "login", + "identity", + "federation", + "identity-aware", + "proxy", + "google", + "github", + "microsoft", + "facebook", + "saml", + "solves", + "bolt-on", + "adds", + "multi-provider", + "2.0", + "existing", + "site", + "without", + "changing", + "backend.", + "handles", + "oauth", + "oidc", + "flows", + "session", + "token", + "issuance", + "per-request", + "enforcement.", + "building", + "scratch", + "requires", + "correctly", + "implementing", + "multiple", + "signing", + "edge", + "enforcem" + ] + }, + { + "id": "templates-catalog#edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "heading": "edge-totp — TOTP MFA / two-factor authentication / OTP challenge / RFC 6238", + "level": 2, + "anchor": "edge-totp-—-totp-mfa-two-factor-authentication-otp-challenge-rfc-6238", + "line_start": 72, + "line_end": 95, + "keywords": [ + "edge-totp", + "totp", + "mfa", + "two-factor", + "authentication", + "otp", + "challenge", + "rfc", + "6238", + "solves", + "adds", + "step", + "front", + "existing", + "site", + "login", + "without", + "modifying", + "backend.", + "customer", + "origin", + "handles", + "password", + "validation", + "app", + "hosts", + "6-digit", + "verifies", + "code", + "replay", + "brute-force", + "protection", + "issues", + "signed", + "session", + "assertion", + "trusts.", + "building", + "scratch", + "requires" + ] + } + ] + }, + { + "id": "templates-edge-sso-integration", + "title": "edge-sso — Origin Integration Reference", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/edge-sso-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app" + ], + "content_sha256": "7301aea1c160744e3ee14e1d9dbe28aa1fb33e22fd28c5a4bc99be69b5bbc361", + "sections": [ + { + "id": "templates-edge-sso-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-sso", + "origin", + "integration", + "reference", + "origins", + "integrating", + "already-deployed", + "template", + "pair", + "auth-app", + "cdn-filter", + "covers", + "contract", + "relies", + "identity", + "delivery", + "auth", + "routes", + "login", + "page", + "customization", + "redirect", + "validation.", + "---" + ] + }, + { + "id": "templates-edge-sso-integration#1-what-sso_variant-means-for-the-origin", + "heading": "1. What `SSO_VARIANT` Means for the Origin", + "level": 2, + "anchor": "1-what-sso_variant-means-for-the-origin", + "line_start": 16, + "line_end": 57, + "keywords": [ + "sso_variant", + "means", + "origin", + "set", + "identically", + "auth-app", + "cdn-filter.", + "controls", + "edge", + "delivers", + "identity", + "responsible", + "verifying.", + "gate-only", + "receives", + "nothing", + "filter", + "enforces", + "allow", + "deny", + "only", + "authenticated", + "requests", + "reach", + "origin.", + "responsibility", + "none.", + "receive", + "token", + "headers", + "sees", + "pas" + ] + }, + { + "id": "templates-edge-sso-integration#2-auth-app-routes", + "heading": "2. Auth-App Routes", + "level": 2, + "anchor": "2-auth-app-routes", + "line_start": 58, + "line_end": 80, + "keywords": [ + "auth-app", + "routes", + "served", + "under", + "auth_prefix", + "default", + "auth", + "single-domain", + "routing", + "cdn", + "origin", + "customer", + "own", + "domain.", + "cdn-filter", + "bypasses", + "gate", + "routes.", + "route", + "method", + "caller", + "purpose", + "---", + "get", + "browser", + "hosted", + "login", + "page", + "server-rendered", + "branded", + "provider", + "buttons.", + "honours", + "redirect", + "providers" + ] + }, + { + "id": "templates-edge-sso-integration#3-login-page-customization-tiers", + "heading": "3. Login Page Customization Tiers", + "level": 2, + "anchor": "3-login-page-customization-tiers", + "line_start": 81, + "line_end": 134, + "keywords": [ + "login", + "page", + "customization", + "tiers", + "tier", + "env", + "var", + "branding", + "recommended", + "default", + "built-in", + "hosted", + "reads", + "vars", + "per-request.", + "code", + "changes", + "required.", + "effect", + "---", + "login_page_title", + "sign", + "title", + "login_page_subtitle", + "choose", + "sign-in", + "method", + "subheading", + "login_page_logo_url", + "logo", + "image", + "login_page_favicon_url", + "tab", + "favicon", + "login_page_accent_" + ] + }, + { + "id": "templates-edge-sso-integration#4-json-contracts", + "heading": "4. JSON Contracts", + "level": 2, + "anchor": "4-json-contracts", + "line_start": 135, + "line_end": 173, + "keywords": [ + "json", + "contracts", + "get", + "auth", + "providers", + "jsonc", + "redirect", + "cart", + "google", + "label", + "loginurl", + "login", + "2fcart", + "github", + "saml", + "sso", + "stable", + "identifier", + "matches", + "value", + "used", + "sso_providers", + "human" + ] + }, + { + "id": "templates-edge-sso-integration#5-redirect-validation", + "heading": "5. Redirect Validation", + "level": 2, + "anchor": "5-redirect-validation", + "line_start": 174, + "line_end": 187, + "keywords": [ + "redirect", + "validation", + "parameter", + "validated", + "against", + "sso_allowed_origins", + "route", + "honours", + "read", + "saml_relay", + "cookie.", + "rules", + "relative", + "urls", + "starting", + "always", + "permitted.", + "off-origin", + "absolute", + "silently", + "dropped", + "post-login", + "falls", + "back", + "protocol-relative", + "backslash", + "bypasses", + "e.g.", + "evil.com", + "rejected.", + "permit", + "redirects", + "specific", + "origins", + "set" + ] + }, + { + "id": "templates-edge-sso-integration#6-shared-config-the-origin-needs-to-know-about", + "heading": "6. Shared Config the Origin Needs to Know About", + "level": 2, + "anchor": "6-shared-config-the-origin-needs-to-know-about", + "line_start": 188, + "line_end": 207, + "keywords": [ + "shared", + "config", + "origin", + "needs", + "know", + "about", + "env", + "vars", + "affect", + "contract", + "operates", + "under.", + "they", + "set", + "fastedge", + "apps", + "auth-app", + "cdn-filter", + "integration", + "depends", + "their", + "values.", + "var", + "concern", + "---", + "sso_variant", + "match", + "determines", + "receives", + "nothing", + "gate-only", + "jwt", + "cookie", + "verify", + "identity", + "headers", + "header", + "sso_audience" + ] + }, + { + "id": "templates-edge-sso-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 208, + "line_end": 215, + "keywords": [ + "see", + "edge-sso", + "template", + "readme", + "deployment", + "configuration", + "auth-app", + "cdn-filter", + ".env.example", + "authoritative", + "exhaustive", + "env", + "var", + "lists", + "inline", + "guidance", + "architecture", + "auth-modes", + "sso_variant", + "axis", + "detail", + "security", + "token", + "trust", + "model", + "known", + "limitations", + "including", + "revocation", + "idp", + "single", + "logout", + "overview", + "two-app", + "signing", + "strategy", + "canonical_host" + ] + } + ] + }, + { + "id": "templates-edge-totp-integration", + "title": "edge-totp — Origin Integration", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/edge-totp-integration.md", + "skill": "fastedge-docs", + "category": "reference", + "app_types": [], + "languages": [], + "tags": [ + "reference", + "fastedge-docs", + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password" + ], + "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "sections": [ + { + "id": "templates-edge-totp-integration#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-templates", + "ref", + "main", + "commit", + "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", + "updated", + "2026-08-17", + "edge-totp", + "origin", + "integration", + "reference", + "wiring", + "totp", + "second-factor", + "step", + "existing", + "password", + "login.", + "template", + "otp-app", + "otp-filter", + "already", + "deployed", + "gcore", + "portal", + "document", + "covers", + "three", + "origin-side", + "code", + "changes", + "required", + "connect", + "it.", + "---" + ] + }, + { + "id": "templates-edge-totp-integration#the-three-origin-side-changes", + "heading": "The Three Origin-Side Changes", + "level": 2, + "anchor": "the-three-origin-side-changes", + "line_start": 16, + "line_end": 58, + "keywords": [ + "three", + "origin-side", + "changes", + "only", + "origin", + "auth", + "module", + "changes.", + "rest", + "site", + "untouched.", + "split", + "login", + "password-then-otp", + "password", + "check", + "succeeds", + "instead", + "immediately", + "minting", + "full", + "session", + "sign", + "handoff", + "ticket", + "hmac", + "handoff_key", + "over", + "sub", + "userid", + "next", + "exp", + "now", + "ticket_ttl", + "set", + "short-lived", + "pre_mfa", + "cookie", + "marker", + "remembers" + ] + }, + { + "id": "templates-edge-totp-integration#profile-a-vs-profile-b-—-decision-guide", + "heading": "Profile A vs Profile B — Decision Guide", + "level": 2, + "anchor": "profile-a-vs-profile-b-—-decision-guide", + "line_start": 59, + "line_end": 77, + "keywords": [ + "profile", + "decision", + "guide", + "security-posture", + "decision.", + "wrong", + "choice", + "here", + "real", + "vulnerability", + "style", + "issue.", + "criterion", + "---", + "origin", + "crypto", + "required", + "none", + "handoff_key", + "only", + "es256", + "proof", + "verification", + "via", + "jwks", + "knows", + "user", + "passed", + "mfa", + "filter", + "verifies", + "valid", + "mfa_session", + "exists", + "yes", + "carries", + "sub", + "binds", + "pre_mfa", + "session" + ] + }, + { + "id": "templates-edge-totp-integration#shared-configuration", + "heading": "Shared Configuration", + "level": 2, + "anchor": "shared-configuration", + "line_start": 78, + "line_end": 93, + "keywords": [ + "shared", + "configuration", + "keys", + "endpoints", + "origin", + "edge.", + "components", + "agree", + "values.", + "key", + "endpoint", + "edge", + "---", + "handoff_key", + "hs256", + "signs", + "handoff", + "ticket", + "password", + "success", + "verifies", + "auth_prefix", + "challenge", + "verify", + "jwks", + "profile", + "only", + "fetches", + ".well-known", + "jwks.json", + "via", + "createremotejwkset", + "public", + "cannot", + "forge", + "proofs", + "one" + ] + }, + { + "id": "templates-edge-totp-integration#required-deployment-precondition", + "heading": "Required Deployment Precondition", + "level": 2, + "anchor": "required-deployment-precondition", + "line_start": 94, + "line_end": 103, + "keywords": [ + "required", + "deployment", + "precondition", + "lock", + "origin", + "edge-only", + "traffic.", + "hard", + "requirement", + "profiles", + "suggestion.", + "edge", + "gate", + "profile", + "meaningless", + "directly", + "reachable.", + "hostname", + "accessible", + "without", + "going", + "gcore", + "cdn", + "attacker", + "simply", + "skips", + "entirely", + "mfa_session", + "rust", + "filter", + "signed", + "proof", + "never", + "run.", + "restrict", + "ingress", + "using", + "allowlist", + "authenticat" + ] + }, + { + "id": "templates-edge-totp-integration#recovery-and-self-service-enrollment-caveat", + "heading": "Recovery and Self-Service Enrollment Caveat", + "level": 2, + "anchor": "recovery-and-self-service-enrollment-caveat", + "line_start": 104, + "line_end": 113, + "keywords": [ + "recovery", + "self-service", + "enrollment", + "caveat", + "auth_prefix", + "activate", + "inherits", + "trust", + "level", + "password", + "step", + "user", + "pass", + "check", + "self-enroll", + "new", + "authenticator.", + "correct", + "default", + "deployments", + "sensitive", + "accounts", + "means", + "compromised", + "compromises", + "totp", + "second", + "factor.", + "decision", + "point", + "your", + "threat", + "model", + "requires", + "factor", + "remain", + "independent" + ] + }, + { + "id": "templates-edge-totp-integration#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 114, + "line_end": 121, + "keywords": [ + "see", + "edge-totp", + "storage", + "secrets", + "reference", + "storage-and-secrets.md", + "full", + "env", + "var", + "secret", + "list", + "otp-app", + "otp-filter", + "threat", + "model", + "threat-model.md", + "trust", + "boundaries", + "residual", + "risks", + "risk", + "register", + "including", + "token", + "scope", + "self-enrollment", + "level", + "architecture", + "flow", + "flow.md", + "seed", + "fetched", + "verify", + "time", + "pop", + "replication", + "behavior", + "fastedge", + "store", + "sdk" + ] + } + ] + }, { "id": "test-config", "title": "test-config Reference", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/test-config.md", "skill": "test", "category": "testing", @@ -40113,11 +43965,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40129,7 +43981,7 @@ "request", "cdn" ], - "content_sha256": "009f7caa6b308ade8af63bdd256f2e553729893224f02cf420a8b5275e280424", + "content_sha256": "cf3e921e482468b97114a9494e3123f97e275060da3afffe2206862b7ffe0469", "sections": [ { "id": "test-config#introduction", @@ -40144,11 +43996,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "test-config", "reference", "fastedge-config.test.json", @@ -40425,7 +44277,7 @@ { "id": "test-framework", "title": "FastEdge Test Framework API", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/test-framework.md", "skill": "test", "category": "testing", @@ -40442,11 +44294,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "framework", "api", @@ -40457,7 +44309,7 @@ "runner", "apps." ], - "content_sha256": "538ad571eed95fce3e11226e6ac4355e725c8370b05c21a4bf3854ba991b527b", + "content_sha256": "e125a779b09525ed8048d05e09b9979c2ceea4c1c90bbc4a988c1ce98a9fcda1", "sections": [ { "id": "test-framework#introduction", @@ -40472,11 +44324,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "test", "framework", @@ -40841,7 +44693,7 @@ { "id": "vscode-debugger", "title": "FastEdge Visual Debugger", - "description": "auto-updated: true sources: - id: fastedge-test ref: v0.2.5 commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", "path": "plugins/gcore-fastedge/skills/test/reference/vscode-debugger.md", "skill": "test", "category": "testing", @@ -40855,11 +44707,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", @@ -40871,7 +44723,7 @@ "gcoredev", "same" ], - "content_sha256": "dca66acac8714a6340718db344e69a3189c65884f062ac5b3a725420213a3e85", + "content_sha256": "627d9b7700750420febf279bfb44b3cea3ea212faa41800f660fa1f74a5965e5", "sections": [ { "id": "vscode-debugger#introduction", @@ -40886,11 +44738,11 @@ "sources", "fastedge-test", "ref", - "v0.2.5", + "main", "commit", "61497eca6ead033ac810165bc1e20e1d6dd4678f", "updated", - "2026-07-23", + "2026-08-17", "fastedge", "visual", "debugger", From 462c6f02c9555c771a52286dc46fd88478884bd1 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson <godronus@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:40:52 +0100 Subject: [PATCH 2/2] fix(ci): use GitHub App token for release PR creation GH_TOKEN default is blocked by org policy from creating PRs. Switch to the App token already used for the branch push and MCP dispatch. --- .github/workflows/release-plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-plugin.yaml b/.github/workflows/release-plugin.yaml index efd8b13..f4afebd 100644 --- a/.github/workflows/release-plugin.yaml +++ b/.github/workflows/release-plugin.yaml @@ -208,7 +208,7 @@ jobs: if: steps.detect.outputs.changed == 'true' || inputs.force == true continue-on-error: true env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | VERSION="${{ steps.bump.outputs.version }}" BRANCH="release/v${VERSION}"