diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2c9c3f3..d52437f 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.6" + "version": "0.2.7" } ] } diff --git a/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json b/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json index 3e0afd3..fd0f10b 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.6", + "version": "0.2.7", "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 95cea94..023dde4 100644 --- a/plugins/gcore-fastedge-codex/docs-index.json +++ b/plugins/gcore-fastedge-codex/docs-index.json @@ -1,16 +1,16 @@ { "schema_version": "1.0.0", - "generated_at": "2026-08-18T07:33:55.044Z", + "generated_at": "2026-08-20T15:14:07.293Z", "source": { "repo": "fastedge-plugin", - "commit": "067d44e", + "commit": "deaf77e", "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-20 -->", "path": "plugins/gcore-fastedge-codex/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-20", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "24a86619b212c904d2bae26e563353bfed0d28ff4743c99c0eb0d1908ee0d1fb", + "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -60,7 +60,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "rust", "sdk", @@ -250,7 +250,9 @@ "---------------------------------------------------", "on_http_request_headers", "num_headers", + "usize", "end_of_stream", + "bool", "action", "request", "headers", @@ -258,8 +260,7 @@ "modify", "forwarding", "on_http_request_body", - "body_size", - "end_of_strea" + "body_size" ] }, { @@ -1000,7 +1001,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1025,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1034,7 +1035,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", + "content_sha256": "02a0716c80b4d8908f4996e26cf4a36904a49faf5c7a78d99d5abacf7512e387", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1054,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1650,63 +1651,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1730,7 +1681,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1740,7 +1691,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", + "content_sha256": "d47decc23a11899c2cdd3360355fb8c5e9db6fb48d6b2ca5c90bee4cc5c8ae2b", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1759,7 +1710,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -2114,183 +2065,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2314,7 +2095,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "api", "key", "validation", @@ -2324,7 +2105,7 @@ "requests", "against" ], - "content_sha256": "dcffdcc456556e41a80a866fcd1ccb02dcc074a7882e5357bed515977ec38748", + "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2343,7 +2124,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "api", "key", "validation", @@ -2588,6 +2369,56 @@ "traits", "types" ] + }, + { + "id": "cdn-examples-api-key-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 163, + "line_end": 279, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "api_key", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "api", + "key", + "validation.", + "validates", + "requests", + "using", + "x-api-key", + "header", + "checked", + "against", + "stored", + "secret.", + "simpler", + "alternative", + "jwt", + "token", + "expiry", + "claims", + "needed.", + "required", + "configuration", + "secret", + "use" + ] } ] }, @@ -3652,7 +3483,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3676,7 +3507,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -3686,7 +3517,7 @@ "app_type", "languages" ], - "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", + "content_sha256": "94226f2f7783dd5978e697ebea97008427e93dbddfd4b033a8c05d74268e9a34", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3705,7 +3536,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -4132,139 +3963,6 @@ "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." - ] } ] }, @@ -5943,7 +5641,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5967,7 +5665,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "example", "convert", "image", @@ -5977,7 +5675,7 @@ "avif", "format" ], - "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", + "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5996,7 +5694,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "cdn", "example", "convert", @@ -6461,13 +6159,129 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6491,7 +6305,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "response-header", "injection", @@ -6501,7 +6315,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", + "content_sha256": "dcfb3c1c7467c4e745a43d3b8821e1ef8725789583876c7d2ffafd9955cc8efa", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6520,7 +6334,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "assemblyscript", "cdn", @@ -6907,7 +6721,7 @@ "level": 2, "anchor": "source-material", "line_start": 197, - "line_end": 318, + "line_end": 316, "keywords": [ "source", "material", @@ -6945,8 +6759,8 @@ "heading": "What it does", "level": 2, "anchor": "what-it-does", - "line_start": 319, - "line_end": 324, + "line_start": 317, + "line_end": 322, "keywords": [ "onresponseheaders", "requests", @@ -6993,8 +6807,8 @@ "heading": "Configuration", "level": 2, "anchor": "configuration", - "line_start": 325, - "line_end": 333, + "line_start": 323, + "line_end": 331, "keywords": [ "configuration", "set", @@ -7033,8 +6847,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 334, - "line_end": 347, + "line_start": 332, + "line_end": 345, "keywords": [ "build", "pnpm", @@ -7063,8 +6877,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 348, - "line_end": 352, + "line_start": 346, + "line_end": 350, "keywords": [ "deploy", "upload", @@ -7643,7 +7457,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7667,7 +7481,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -7677,7 +7491,7 @@ "response-headers", "response-body" ], - "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", + "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7696,7 +7510,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8126,6 +7940,147 @@ "hook", "sequencing" ] + }, + { + "id": "cdn-examples-custom-error-pages-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 296, + "line_end": 501, + "keywords": [ + "source", + "material", + "file", + "examples", + "customerrorpages", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "buffertypevalues", + "stream_context", + "setloglevel", + "fastedge", + "class", + "errorpagesroot" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 502, + "line_end": 514, + "keywords": [ + "onresponseheaders", + "app", + "reads", + "response.status", + "property.", + "400-599", + "range", + "sets", + "content-type", + "text", + "html", + "prepares", + "body", + "replacement.", + "onresponsebody", + "buffers", + "full", + "response", + "replaces", + "styled", + "page", + "containing", + "numeric", + "status", + "code", + "human-readable", + "error", + "title", + "e.g.", + "found", + "bad", + "gateway", + "description", + "explaining", + "went", + "wrong", + "category", + "label", + "client", + "server" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 515, + "line_end": 528, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "customerrorpages.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "customerrorpages-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 529, + "line_end": 533, + "keywords": [ + "deploy", + "upload", + "build", + "customerrorpages.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, @@ -8243,7 +8198,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -8267,7 +8222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8277,7 +8232,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", + "content_sha256": "a73e35e2bec26a8caef88e750c20eec94b8a4e22bd45622922f8ab9f586d554a", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -8296,7 +8251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8678,59 +8633,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- 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-20 --> --- 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", @@ -8754,7 +8663,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8765,7 +8674,7 @@ "secrets", "environment" ], - "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", + "content_sha256": "0c73ed18f10ce8d8e06a88f54b4d3f23ac0b67968d5c25e974066c47408cfacb", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8784,7 +8693,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9195,221 +9104,6 @@ "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." - ] } ] }, @@ -9872,7 +9566,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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md", "skill": "fastedge-docs", "category": "examples", @@ -9894,9 +9588,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-20", "---", "type", "example", @@ -9906,7 +9600,7 @@ "geo-routing", "header-manipulation" ], - "content_sha256": "d809b1aeaf9eef762fe336d6a7b4e21ad50201bb525e0716c678d58114a67153", + "content_sha256": "77bc8d7bb75d5073d55729d167f95732aae1c14e0d2cc8f5dbd17a584fe83bdf", "sections": [ { "id": "cdn-examples-geo-redirect-as#introduction", @@ -9923,9 +9617,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-20", "---", "type", "example", @@ -10132,7 +9826,7 @@ "level": 2, "anchor": "runtime-properties-used", "line_start": 106, - "line_end": 118, + "line_end": 120, "keywords": [ "runtime", "properties", @@ -10181,8 +9875,8 @@ "heading": "API Calls", "level": 2, "anchor": "api-calls", - "line_start": 119, - "line_end": 150, + "line_start": 121, + "line_end": 152, "keywords": [ "api", "calls", @@ -10222,8 +9916,8 @@ "heading": "Environment Variables", "level": 2, "anchor": "environment-variables", - "line_start": 151, - "line_end": 161, + "line_start": 153, + "line_end": 163, "keywords": [ "environment", "variables", @@ -10272,8 +9966,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 162, - "line_end": 171, + "line_start": 164, + "line_end": 173, "keywords": [ "error", "responses", @@ -10312,8 +10006,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 172, - "line_end": 183, + "line_start": 174, + "line_end": 185, "keywords": [ "registration", "typescript", @@ -10335,8 +10029,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 184, - "line_end": 200, + "line_start": 186, + "line_end": 209, "keywords": [ "build", "pnpm", @@ -10356,7 +10050,17 @@ "--target", "release", "runs", - "---" + "output", + "file", + "description", + "------", + "-------------", + "georedirect.wasm", + "optimised", + "binary", + "upload", + "fastedge", + "georedirect-debug.wasm" ] }, { @@ -10364,8 +10068,8 @@ "heading": "Key Patterns", "level": 2, "anchor": "key-patterns", - "line_start": 201, - "line_end": 229, + "line_start": 210, + "line_end": 248, "keywords": [ "key", "patterns", @@ -10401,8 +10105,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 230, - "line_end": 241, + "line_start": 249, + "line_end": 261, "keywords": [ "constraints", "gotchas", @@ -10450,8 +10154,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 242, - "line_end": 248, + "line_start": 262, + "line_end": 268, "keywords": [ "see", "geoblock", @@ -12102,7 +11806,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12128,7 +11832,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12138,7 +11842,7 @@ "headers", "request-headers" ], - "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", + "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -12157,7 +11861,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12295,11 +11999,9 @@ "method", "signature", "description", - "---------------------------------------", - "----------------------------------------------------", - "------------------------------------------------------------------------", - "get", - "name" + "-------------------", + "-----------------------------------------------", + "--------------------------------------------------------------------------------------------------------" ] }, { @@ -12549,8 +12251,8 @@ "meaning", "trigger", "----", - "---------------------------------", - "-----------------------------------------------------------", + "------------------------------", + "----------------------------------------------------------", "550", "headers", "present", @@ -12564,7 +12266,8 @@ "get", "552", "mismatch", - "mutation" + "mutation", + "validateheaders" ] }, { @@ -12667,7 +12370,7 @@ "file", "description", "--------------------------", - "----------------------------------------------", + "---------------------------------------------", "headers.wasm", "optimised", "release", @@ -12683,7 +12386,8 @@ "package.json", "script", "command", - "---------------" + "-----------------", + "---" ] }, { @@ -12735,7 +12439,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12759,7 +12463,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12769,7 +12473,7 @@ "headers", "request-headers" ], - "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", + "content_sha256": "26db929462e18b733d0d8ff6392d1a509520adb603ee47545516c24043493a11", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -12788,7 +12492,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -13128,58 +12832,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "title": "HTTP Call", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13205,7 +12864,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "overview", "httpcall", "dispatches", @@ -13216,7 +12875,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", + "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -13235,7 +12894,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -13507,13 +13166,209 @@ "response", "patterns" ] + }, + { + "id": "cdn-examples-http-call-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 325, + "line_end": 485, + "keywords": [ + "source", + "material", + "file", + "examples", + "httpcall", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "basecontext", + "buffertypevalues", + "context", + "filterheadersstatusvalues", + "get_buffer_bytes", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "wasmresultvalues", + "proxy-wasm-sdk-a" + ] + }, + { + "id": "cdn-examples-http-call-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 486, + "line_end": 497, + "keywords": [ + "onrequestheaders", + "app", + "dispatches", + "outbound", + "http", + "get", + "request", + "httpbin.org", + "pauses", + "hook", + "stopiteration", + "until", + "response", + "arrives.", + "dispatch", + "latched", + "instance", + "field", + "second", + "invocation", + "processed", + "returns", + "continue", + "instead", + "dispatching", + "again.", + "inside", + "callback", + "passed", + "httpcall", + "checks", + "whether", + "call", + "succeeded", + "headers", + "value", + "indicates", + "failure", + "timeout", + "dns" + ] + }, + { + "id": "cdn-examples-http-call-as#key-concepts", + "heading": "Key concepts", + "level": 2, + "anchor": "key-concepts", + "line_start": 498, + "line_end": 504, + "keywords": [ + "key", + "concepts", + "httpcall", + "rootcontext", + "dispatches", + "async", + "http", + "request.", + "accepts", + "cluster", + "host", + "headers", + "body", + "trailers", + "timeout", + "milliseconds", + "originating", + "context", + "callback.", + "fastedge", + "resume", + "model.", + "returning", + "filterheadersstatusvalues.stopiteration", + "pauses", + "hook.", + "runtime", + "processes", + "response", + "invokes", + "callback", + "re-invokes", + "onrequestheaders", + "same", + "instance", + "httpcalldispatched", + "latch", + "gates", + "re-dispatch", + "hoo" + ] + }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 505, + "line_end": 518, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-http-call-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 519, + "line_end": 523, + "keywords": [ + "deploy", + "upload", + "build", + "httpcall.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13537,7 +13392,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "makes", @@ -13547,7 +13402,7 @@ "services", "timeout" ], - "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", + "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13566,7 +13421,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "cdn", @@ -14009,7 +13864,7 @@ "level": 2, "anchor": "gotchas", "line_start": 360, - "line_end": 370, + "line_end": 371, "keywords": [ "gotchas", "action", @@ -14058,8 +13913,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 371, - "line_end": 377, + "line_start": 372, + "line_end": 378, "keywords": [ "see", "proxy-wasm", @@ -14086,7 +13941,7 @@ { "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14112,7 +13967,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14122,7 +13977,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", + "content_sha256": "7c82be09762a859504b8de768a520016b4dd63da93c478964e74af2344eb76b3", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -14141,7 +13996,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14373,7 +14228,7 @@ "level": 2, "anchor": "lifecycle-hooks", "line_start": 143, - "line_end": 182, + "line_end": 184, "keywords": [ "lifecycle", "hooks", @@ -14416,8 +14271,8 @@ "heading": "Query Parameters", "level": 2, "anchor": "query-parameters", - "line_start": 183, - "line_end": 201, + "line_start": 185, + "line_end": 203, "keywords": [ "query", "parameters", @@ -14451,8 +14306,8 @@ "heading": "Action Dispatch", "level": 2, "anchor": "action-dispatch", - "line_start": 202, - "line_end": 240, + "line_start": 204, + "line_end": 247, "keywords": [ "action", "dispatch", @@ -14471,6 +14326,7 @@ "found", "non-null", "string.utf8.decode", + "break", "scan", "match", "keys", @@ -14478,8 +14334,7 @@ "returns", "string", "keys.join", - "zrange", - "cons" + "cas" ] }, { @@ -14487,8 +14342,8 @@ "heading": "Response Body Format", "level": 2, "anchor": "response-body-format", - "line_start": 241, - "line_end": 258, + "line_start": 248, + "line_end": 265, "keywords": [ "response", "body", @@ -14514,6 +14369,8 @@ "via", "response.status", "property", + "advisory", + "only", "---" ] }, @@ -14522,8 +14379,8 @@ "heading": "Query Param Validation", "level": 2, "anchor": "query-param-validation", - "line_start": 259, - "line_end": 278, + "line_start": 266, + "line_end": 293, "keywords": [ "query", "param", @@ -14572,8 +14429,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 279, - "line_end": 288, + "line_start": 294, + "line_end": 303, "keywords": [ "logging", "typescript", @@ -14596,8 +14453,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 303, + "line_start": 304, + "line_end": 318, "keywords": [ "build", "npm", @@ -14630,8 +14487,8 @@ "heading": "Deployment Requirements", "level": 2, "anchor": "deployment-requirements", - "line_start": 304, - "line_end": 315, + "line_start": 319, + "line_end": 330, "keywords": [ "deployment", "requirements", @@ -14679,8 +14536,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 316, - "line_end": 328, + "line_start": 331, + "line_end": 344, "keywords": [ "gotchas", "kvstore.get", @@ -14724,8 +14581,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 329, - "line_end": 334, + "line_start": 345, + "line_end": 350, "keywords": [ "see", "kvstore", @@ -14758,7 +14615,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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14782,7 +14639,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "capabilities", "kv-store", @@ -14793,7 +14650,7 @@ "example", "app_type" ], - "content_sha256": "3e66a0f9ea746ad8415d020dfa703809ee9c371e9bde63f98aae6872310a47ad", + "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14812,7 +14669,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "capabilities", "kv-store", @@ -15054,55 +14911,6 @@ "vs.", "differences" ] - }, - { - "id": "cdn-examples-kv-store-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 244, - "line_end": 516, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "key_value", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "store", - "operations", - "via", - "proxy-wasm", - "interface.", - "supports", - "query", - "parameters", - "name", - "action", - "get", - "key", - "scan", - "match", - "pattern", - "zrange", - "min", - "f64", - "max", - "zscan", - "bfexists", - "ite" - ] } ] }, @@ -18757,7 +18565,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -18781,7 +18589,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example", @@ -18792,7 +18600,7 @@ "visitors", "test" ], - "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", + "content_sha256": "25feb56e225dc3c2de096f569836c7a02371f3ad66f47c95f09b1d43cae9d0af", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -18811,7 +18619,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example" @@ -19235,7 +19043,7 @@ "level": 2, "anchor": "gotchas", "line_start": 200, - "line_end": 210, + "line_end": 212, "keywords": [ "gotchas", "headers", @@ -19278,20 +19086,14 @@ ] }, { - "id": "http-examples-ab-testing-js#source-material", - "heading": "Source Material", + "id": "http-examples-ab-testing-js#source", + "heading": "Source", "level": 2, - "anchor": "source-material", - "line_start": 211, - "line_end": 328, + "anchor": "source", + "line_start": 213, + "line_end": 303, "keywords": [ "source", - "material", - "file", - "examples", - "ab-testing", - "src", - "index.js", "import", "getenv", "fastedge", @@ -19315,7 +19117,8 @@ "slicedheaders", "sliceabtestidfromcookie", "headers", - "createabtesthe" + "createabtestheaders", + "outbo" ] } ] @@ -20785,7 +20588,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20809,7 +20612,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -20819,7 +20622,7 @@ "outbound-http", "query-params" ], - "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", + "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20838,7 +20641,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21260,13 +21063,207 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21290,7 +21287,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21300,7 +21297,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", + "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21319,7 +21316,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21772,188 +21769,6 @@ "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" - ] } ] }, @@ -22412,7 +22227,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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22438,7 +22253,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "cache", "module", @@ -22448,7 +22263,7 @@ "2.3.0", "app" ], - "content_sha256": "137b45736825a3dd07f53e14f0a413bbef13b403e482856fabf51695c93a1433", + "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22467,7 +22282,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "cache", "javascript", @@ -22796,6 +22611,56 @@ "response", "construction" ] + }, + { + "id": "http-examples-cache-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 335, + "line_end": 610, + "keywords": [ + "source", + "material", + "file", + "examples", + "cache", + "src", + "index.ts", + "fastedge", + "flagship", + "patterns", + "example", + "demonstrates", + "three", + "highest-value", + "uses", + "module", + "per-ip", + "rate", + "limiting", + "atomic", + "counters", + "origin-cache", + "proxy", + "manual", + "get", + "set", + "conditional", + "caching", + "json", + "memoisation", + "getorset", + "computed", + "populator", + "rely", + "strongly", + "consistent", + "within", + "pop", + "incr", + "returns" + ] } ] }, @@ -26213,7 +26078,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26237,7 +26102,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26247,7 +26112,7 @@ "hello-world", "wasi" ], - "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", + "content_sha256": "b15ce34c19d4cf004bf6f602e6bf370c34f33abd962e77ce72f4bc13969016fe", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -26266,7 +26131,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26659,127 +26524,6 @@ "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" - ] } ] }, @@ -27178,7 +26922,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27202,7 +26946,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27212,7 +26956,7 @@ "languages", "store" ], - "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", + "content_sha256": "315969c42f8aa2c0325e094018cc3d59e5f68550acd5744a480b2302a15d0860", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -27231,7 +26975,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27658,222 +27402,6 @@ "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" - ] } ] }, @@ -28795,7 +28323,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28819,7 +28347,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -28829,7 +28357,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", + "content_sha256": "7d9b8470977ba554efeff699feeee8f078a5b682b5d396d070f0589f49f8044f", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -28848,7 +28376,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29216,171 +28744,6 @@ "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" - ] } ] }, @@ -29837,7 +29200,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29861,7 +29224,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29871,7 +29234,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", + "content_sha256": "f6509335e7b9957b364049295ea3a84967dce13ae1e03c5ebbd8412f6e0dfb05", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -29890,7 +29253,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -30208,15 +29571,15 @@ ] }, { - "id": "http-examples-outbound-modify-response-wasi-rust#source-material", - "heading": "Source Material", + "id": "http-examples-outbound-modify-response-wasi-rust#full-source", + "heading": "Full Source", "level": 2, - "anchor": "source-material", + "anchor": "full-source", "line_start": 186, "line_end": 277, "keywords": [ + "full", "source", - "material", "file", "examples", "http", @@ -30936,7 +30299,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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30962,7 +30325,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "proxy", "response", "transform", @@ -30971,7 +30334,7 @@ "fastedge", "apps" ], - "content_sha256": "1199a5c5c761e3723a541167ff50b4e4009071ddc549726456bb8e300d9b9c94", + "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30990,7 +30353,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "proxy", "response", "transform", @@ -31424,13 +30787,356 @@ "kvstore", "api" ] + }, + { + "id": "http-examples-proxy-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 264, + "keywords": [ + "source", + "material", + "file", + "docs", + "proxy_patterns.md", + "maintenance", + "hand-authored.", + "produced", + "fastedge-plugin-source", + "generate-docs.sh.", + "edit", + "directly.", + "proxy", + "response", + "transform", + "patterns", + "fastedge", + "http", + "apps", + "act", + "thin", + "front", + "origin", + "upstream", + "api", + "fetching", + "transforming", + "returning", + "result.", + "document", + "covers", + "common", + "patterns." + ] + }, + { + "id": "http-examples-proxy-js#simple-proxy", + "heading": "Simple Proxy", + "level": 2, + "anchor": "simple-proxy", + "line_start": 265, + "line_end": 291, + "keywords": [ + "simple", + "proxy", + "fetch", + "upstream", + "return", + "body", + "unchanged", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "method", + "request.method", + "headers", + "request.headers", + "get", + "head", + "request.arraybuffer", + "undefined", + "addeventlistener" + ] + }, + { + "id": "http-examples-proxy-js#proxy-with-json-transform", + "heading": "Proxy with JSON Transform", + "level": 2, + "anchor": "proxy-with-json-transform", + "line_start": 292, + "line_end": 320, + "keywords": [ + "proxy", + "json", + "transform", + "modify", + "response", + "body", + "returning", + "it.", + "pattern", + "examples", + "outbound-modify-response", + "typescript", + "async", + "function", + "handle", + "const", + "upstream", + "await", + "fetch", + "https", + "jsonplaceholder.typicode.com", + "users", + "upstream.json", + "transformed", + "users.slice", + "total", + "skip", + "limit", + "return", + "new", + "json.stringify", + "status", + "200", + "headers", + "content-type" + ] + }, + { + "id": "http-examples-proxy-js#hono-proxy-with-transform", + "heading": "Hono Proxy with Transform", + "level": 2, + "anchor": "hono-proxy-with-transform", + "line_start": 321, + "line_end": 351, + "keywords": [ + "hono", + "proxy", + "transform", + "inside", + "app", + "use", + "c.req.raw.headers", + "forward", + "inbound", + "headers", + "c.req.arraybuffer", + "body", + "typescript", + "import", + "const", + "new", + "app.all", + "api", + "async", + "url", + "c.req.url", + "upstream", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "fetch", + "method", + "c.req.method", + "get", + "c.r" + ] + }, + { + "id": "http-examples-proxy-js#header-manipulation-in-proxies", + "heading": "Header Manipulation in Proxies", + "level": 2, + "anchor": "header-manipulation-in-proxies", + "line_start": 352, + "line_end": 380, + "keywords": [ + "header", + "manipulation", + "proxies", + "strip", + "hop-by-hop", + "headers", + "forwarding", + "upstream", + "add", + "diagnostic", + "way", + "back", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "upstreamheaders", + "new", + "request.headers", + "forwarded", + "upstreamheaders.delete", + "connection", + "keep-alive", + "transfer-encoding", + "await", + "fetch", + "https", + "backend.example.com", + "method", + "request.m" + ] + }, + { + "id": "http-examples-proxy-js#cache-aware-proxy-with-kv", + "heading": "Cache-aware Proxy with KV", + "level": 2, + "anchor": "cache-aware-proxy-with-kv", + "line_start": 381, + "line_end": 413, + "keywords": [ + "cache-aware", + "proxy", + "cache", + "upstream", + "responses", + "store", + "avoid", + "repeated", + "outbound", + "calls.", + "note", + "read-only", + "app", + "code", + "writes", + "happen", + "via", + "portal", + "api", + "typescript", + "import", + "kvstore", + "fastedge", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "try", + "kvstore.open", + "api-cache", + "cached", + "cache.get", + "url.pathname", + "null", + "return", + "response" + ] + }, + { + "id": "http-examples-proxy-js#operational-notes", + "heading": "Operational Notes", + "level": 2, + "anchor": "operational-notes", + "line_start": 414, + "line_end": 419, + "keywords": [ + "operational", + "notes", + "outbound", + "fetch", + "budget.", + "invocation", + "limited", + "number", + "requests", + "basic", + "pro", + "parallelise", + "possible", + "promise.all", + "...", + "instead", + "sequential", + "await", + "execution", + "time", + "proxying", + "upstream", + "transforming", + "counts", + "against", + "50ms", + "200ms", + "slow", + "upstreams", + "trip", + "532", + "timeouts.", + "body", + "size", + "limits.", + "inbound", + "bodies", + "subject", + "configured", + "request" + ] + }, + { + "id": "http-examples-proxy-js#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 420, + "line_end": 428, + "keywords": [ + "see", + "examples", + "outbound-modify-response", + "json", + "transform", + "upstream", + "response", + "outbound-fetch", + "basic", + "outbound", + "fetch", + "patterns", + "headers", + "request", + "header", + "manipulation", + "kv-store", + "kv-backed", + "caching" + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31454,7 +31160,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31464,7 +31170,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", + "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31483,7 +31189,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31922,13 +31628,247 @@ "documentation", "external" ] + }, + { + "id": "http-examples-s3upload-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 369, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "s3upload", + "src", + "lib.rs", + "rust", + "use", + "std", + "time", + "duration", + "fastedge", + "body", + "header", + "error", + "method", + "request", + "response", + "statuscode", + "rusty_s3", + "bucket", + "credentials", + "s3action", + "urlstyle", + "collections", + "hashmap", + "env", + "url", + "main", + "req", + "result", + "match", + "req.method", + "allow", + "only", + "post", + "put" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 370, + "line_end": 379, + "keywords": [ + "accepts", + "post", + "put", + "only", + "returns", + "405", + "methods", + "requires", + "name", + "filename", + "query", + "parameter", + "non-empty", + "body", + "400", + "otherwise", + "enforces", + "max_file_size", + "set", + "413", + "exceeded", + "calls", + "prepare_s3", + "build", + "1-hour", + "presigned", + "url", + "using", + "rusty_s3", + "forwards", + "file", + "via", + "fastedge", + "send_request", + "success", + "200", + "responds", + "clean", + "object", + "string" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 380, + "line_end": 393, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "access_key", + "access", + "key", + "secret_key", + "secret", + "region", + "e.g.", + "s-ed1", + "base_hostname", + "base", + "hostname", + "cloud.gcore.lu", + "bucket", + "name", + "scheme", + "optional", + "url", + "defaults", + "http", + "max_file_size", + "maximum", + "upload", + "size", + "bytes", + "limit", + "unset", + "constructed", + "endpoint", + "bucke" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 394, + "line_end": 400, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "s3upload.wasm" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 401, + "line_end": 411, + "keywords": [ + "usage", + "post", + "upload", + "name", + "photo.jpg", + "content-type", + "image", + "jpeg", + "file", + "bytes", + "success", + "200", + "response", + "body", + "clean", + "object", + "url", + "presign", + "query", + "parameters", + "stripped" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 412, + "line_end": 418, + "keywords": [ + "notes", + "options", + "method", + "returns", + "204", + "include", + "cors", + "headers", + "add", + "access-control-allow-", + "browser", + "preflight", + "support", + "needed.", + "presigned", + "url", + "expires", + "hour", + "since", + "upload", + "performed", + "server-side", + "practical", + "impact.", + "max_file_size", + "silently", + "ignored", + "set", + "non-numeric", + "value." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31952,7 +31892,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "example", @@ -31962,7 +31902,7 @@ "fastedge", "platform" ], - "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", + "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31981,7 +31921,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "rust", @@ -32371,284 +32311,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32672,7 +32341,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "secret", "rollover", "wasi", @@ -32683,7 +32352,7 @@ "rotation", "using" ], - "content_sha256": "0ca24f700d200210c2587774b740319ee8b982f0044d22467b92fc8536a433c1", + "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32702,7 +32371,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "secret", "rollover", "wasi", @@ -32958,14 +32627,30 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 150, + "line_end": 178, "keywords": [ "implementation", "pattern", "rust", "use", + "std", + "time", + "systemtime", + "unix_epoch", + "anyhow", "fastedge", "secret", + "serde_json", + "json", + "wstd", + "http", + "body", + "request", + "response", + "http_server", + "async", + "main", + "result", "read", "slot", "header", @@ -32975,29 +32660,13 @@ "timestamp", "let", "u32", - "request", ".headers", ".get", "x-slot", ".and_then", "v.to_str", ".ok", - "v.parse", - ".unwrap_or_else", - "systemtime", - "now", - ".duration_since", - "unix_epoch", - ".expect", - "time", - "went", - "backwards", - ".as_secs", - "get", - "latest", - "value", - "secret_name", - ".map_err" + "v.p" ] }, { @@ -33005,8 +32674,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 151, - "line_end": 158, + "line_start": 179, + "line_end": 186, "keywords": [ "constraints", "gotchas", @@ -33055,8 +32724,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 159, - "line_end": 165, + "line_start": 187, + "line_end": 193, "keywords": [ "build", "cargo", @@ -33073,8 +32742,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 166, - "line_end": 172, + "line_start": 194, + "line_end": 200, "keywords": [ "see", "fastedge", @@ -33105,7 +32774,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33129,7 +32798,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33139,7 +32808,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", + "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -33158,7 +32827,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33523,193 +33192,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33733,7 +33222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "type", "example", @@ -33743,7 +33232,7 @@ "outbound-http", "env-vars" ], - "content_sha256": "0fba2c9ea0ce15a73c795ca6e7351997b10aad648632c67f484f418df2976d8e", + "content_sha256": "3195fb4f9d583f55e2834ef95604d838f310817302a02705bec7c99b51685485", "sections": [ { "id": "http-examples-smart-switch-basic-rust#introduction", @@ -33762,7 +33251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "type", "example", @@ -34064,7 +33553,7 @@ "level": 2, "anchor": "key-gotchas", "line_start": 179, - "line_end": 188, + "line_end": 189, "keywords": [ "key", "gotchas", @@ -34113,8 +33602,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 189, - "line_end": 197, + "line_start": 190, + "line_end": 198, "keywords": [ "build", "cargo", @@ -34133,8 +33622,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 198, - "line_end": 204, + "line_start": 199, + "line_end": 205, "keywords": [ "see", "api_wrapper", @@ -34778,7 +34267,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34802,7 +34291,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34812,7 +34301,7 @@ "streaming", "async" ], - "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", + "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34831,7 +34320,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -35160,144 +34649,6 @@ "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" - ] } ] }, @@ -42906,7 +42257,7 @@ { "id": "templates-catalog", "title": "FastEdge Bolt-On Templates", - "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md", "skill": "fastedge-docs", "category": "reference", @@ -42924,7 +42275,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -42936,7 +42287,7 @@ "scratch", "check" ], - "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "content_sha256": "910cf26789610f4f959d807e57f149e1cf0af1cb1bcdcc089f45792bb595ff83", "sections": [ { "id": "templates-catalog#introduction", @@ -42955,7 +42306,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -43040,7 +42391,7 @@ "level": 2, "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", "line_start": 32, - "line_end": 47, + "line_end": 49, "keywords": [ "harden-cookies", "cookie", @@ -43089,8 +42440,8 @@ "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, + "line_start": 50, + "line_end": 73, "keywords": [ "edge-sso", "sso", @@ -43139,8 +42490,8 @@ "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, + "line_start": 74, + "line_end": 99, "keywords": [ "edge-totp", "totp", @@ -43574,7 +42925,7 @@ { "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 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md", "skill": "fastedge-docs", "category": "reference", @@ -43592,7 +42943,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", @@ -43603,7 +42954,7 @@ "existing", "password" ], - "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "content_sha256": "2f2493935877ac046f328513d03575666424c44043fa8f4d319e59f96b93dccb", "sections": [ { "id": "templates-edge-totp-integration#introduction", @@ -43622,7 +42973,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn-apps-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn-apps-rust.md index 6d77329..bdad5fe 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn-apps-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn-apps-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) @@ -197,10 +197,10 @@ impl HttpContext for HelloWorld { | Callback | Phase | Description | | ----------------------------------------------------------------- | ---------------- | --------------------------------------------------- | -| `on_http_request_headers(num_headers, end_of_stream) -> Action` | Request headers | Inspect or modify request headers before forwarding | -| `on_http_request_body(body_size, end_of_stream) -> Action` | Request body | Inspect or modify request body before forwarding | -| `on_http_response_headers(num_headers, end_of_stream) -> Action` | Response headers | Inspect or modify response headers from origin | -| `on_http_response_body(body_size, end_of_stream) -> Action` | Response body | Inspect or modify response body from origin | +| `on_http_request_headers(num_headers: usize, end_of_stream: bool) -> Action` | Request headers | Inspect or modify request headers before forwarding | +| `on_http_request_body(body_size: usize, end_of_stream: bool) -> Action` | Request body | Inspect or modify request body before forwarding | +| `on_http_response_headers(num_headers: usize, end_of_stream: bool) -> Action` | Response headers | Inspect or modify response headers from origin | +| `on_http_response_body(body_size: usize, end_of_stream: bool) -> Action` | Response body | Inspect or modify response body from origin | All callbacks have default no-op implementations. Override only the phases your app needs to process. 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 0205b81..a8e55fb 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-08-17 + updated: 2026-08-20 --> --- @@ -248,191 +248,3 @@ 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 fe6573a..4807ec3 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -199,168 +199,3 @@ Upload `build/apiKey.wasm` to the FastEdge portal and attach it to a CDN applica - proxy-wasm-sdk-as reference (full `Context`, `RootContext`, `FilterHeadersStatusValues` API) - FastEdge secrets management (how to set and rotate application secrets) - CDN app deployment guide - -## Source Material - -### FILE: examples/apiKey/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - HeaderPair, - log, - LogLevelValues, - makeHeaderPair, - registerRootContext, - RootContext, - send_http_response, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class ApiKeyRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new ApiKeyContext(context_id, this); - } -} - -class ApiKeyContext extends Context { - constructor(context_id: u32, root_context: ApiKeyRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const expectedKey = getSecret("API_KEY"); - if (expectedKey === "") { - log(LogLevelValues.error, "API_KEY secret not configured"); - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const providedKey = stream_context.headers.request.get("X-API-Key"); - - if (providedKey === "") { - const authHeaders = new Array(); - authHeaders.push(makeHeaderPair("WWW-Authenticate", "API-Key")); - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Missing X-API-Key header"), - authHeaders, - ); - return FilterHeadersStatusValues.StopIteration; - } - - if (providedKey !== expectedKey) { - log(LogLevelValues.info, "API key validation failed"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Invalid API key"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // .remove() sets the header value to "" rather than deleting it entirely — - // the upstream will see X-API-Key: "" rather than a missing header. - stream_context.headers.request.remove("X-API-Key"); - - log(LogLevelValues.info, "API key validated successfully"); - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new ApiKeyRoot(context_id); -}, "apiKey"); -``` - - -### FILE: examples/apiKey/package.json - -```json -{ - "name": "fastedge-as-example-api-key", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: API Key — validate X-API-Key header against a secret", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/apiKey/README.md - -``` -[← Back to examples](../README.md) - -# API Key - -This application validates requests using an `X-API-Key` header checked against a stored secret. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the expected API key from the `API_KEY` secret. -2. Checks the `X-API-Key` request header. -3. Returns `401 Unauthorized` if the header is missing. -4. Returns `403 Forbidden` if the key does not match. -5. On success, clears the `X-API-Key` header before forwarding to the upstream origin (proxy-wasm `.remove()` sets the header value to an empty string rather than deleting it). - -This is a simpler alternative to JWT validation when you need basic API authentication without token expiry or claims. - -> **Production note:** The key comparison (`providedKey !== expectedKey`) is not constant-time, which opens a timing side-channel for a high-volume attacker. For production use, replace the comparison with a constant-time HMAC equality check or use the `jwt` example which includes proper cryptographic validation. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -|------|------|-------------| -| `API_KEY` | Secret | The expected API key value | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/apiKey.wasm` | Optimised release binary — upload this to FastEdge | -| `build/apiKey-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/apiKey.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `API_KEY` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md index d9b973e..5aeb1a4 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # API Key Validation — CDN (Rust) @@ -159,3 +159,120 @@ return Action::Pause; - Host services reference — secrets API (`fastedge::proxywasm::secret`) and other CDN host services - CDN apps reference — proxy-wasm app structure, `RootContext`/`HttpContext` setup, `Action` enum, and request property encodings - SDK API reference — Rust CDN SDK traits and types + +## 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 { Box::new(ApiKeyRoot) }); +}} + +struct ApiKeyRoot; + +impl Context for ApiKeyRoot {} + +impl RootContext for ApiKeyRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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/fastedge-docs/reference/cdn/examples-body-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-body-as.md index e0e5128..d98a1dc 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -337,216 +337,3 @@ When modifying body content: - host-services-rust (for equivalent Rust patterns) - platform-overview (CDN app lifecycle, hook execution order) - examples-headers-as (header-only manipulation without body buffering) - -## Source Material - -### FILE: examples/body/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - BufferTypeValues, - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - get_buffer_bytes, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - set_buffer_bytes, - set_property, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class HttpBodyRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug - return new HttpBody(context_id, this); - } -} - -class HttpBody extends Context { - constructor(context_id: u32, root_context: HttpBodyRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool - ): FilterHeadersStatusValues { - log(LogLevelValues.debug, "onRequestHeaders >>"); - // Remove the "content-length" header - stream_context.headers.request.remove("content-length"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool - ): FilterDataStatusValues { - log(LogLevelValues.debug, "onRequestBody >>"); - if (!end_of_stream) { - // Wait until the complete body is buffered - return FilterDataStatusValues.StopIterationAndBuffer; - } - - // Retrieve the body from the HttpRequestBody buffer - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpRequestBody, - 0, - body_buffer_length - ); - - if (bodyBytes.byteLength > 0) { - const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); - if (bodyStr.includes("Client")) { - const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; - set_buffer_bytes( - BufferTypeValues.HttpRequestBody, - 0, - body_buffer_length, - String.UTF8.encode(newBody) - ); - } - } - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.debug, "onResponseHeaders >>"); - - // Remove "content-length" header as the body size will change - stream_context.headers.response.remove("content-length"); - - // Set "transfer-encoding" to "chunked" - stream_context.headers.response.replace("transfer-encoding", "Chunked"); - - const contentType = stream_context.headers.response.get("content-type"); - if (contentType.length > 0) { - set_property("response.content_type", String.UTF8.encode(contentType)); - } - - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool - ): FilterDataStatusValues { - log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); - - if (!end_of_stream) { - // Wait until the complete body is buffered - return FilterDataStatusValues.StopIterationAndBuffer; - } - - log( - LogLevelValues.debug, - "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() - ); - - // Retrieve the request URL - const urlBytes = get_property("request.url"); - const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); - if (url !== "") { - log(LogLevelValues.info, `url=${url}`); - } - - // Retrieve the response content type stored in onResponseHeaders - const contentTypeBytes = get_property("response.content_type"); - const contentType = - contentTypeBytes.byteLength === 0 - ? "" - : String.UTF8.decode(contentTypeBytes); - if (contentType !== "") { - log(LogLevelValues.info, `contentType=${contentType}`); - } - - // Retrieve the body from the HttpRequestBody buffer - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpResponseBody, - 0, - body_buffer_length - ); - - if (bodyBytes.byteLength > 0) { - const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); - } - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HttpBodyRoot(context_id); -}, "httpbody"); -``` - -### FILE: examples/body/package.json - -```json -{ - "name": "fastedge-as-example-body", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Body manipulation", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/body/README.md - -``` -[← Back to examples](../README.md) - -# Body - -This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. - -## What it does - -1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. - -2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. - -3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. - -4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. - -This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: - -- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). -- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| ----------------------- | -------------------------------------------------- | -| `build/body.wasm` | Optimised release binary — upload this to FastEdge | -| `build/body-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. -``` diff --git a/plugins/gcore-fastedge-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 baedbd0..cf31db4 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-08-17 + updated: 2026-08-20 --> # CDN Example: Convert Image (Rust) @@ -202,3 +202,300 @@ 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 613b6e1..090a82c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # CORS — AssemblyScript (CDN) @@ -283,7 +283,6 @@ registerRootContext((context_id: u32) => { }, "cors"); ``` - ### FILE: examples/cors/package.json ```json @@ -306,7 +305,6 @@ registerRootContext((context_id: u32) => { } ``` - ### FILE: examples/cors/README.md ``` diff --git a/plugins/gcore-fastedge-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 9ec6651..bf58305 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -292,3 +292,241 @@ export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; - CDN app platform overview - host-services-rust reference (for Rust equivalent patterns) - best-practices reference (response body buffering, hook sequencing) + +## Source Material + +### FILE: examples/customErrorPages/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + BufferTypeValues, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class ErrorPagesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new ErrorPagesContext(context_id, this); + } +} + +class ErrorPagesContext extends Context { + constructor(context_id: u32, root_context: ErrorPagesRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + if (statusBuf.byteLength < 2) { + return FilterHeadersStatusValues.Continue; + } + + const bytes = Uint8Array.wrap(statusBuf); + const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); + + if (code >= 400 && code < 600) { + stream_context.headers.response.replace("Content-Type", "text/html"); + stream_context.headers.response.remove("Content-Length"); + stream_context.headers.response.replace("Transfer-Encoding", "Chunked"); + + log(LogLevelValues.info, "Error response detected: " + code.toString()); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + // Read response status from property (no instance state between hooks) + const statusBuf = get_property("response.status"); + if (statusBuf.byteLength < 2) { + return FilterDataStatusValues.Continue; + } + + const bytes = Uint8Array.wrap(statusBuf); + const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); + + if (code < 400 || code >= 600) { + return FilterDataStatusValues.Continue; + } + + if (!end_of_stream) { + return FilterDataStatusValues.StopIterationAndBuffer; + } + const title = this.getErrorTitle(code); + const description = this.getErrorDescription(code); + const category = code >= 500 ? "Server Error" : "Client Error"; + + const html = + '' + + '' + + "" + + code.toString() + + " — " + + title + + "" + + "
" + + "

" + + code.toString() + + "

" + + "

" + + title + + "

" + + "

" + + description + + "

" + + "

" + + category + + "

" + + "
"; + + const body = String.UTF8.encode(html); + // Replace the entire original body — pass body_buffer_length as the length + // to replace, not body.byteLength. Otherwise any original bytes beyond the + // new body's length survive at the tail of the response. + set_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length as u32, + body, + ); + + return FilterDataStatusValues.Continue; + } + + private getErrorTitle(code: u32): string { + if (code == 400) return "Bad Request"; + if (code == 401) return "Unauthorized"; + if (code == 403) return "Forbidden"; + if (code == 404) return "Not Found"; + if (code == 405) return "Method Not Allowed"; + if (code == 408) return "Request Timeout"; + if (code == 429) return "Too Many Requests"; + if (code == 500) return "Internal Server Error"; + if (code == 502) return "Bad Gateway"; + if (code == 503) return "Service Unavailable"; + if (code == 504) return "Gateway Timeout"; + if (code >= 500) return "Server Error"; + return "Error"; + } + + private getErrorDescription(code: u32): string { + if (code == 400) + return "The server could not understand the request due to invalid syntax."; + if (code == 401) + return "You need to authenticate to access this resource."; + if (code == 403) + return "You do not have permission to access this resource."; + if (code == 404) + return "The requested page could not be found. It may have been moved or deleted."; + if (code == 405) + return "The request method is not supported for this resource."; + if (code == 408) + return "The server timed out waiting for the request."; + if (code == 429) + return "You have sent too many requests. Please try again later."; + if (code == 500) + return "The server encountered an unexpected condition that prevented it from fulfilling the request."; + if (code == 502) + return "The server received an invalid response from the upstream server."; + if (code == 503) + return "The server is temporarily unavailable. Please try again later."; + if (code == 504) + return "The server did not receive a timely response from the upstream server."; + if (code >= 500) + return "The server encountered an error processing your request."; + return "An error occurred processing your request."; + } +} + +registerRootContext((context_id: u32) => { + return new ErrorPagesRoot(context_id); +}, "customErrorPages"); +``` + + +### FILE: examples/customErrorPages/package.json + +```json +{ + "name": "fastedge-as-example-custom-error-pages", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Custom Error Pages — replace 4xx/5xx responses with branded HTML", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/customErrorPages/README.md + +``` +[← Back to examples](../README.md) + +# Custom Error Pages + +This application intercepts 4xx and 5xx error responses and replaces them with clean, branded HTML error pages. + +## What it does + +In `onResponseHeaders`, the app reads the `response.status` property. If it is in the 400-599 range, it sets the `Content-Type` to `text/html` and prepares for body replacement. + +In `onResponseBody`, the app buffers the full response, then replaces the body with a styled HTML page containing: + +- The numeric status code +- A human-readable error title (e.g. "Not Found", "Bad Gateway") +- A description explaining what went wrong +- An error category label ("Client Error" or "Server Error") + +Covers all common HTTP error codes (400, 401, 403, 404, 405, 408, 429, 500, 502, 503, 504) with specific messages and falls back to generic messages for other codes. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/customErrorPages.wasm` | Optimised release binary — upload this to FastEdge | +| `build/customErrorPages-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. +``` diff --git a/plugins/gcore-fastedge-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 ec7dc83..d049948 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-08-17 + updated: 2026-08-20 --> --- @@ -250,123 +250,3 @@ 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 541ac7c..2039ea1 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- type: example @@ -225,140 +225,3 @@ import { - examples-headers-cdn-as (request/response header manipulation patterns) - platform-overview (secret management and deployment configuration) - best-practices (logging levels and secret handling guidelines) - -## Source Material - -### FILE: examples/variablesAndSecrets/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class VariablesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new VariablesContext(context_id, this); - } -} - -class VariablesContext extends Context { - constructor(context_id: u32, root_context: VariablesRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const username = getEnv("USERNAME"); - const password = getSecret("PASSWORD"); - - log(LogLevelValues.info, "USERNAME: " + username); - log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); - - stream_context.headers.request.add("x-env-username", username); - stream_context.headers.request.add("x-env-password", password); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new VariablesRoot(context_id); -}, "variablesAndSecrets"); -``` - - -### FILE: examples/variablesAndSecrets/package.json - -```json -{ - "name": "fastedge-as-example-variables-and-secrets", - "version": "0.0.1", - "description": "FastEdge AssemblyScript example: Variables and Secrets", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/variablesAndSecrets/README.md - -``` -[← Back to examples](../README.md) - -# Variables and Secrets - -This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the `USERNAME` environment variable using `getEnv`. -2. Reads the `PASSWORD` secret using `getSecret`. -3. Logs that both values were retrieved (without logging the secret value itself). -4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. - -This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. - -> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -| ---------- | -------------------- | -------------------------------------- | -| `USERNAME` | Environment variable | The username value to forward upstream | -| `PASSWORD` | Secret | The password value to forward upstream | - -## Local testing - -The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. - -To test locally with the visual debugger, create `fixtures/.env`: - -``` -FASTEDGE_VAR_ENV_USERNAME=my-username -FASTEDGE_VAR_SECRET_PASSWORD=my-password -``` - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| -------------------------------------- | -------------------------------------------------- | -| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | -| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/variablesAndSecrets.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md index 7535b8a..03f0c0b 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -114,6 +114,8 @@ onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues All properties return `ArrayBuffer`. Decode with `String.UTF8.decode(buf)` before use. Check `buf.byteLength > 0` before decoding to detect missing values. +Routing to the selected origin is achieved by writing `request.url` — this is not an HTTP redirect. No `Location` header is set and no 3xx response is sent to the client. The upstream fetch target is rewritten transparently. + --- ## API Calls @@ -196,6 +198,13 @@ Build scripts (from `package.json`): | `asbuild:release` | `asc assembly/index.ts --target release` | | `asbuild` | Runs both debug and release | +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + --- ## Key Patterns @@ -225,6 +234,16 @@ const defaultOrigin = getEnv("DEFAULT"); if (!defaultOrigin) { /* handles both null and empty string */ } ``` +**Logging country code and matched origin for observability:** +```typescript +log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, +); +``` + --- ## Constraints and Gotchas @@ -236,6 +255,7 @@ if (!defaultOrigin) { /* handles both null and empty string */ } - Country code matching is case-sensitive and depends on the exact casing provided by `request.country` at runtime. - `getEnv` returns `""` (empty string) when a country-code variable is not set; the `=== ""` check must be used for country-specific origin fallback (not a null check). - The `DEFAULT` check uses `!defaultOrigin` (falsy), catching both null and empty string returns. +- The app logs at INFO level: country code with matched origin, host value (if present), and final request URL. These are visible in FastEdge application logs. --- diff --git a/plugins/gcore-fastedge-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 61f35b9..981cd2f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -66,14 +66,14 @@ registerRootContext((context_id: u32) => { All header operations are accessed via `stream_context.headers.request` or `stream_context.headers.response`. -| Method | Signature | Description | -| --------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ | -| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | -| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | -| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | -| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | -| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | -| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | +| Method | Signature | Description | +| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | +| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | +| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | +| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | +| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | +| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | ### `Headers` / `HeaderPair` type @@ -120,11 +120,11 @@ Operations performed in order: Expected post-mutation request headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Response Phase — `onResponseHeaders` @@ -144,11 +144,11 @@ Operations performed in order: Expected post-mutation response headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Header Validation Pattern @@ -212,11 +212,11 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { ## Error Response Codes Used -| Code | Meaning | Trigger | -| ---- | --------------------------------- | ----------------------------------------------------------- | -| 550 | No headers present | `get_headers()` returns empty collection | -| 551 | Host header present but empty | `get("host")` returns `""` | -| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | +| Code | Meaning | Trigger | +| ---- | ------------------------------ | ---------------------------------------------------------- | +| 550 | No headers present | `get_headers()` returns empty collection | +| 551 | Host header present but empty | `get("host")` returns `""` | +| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | All errors use `send_http_response(code, "internal server error", body, [])`. @@ -259,18 +259,18 @@ pnpm install pnpm run asbuild ``` -| Output file | Description | -| -------------------------- | ---------------------------------------------- | -| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | -| `build/headers-debug.wasm` | Debug binary with source maps | +| Output file | Description | +| -------------------------- | --------------------------------------------- | +| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | +| `build/headers-debug.wasm` | Debug binary with source maps | Build scripts defined in `package.json`: -| Script | Command | -| ------------------- | ------------------------------------------ | -| `asbuild:debug` | `asc assembly/index.ts --target debug` | -| `asbuild:release` | `asc assembly/index.ts --target release` | -| `asbuild` | Runs both debug and release builds | +| Script | Command | +| ----------------- | ---------------------------------------- | +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | Runs both debug and release builds | ## Deployment diff --git a/plugins/gcore-fastedge-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 97aea43..bbfc088 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-08-17 + updated: 2026-08-20 --> --- @@ -230,356 +230,3 @@ 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 9509c21..bc289c7 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> ## Overview @@ -174,7 +174,7 @@ class HttpCallContext extends Context { onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { // FastEdge re-invokes this hook after the httpCall response is processed. // The latch gates re-dispatch so the second invocation returns Continue - // instead of firing another HTTP call. + // instead of firing another HTTP call. See SDK docs → Outbound HTTP. if (this.httpCallDispatched) { log( LogLevelValues.info, @@ -321,3 +321,202 @@ Dependencies (from `package.json`): - SDK API reference — full lifecycle hook signatures, `RootContext` and `Context` base class APIs, all enum values (`WasmResultValues`, `FilterHeadersStatusValues`, `BufferTypeValues`, `LogLevelValues`) - Platform overview — FastEdge CDN execution model, context lifecycle, how the runtime re-enters hooks after async callbacks - Best practices — timeout tuning, upstream cluster naming, error response patterns + +## Source Material + +### FILE: examples/httpCall/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BaseContext, + BufferTypeValues, + Context, + FilterHeadersStatusValues, + get_buffer_bytes, + HeaderPair, + log, + LogLevelValues, + makeHeaderPair, + registerRootContext, + RootContext, + send_http_response, + stream_context, + WasmResultValues, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const INTERNAL_SERVER_ERROR: u32 = 500; + +function handleHttpCallResponse( + ctx: BaseContext, + hdrs: u32, + bodySize: usize, + trls: u32, +): void { + if (hdrs == 0) { + log(LogLevelValues.error, "HTTP call failed — no response received"); + return; + } + + const userAgent = stream_context.headers.http_callback.get("user-agent"); + if (userAgent !== "") { + log(LogLevelValues.info, "User-Agent: " + userAgent); + } + + if (bodySize > 0) { + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpCallResponseBody, + 0, + bodySize as u32, + ); + const bodyStr = String.UTF8.decode(bodyBytes); + log( + LogLevelValues.info, + "Response body (" + bodySize.toString() + " bytes): " + bodyStr, + ); + } else { + log(LogLevelValues.info, "Response body: empty"); + } +} + +class HttpCallRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new HttpCallContext(context_id, this); + } +} + +class HttpCallContext extends Context { + httpCallDispatched: bool = false; + + constructor(context_id: u32, root_context: HttpCallRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // FastEdge re-invokes this hook after the httpCall response is processed. + // The latch gates re-dispatch so the second invocation returns Continue + // instead of firing another HTTP call. See SDK docs → Outbound HTTP. + if (this.httpCallDispatched) { + log( + LogLevelValues.info, + "HTTP call response received, resuming request.", + ); + return FilterHeadersStatusValues.Continue; + } + + log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); + + const headers = new Array(); + headers.push(makeHeaderPair(":scheme", "https")); + headers.push(makeHeaderPair(":authority", "httpbin.org")); + headers.push(makeHeaderPair(":path", "/ip")); + headers.push(makeHeaderPair(":method", "GET")); + headers.push(makeHeaderPair("User-Agent", "fastedge")); + + // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. + const result = (this.root_context as HttpCallRoot).httpCall( + "httpbin.org", + headers, + new ArrayBuffer(0), + new Array(), + 3000, + this, + handleHttpCallResponse, + ); + + if (result != WasmResultValues.Ok) { + log( + LogLevelValues.error, + "Failed to dispatch HTTP call: " + result.toString(), + ); + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Failed to dispatch HTTP call"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + this.httpCallDispatched = true; + log(LogLevelValues.info, "HTTP call dispatched, pausing request"); + + return FilterHeadersStatusValues.StopIteration; + } +} + +registerRootContext((context_id: u32) => { + return new HttpCallRoot(context_id); +}, "httpCall"); +``` + +### FILE: examples/httpCall/package.json + +```json +{ + "name": "fastedge-as-example-http-call", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: HTTP Call — async HTTP dispatch with callback", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/httpCall/README.md + +``` +[← Back to examples](../README.md) + +# HTTP Call + +This application makes an asynchronous HTTP call to an external service using the proxy-wasm HTTP dispatch API. + +## What it does + +In `onRequestHeaders`, the app dispatches an outbound HTTP GET request to `httpbin.org/ip` and pauses the hook (`StopIteration`) until the response arrives. Dispatch is latched on an instance field so the second invocation of the hook (after the response is processed) returns `Continue` instead of dispatching again. + +Inside the response callback passed to `httpCall`: + +1. Checks whether the call succeeded (a `headers` value of `0` indicates failure — timeout, DNS error, etc.). +2. Reads and logs the `User-Agent` response header via `stream_context.headers.http_callback`. +3. Reads and logs the response body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`. + +If the dispatch itself fails (e.g. invalid arguments), the app returns a `500` error to the client. + +## Key concepts + +- **`httpCall()`** on `RootContext` dispatches an async HTTP request. It accepts a cluster (host), headers, body, trailers, a timeout in milliseconds, the originating context, and a callback. +- **FastEdge resume model.** Returning `FilterHeadersStatusValues.StopIteration` pauses the hook. The runtime processes the HTTP response, invokes the callback, then **re-invokes `onRequestHeaders` on the same `Context` instance**. The `httpCallDispatched` latch gates re-dispatch so the hook returns `Continue` the second time. This differs from canonical proxy-wasm — calling `continueRequest()` is not required (and has no effect on FastEdge). +- **`stream_context.headers.http_callback`** provides access to the HTTP call response headers inside the callback. The SDK sets the effective context before the callback fires, so these lookups resolve against the originating request. +- **`get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`** reads the response body inside the callback. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | +| `build/httpCall-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/httpCall.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. +``` diff --git a/plugins/gcore-fastedge-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 3959d1b..35d0930 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-08-17 + updated: 2026-08-20 --> # HTTP Call — CDN (Rust) @@ -365,6 +365,7 @@ fn to_status_code(status: Status) -> u32 { - 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`. +- The callback may fire before or after response hooks depending on runtime timing — state tracking via struct fields is the correct synchronization mechanism. --- 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 dd4a9e0..0a8fd7b 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -165,12 +165,14 @@ if (!end_of_stream) { **Execution flow**: 1. Read query string: `get_property("request.query")` → `ArrayBuffer`, decode to string -2. Validate query params via `validateQueryParams(query)` → `Map` -3. Open store: `KvStore.open(store)` — send error response if `null` -4. Dispatch on `action` parameter -5. Serialize result map with `stringifyMap` → JSON string -6. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` -7. Returns `FilterDataStatusValues.Continue` +2. If query string is empty, send error response immediately +3. Validate query params via `validateQueryParams(query)` → `Map` +4. Check `params.has("error")` — send error response if validation failed +5. Open store: `KvStore.open(store)` — send error response if `null` +6. Dispatch on `action` parameter +7. Serialize result map with `stringifyMap` → JSON string +8. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` +9. Returns `FilterDataStatusValues.Continue` **Error path**: calls `sendErrorResponse(msg, body_buffer_length)` which: - Sets `response.status` to `545` via `set_property("response.status", ...)` @@ -208,11 +210,13 @@ switch (action) { const storeArrBuff = myStore.get(key); // null → responseBodyMap.set("Response", "null (Not found)") // non-null → responseBodyMap.set("Response", String.UTF8.decode(storeArrBuff)) + break; } case "scan": { const match = params.get("match"); const keys = myStore.scan(match); // returns string[] — responseBodyMap.set("Response", keys.join(", ")) + break; } case "zrange": { const key = params.get("key"); @@ -220,18 +224,21 @@ switch (action) { const max = params.get("max"); const tuples = myStore.zrangeByScore(key, parseFloat(min), parseFloat(max)); // returns ValueScoreTuple[] + break; } case "zscan": { const key = params.get("key"); const match = params.get("match"); const tuples = myStore.zscan(key, match); // returns ValueScoreTuple[] + break; } case "bfExists": { const key = params.get("key"); const item = params.get("item"); const exists = myStore.bfExists(key, item); // returns bool → responseBodyMap.set("Response", exists ? "true" : "false") + break; } } ``` @@ -252,7 +259,7 @@ Error response: { "error": "" } ``` -Error HTTP status: `545` (set via `response.status` property). +Error HTTP status: `545` (set via `response.status` property — advisory only). --- @@ -274,6 +281,14 @@ Internal helper (not exported). Splits query string on `&`, handles `key=value` Decodes `%xx` hex sequences and converts `+` to space. If `%xx` is not a valid hex sequence, passes through literally. +### `stringifyMap(map: Map): string` + +Exported helper. Serializes a `Map` to a JSON object string. Iterates keys in insertion order, produces `{ "key": "value", ... }` format. + +### `stringifyValueScoreTuples(arr: Array): string` + +Exported helper. Serializes an array of `ValueScoreTuple` to a comma-separated string of `{ value: , score: }` entries. Decodes each `tuple.value` (`ArrayBuffer`) with `String.UTF8.decode`. + --- ## Logging @@ -323,6 +338,7 @@ npm run asbuild:release # release only - `response.status` set via `set_property` in `onResponseBody` is advisory only — the origin HTTP status passes through to the client; the JSON error body is the authoritative error signal - Empty query string is treated as an error — app responds with `545` and a JSON error body - `validateQueryParams` returns a map containing key `"error"` on failure; the caller must check `params.has("error")` before proceeding +- `ArrayBuffer` decoding: both `KvStore.get` return values and `ValueScoreTuple.value` fields must be decoded with `String.UTF8.decode` before use as strings --- diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md index c6d043a..56aad05 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -165,7 +165,7 @@ let result = match action { "zrange" => self.handle_zrange(&store, ¶ms), "zscan" => self.handle_zscan(&store, ¶ms), "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!("Invalid action '{}'", action)), + _ => Err(format!("Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", action)), }; ``` @@ -240,276 +240,3 @@ Sorted set entries shape: `[{"value": "", "score": }, ...]` - Host services reference — full KV store, secrets, and dictionary API documentation for CDN (proxy-wasm) apps - CDN apps reference — proxy-wasm lifecycle hooks, request properties, header and body manipulation patterns - SDK reference (Rust) — `fastedge` crate modules, feature flags, and component model vs. proxy-wasm differences - -## Source Material - -### FILE: examples/cdn/key_value/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating KV Store operations via the proxy-wasm 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 fastedge::proxywasm::key_value::Store; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde_json::json; -use std::collections::HashMap; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(KvStoreRoot) }); -}} - -struct KvStoreRoot; - -impl Context for KvStoreRoot {} - -impl RootContext for KvStoreRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(KvStoreContext)) - } -} - -struct KvStoreContext; - -impl Context for KvStoreContext {} - -impl HttpContext for KvStoreContext { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Remove content-length since we replace the body - self.set_http_response_header("content-length", None); - self.set_http_response_header("content-type", Some("application/json")); - self.set_http_response_header("transfer-encoding", Some("chunked")); - 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 query = self - .get_property(vec!["request", "query"]) - .and_then(|bytes| String::from_utf8(bytes).ok()) - .unwrap_or_default(); - - if query.is_empty() { - self.send_error("App must be called with query parameters", body_size); - return Action::Continue; - } - - let params: HashMap<&str, &str> = querystring::querify(&query).into_iter().collect(); - - let Some(store_name) = params.get("store") else { - self.send_error("Missing required param 'store'", body_size); - return Action::Continue; - }; - - let action = params.get("action").copied().unwrap_or("get"); - - let store = match Store::open(store_name) { - Ok(s) => s, - Err(e) => { - self.send_error(&format!("Failed to open KvStore '{}': {}", store_name, e), body_size); - return Action::Continue; - } - }; - - let result = match action { - "get" => self.handle_get(&store, ¶ms), - "scan" => self.handle_scan(&store, ¶ms), - "zrange" => self.handle_zrange(&store, ¶ms), - "zscan" => self.handle_zscan(&store, ¶ms), - "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!( - "Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", - action - )), - }; - - let body = match result { - Ok(json) => json, - Err(msg) => { - self.send_error(&msg, body_size); - return Action::Continue; - } - }; - - self.set_http_response_body(0, body_size, body.as_bytes()); - - Action::Continue - } -} - -impl KvStoreContext { - fn handle_get(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'get' action")?; - 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(format!("KV get error: {}", e)), - } - } - - fn handle_scan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'scan' action")?; - 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(format!("KV scan error: {}", e)), - } - } - - fn handle_zrange(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zrange' action")?; - let min: f64 = params - .get("min") - .ok_or("Missing required param 'min' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'min' value: must be a number".to_string())?; - let max: f64 = params - .get("max") - .ok_or("Missing required param 'max' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'max' value: must be a number".to_string())?; - - 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(format!("KV zrange error: {}", e)), - } - } - - fn handle_zscan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zscan' action")?; - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'zscan' action")?; - - 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(format!("KV zscan error: {}", e)), - } - } - - fn handle_bf_exists(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'bfExists' action")?; - let item = *params.get("item").ok_or("Missing required param 'item' for 'bfExists' action")?; - - 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(format!("KV bfExists error: {}", e)), - } - } - - fn send_error(&self, msg: &str, body_size: usize) { - println!("{}", msg); - self.set_property( - vec!["response", "status"], - Some(b"500"), - ); - let error_body = json!({"error": msg}).to_string(); - self.set_http_response_body(0, body_size, error_body.as_bytes()); - } -} -``` - - -### FILE: examples/cdn/key_value/Cargo.toml - -```toml -[workspace] - -[package] -name = "key_value" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -querystring = "1.1" -serde_json = "1" -``` - - -### FILE: examples/cdn/key_value/README.md - -``` -[← Back to examples](../../README.md) - -# Key Value (CDN) - -Implements KV store operations via query parameters — get, scan, zrange, zscan, and bfExists — using the proxy-wasm ABI. -``` 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 d1e2c6e..4c0c294 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-08-17 + updated: 2026-08-20 --> # A/B Testing — FastEdge Example @@ -208,9 +208,9 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **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 +## Source ```js import { getEnv } from 'fastedge::env'; @@ -232,8 +232,6 @@ async function eventHandler({ 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', { @@ -243,7 +241,6 @@ async function eventHandler({ request }) { 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', @@ -261,13 +258,10 @@ addEventListener('fetch', (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); @@ -277,7 +271,6 @@ const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { return [xid, headers]; } const randomXid = `${Math.random()}`.slice(1, 5); - // Request does not contain A/B cookie, return random number return [randomXid, headers]; }; @@ -307,21 +300,3 @@ const createAbTestHeaders = (reqHeaders, testConfig, xid) => { 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 d754a7e..6de33d5 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-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,114 @@ 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 21d90c6..d9377ed 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-08-17 + updated: 2026-08-20 --> --- @@ -150,162 +150,3 @@ 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-js.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-js.md index 7032040..c3bc2f1 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-js.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> # FastEdge Cache — JavaScript Examples @@ -331,3 +331,279 @@ Validation errors (e.g., conflicting `WriteOptions`) are thrown synchronously. H - platform-overview (POP topology, request lifecycle) - deploy skill reference (fastedge-build CLI, binary upload) - best-practices (error handling patterns, response construction) + +## 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 { + // `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 { + // 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 { + 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 { + 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/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 2f17c5e..7fe9ca6 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-08-17 + updated: 2026-08-20 --> --- @@ -133,77 +133,3 @@ 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-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md index 3766914..99f207d 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-08-17 + updated: 2026-08-20 --> --- @@ -378,277 +378,3 @@ 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-outbound-fetch-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md index f25c8e7..50f4454 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-08-17 + updated: 2026-08-20 --> --- @@ -186,120 +186,3 @@ 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 6be9762..83b2f9f 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-08-17 + updated: 2026-08-20 --> --- @@ -183,7 +183,7 @@ Ok(Response::builder() - wstd HTTP client documentation - serde_json crate documentation -## Source Material +## Full Source ### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md index a1963fe..e9a2822 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,3 +250,178 @@ Returns: `ArrayBuffer | null` — `null` if the key does not exist. - examples/kv-store — KV-backed caching patterns - platform-overview reference — plan limits, execution budget, and body size constraints - sdk-reference-js reference — full KvStore API + +## Source Material + +### FILE: docs/PROXY_PATTERNS.md + +``` + + +# Proxy and Response Transform Patterns + +FastEdge HTTP apps can act as a thin proxy in front of an origin or upstream API — fetching a response, transforming it, and returning the result. This document covers the common proxy and transform patterns. + +## Simple Proxy + +Fetch from an upstream and return the body unchanged: + +```typescript +async function handle(request) { + const url = new URL(request.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: request.method, + headers: request.headers, + body: request.method !== "GET" && request.method !== "HEAD" + ? await request.arrayBuffer() + : undefined, + }); + + return response; +} + +addEventListener("fetch", (event) => { + event.respondWith(handle(event.request)); +}); +``` + +`fetch()` returns a streamable `Response`; returning it directly forwards body chunks without buffering everything in memory. + +## Proxy with JSON Transform + +Modify the response body before returning it. This is the pattern from `examples/outbound-modify-response/`: + +```typescript +async function handle() { + const upstream = await fetch("https://jsonplaceholder.typicode.com/users"); + const users = await upstream.json(); + + const transformed = { + users: users.slice(0, 5), + total: 5, + skip: 0, + limit: 30, + }; + + return new Response(JSON.stringify(transformed), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +addEventListener("fetch", (event) => { + event.respondWith(handle()); +}); +``` + +Reading `await upstream.json()` consumes the body; you cannot read it again. Read the body once, transform what you need, then return. + +## Hono Proxy with Transform + +Inside a Hono app, use `c.req.raw.headers` to forward the inbound headers and `c.req.arrayBuffer()` for the body: + +```typescript +import { Hono } from "hono"; + +const app = new Hono(); + +app.all("/api/*", async (c) => { + const url = new URL(c.req.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: c.req.method, + headers: c.req.raw.headers, + body: c.req.method !== "GET" && c.req.method !== "HEAD" + ? await c.req.arrayBuffer() + : undefined, + }); + + const data = await response.json(); + data.processedAt = new Date().toISOString(); + return c.json(data, response.status); +}); + +addEventListener("fetch", (event) => { + event.respondWith(app.fetch(event.request)); +}); +``` + +## Header Manipulation in Proxies + +Strip hop-by-hop headers before forwarding to upstream, and add diagnostic headers on the way back: + +```typescript +async function handle(request) { + const upstreamHeaders = new Headers(request.headers); + // Hop-by-hop headers should not be forwarded + upstreamHeaders.delete("connection"); + upstreamHeaders.delete("keep-alive"); + upstreamHeaders.delete("transfer-encoding"); + + const upstream = await fetch("https://backend.example.com", { + method: request.method, + headers: upstreamHeaders, + }); + + const responseHeaders = new Headers(upstream.headers); + responseHeaders.set("X-Proxied-By", "FastEdge"); + + return new Response(upstream.body, { + status: upstream.status, + headers: responseHeaders, + }); +} +``` + +`new Response(upstream.body, ...)` streams the body through without reading it into memory — preferred for large responses. + +## Cache-aware Proxy with KV + +Cache upstream responses in the KV store to avoid repeated outbound calls. Note: KV is read-only from app code; writes happen via the portal/API: + +```typescript +import { KvStore } from "fastedge::kv"; + +async function handle(request) { + const url = new URL(request.url); + + try { + const cache = KvStore.open("api-cache"); + const cached = cache.get(url.pathname); + if (cached !== null) { + return new Response(cached, { + status: 200, + headers: { "content-type": "application/json", "x-cache": "hit" }, + }); + } + } catch { + // KV store unavailable — fall through to upstream fetch + } + + const upstream = await fetch(`https://backend.example.com${url.pathname}`); + return new Response(await upstream.arrayBuffer(), { + status: upstream.status, + headers: { ...Object.fromEntries(upstream.headers), "x-cache": "miss" }, + }); +} +``` + +`KvStore.open(name)` returns a `KvStoreInstance` (it does not return null) but can throw if the named store is not provisioned — wrap the open call in `try/catch`. `cache.get(key)` returns `ArrayBuffer | null`; check for `null`, not falsy, since an empty buffer is a valid value. + +## Operational Notes + +- **Outbound fetch budget.** Each invocation has a limited number of outbound requests (5 on Basic, 20 on Pro). Parallelise where possible with `Promise.all([...])` instead of sequential `await fetch(...)`. +- **Execution time budget.** Proxying upstream + transforming counts against the 50ms (Basic) / 200ms (Pro) execution budget. Slow upstreams will trip 532 timeouts. +- **Body size limits.** Inbound and outbound bodies are subject to the configured request/response size limits. Stream where possible rather than buffering with `arrayBuffer()` / `text()` / `json()`. + +## See Also + +- `examples/outbound-modify-response/` — JSON transform of upstream response +- `examples/outbound-fetch/` — basic outbound `fetch()` patterns +- `examples/headers/` — request/response header manipulation +- `examples/kv-store/` — KV-backed caching patterns + +``` 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 2f2a35a..f789dad 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-08-17 + updated: 2026-08-20 --> --- @@ -182,3 +182,236 @@ cargo build --release - sdk-reference-rust (`fastedge::send_request`, `Body`, `Request`, `Response`) - host-services-rust (outbound HTTP) - rusty-s3 crate documentation (external) + +## Source Material + +### FILE: examples/http/basic/s3upload/src/lib.rs + +```rust +use std::time::Duration; + +use fastedge::{ + body::Body, + http::{header, Error, Method, Request, Response, StatusCode}, +}; +use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle}; +use std::{collections::HashMap, env}; +use url::Url; + +#[fastedge::http] +fn main(req: Request) -> Result, Error> { + match req.method() { + // Allow only POST and PUT requests + &Method::POST | &Method::PUT => (), + + &Method::OPTIONS => { + return Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()); + } + + // Deny anything else + _ => { + return Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header(header::ALLOW, "PUT, POST") + .body(Body::from("This method is not allowed\n")); + } + }; + + /* get request params */ + let query_pairs = |q: &str| { + q.split('&') + .filter_map(|q| { + let mut i = q.splitn(2, '='); + let k = i.next()?; + let v = i.next()?; + Some((k, v)) + }) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>() + }; + let hash_query: HashMap = req.uri().query().map_or(HashMap::new(), query_pairs); + + let fname = match hash_query.get("name") { + None => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")) + } + Some(i) => i, + }; + if req.body().is_empty() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")); + } + let content_type = match req.headers().get("Content-Type") { + None => "application/octet-stream", + Some(v) => v.to_str().unwrap_or("application/octet-stream"), + }; + let content_type = content_type.to_owned(); + let content = req.into_body(); + + match env::var("MAX_FILE_SIZE").ok() { + None => {} + Some(l) => match l.parse::() { + Err(_) => {} + Ok(v) => { + if content.len() > v { + let msg = format!("File exceeds allowed limit of {} bytes\n", v); + return Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::from(msg.as_str().to_owned())); + } + } + }, + } + + let (signed_url, host) = match prepare_s3(fname) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("App misconfigured\n")) + } + Ok((u, h)) => (u, h), + }; + + /* build outgoing req */ + let out_req = Request::builder() + .method(Method::PUT) + .uri(signed_url.as_str()) + .header("Host", host) + .header("Accept-Encoding", "identity") + .header("Content-Length", content.len().to_string()) + .header("Content-Type", content_type); + + let Ok(req) = out_req.body(content) else { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Malformed request\n")); + }; + + let rsp = match fastedge::send_request(req) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + } + Ok(r) => r, + }; + let (parts, body) = rsp.into_parts(); + let body = if parts.status == StatusCode::OK { + let mut tmp_url = signed_url.clone(); + tmp_url.set_query(None); + Body::from(tmp_url.to_string()) + } else { + body + }; + Ok(Response::from_parts(parts, body)) +} + +fn prepare_s3(fname: &str) -> anyhow::Result<(Url, String)> { + /* read S3 access params from env */ + let access_key = env::var("ACCESS_KEY")?; + let secret_key = env::var("SECRET_KEY")?; + let region = env::var("REGION")?; + let base_hostname = env::var("BASE_HOSTNAME")?; + let bucket = env::var("BUCKET")?; + let scheme = env::var("SCHEME").unwrap_or_else(|_| "http".to_string()); + + /* set S3 request params */ + let host = region.clone() + "." + base_hostname.as_str(); + let upload_url = scheme + "://" + host.as_str(); + let parsed_url = upload_url.parse()?; + let bucket = Bucket::new(parsed_url, UrlStyle::Path, bucket, region)?; + + let creds = Credentials::new(access_key, secret_key); + let action = bucket.put_object(Some(&creds), fname); + let signed_url = action.sign(Duration::from_secs(60 * 60)); + + Ok((signed_url, host)) +} +``` + + +### FILE: examples/http/basic/s3upload/Cargo.toml + +```toml +[workspace] + +[package] +name = "s3upload" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +url = "2.3" +rusty-s3 = "0.5" +anyhow = "1" +``` + + +### FILE: examples/http/basic/s3upload/README.md + +``` +[← Back to examples](../../../README.md) + +# S3 Upload + +FastEdge edge function that accepts a file upload, signs an S3 PUT request on the fly, uploads the file directly to an S3-compatible bucket, and returns the clean object URL to the caller. + +> **Legacy handler:** Uses `#[fastedge::http]` (sync, `wasm32-wasip1`). For new apps prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +1. Accepts `POST` or `PUT` only — returns 405 for other methods +2. Requires `?name=` query parameter and a non-empty body — returns 400 otherwise +3. Enforces `MAX_FILE_SIZE` if set — returns 413 if exceeded +4. Calls `prepare_s3()` to build a 1-hour presigned `PUT` URL using `rusty_s3` +5. Forwards the file body to S3 via `fastedge::send_request` +6. On success (S3 returns 200): responds with the clean object URL (no query string) +7. On S3 error: forwards the S3 status and error body back to the caller + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ACCESS_KEY` | ✅ | S3 access key | +| `SECRET_KEY` | ✅ | S3 secret key | +| `REGION` | ✅ | S3 region (e.g. `s-ed1`) | +| `BASE_HOSTNAME` | ✅ | S3 base hostname (e.g. `cloud.gcore.lu`) | +| `BUCKET` | ✅ | S3 bucket name | +| `SCHEME` | optional | URL scheme — defaults to `http` | +| `MAX_FILE_SIZE` | optional | Maximum upload size in bytes — no limit if unset | + +The constructed endpoint is `://.//`. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/s3upload.wasm +``` + +## Usage + +``` +POST /upload?name=photo.jpg +Content-Type: image/jpeg + + +``` + +On success (200), the response body is the clean S3 object URL (presign query parameters stripped). + +## Notes + +- The `OPTIONS` method returns 204 but does **not** include CORS headers — add `Access-Control-Allow-*` headers if browser preflight support is needed. +- The presigned URL expires after 1 hour, but since the upload is performed server-side this has no practical impact. +- `MAX_FILE_SIZE` is silently ignored if set to a non-numeric value. +``` 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 3d9715b..d65296b 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-08-17 + updated: 2026-08-20 --> # Secret Access — Rust HTTP Example @@ -251,185 +251,3 @@ 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-secret-rollover-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md index ac3b308..df75287 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # Secret Rollover (WASI, Rust) @@ -124,28 +124,56 @@ Slots are ordered by slot number; any non-negative `u32` value is valid as a slo ## Implementation Pattern ```rust -use fastedge::secret; +use std::time::{SystemTime, UNIX_EPOCH}; -// Read slot from header, default to current unix timestamp -let slot: u32 = request - .headers() - .get("x-slot") - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.parse().ok()) - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as u32 +use anyhow::anyhow; +use fastedge::secret; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + // Read slot from header, default to current unix timestamp + let slot: u32 = request + .headers() + .get("x-slot") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as u32 + }); + + let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + + // Get current (latest) value + let current = secret::get(secret_name) + .map_err(|e| anyhow!("secret::get failed: {e}"))?; + + // Get 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}"))?; + + let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, }); -// Get current (latest) value -let current = secret::get(secret_name) - .map_err(|e| anyhow!("secret::get failed: {e}"))?; - -// Get 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}"))?; + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} ``` ## Constraints and Gotchas 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 3370f1b..5d19fe4 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-08-17 + updated: 2026-08-20 --> --- @@ -147,123 +147,3 @@ 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-smart-switch-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md index 4be6846..caea588 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -183,6 +183,7 @@ Build output: `target/wasm32-wasip1/release/smart_switch.wasm` - **`fastedge::send_request` errors map to `StatusCode`**: These are not `anyhow::Error` or `std::error::Error`; match on `fastedge::Error` variants explicitly. - **`"ACCEPTED"` string determines success**: The command result is compared as a string; only `"ACCEPTED"` maps to `204`. Any other value maps to `404`. - **Auth is a plain header match**: `Authorization` header value is compared directly to `PASSWORD` env var — no bearer prefix stripping, no hashing. +- **`serde` listed as dependency but not directly used in source**: `serde_json` handles all JSON parsing; `serde` is a transitive requirement. --- 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 b8e56f8..8a6e7c9 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-08-17 + updated: 2026-08-20 --> --- @@ -143,105 +143,3 @@ 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/templates/catalog.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md index 91bb0f1..45de29b 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/catalog.md @@ -4,7 +4,7 @@ - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 - updated: 2026-08-17 + updated: 2026-08-20 --> # FastEdge Bolt-On Templates @@ -41,6 +41,8 @@ Before writing authentication, cookie-hardening, or content-transform logic from **Attribute application:** `Secure` and `HttpOnly` are added only if not already present. `SameSite=Strict` is set unconditionally, overriding any existing `SameSite` value. +**Implementation notes:** Duplicate `Set-Cookie` headers are preserved — the host collapses them to one, so the filter clears the header and re-adds each cookie as its own occurrence. Empty segments from trailing semicolons are dropped when re-joining. Headers are only rewritten when at least one cookie value actually changes. + **Origin integration:** Configured entirely through environment variables (`COOKIE_NAME`, `SECURE`, `HTTPONLY`, `SAMESITE`) — no origin code required. --- @@ -63,9 +65,9 @@ Before writing authentication, cookie-hardening, or content-transform logic from **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). +**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). `AUTH_PREFIX` sets the path prefix reserved for auth routes (default: `/auth`). -**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. +**Origin integration:** This template has an integration reference. 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. --- @@ -88,7 +90,9 @@ Before writing authentication, cookie-hardening, or content-transform logic from - 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 +- The edge `mfa_session` is short-lived (8h, non-sliding) and not cross-PoP revocable +- CDN logs include the session subject (`sub`) and request path on each authorized request — review log retention policy if `sub` contains PII **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. +**Origin integration:** This template has an integration reference. 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-totp-integration.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/templates/edge-totp-integration.md index 4b3a82a..61b1457 100644 --- 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 @@ -4,7 +4,7 @@ - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 - updated: 2026-08-17 + updated: 2026-08-20 --> # edge-totp — Origin Integration @@ -107,7 +107,7 @@ Self-service enrollment (`{AUTH_PREFIX}/activate`) inherits the trust level of t **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 the edge-totp threat model reference (threat-model.md, risk R5) before relying on self-service enrollment for sensitive accounts. --- 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 d2b1159..695920d 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -222,233 +222,3 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide - -## Source Material - -### FILE: examples/abTesting/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - set_property, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; -import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; - -class AbTestingRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new AbTestingContext(context_id, this); - } -} - -class AbTestingContext extends Context { - constructor(context_id: u32, root_context: AbTestingRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const experimentName = getEnv("EXPERIMENT_NAME"); - if (experimentName === "") { - send_http_response( - 500, - "internal server error", - String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const variantAPath = getEnv("VARIANT_A_PATH"); - const variantBPath = getEnv("VARIANT_B_PATH"); - if (variantAPath === "" || variantBPath === "") { - send_http_response( - 500, - "internal server error", - String.UTF8.encode( - "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", - ), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // Check for existing experiment cookie - const cookieName = "fe_exp_" + experimentName; - const cookieHeader = stream_context.headers.request.get("Cookie"); - let assignedVariant = this.getCookieValue(cookieHeader, cookieName); - - // Assign variant if not already set - if (assignedVariant !== "A" && assignedVariant !== "B") { - // Use current time as a simple entropy source for 50/50 split - const now = getCurrentTime(); - assignedVariant = now % 2 == 0 ? "A" : "B"; - } - - // Rewrite the request path to the variant path - const pathArrBuf = get_property("request.path"); - if (pathArrBuf.byteLength === 0) { - return FilterHeadersStatusValues.Continue; - } - const originalPath = String.UTF8.decode(pathArrBuf); - const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; - const newPath = variantPath + originalPath; - - // Reconstruct request.url from its decomposed parts rather than splicing - // the path out of the full URL — splicing breaks when the path happens to - // appear inside the host, and it can silently lose the query string. - const schemeBuf = get_property("request.scheme"); - const hostBuf = get_property("request.host"); - if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { - const scheme = String.UTF8.decode(schemeBuf); - const host = String.UTF8.decode(hostBuf); - const queryBuf = get_property("request.query"); - const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; - const newUrl = - scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); - log(LogLevelValues.info, `A/B routing: ${newUrl}`); - set_property("request.url", String.UTF8.encode(newUrl)); - } - - // Add variant header for upstream visibility - stream_context.headers.request.add("X-Experiment", experimentName); - stream_context.headers.request.add("X-Variant", assignedVariant); - - log( - LogLevelValues.info, - `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, - ); - - return FilterHeadersStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Recover the assigned variant from the request header set in onRequestHeaders. - // Instance state (this.variant) does not survive the nginx -> core-proxy hop. - const variant = stream_context.headers.request.get("X-Variant"); - if (variant === "") { - return FilterHeadersStatusValues.Continue; - } - - const experimentName = getEnv("EXPERIMENT_NAME"); - const cookieName = "fe_exp_" + experimentName; - - // Set the experiment cookie so subsequent requests stick to the same variant - stream_context.headers.response.add( - "Set-Cookie", - cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", - ); - - // Add variant as response header for observability - stream_context.headers.response.add("X-Variant", variant); - - return FilterHeadersStatusValues.Continue; - } - - private getCookieValue(cookieHeader: string, name: string): string { - if (cookieHeader === "") return ""; - const pairs = cookieHeader.split(";"); - const prefix = name + "="; - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i].trim(); - if (pair.startsWith(prefix)) { - return pair.substring(prefix.length); - } - } - return ""; - } -} - -registerRootContext((context_id: u32) => { - return new AbTestingRoot(context_id); -}, "abTesting"); -``` - - -### FILE: examples/abTesting/package.json - -```json -{ - "name": "fastedge-as-example-ab-testing", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/abTesting/README.md - -``` -[← Back to examples](../README.md) - -# A/B Testing - -This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. - -## What it does - -In `onRequestHeaders`, the app: - -1. Checks for an existing experiment cookie (`fe_exp_`). -2. If no cookie is found, assigns the user to variant **A** or **B** (50/50 split). -3. Rewrites the request path by prepending the variant-specific path prefix (e.g. `/variant-a/original/path`). -4. Adds `X-Experiment` and `X-Variant` request headers for upstream visibility. - -In `onResponseHeaders`, the app sets a `Set-Cookie` header to persist the variant assignment for subsequent requests (24-hour TTL). - -> **Note on variant assignment entropy:** New-visitor assignment uses `getCurrentTime() % 2` as a simple 50/50 source. This is illustrative — it is not sticky across two requests that arrive in the same millisecond and is not reproducible in tests. Production A/B implementations typically hash a stable visitor identifier (e.g. client IP or session token) for deterministic, sticky pre-cookie assignment. - -## Configuration - -Set the following environment variables on your FastEdge application: - -| Variable | Example | Description | -|----------|---------|-------------| -| `EXPERIMENT_NAME` | `homepage-redesign` | Name of the experiment (required) | -| `VARIANT_A_PATH` | `/variant-a` | Path prefix for variant A (required) | -| `VARIANT_B_PATH` | `/variant-b` | Path prefix for variant B (required) | - -Your origin server should serve different content at each variant path prefix. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/abTesting.wasm` | Optimised release binary — upload this to FastEdge | -| `build/abTesting-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/abTesting.wasm` to the FastEdge portal and attach it to your CDN application. Configure the experiment environment variables in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md index 4e444bf..4295b4b 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-08-17 + updated: 2026-08-20 --> --- @@ -212,122 +212,3 @@ 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 { Box::new(ApiKeyRoot) }); -}} - -struct ApiKeyRoot; - -impl Context for ApiKeyRoot {} - -impl RootContext for ApiKeyRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - 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 ff3c689..07cdb77 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -267,206 +267,3 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint - -## Source Material - -### FILE: examples/jwt/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class AuthRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes - return new Auth(context_id, this); - } -} - -class Auth extends Context { - constructor(context_id: u32, root_context: AuthRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const secret = getSecret("SECRET"); - if (!secret) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const authHeader = stream_context.headers.request.get("Authorization"); - if (!authHeader) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("No Authorization header"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - if (!authHeader.startsWith("Bearer ")) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Authorization header must use Bearer scheme"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const token = authHeader.slice(7); // strip "Bearer " prefix - if (!token) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Token not found"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // Decode the JWT token - const jwtResult = jwtVerify(token, secret); - if (jwtResult !== JwtValidation.Ok) { - if (jwtResult === JwtValidation.Expired) { - log(LogLevelValues.info, "Token Expired"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Expired token"), - [], - ); - } else { - log(LogLevelValues.info, "Bad Token"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Invalid token"), - [], - ); - } - return FilterHeadersStatusValues.StopIteration; - } - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new AuthRoot(context_id); -}, "auth"); -``` - - -### FILE: examples/jwt/package.json - -```json -{ - "name": "fastedge-as-example-jwt", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: JWT validation", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/as-jwt": "^1.0.3", - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", - "assemblyscript-json": "^1.1.0" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/jwt/README.md - -``` -[← Back to examples](../README.md) - -# JWT Validation - -This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. -2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. -3. Verifies the token signature and expiry using `jwtVerify()`. -4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. - -## Configuration - -Set the following secret variable on your FastEdge application: - -| Secret | Description | -| -------- | ------------------------------------------------------------------ | -| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | - -## Testing tokens - -**Expired token** (will return `403 Forbidden`): - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ -``` - -**Valid token** (expiry: 2035-01-01, will return `200 OK`): - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU -``` - -Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| ---------------------- | -------------------------------------------------- | -| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | -| `build/jwt-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/jwt.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. - -For more on secrets and secret rotation slots, see the [FastEdge secrets documentation](https://gcore.com/docs/fastedge/secrets-manager/slots). -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md index 28c7eda..c3da37d 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-08-17 + updated: 2026-08-20 --> --- @@ -259,3 +259,142 @@ 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 { 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 {})) + } + + fn get_type(&self) -> Option { + 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::() 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::(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 8b560bd..3f82231 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -15,7 +15,7 @@ template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN AssemblyScript @@ -263,3 +263,118 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) + +## Source Material + +### FILE: examples/helloWorld/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; + +class HelloWorldRoot extends RootContext { + createContext(context_id: u32): Context { + return new HelloWorld(context_id, this); + } +} + +class HelloWorld extends Context { + constructor(context_id: u32, root_context: HelloWorldRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool, + ): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + log(LogLevelValues.info, "onRequestBody >> Hello World!"); + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + log(LogLevelValues.info, "onResponseBody >> Hello World!"); + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HelloWorldRoot(context_id); +}, "helloWorld"); +``` + + +### FILE: examples/helloWorld/package.json + +```json +{ + "name": "fastedge-as-example-hello-world", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/helloWorld/asconfig.json + +```json +{ + "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", + "targets": { + "debug": { + "outFile": "build/helloWorld-debug.wasm", + "textFile": "build/helloWorld-debug.wat", + "sourceMap": true, + "debug": true + }, + "release": { + "outFile": "build/helloWorld.wasm", + "textFile": "build/helloWorld.wat", + "sourceMap": true, + "optimizeLevel": 3, + "shrinkLevel": 0, + "converge": false, + "noAssert": false + } + }, + "options": { + "bindings": "esm", + "use": "abort=abort_proc_exit" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md index d4f417a..d3ad5df 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-08-17 + updated: 2026-08-20 --> --- @@ -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-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN Rust @@ -208,3 +208,77 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { Action::Continue } ``` + +## 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 { Box::new(HelloWorldRoot) }); +}} + +struct HelloWorldRoot; + +impl Context for HelloWorldRoot {} + +impl RootContext for HelloWorldRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(HelloWorld)) + } +} + +struct HelloWorld; + +impl Context for HelloWorld {} + +impl HttpContext for HelloWorld { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_headers"); + Action::Continue + } + + fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_body"); + Action::Continue + } + + 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 + } + + fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_response_body"); + 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 e3da1c6..837648f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -237,7 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. +Called at the end of the request lifecycle. Inherited from `Context`. Used for final audit logging. Not implemented in this example's source. ## Registration diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md index e8b9ba2..4b49934 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-08-17 + updated: 2026-08-20 --> --- @@ -243,125 +243,3 @@ 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 { 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)) - } -} - -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 2987a96..f49e70c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -187,191 +187,3 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) - -## Source Material - -### FILE: examples/cacheControl/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class CacheControlRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new CacheControlContext(context_id, this); - } -} - -class CacheControlContext extends Context { - constructor(context_id: u32, root_context: CacheControlRoot) { - super(context_id, root_context); - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const statusBuf = get_property("response.status"); - let statusCode: u32 = 200; - if (statusBuf.byteLength >= 2) { - const bytes = Uint8Array.wrap(statusBuf); - statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); - } - - // Only cache successful responses - if (statusCode < 200 || statusCode >= 400) { - stream_context.headers.response.replace( - "Cache-Control", - "no-store", - ); - return FilterHeadersStatusValues.Continue; - } - - // Determine cache policy based on content type - const contentType = stream_context.headers.response.get("Content-Type"); - - const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); - const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; - const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); - const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; - const rawApiMaxAge = getEnv("API_MAX_AGE"); - const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; - - let cacheControl: string; - - if (this.isStaticAsset(contentType)) { - // Static assets: long cache, immutable - cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; - } else if (contentType.includes("text/html")) { - // HTML: short cache, must revalidate - cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; - stream_context.headers.response.add("Vary", "Accept-Encoding"); - } else if ( - contentType.includes("application/json") || - contentType.includes("application/xml") - ) { - // API responses: configurable, private by default - if (apiMaxAge === "0") { - cacheControl = "no-cache, no-store, must-revalidate"; - } else { - cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; - } - stream_context.headers.response.add("Vary", "Accept, Authorization"); - } else { - // Default: moderate cache - cacheControl = "public, max-age=600"; - } - - stream_context.headers.response.replace("Cache-Control", cacheControl); - - log( - LogLevelValues.info, - "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", - ); - - return FilterHeadersStatusValues.Continue; - } - - private isStaticAsset(contentType: string): bool { - return ( - contentType.includes("image/") || - contentType.includes("font/") || - contentType.includes("application/javascript") || - contentType.includes("text/css") || - contentType.includes("text/javascript") || - contentType.includes("application/wasm") - ); - } -} - -registerRootContext((context_id: u32) => { - return new CacheControlRoot(context_id); -}, "cacheControl"); -``` - - -### FILE: examples/cacheControl/package.json - -```json -{ - "name": "fastedge-as-example-cache-control", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/cacheControl/README.md - -``` -[← Back to examples](../README.md) - -# Cache Control - -This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. - -## What it does - -In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: - -| Content Type | Cache Policy | Default Max-Age | -|---|---|---| -| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | -| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | -| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | -| Other | `public, max-age=600` | 10 minutes | -| Error responses (4xx/5xx) | `no-store` | — | - -Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). - -## Configuration - -All environment variables are optional — sensible defaults are applied when unset. - -| Variable | Default | Description | -|----------|---------|-------------| -| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | -| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | -| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | -| `build/cacheControl-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-rust.md index b49ed2a..0565961 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-08-17 + updated: 2026-08-20 --> --- @@ -219,3 +219,144 @@ 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 { Box::new(CacheControlRoot) }); +}} + +struct CacheControlRoot; + +impl Context for CacheControlRoot {} + +impl RootContext for CacheControlRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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 69d0e83..e156022 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-08-17 + updated: 2026-08-20 --> --- @@ -84,6 +84,9 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action **Logic:** 1. Always add `Image-Format: original` — serves as the cache key for the unconverted response. + ```rust + self.add_http_request_header("Image-Format", "original"); + ``` 2. Read `request.extension` property: ```rust let raw_ext = self.get_property(vec!["request.extension"]); @@ -284,300 +287,3 @@ 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 { 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/scaffold/reference/cdn/custom-error-pages-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-as.md index 0bdc459..bb0b1ac 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -118,13 +118,34 @@ onResponseBody( const description = this.getErrorDescription(code); const category = code >= 500 ? "Server Error" : "Client Error"; - const html = /* ... build HTML string ... */; + const html = + '' + + '' + + "" + code.toString() + " — " + title + "" + + "
" + + "

" + code.toString() + "

" + + "

" + title + "

" + + "

" + description + "

" + + "

" + category + "

" + + "
"; const body = String.UTF8.encode(html); + // Replace the entire original body — pass body_buffer_length as the length + // to replace, not body.byteLength. Otherwise any original bytes beyond the + // new body's length survive at the tail of the response. set_buffer_bytes( BufferTypeValues.HttpResponseBody, 0, - body_buffer_length as u32, // use original length, NOT body.byteLength + body_buffer_length as u32, body, ); @@ -158,18 +179,30 @@ private getErrorTitle(code: u32): string { } private getErrorDescription(code: u32): string { - if (code == 400) return "The server could not understand the request due to invalid syntax."; - if (code == 401) return "You need to authenticate to access this resource."; - if (code == 403) return "You do not have permission to access this resource."; - if (code == 404) return "The requested page could not be found. It may have been moved or deleted."; - if (code == 405) return "The request method is not supported for this resource."; - if (code == 408) return "The server timed out waiting for the request."; - if (code == 429) return "You have sent too many requests. Please try again later."; - if (code == 500) return "The server encountered an unexpected condition that prevented it from fulfilling the request."; - if (code == 502) return "The server received an invalid response from the upstream server."; - if (code == 503) return "The server is temporarily unavailable. Please try again later."; - if (code == 504) return "The server did not receive a timely response from the upstream server."; - if (code >= 500) return "The server encountered an error processing your request."; + if (code == 400) + return "The server could not understand the request due to invalid syntax."; + if (code == 401) + return "You need to authenticate to access this resource."; + if (code == 403) + return "You do not have permission to access this resource."; + if (code == 404) + return "The requested page could not be found. It may have been moved or deleted."; + if (code == 405) + return "The request method is not supported for this resource."; + if (code == 408) + return "The server timed out waiting for the request."; + if (code == 429) + return "You have sent too many requests. Please try again later."; + if (code == 500) + return "The server encountered an unexpected condition that prevented it from fulfilling the request."; + if (code == 502) + return "The server received an invalid response from the upstream server."; + if (code == 503) + return "The server is temporarily unavailable. Please try again later."; + if (code == 504) + return "The server did not receive a timely response from the upstream server."; + if (code >= 500) + return "The server encountered an error processing your request."; return "An error occurred processing your request."; } ``` @@ -259,6 +292,14 @@ pnpm run asbuild | `build/customErrorPages.wasm` | Optimised release binary — upload to FastEdge | | `build/customErrorPages-debug.wasm` | Debug binary with source maps | +Build scripts defined in `package.json`: + +| Script | Command | +|---|---| +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | runs both debug and release | + --- ## Deploy @@ -273,6 +314,7 @@ Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to you - **Buffer entire body before replacing**: Return `FilterDataStatusValues.StopIterationAndBuffer` until `end_of_stream` is true, then replace. - **Replace length must be `body_buffer_length`**: Using the new body's byte length as the replacement length will leave original response bytes at the tail. - **Status buffer is big-endian 2 bytes**: Always check `byteLength >= 2` before decoding. +- **Status lookup methods must be private class methods**: No closures, no default args on nested functions. --- diff --git a/plugins/gcore-fastedge-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 d8d3907..eca26cd 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-08-17 + updated: 2026-08-20 --> --- @@ -310,207 +310,3 @@ 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 { 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)) - } -} - -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/.jpg` -2. Add a message file: `public/messages/.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 de7cb9f..a95115f 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-08-17 + updated: 2026-08-20 --> --- @@ -88,7 +88,7 @@ impl HttpContext for HttpHeaders { return Action::Pause; }; - // Trim leading '/' + //trim first '/' let path = if path.starts_with('/') { &path[1..] } else { @@ -260,123 +260,3 @@ 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 { 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/scaffold/reference/cdn/env-secrets-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-as.md index 912df27..0a75003 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -179,138 +179,3 @@ Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to - cdn-base skeleton (base request/response handling structure) - fastedge-test reference (local WASM test runner and fixture format) - platform-overview reference (FastEdge application configuration and secret management) - -## Source Material - -### FILE: examples/variablesAndSecrets/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class VariablesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new VariablesContext(context_id, this); - } -} - -class VariablesContext extends Context { - constructor(context_id: u32, root_context: VariablesRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const username = getEnv("USERNAME"); - const password = getSecret("PASSWORD"); - - log(LogLevelValues.info, "USERNAME: " + username); - log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); - - stream_context.headers.request.add("x-env-username", username); - stream_context.headers.request.add("x-env-password", password); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new VariablesRoot(context_id); -}, "variablesAndSecrets"); -``` - -### FILE: examples/variablesAndSecrets/package.json - -```json -{ - "name": "fastedge-as-example-variables-and-secrets", - "version": "0.0.1", - "description": "FastEdge AssemblyScript example: Variables and Secrets", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/variablesAndSecrets/README.md - -``` -[← Back to examples](../README.md) - -# Variables and Secrets - -This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the `USERNAME` environment variable using `getEnv`. -2. Reads the `PASSWORD` secret using `getSecret`. -3. Logs that both values were retrieved (without logging the secret value itself). -4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. - -This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. - -> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -| ---------- | -------------------- | -------------------------------------- | -| `USERNAME` | Environment variable | The username value to forward upstream | -| `PASSWORD` | Secret | The password value to forward upstream | - -## Local testing - -The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. - -To test locally with the visual debugger, create `fixtures/.env`: - -``` -FASTEDGE_VAR_ENV_USERNAME=my-username -FASTEDGE_VAR_SECRET_PASSWORD=my-password -``` - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| -------------------------------------- | -------------------------------------------------- | -| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | -| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md index 9336819..e8a660a 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-08-17 + updated: 2026-08-20 --> --- @@ -175,104 +175,3 @@ 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 { Box::new(VariablesRoot) }); -}} - -struct VariablesRoot; - -impl Context for VariablesRoot {} - -impl RootContext for VariablesRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - 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 ae208d8..c505afe 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -266,185 +266,3 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) - -## Source Material - -### FILE: examples/geoRedirect/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - set_property, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const BAD_GATEWAY: u32 = 502; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class GeoRedirectRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new GeoRedirect(context_id, this); - } -} - -class GeoRedirect extends Context { - constructor(context_id: u32, root_context: GeoRedirectRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> "); - - const defaultOrigin = getEnv("DEFAULT"); - - if (!defaultOrigin) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured - DEFAULT must be set"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const countryArrBuf = get_property("request.country"); - if (countryArrBuf.byteLength === 0) { - send_http_response( - BAD_GATEWAY, - "bad gateway", - String.UTF8.encode("Missing country information"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - const countryCode = String.UTF8.decode(countryArrBuf); - const countrySpecificOrigin = getEnv(countryCode); - - log( - LogLevelValues.info, - `Country code: ( ${countryCode} ): ${ - countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin - }`, - ); - - const hostArrBuf = get_property("request.host"); - if (hostArrBuf.byteLength > 0) { - const host = String.UTF8.decode(hostArrBuf); - log(LogLevelValues.info, `Provided Host: ${host}`); - } - - const pathArrBuf = get_property("request.path"); - if (pathArrBuf.byteLength === 0) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("Internal server error - no request path"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const path = String.UTF8.decode(pathArrBuf); - const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; - // remove trailing slashes from the origin - const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; - - const requestUrl = `${cleanedOrigin}${path}`; - - log(LogLevelValues.info, `request-url: ${requestUrl}`); - - set_property("request.url", String.UTF8.encode(requestUrl)); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new GeoRedirectRoot(context_id); -}, "geoRedirect"); -``` - - -### FILE: examples/geoRedirect/package.json - -```json -{ - "name": "fastedge-as-example-georedirect", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Geo Redirect", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/geoRedirect/README.md - -``` -[← Back to examples](../README.md) - -# Geo Redirect - -This application redirects requests to different origin URLs based on the client's country code. - -## What it does - -In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. - -- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. -- Otherwise it falls back to the `DEFAULT` origin. -- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. - -> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. - -> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. - -## Configuration - -Set the following environment variables on your FastEdge application: - -| Variable | Example | Description | -|----------|---------|-------------| -| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | -| `` | `DE=https://de.example.com` | Per-country origin URL (optional, one per country) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | -| `build/geoRedirect-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/geoRedirect.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `DEFAULT` environment variable and any per-country overrides in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-rust.md index dab63e7..a71fca0 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-08-17 + updated: 2026-08-20 --> --- @@ -152,3 +152,128 @@ proxy_wasm::main! {{ - FastEdge-sdk-rust CDN examples overview - proxy-wasm HttpContext trait reference - host-services-rust reference (property access, header manipulation) + +## Source Material + +### FILE: examples/cdn/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating geo-based origin routing. + +Routes requests to country-specific origins based on the request's +geo-IP country code. Falls back to a DEFAULT origin when no +country-specific mapping is configured. + +Required configuration: + - Environment variable: DEFAULT (fallback origin URL) + - Environment variable: (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use proxy_wasm::traits::*; +use std::env; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoRedirectRoot) }); +}} + +struct GeoRedirectRoot; + +impl Context for GeoRedirectRoot {} + +impl RootContext for GeoRedirectRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(GeoRedirectContext)) + } +} + +struct GeoRedirectContext; + +impl Context for GeoRedirectContext {} + +impl HttpContext for GeoRedirectContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(default_origin) = env::var("DEFAULT") else { + self.send_http_response(500, vec![], Some(b"App misconfigured - DEFAULT must be set")); + return Action::Pause; + }; + + let country_code = self + .get_property(vec!["request.country"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(); + + if country_code.is_empty() { + self.send_http_response(502, vec![], Some(b"Missing country information")); + return Action::Pause; + } + + let origin = env::var(&country_code).unwrap_or(default_origin); + let origin = origin.trim_end_matches('/'); + + let path = self + .get_property(vec!["request.path"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| "/".to_string()); + + // Preserve the Host header if present + if let Some(host) = self + .get_property(vec!["request.host"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + { + self.set_http_request_header("Host", Some(&host)); + } + + let request_url = format!("{}{}", origin, path); + + println!("Redirecting to: {}", request_url); + + self.set_property(vec!["request.url"], Some(request_url.as_bytes())); + + Action::Continue + } +} +``` + + +### FILE: examples/cdn/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + + +### FILE: examples/cdn/geo_redirect/README.md + +``` +[← Back to examples](../../README.md) + +# Geo Redirect (CDN) + +Routes CDN requests to country-specific origins based on the geoIP country code using the proxy-wasm ABI. + +## Configuration + +- Environment variable: `DEFAULT` — fallback origin URL +- Environment variable: `` — optional per-country origin URLs (e.g. `US`, `DE`, `GB`) +``` 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 189abf0..1f39fe8 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-08-17 + updated: 2026-08-20 --> --- @@ -206,112 +206,3 @@ Output binary: `target/wasm32-wasip1/release/.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 { Box::new(GeoblockRoot) }); -}} - -struct GeoblockRoot; - -impl Context for GeoblockRoot {} - -impl RootContext for GeoblockRoot { - fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(GeoblockContext {})) - } - - fn get_type(&self) -> Option { - 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::() else { - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - - let Ok(tw_end) = tw_end.parse::() 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-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-as.md index bf8dc73..0c7d16c 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -280,6 +280,32 @@ Resulting effective request headers after mutation: The same add/remove/replace sequence is repeated identically in `onResponseHeaders` for response headers. +## Complete Mutation Sequence (Response Phase) + +```typescript +// Add headers +stream_context.headers.response.add("new-header-01", "value-01"); +stream_context.headers.response.add("new-header-02", "value-02"); +stream_context.headers.response.add("new-header-03", "value-03"); + +// Remove (sets to empty string in nginx — does not delete) +stream_context.headers.response.remove("new-header-01"); + +// Replace value +stream_context.headers.response.replace("new-header-02", "new-value-02"); + +// Add a second value for the same header name +stream_context.headers.response.add("new-header-03", "value-03-a"); +``` + +Resulting effective response headers after mutation: + +| Header | Value | +|---|---| +| `new-header-01` | `""` (empty — nginx cannot delete) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03` and `value-03-a` (multi-value) | + ## Class Structure ```typescript diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md index f2b6798..0fb98ea 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-08-17 + updated: 2026-08-20 --> --- @@ -327,3 +327,356 @@ Called after the request/response cycle completes. Use for per-request logging o - proxy-wasm Rust SDK reference (sdk-reference-rust) - FastEdge CDN app examples index (examples CDN) - Host services Rust reference (host-services-rust) + +## 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/scaffold/reference/cdn/large-dictionary-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md index 8d280e2..aaa3722 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -138,3 +138,129 @@ No additional dependencies beyond the cdn-base skeleton. - cdn-base skeleton reference - proxy-wasm-sdk-as SDK reference (AssemblyScript) - platform-overview reference for environment variable configuration + +## Source Material + +### FILE: examples/largeDictionary/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getDictionary, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class LargeDictionaryRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new LargeDictionaryContext(context_id, this); + } +} + +class LargeDictionaryContext extends Context { + constructor(context_id: u32, root_context: LargeDictionaryRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. + const config = getDictionary("LARGE_CONFIG"); + + const size = config.length; + log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); + + stream_context.headers.request.add("x-config-size", size.toString()); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new LargeDictionaryRoot(context_id); +}, "largeDictionary"); +``` + + +### FILE: examples/largeDictionary/package.json + +```json +{ + "name": "fastedge-as-example-large-dictionary", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/largeDictionary/README.md + +``` +[← Back to examples](../README.md) + +# Large Dictionary + +This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. + +## When to use `getDictionary` vs `getEnv` + +| Function | Use when | +|----------|----------| +| `getEnv(name)` | Variable value is under 64 KB (most cases) | +| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | + +The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. + +For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. + +## What it does + +In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | +| `build/largeDictionary-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-rust.md index a8334a3..dda7cb2 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-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,111 @@ 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 { Box::new(LargeEnvRoot) }); +}} + +struct LargeEnvRoot; + +impl Context for LargeEnvRoot {} + +impl RootContext for LargeEnvRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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/http/ab-testing-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/ab-testing-wasi-rust.md index 9f1a6a0..0edcedd 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-08-17 + updated: 2026-08-20 --> --- @@ -217,3 +217,214 @@ 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-` +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) -> anyhow::Result> { + 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-` 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::>() + .join("; ") +} + +fn is_valid_xid(xid: &str) -> bool { + matches!(xid.parse::(), 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 f2b3419..671c3e6 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-08-17 + updated: 2026-08-20 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-17 +updated: 2026-08-20 --- @@ -194,10 +194,8 @@ async fn main(request: Request) -> anyhow::Result> { "Hello, you made a wasi request to {url}" )))?) } - ``` - ### FILE: examples/http/wasi/hello_world/Cargo.toml ```toml @@ -214,5 +212,4 @@ 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 f81acb2..ba67275 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-08-17 + updated: 2026-08-20 --> --- @@ -176,67 +176,3 @@ 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 ad66102..c90f65f 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-08-17 + updated: 2026-08-20 --> --- @@ -163,6 +163,9 @@ async fn main(req: Request) -> anyhow::Result> { .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 })); } @@ -190,117 +193,3 @@ fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result) -> 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" -``` 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 8e33d7f..914ed37 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-08-17 + updated: 2026-08-20 --> --- @@ -231,117 +231,3 @@ 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 (UTF-8) - // entry.json() -> Promise (parsed JSON) - // entry.arrayBuffer() -> Promise - 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 5579117..4a59ac4 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-08-17 + updated: 2026-08-20 --> --- @@ -315,281 +315,3 @@ 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 { - // `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 { - // 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 { - 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 { - 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 20e88de..78af466 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-08-17 + updated: 2026-08-20 --> --- @@ -176,6 +176,18 @@ let body_bytes = body.contents().await?.to_vec(); Must call `.into_body()` before reading; `.contents().await?` buffers the full response body. +### Collect Upstream Headers + +```rust +let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); +``` + +Headers must be collected before consuming the response body via `.into_body()`. + ### Conditional Cache Write ```rust @@ -215,6 +227,7 @@ Purge all example: ```rust if path_and_query == "/purge" { let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); return Ok(Response::builder().status(204).body(Body::empty())?); } ``` @@ -225,6 +238,7 @@ Purge prefix example: 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())?); } ``` @@ -235,6 +249,7 @@ Delete single key example: 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())?); } ``` @@ -273,3 +288,201 @@ 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/ — 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 + +```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>)` +- `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 3331544..10afb0e 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-08-17 + updated: 2026-08-20 --> --- @@ -260,100 +260,3 @@ Configure via the FastEdge secrets API or the manage skill before deploying. - WebCrypto API (crypto.subtle) — available as a global in the FastEdge JS runtime - http-base skeleton (base event listener and Response patterns) - FastEdge deploy skill (building and uploading WASM, configuring secrets) - -## Source Material - -### FILE: examples/crypto-hmac-jwt/src/index.js - -```js -import { getSecret } from 'fastedge::secret'; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - -function base64urlToBytes(str) { - const padded = str.replace(/-/gu, '+').replace(/_/gu, '/') + '='.repeat((4 - (str.length % 4)) % 4); - const binary = atob(padded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.codePointAt(i); - } - return bytes; -} - -async function verifyJwtHs256(token, secret) { - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('malformed token: expected three segments'); - } - const [encodedHeader, encodedPayload, encodedSignature] = parts; - - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(secret), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['verify'], - ); - - const signature = base64urlToBytes(encodedSignature); - const signedData = encoder.encode(`${encodedHeader}.${encodedPayload}`); - - const valid = await crypto.subtle.verify('HMAC', key, signature, signedData); - if (!valid) { - throw new Error('invalid signature'); - } - - const claims = JSON.parse(decoder.decode(base64urlToBytes(encodedPayload))); - - if (typeof claims.exp === 'number' && Math.floor(Date.now() / 1000) >= claims.exp) { - throw new Error('token expired'); - } - - return claims; -} - -async function app(event) { - const auth = event.request.headers.get('authorization') ?? ''; - const match = auth.match(/^Bearer\s+(.+)$/iu); - if (!match) { - return Response.json( - { ok: false, error: 'missing or malformed Authorization header' }, - { status: 401 }, - ); - } - - const secret = getSecret('JWT_SECRET'); - if (!secret) { - return Response.json({ ok: false, error: 'JWT_SECRET is not configured' }, { status: 500 }); - } - - try { - const claims = await verifyJwtHs256(match[1], secret); - return Response.json({ ok: true, claims }); - } catch (error) { - return Response.json({ ok: false, error: error.message }, { status: 401 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/crypto-hmac-jwt/package.json - -```json -{ - "name": "fastedge-example-crypto-hmac-jwt", - "version": "1.0.0", - "description": "FastEdge JS example: verify HS256 JWTs with the Web Crypto API", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/crypto-hmac-jwt.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` 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 64f2d27..c4395fb 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-08-17 + updated: 2026-08-20 --> --- @@ -44,8 +44,8 @@ crate-type = ["cdylib"] ## Required Configuration -| Variable | Type | Required | Description | -|--------------|--------|----------|--------------------------------| +| Variable | Type | Required | Description | +|--------------|--------|----------|---------------------------------| | `ORIGIN_URL` | string | yes | Origin URL to proxy requests to | ## API @@ -190,103 +190,3 @@ async fn main(req: Request) -> anyhow::Result> { - 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= 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" -``` 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 85ad7d9..4554a90 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-08-17 + updated: 2026-08-20 --> --- @@ -154,3 +154,119 @@ 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: (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/scaffold/reference/http/kv-store-basic-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-basic-ts.md index a21b2d4..54b3f87 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-basic-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-basic-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -127,47 +127,3 @@ Build script from `package.json`: - fastedge-sdk-js SDK reference - FastEdge app configuration (store attachment) - BUILD_CLI reference (fastedge-build options) - -## Source Material - -### FILE: examples/kv-store-basic/src/index.js - -```js -import { KvStore } from 'fastedge::kv'; - -async function eventHandler(event) { - try { - const myStore = KvStore.open('kv-store-name-as-defined-on-app'); - const entry = await myStore.getEntry('key'); - - if (entry === null) { - return new Response('Key not found', { status: 404 }); - } - - return new Response(`The KV Store responded with: ${await entry.text()}`); - } catch (error) { - return Response.json({ error: error.message }, { status: 500 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store-basic/package.json - -```json -{ - "name": "fastedge-example-kv-store-basic", - "version": "1.0.0", - "description": "FastEdge JS example: simple KV Store get operation", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/kv-store-basic.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-wasi-rust.md index 02ee4c4..a23be87 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -250,10 +250,10 @@ The app accepts these query parameters: | Parameter | Required | Description | |-----------|----------|-------------| -| `store` | Yes | Name of the KV store to open | -| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | -| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | -| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | +| `store` | Yes | Name of the KV store to open | +| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | +| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | +| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | | `min` | Yes for `zrange` | Minimum score bound (float); no default — must be provided | | `max` | Yes for `zrange` | Maximum score bound (float); no default — must be provided | | `item` | Yes for `bfExists` | Item to check in Bloom Filter | @@ -350,3 +350,218 @@ target = "wasm32-wasip1" - fastedge-sdk-rust key_value module documentation - http-base skeleton reference - wstd HTTP server documentation + +## 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" +``` 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 54b0eef..8a36751 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-08-17 + updated: 2026-08-20 --> --- @@ -400,282 +400,3 @@ 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(url: string): Promise { - 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(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(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(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 c1a13e7..aeaede1 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-08-17 + updated: 2026-08-20 --> --- @@ -127,3 +127,90 @@ 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) -> 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}"))?; + + // 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 f802a94..8c88e7f 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-08-17 + updated: 2026-08-20 --> --- @@ -145,49 +145,3 @@ 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 9d4a66e..c33ea71 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-08-17 + updated: 2026-08-20 --> --- @@ -216,3 +216,82 @@ async fn main(_request: Request) -> anyhow::Result> { - 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) -> 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" +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rotation-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rotation-ts.md index 61b3fbb..5e3fdf9 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rotation-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/secret-rotation-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -45,13 +45,14 @@ import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; #### `getSecretEffectiveAt(name, slot)` -| Parameter | Type | Description | -|-----------|--------|----------------------------------------------| -| `name` | string | Secret name (key) | -| `slot` | number | Slot index or unix timestamp (non-negative integer) | +| Parameter | Type | Description | +|-----------|--------|---------------------------------------------------------------------| +| `name` | string | Secret name (key) | +| `slot` | number | Slot index or unix timestamp (non-negative integer) | - **Returns**: `string | null` — the value from the highest slot `<= slot`, or `null` if no slot satisfies the constraint. - **Slot model**: slots are interpreted as either indices (`0, 1, 2, …`) or unix timestamps. The host resolves by returning the value stored at the highest slot number that is less than or equal to the supplied slot number. +- Both functions are synchronous — no `await` required. --- @@ -207,3 +208,65 @@ This shape is the diagnostic surface for verifying that rotation is working corr - http-base skeleton - deploy skill (for uploading secrets via the API before deploying) - manage skill (`secrets` subcommand for secret CRUD operations) + +## Source Material + +### FILE: examples/secret-rotation/src/index.js + +```js +import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; + +function app(event) { + const { request } = event; + + // Read the slot from the x-slot header, defaulting to the current unix timestamp. + // Slots can be interpreted either as indices (0, 1, 2...) or as unix timestamps; + // the host returns the value from the highest slot <= this number. + const slotHeader = request.headers.get('x-slot'); + const slot = + slotHeader !== null ? Number.parseInt(slotHeader, 10) : Math.floor(Date.now() / 1000); + + if (!Number.isFinite(slot) || slot < 0) { + return new Response('x-slot header must be a non-negative integer', { status: 400 }); + } + + const secretName = request.headers.get('x-secret-name') ?? 'TOKEN_SECRET'; + + const current = getSecret(secretName); + const effective = getSecretEffectiveAt(secretName, slot); + + const body = JSON.stringify({ + secret_name: secretName, + slot, + current, + effective_at_slot: effective, + is_same: current === effective, + }); + + return new Response(body, { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/secret-rotation/package.json + +```json +{ + "name": "fastedge-example-secret-rotation", + "version": "1.0.0", + "description": "FastEdge JS example: slot-based secret retrieval for rotation", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/secret-rotation.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/simple-fetch-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/simple-fetch-wasi-rust.md index f9fc9d2..cf34807 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -131,6 +131,19 @@ use wstd::http::{Client, Request, Response}; | Missing `x-fetch-url` header | Falls back to `https://httpbin.org/get` | | Non-UTF-8 header value | `to_str().ok()` returns `None`; fallback default applied | +## Build + +```bash +cargo component build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Example Usage + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + ## Source Material ### FILE: examples/http/wasi/simple_fetch/src/lib.rs diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-wasi-rust.md index e859b3e..1eecd03 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -32,6 +32,7 @@ include_str!("../assets/filename.ext") - Embeds a UTF-8 text file into the binary at compile time. - Path is relative to the source file containing the macro. - Returns `&'static str`. +- Build fails if the target file does not exist at compile time. ```rust include_bytes!("../assets/filename.ext") @@ -127,6 +128,13 @@ async fn main(req: Request) -> anyhow::Result> { } ``` +## Imports + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response, StatusCode}; +``` + ## Cargo.toml ```toml @@ -165,6 +173,7 @@ All files in `assets/` must exist at compile time; the build will fail if `inclu - Asset paths in `include_str!` are relative to the `.rs` source file, not the crate root. - All assets are compiled into the WASM binary — large assets increase binary size proportionally. - No dynamic file loading is possible at runtime; adding an asset requires a rebuild. +- The WASM runtime has no filesystem; there is no alternative to compile-time embedding. ## See Also 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 8155e9b..053da59 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-08-17 + updated: 2026-08-20 --> --- @@ -115,3 +115,50 @@ 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 e011ba1..956f361 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-08-17 + updated: 2026-08-20 --> --- @@ -141,3 +141,67 @@ async fn main(_request: Request) -> anyhow::Result> { - 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:///` (`-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" +``` 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 5d7002e..36b8b19 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-08-17 + updated: 2026-08-20 --> --- @@ -202,3 +202,86 @@ 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 c23693c..00d44d9 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-08-17 + updated: 2026-08-20 --> --- @@ -135,3 +135,89 @@ 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) -> anyhow::Result> { + 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: , Password: +``` + +## 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/.cursor-plugin/plugin.json b/plugins/gcore-fastedge-cursor/.cursor-plugin/plugin.json index ed7ccd5..27d14ec 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.6", + "version": "0.2.7", "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 369f101..5592474 100644 --- a/plugins/gcore-fastedge-cursor/docs-index.json +++ b/plugins/gcore-fastedge-cursor/docs-index.json @@ -1,16 +1,16 @@ { "schema_version": "1.0.0", - "generated_at": "2026-08-18T07:33:55.154Z", + "generated_at": "2026-08-20T15:14:07.398Z", "source": { "repo": "fastedge-plugin", - "commit": "067d44e", + "commit": "deaf77e", "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-20 -->", "path": "plugins/gcore-fastedge-cursor/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-20", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "24a86619b212c904d2bae26e563353bfed0d28ff4743c99c0eb0d1908ee0d1fb", + "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -60,7 +60,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "rust", "sdk", @@ -250,7 +250,9 @@ "---------------------------------------------------", "on_http_request_headers", "num_headers", + "usize", "end_of_stream", + "bool", "action", "request", "headers", @@ -258,8 +260,7 @@ "modify", "forwarding", "on_http_request_body", - "body_size", - "end_of_strea" + "body_size" ] }, { @@ -1000,7 +1001,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1025,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1034,7 +1035,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", + "content_sha256": "02a0716c80b4d8908f4996e26cf4a36904a49faf5c7a78d99d5abacf7512e387", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1054,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1650,63 +1651,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1730,7 +1681,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1740,7 +1691,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", + "content_sha256": "d47decc23a11899c2cdd3360355fb8c5e9db6fb48d6b2ca5c90bee4cc5c8ae2b", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1759,7 +1710,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -2114,183 +2065,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2314,7 +2095,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "api", "key", "validation", @@ -2324,7 +2105,7 @@ "requests", "against" ], - "content_sha256": "dcffdcc456556e41a80a866fcd1ccb02dcc074a7882e5357bed515977ec38748", + "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2343,7 +2124,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "api", "key", "validation", @@ -2588,6 +2369,56 @@ "traits", "types" ] + }, + { + "id": "cdn-examples-api-key-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 163, + "line_end": 279, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "api_key", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "api", + "key", + "validation.", + "validates", + "requests", + "using", + "x-api-key", + "header", + "checked", + "against", + "stored", + "secret.", + "simpler", + "alternative", + "jwt", + "token", + "expiry", + "claims", + "needed.", + "required", + "configuration", + "secret", + "use" + ] } ] }, @@ -3652,7 +3483,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3676,7 +3507,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -3686,7 +3517,7 @@ "app_type", "languages" ], - "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", + "content_sha256": "94226f2f7783dd5978e697ebea97008427e93dbddfd4b033a8c05d74268e9a34", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3705,7 +3536,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -4132,139 +3963,6 @@ "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." - ] } ] }, @@ -5943,7 +5641,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5967,7 +5665,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "example", "convert", "image", @@ -5977,7 +5675,7 @@ "avif", "format" ], - "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", + "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5996,7 +5694,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "cdn", "example", "convert", @@ -6461,13 +6159,129 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6491,7 +6305,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "response-header", "injection", @@ -6501,7 +6315,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", + "content_sha256": "dcfb3c1c7467c4e745a43d3b8821e1ef8725789583876c7d2ffafd9955cc8efa", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6520,7 +6334,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "assemblyscript", "cdn", @@ -6907,7 +6721,7 @@ "level": 2, "anchor": "source-material", "line_start": 197, - "line_end": 318, + "line_end": 316, "keywords": [ "source", "material", @@ -6945,8 +6759,8 @@ "heading": "What it does", "level": 2, "anchor": "what-it-does", - "line_start": 319, - "line_end": 324, + "line_start": 317, + "line_end": 322, "keywords": [ "onresponseheaders", "requests", @@ -6993,8 +6807,8 @@ "heading": "Configuration", "level": 2, "anchor": "configuration", - "line_start": 325, - "line_end": 333, + "line_start": 323, + "line_end": 331, "keywords": [ "configuration", "set", @@ -7033,8 +6847,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 334, - "line_end": 347, + "line_start": 332, + "line_end": 345, "keywords": [ "build", "pnpm", @@ -7063,8 +6877,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 348, - "line_end": 352, + "line_start": 346, + "line_end": 350, "keywords": [ "deploy", "upload", @@ -7643,7 +7457,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7667,7 +7481,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -7677,7 +7491,7 @@ "response-headers", "response-body" ], - "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", + "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7696,7 +7510,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8126,6 +7940,147 @@ "hook", "sequencing" ] + }, + { + "id": "cdn-examples-custom-error-pages-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 296, + "line_end": 501, + "keywords": [ + "source", + "material", + "file", + "examples", + "customerrorpages", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "buffertypevalues", + "stream_context", + "setloglevel", + "fastedge", + "class", + "errorpagesroot" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 502, + "line_end": 514, + "keywords": [ + "onresponseheaders", + "app", + "reads", + "response.status", + "property.", + "400-599", + "range", + "sets", + "content-type", + "text", + "html", + "prepares", + "body", + "replacement.", + "onresponsebody", + "buffers", + "full", + "response", + "replaces", + "styled", + "page", + "containing", + "numeric", + "status", + "code", + "human-readable", + "error", + "title", + "e.g.", + "found", + "bad", + "gateway", + "description", + "explaining", + "went", + "wrong", + "category", + "label", + "client", + "server" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 515, + "line_end": 528, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "customerrorpages.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "customerrorpages-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 529, + "line_end": 533, + "keywords": [ + "deploy", + "upload", + "build", + "customerrorpages.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, @@ -8243,7 +8198,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -8267,7 +8222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8277,7 +8232,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", + "content_sha256": "a73e35e2bec26a8caef88e750c20eec94b8a4e22bd45622922f8ab9f586d554a", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -8296,7 +8251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8678,59 +8633,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- 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-20 --> --- 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", @@ -8754,7 +8663,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8765,7 +8674,7 @@ "secrets", "environment" ], - "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", + "content_sha256": "0c73ed18f10ce8d8e06a88f54b4d3f23ac0b67968d5c25e974066c47408cfacb", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8784,7 +8693,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9195,221 +9104,6 @@ "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." - ] } ] }, @@ -9872,7 +9566,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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md", "skill": "fastedge-docs", "category": "examples", @@ -9894,9 +9588,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-20", "---", "type", "example", @@ -9906,7 +9600,7 @@ "geo-routing", "header-manipulation" ], - "content_sha256": "d809b1aeaf9eef762fe336d6a7b4e21ad50201bb525e0716c678d58114a67153", + "content_sha256": "77bc8d7bb75d5073d55729d167f95732aae1c14e0d2cc8f5dbd17a584fe83bdf", "sections": [ { "id": "cdn-examples-geo-redirect-as#introduction", @@ -9923,9 +9617,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-05-20", + "2026-08-20", "---", "type", "example", @@ -10132,7 +9826,7 @@ "level": 2, "anchor": "runtime-properties-used", "line_start": 106, - "line_end": 118, + "line_end": 120, "keywords": [ "runtime", "properties", @@ -10181,8 +9875,8 @@ "heading": "API Calls", "level": 2, "anchor": "api-calls", - "line_start": 119, - "line_end": 150, + "line_start": 121, + "line_end": 152, "keywords": [ "api", "calls", @@ -10222,8 +9916,8 @@ "heading": "Environment Variables", "level": 2, "anchor": "environment-variables", - "line_start": 151, - "line_end": 161, + "line_start": 153, + "line_end": 163, "keywords": [ "environment", "variables", @@ -10272,8 +9966,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 162, - "line_end": 171, + "line_start": 164, + "line_end": 173, "keywords": [ "error", "responses", @@ -10312,8 +10006,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 172, - "line_end": 183, + "line_start": 174, + "line_end": 185, "keywords": [ "registration", "typescript", @@ -10335,8 +10029,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 184, - "line_end": 200, + "line_start": 186, + "line_end": 209, "keywords": [ "build", "pnpm", @@ -10356,7 +10050,17 @@ "--target", "release", "runs", - "---" + "output", + "file", + "description", + "------", + "-------------", + "georedirect.wasm", + "optimised", + "binary", + "upload", + "fastedge", + "georedirect-debug.wasm" ] }, { @@ -10364,8 +10068,8 @@ "heading": "Key Patterns", "level": 2, "anchor": "key-patterns", - "line_start": 201, - "line_end": 229, + "line_start": 210, + "line_end": 248, "keywords": [ "key", "patterns", @@ -10401,8 +10105,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 230, - "line_end": 241, + "line_start": 249, + "line_end": 261, "keywords": [ "constraints", "gotchas", @@ -10450,8 +10154,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 242, - "line_end": 248, + "line_start": 262, + "line_end": 268, "keywords": [ "see", "geoblock", @@ -12102,7 +11806,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12128,7 +11832,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12138,7 +11842,7 @@ "headers", "request-headers" ], - "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", + "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -12157,7 +11861,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12295,11 +11999,9 @@ "method", "signature", "description", - "---------------------------------------", - "----------------------------------------------------", - "------------------------------------------------------------------------", - "get", - "name" + "-------------------", + "-----------------------------------------------", + "--------------------------------------------------------------------------------------------------------" ] }, { @@ -12549,8 +12251,8 @@ "meaning", "trigger", "----", - "---------------------------------", - "-----------------------------------------------------------", + "------------------------------", + "----------------------------------------------------------", "550", "headers", "present", @@ -12564,7 +12266,8 @@ "get", "552", "mismatch", - "mutation" + "mutation", + "validateheaders" ] }, { @@ -12667,7 +12370,7 @@ "file", "description", "--------------------------", - "----------------------------------------------", + "---------------------------------------------", "headers.wasm", "optimised", "release", @@ -12683,7 +12386,8 @@ "package.json", "script", "command", - "---------------" + "-----------------", + "---" ] }, { @@ -12735,7 +12439,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12759,7 +12463,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12769,7 +12473,7 @@ "headers", "request-headers" ], - "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", + "content_sha256": "26db929462e18b733d0d8ff6392d1a509520adb603ee47545516c24043493a11", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -12788,7 +12492,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -13128,58 +12832,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "title": "HTTP Call", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13205,7 +12864,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "overview", "httpcall", "dispatches", @@ -13216,7 +12875,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", + "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -13235,7 +12894,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -13507,13 +13166,209 @@ "response", "patterns" ] + }, + { + "id": "cdn-examples-http-call-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 325, + "line_end": 485, + "keywords": [ + "source", + "material", + "file", + "examples", + "httpcall", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "basecontext", + "buffertypevalues", + "context", + "filterheadersstatusvalues", + "get_buffer_bytes", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "wasmresultvalues", + "proxy-wasm-sdk-a" + ] + }, + { + "id": "cdn-examples-http-call-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 486, + "line_end": 497, + "keywords": [ + "onrequestheaders", + "app", + "dispatches", + "outbound", + "http", + "get", + "request", + "httpbin.org", + "pauses", + "hook", + "stopiteration", + "until", + "response", + "arrives.", + "dispatch", + "latched", + "instance", + "field", + "second", + "invocation", + "processed", + "returns", + "continue", + "instead", + "dispatching", + "again.", + "inside", + "callback", + "passed", + "httpcall", + "checks", + "whether", + "call", + "succeeded", + "headers", + "value", + "indicates", + "failure", + "timeout", + "dns" + ] + }, + { + "id": "cdn-examples-http-call-as#key-concepts", + "heading": "Key concepts", + "level": 2, + "anchor": "key-concepts", + "line_start": 498, + "line_end": 504, + "keywords": [ + "key", + "concepts", + "httpcall", + "rootcontext", + "dispatches", + "async", + "http", + "request.", + "accepts", + "cluster", + "host", + "headers", + "body", + "trailers", + "timeout", + "milliseconds", + "originating", + "context", + "callback.", + "fastedge", + "resume", + "model.", + "returning", + "filterheadersstatusvalues.stopiteration", + "pauses", + "hook.", + "runtime", + "processes", + "response", + "invokes", + "callback", + "re-invokes", + "onrequestheaders", + "same", + "instance", + "httpcalldispatched", + "latch", + "gates", + "re-dispatch", + "hoo" + ] + }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 505, + "line_end": 518, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-http-call-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 519, + "line_end": 523, + "keywords": [ + "deploy", + "upload", + "build", + "httpcall.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13537,7 +13392,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "makes", @@ -13547,7 +13402,7 @@ "services", "timeout" ], - "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", + "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13566,7 +13421,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "cdn", @@ -14009,7 +13864,7 @@ "level": 2, "anchor": "gotchas", "line_start": 360, - "line_end": 370, + "line_end": 371, "keywords": [ "gotchas", "action", @@ -14058,8 +13913,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 371, - "line_end": 377, + "line_start": 372, + "line_end": 378, "keywords": [ "see", "proxy-wasm", @@ -14086,7 +13941,7 @@ { "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14112,7 +13967,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14122,7 +13977,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", + "content_sha256": "7c82be09762a859504b8de768a520016b4dd63da93c478964e74af2344eb76b3", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -14141,7 +13996,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14373,7 +14228,7 @@ "level": 2, "anchor": "lifecycle-hooks", "line_start": 143, - "line_end": 182, + "line_end": 184, "keywords": [ "lifecycle", "hooks", @@ -14416,8 +14271,8 @@ "heading": "Query Parameters", "level": 2, "anchor": "query-parameters", - "line_start": 183, - "line_end": 201, + "line_start": 185, + "line_end": 203, "keywords": [ "query", "parameters", @@ -14451,8 +14306,8 @@ "heading": "Action Dispatch", "level": 2, "anchor": "action-dispatch", - "line_start": 202, - "line_end": 240, + "line_start": 204, + "line_end": 247, "keywords": [ "action", "dispatch", @@ -14471,6 +14326,7 @@ "found", "non-null", "string.utf8.decode", + "break", "scan", "match", "keys", @@ -14478,8 +14334,7 @@ "returns", "string", "keys.join", - "zrange", - "cons" + "cas" ] }, { @@ -14487,8 +14342,8 @@ "heading": "Response Body Format", "level": 2, "anchor": "response-body-format", - "line_start": 241, - "line_end": 258, + "line_start": 248, + "line_end": 265, "keywords": [ "response", "body", @@ -14514,6 +14369,8 @@ "via", "response.status", "property", + "advisory", + "only", "---" ] }, @@ -14522,8 +14379,8 @@ "heading": "Query Param Validation", "level": 2, "anchor": "query-param-validation", - "line_start": 259, - "line_end": 278, + "line_start": 266, + "line_end": 293, "keywords": [ "query", "param", @@ -14572,8 +14429,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 279, - "line_end": 288, + "line_start": 294, + "line_end": 303, "keywords": [ "logging", "typescript", @@ -14596,8 +14453,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 303, + "line_start": 304, + "line_end": 318, "keywords": [ "build", "npm", @@ -14630,8 +14487,8 @@ "heading": "Deployment Requirements", "level": 2, "anchor": "deployment-requirements", - "line_start": 304, - "line_end": 315, + "line_start": 319, + "line_end": 330, "keywords": [ "deployment", "requirements", @@ -14679,8 +14536,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 316, - "line_end": 328, + "line_start": 331, + "line_end": 344, "keywords": [ "gotchas", "kvstore.get", @@ -14724,8 +14581,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 329, - "line_end": 334, + "line_start": 345, + "line_end": 350, "keywords": [ "see", "kvstore", @@ -14758,7 +14615,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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14782,7 +14639,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "capabilities", "kv-store", @@ -14793,7 +14650,7 @@ "example", "app_type" ], - "content_sha256": "3e66a0f9ea746ad8415d020dfa703809ee9c371e9bde63f98aae6872310a47ad", + "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14812,7 +14669,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "capabilities", "kv-store", @@ -15054,55 +14911,6 @@ "vs.", "differences" ] - }, - { - "id": "cdn-examples-kv-store-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 244, - "line_end": 516, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "key_value", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "store", - "operations", - "via", - "proxy-wasm", - "interface.", - "supports", - "query", - "parameters", - "name", - "action", - "get", - "key", - "scan", - "match", - "pattern", - "zrange", - "min", - "f64", - "max", - "zscan", - "bfexists", - "ite" - ] } ] }, @@ -18757,7 +18565,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -18781,7 +18589,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example", @@ -18792,7 +18600,7 @@ "visitors", "test" ], - "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", + "content_sha256": "25feb56e225dc3c2de096f569836c7a02371f3ad66f47c95f09b1d43cae9d0af", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -18811,7 +18619,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example" @@ -19235,7 +19043,7 @@ "level": 2, "anchor": "gotchas", "line_start": 200, - "line_end": 210, + "line_end": 212, "keywords": [ "gotchas", "headers", @@ -19278,20 +19086,14 @@ ] }, { - "id": "http-examples-ab-testing-js#source-material", - "heading": "Source Material", + "id": "http-examples-ab-testing-js#source", + "heading": "Source", "level": 2, - "anchor": "source-material", - "line_start": 211, - "line_end": 328, + "anchor": "source", + "line_start": 213, + "line_end": 303, "keywords": [ "source", - "material", - "file", - "examples", - "ab-testing", - "src", - "index.js", "import", "getenv", "fastedge", @@ -19315,7 +19117,8 @@ "slicedheaders", "sliceabtestidfromcookie", "headers", - "createabtesthe" + "createabtestheaders", + "outbo" ] } ] @@ -20785,7 +20588,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20809,7 +20612,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -20819,7 +20622,7 @@ "outbound-http", "query-params" ], - "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", + "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20838,7 +20641,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21260,13 +21063,207 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21290,7 +21287,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21300,7 +21297,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", + "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21319,7 +21316,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21772,188 +21769,6 @@ "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" - ] } ] }, @@ -22412,7 +22227,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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22438,7 +22253,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "cache", "module", @@ -22448,7 +22263,7 @@ "2.3.0", "app" ], - "content_sha256": "137b45736825a3dd07f53e14f0a413bbef13b403e482856fabf51695c93a1433", + "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22467,7 +22282,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "fastedge", "cache", "javascript", @@ -22796,6 +22611,56 @@ "response", "construction" ] + }, + { + "id": "http-examples-cache-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 335, + "line_end": 610, + "keywords": [ + "source", + "material", + "file", + "examples", + "cache", + "src", + "index.ts", + "fastedge", + "flagship", + "patterns", + "example", + "demonstrates", + "three", + "highest-value", + "uses", + "module", + "per-ip", + "rate", + "limiting", + "atomic", + "counters", + "origin-cache", + "proxy", + "manual", + "get", + "set", + "conditional", + "caching", + "json", + "memoisation", + "getorset", + "computed", + "populator", + "rely", + "strongly", + "consistent", + "within", + "pop", + "incr", + "returns" + ] } ] }, @@ -26213,7 +26078,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26237,7 +26102,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26247,7 +26112,7 @@ "hello-world", "wasi" ], - "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", + "content_sha256": "b15ce34c19d4cf004bf6f602e6bf370c34f33abd962e77ce72f4bc13969016fe", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -26266,7 +26131,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26659,127 +26524,6 @@ "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" - ] } ] }, @@ -27178,7 +26922,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27202,7 +26946,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27212,7 +26956,7 @@ "languages", "store" ], - "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", + "content_sha256": "315969c42f8aa2c0325e094018cc3d59e5f68550acd5744a480b2302a15d0860", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -27231,7 +26975,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27658,222 +27402,6 @@ "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" - ] } ] }, @@ -28795,7 +28323,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28819,7 +28347,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -28829,7 +28357,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", + "content_sha256": "7d9b8470977ba554efeff699feeee8f078a5b682b5d396d070f0589f49f8044f", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -28848,7 +28376,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29216,171 +28744,6 @@ "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" - ] } ] }, @@ -29837,7 +29200,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29861,7 +29224,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29871,7 +29234,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", + "content_sha256": "f6509335e7b9957b364049295ea3a84967dce13ae1e03c5ebbd8412f6e0dfb05", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -29890,7 +29253,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -30208,15 +29571,15 @@ ] }, { - "id": "http-examples-outbound-modify-response-wasi-rust#source-material", - "heading": "Source Material", + "id": "http-examples-outbound-modify-response-wasi-rust#full-source", + "heading": "Full Source", "level": 2, - "anchor": "source-material", + "anchor": "full-source", "line_start": 186, "line_end": 277, "keywords": [ + "full", "source", - "material", "file", "examples", "http", @@ -30936,7 +30299,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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30962,7 +30325,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "proxy", "response", "transform", @@ -30971,7 +30334,7 @@ "fastedge", "apps" ], - "content_sha256": "1199a5c5c761e3723a541167ff50b4e4009071ddc549726456bb8e300d9b9c94", + "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30990,7 +30353,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-07-23", + "2026-08-20", "proxy", "response", "transform", @@ -31424,13 +30787,356 @@ "kvstore", "api" ] + }, + { + "id": "http-examples-proxy-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 264, + "keywords": [ + "source", + "material", + "file", + "docs", + "proxy_patterns.md", + "maintenance", + "hand-authored.", + "produced", + "fastedge-plugin-source", + "generate-docs.sh.", + "edit", + "directly.", + "proxy", + "response", + "transform", + "patterns", + "fastedge", + "http", + "apps", + "act", + "thin", + "front", + "origin", + "upstream", + "api", + "fetching", + "transforming", + "returning", + "result.", + "document", + "covers", + "common", + "patterns." + ] + }, + { + "id": "http-examples-proxy-js#simple-proxy", + "heading": "Simple Proxy", + "level": 2, + "anchor": "simple-proxy", + "line_start": 265, + "line_end": 291, + "keywords": [ + "simple", + "proxy", + "fetch", + "upstream", + "return", + "body", + "unchanged", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "method", + "request.method", + "headers", + "request.headers", + "get", + "head", + "request.arraybuffer", + "undefined", + "addeventlistener" + ] + }, + { + "id": "http-examples-proxy-js#proxy-with-json-transform", + "heading": "Proxy with JSON Transform", + "level": 2, + "anchor": "proxy-with-json-transform", + "line_start": 292, + "line_end": 320, + "keywords": [ + "proxy", + "json", + "transform", + "modify", + "response", + "body", + "returning", + "it.", + "pattern", + "examples", + "outbound-modify-response", + "typescript", + "async", + "function", + "handle", + "const", + "upstream", + "await", + "fetch", + "https", + "jsonplaceholder.typicode.com", + "users", + "upstream.json", + "transformed", + "users.slice", + "total", + "skip", + "limit", + "return", + "new", + "json.stringify", + "status", + "200", + "headers", + "content-type" + ] + }, + { + "id": "http-examples-proxy-js#hono-proxy-with-transform", + "heading": "Hono Proxy with Transform", + "level": 2, + "anchor": "hono-proxy-with-transform", + "line_start": 321, + "line_end": 351, + "keywords": [ + "hono", + "proxy", + "transform", + "inside", + "app", + "use", + "c.req.raw.headers", + "forward", + "inbound", + "headers", + "c.req.arraybuffer", + "body", + "typescript", + "import", + "const", + "new", + "app.all", + "api", + "async", + "url", + "c.req.url", + "upstream", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "fetch", + "method", + "c.req.method", + "get", + "c.r" + ] + }, + { + "id": "http-examples-proxy-js#header-manipulation-in-proxies", + "heading": "Header Manipulation in Proxies", + "level": 2, + "anchor": "header-manipulation-in-proxies", + "line_start": 352, + "line_end": 380, + "keywords": [ + "header", + "manipulation", + "proxies", + "strip", + "hop-by-hop", + "headers", + "forwarding", + "upstream", + "add", + "diagnostic", + "way", + "back", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "upstreamheaders", + "new", + "request.headers", + "forwarded", + "upstreamheaders.delete", + "connection", + "keep-alive", + "transfer-encoding", + "await", + "fetch", + "https", + "backend.example.com", + "method", + "request.m" + ] + }, + { + "id": "http-examples-proxy-js#cache-aware-proxy-with-kv", + "heading": "Cache-aware Proxy with KV", + "level": 2, + "anchor": "cache-aware-proxy-with-kv", + "line_start": 381, + "line_end": 413, + "keywords": [ + "cache-aware", + "proxy", + "cache", + "upstream", + "responses", + "store", + "avoid", + "repeated", + "outbound", + "calls.", + "note", + "read-only", + "app", + "code", + "writes", + "happen", + "via", + "portal", + "api", + "typescript", + "import", + "kvstore", + "fastedge", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "try", + "kvstore.open", + "api-cache", + "cached", + "cache.get", + "url.pathname", + "null", + "return", + "response" + ] + }, + { + "id": "http-examples-proxy-js#operational-notes", + "heading": "Operational Notes", + "level": 2, + "anchor": "operational-notes", + "line_start": 414, + "line_end": 419, + "keywords": [ + "operational", + "notes", + "outbound", + "fetch", + "budget.", + "invocation", + "limited", + "number", + "requests", + "basic", + "pro", + "parallelise", + "possible", + "promise.all", + "...", + "instead", + "sequential", + "await", + "execution", + "time", + "proxying", + "upstream", + "transforming", + "counts", + "against", + "50ms", + "200ms", + "slow", + "upstreams", + "trip", + "532", + "timeouts.", + "body", + "size", + "limits.", + "inbound", + "bodies", + "subject", + "configured", + "request" + ] + }, + { + "id": "http-examples-proxy-js#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 420, + "line_end": 428, + "keywords": [ + "see", + "examples", + "outbound-modify-response", + "json", + "transform", + "upstream", + "response", + "outbound-fetch", + "basic", + "outbound", + "fetch", + "patterns", + "headers", + "request", + "header", + "manipulation", + "kv-store", + "kv-backed", + "caching" + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31454,7 +31160,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31464,7 +31170,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", + "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31483,7 +31189,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31922,13 +31628,247 @@ "documentation", "external" ] + }, + { + "id": "http-examples-s3upload-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 369, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "s3upload", + "src", + "lib.rs", + "rust", + "use", + "std", + "time", + "duration", + "fastedge", + "body", + "header", + "error", + "method", + "request", + "response", + "statuscode", + "rusty_s3", + "bucket", + "credentials", + "s3action", + "urlstyle", + "collections", + "hashmap", + "env", + "url", + "main", + "req", + "result", + "match", + "req.method", + "allow", + "only", + "post", + "put" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 370, + "line_end": 379, + "keywords": [ + "accepts", + "post", + "put", + "only", + "returns", + "405", + "methods", + "requires", + "name", + "filename", + "query", + "parameter", + "non-empty", + "body", + "400", + "otherwise", + "enforces", + "max_file_size", + "set", + "413", + "exceeded", + "calls", + "prepare_s3", + "build", + "1-hour", + "presigned", + "url", + "using", + "rusty_s3", + "forwards", + "file", + "via", + "fastedge", + "send_request", + "success", + "200", + "responds", + "clean", + "object", + "string" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 380, + "line_end": 393, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "access_key", + "access", + "key", + "secret_key", + "secret", + "region", + "e.g.", + "s-ed1", + "base_hostname", + "base", + "hostname", + "cloud.gcore.lu", + "bucket", + "name", + "scheme", + "optional", + "url", + "defaults", + "http", + "max_file_size", + "maximum", + "upload", + "size", + "bytes", + "limit", + "unset", + "constructed", + "endpoint", + "bucke" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 394, + "line_end": 400, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "s3upload.wasm" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 401, + "line_end": 411, + "keywords": [ + "usage", + "post", + "upload", + "name", + "photo.jpg", + "content-type", + "image", + "jpeg", + "file", + "bytes", + "success", + "200", + "response", + "body", + "clean", + "object", + "url", + "presign", + "query", + "parameters", + "stripped" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 412, + "line_end": 418, + "keywords": [ + "notes", + "options", + "method", + "returns", + "204", + "include", + "cors", + "headers", + "add", + "access-control-allow-", + "browser", + "preflight", + "support", + "needed.", + "presigned", + "url", + "expires", + "hour", + "since", + "upload", + "performed", + "server-side", + "practical", + "impact.", + "max_file_size", + "silently", + "ignored", + "set", + "non-numeric", + "value." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31952,7 +31892,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "example", @@ -31962,7 +31902,7 @@ "fastedge", "platform" ], - "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", + "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31981,7 +31921,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "rust", @@ -32371,284 +32311,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32672,7 +32341,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "secret", "rollover", "wasi", @@ -32683,7 +32352,7 @@ "rotation", "using" ], - "content_sha256": "0ca24f700d200210c2587774b740319ee8b982f0044d22467b92fc8536a433c1", + "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32702,7 +32371,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "secret", "rollover", "wasi", @@ -32958,14 +32627,30 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 150, + "line_end": 178, "keywords": [ "implementation", "pattern", "rust", "use", + "std", + "time", + "systemtime", + "unix_epoch", + "anyhow", "fastedge", "secret", + "serde_json", + "json", + "wstd", + "http", + "body", + "request", + "response", + "http_server", + "async", + "main", + "result", "read", "slot", "header", @@ -32975,29 +32660,13 @@ "timestamp", "let", "u32", - "request", ".headers", ".get", "x-slot", ".and_then", "v.to_str", ".ok", - "v.parse", - ".unwrap_or_else", - "systemtime", - "now", - ".duration_since", - "unix_epoch", - ".expect", - "time", - "went", - "backwards", - ".as_secs", - "get", - "latest", - "value", - "secret_name", - ".map_err" + "v.p" ] }, { @@ -33005,8 +32674,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 151, - "line_end": 158, + "line_start": 179, + "line_end": 186, "keywords": [ "constraints", "gotchas", @@ -33055,8 +32724,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 159, - "line_end": 165, + "line_start": 187, + "line_end": 193, "keywords": [ "build", "cargo", @@ -33073,8 +32742,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 166, - "line_end": 172, + "line_start": 194, + "line_end": 200, "keywords": [ "see", "fastedge", @@ -33105,7 +32774,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33129,7 +32798,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33139,7 +32808,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", + "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -33158,7 +32827,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33523,193 +33192,13 @@ "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-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33733,7 +33222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "type", "example", @@ -33743,7 +33232,7 @@ "outbound-http", "env-vars" ], - "content_sha256": "0fba2c9ea0ce15a73c795ca6e7351997b10aad648632c67f484f418df2976d8e", + "content_sha256": "3195fb4f9d583f55e2834ef95604d838f310817302a02705bec7c99b51685485", "sections": [ { "id": "http-examples-smart-switch-basic-rust#introduction", @@ -33762,7 +33251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-07-23", + "2026-08-20", "---", "type", "example", @@ -34064,7 +33553,7 @@ "level": 2, "anchor": "key-gotchas", "line_start": 179, - "line_end": 188, + "line_end": 189, "keywords": [ "key", "gotchas", @@ -34113,8 +33602,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 189, - "line_end": 197, + "line_start": 190, + "line_end": 198, "keywords": [ "build", "cargo", @@ -34133,8 +33622,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 198, - "line_end": 204, + "line_start": 199, + "line_end": 205, "keywords": [ "see", "api_wrapper", @@ -34778,7 +34267,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34802,7 +34291,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34812,7 +34301,7 @@ "streaming", "async" ], - "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", + "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34831,7 +34320,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -35160,144 +34649,6 @@ "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" - ] } ] }, @@ -42906,7 +42257,7 @@ { "id": "templates-catalog", "title": "FastEdge Bolt-On Templates", - "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md", "skill": "fastedge-docs", "category": "reference", @@ -42924,7 +42275,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -42936,7 +42287,7 @@ "scratch", "check" ], - "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "content_sha256": "910cf26789610f4f959d807e57f149e1cf0af1cb1bcdcc089f45792bb595ff83", "sections": [ { "id": "templates-catalog#introduction", @@ -42955,7 +42306,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -43040,7 +42391,7 @@ "level": 2, "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", "line_start": 32, - "line_end": 47, + "line_end": 49, "keywords": [ "harden-cookies", "cookie", @@ -43089,8 +42440,8 @@ "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, + "line_start": 50, + "line_end": 73, "keywords": [ "edge-sso", "sso", @@ -43139,8 +42490,8 @@ "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, + "line_start": 74, + "line_end": 99, "keywords": [ "edge-totp", "totp", @@ -43574,7 +42925,7 @@ { "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 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md", "skill": "fastedge-docs", "category": "reference", @@ -43592,7 +42943,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", @@ -43603,7 +42954,7 @@ "existing", "password" ], - "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "content_sha256": "2f2493935877ac046f328513d03575666424c44043fa8f4d319e59f96b93dccb", "sections": [ { "id": "templates-edge-totp-integration#introduction", @@ -43622,7 +42973,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn-apps-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn-apps-rust.md index 6d77329..bdad5fe 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn-apps-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn-apps-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) @@ -197,10 +197,10 @@ impl HttpContext for HelloWorld { | Callback | Phase | Description | | ----------------------------------------------------------------- | ---------------- | --------------------------------------------------- | -| `on_http_request_headers(num_headers, end_of_stream) -> Action` | Request headers | Inspect or modify request headers before forwarding | -| `on_http_request_body(body_size, end_of_stream) -> Action` | Request body | Inspect or modify request body before forwarding | -| `on_http_response_headers(num_headers, end_of_stream) -> Action` | Response headers | Inspect or modify response headers from origin | -| `on_http_response_body(body_size, end_of_stream) -> Action` | Response body | Inspect or modify response body from origin | +| `on_http_request_headers(num_headers: usize, end_of_stream: bool) -> Action` | Request headers | Inspect or modify request headers before forwarding | +| `on_http_request_body(body_size: usize, end_of_stream: bool) -> Action` | Request body | Inspect or modify request body before forwarding | +| `on_http_response_headers(num_headers: usize, end_of_stream: bool) -> Action` | Response headers | Inspect or modify response headers from origin | +| `on_http_response_body(body_size: usize, end_of_stream: bool) -> Action` | Response body | Inspect or modify response body from origin | All callbacks have default no-op implementations. Override only the phases your app needs to process. 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 0205b81..a8e55fb 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-08-17 + updated: 2026-08-20 --> --- @@ -248,191 +248,3 @@ 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-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 fe6573a..4807ec3 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -199,168 +199,3 @@ Upload `build/apiKey.wasm` to the FastEdge portal and attach it to a CDN applica - proxy-wasm-sdk-as reference (full `Context`, `RootContext`, `FilterHeadersStatusValues` API) - FastEdge secrets management (how to set and rotate application secrets) - CDN app deployment guide - -## Source Material - -### FILE: examples/apiKey/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - HeaderPair, - log, - LogLevelValues, - makeHeaderPair, - registerRootContext, - RootContext, - send_http_response, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class ApiKeyRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new ApiKeyContext(context_id, this); - } -} - -class ApiKeyContext extends Context { - constructor(context_id: u32, root_context: ApiKeyRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const expectedKey = getSecret("API_KEY"); - if (expectedKey === "") { - log(LogLevelValues.error, "API_KEY secret not configured"); - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const providedKey = stream_context.headers.request.get("X-API-Key"); - - if (providedKey === "") { - const authHeaders = new Array(); - authHeaders.push(makeHeaderPair("WWW-Authenticate", "API-Key")); - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Missing X-API-Key header"), - authHeaders, - ); - return FilterHeadersStatusValues.StopIteration; - } - - if (providedKey !== expectedKey) { - log(LogLevelValues.info, "API key validation failed"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Invalid API key"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // .remove() sets the header value to "" rather than deleting it entirely — - // the upstream will see X-API-Key: "" rather than a missing header. - stream_context.headers.request.remove("X-API-Key"); - - log(LogLevelValues.info, "API key validated successfully"); - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new ApiKeyRoot(context_id); -}, "apiKey"); -``` - - -### FILE: examples/apiKey/package.json - -```json -{ - "name": "fastedge-as-example-api-key", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: API Key — validate X-API-Key header against a secret", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/apiKey/README.md - -``` -[← Back to examples](../README.md) - -# API Key - -This application validates requests using an `X-API-Key` header checked against a stored secret. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the expected API key from the `API_KEY` secret. -2. Checks the `X-API-Key` request header. -3. Returns `401 Unauthorized` if the header is missing. -4. Returns `403 Forbidden` if the key does not match. -5. On success, clears the `X-API-Key` header before forwarding to the upstream origin (proxy-wasm `.remove()` sets the header value to an empty string rather than deleting it). - -This is a simpler alternative to JWT validation when you need basic API authentication without token expiry or claims. - -> **Production note:** The key comparison (`providedKey !== expectedKey`) is not constant-time, which opens a timing side-channel for a high-volume attacker. For production use, replace the comparison with a constant-time HMAC equality check or use the `jwt` example which includes proper cryptographic validation. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -|------|------|-------------| -| `API_KEY` | Secret | The expected API key value | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/apiKey.wasm` | Optimised release binary — upload this to FastEdge | -| `build/apiKey-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/apiKey.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `API_KEY` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md index d9b973e..5aeb1a4 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # API Key Validation — CDN (Rust) @@ -159,3 +159,120 @@ return Action::Pause; - Host services reference — secrets API (`fastedge::proxywasm::secret`) and other CDN host services - CDN apps reference — proxy-wasm app structure, `RootContext`/`HttpContext` setup, `Action` enum, and request property encodings - SDK API reference — Rust CDN SDK traits and types + +## 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 { Box::new(ApiKeyRoot) }); +}} + +struct ApiKeyRoot; + +impl Context for ApiKeyRoot {} + +impl RootContext for ApiKeyRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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/fastedge-docs/reference/cdn/examples-body-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-body-as.md index e0e5128..d98a1dc 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -337,216 +337,3 @@ When modifying body content: - host-services-rust (for equivalent Rust patterns) - platform-overview (CDN app lifecycle, hook execution order) - examples-headers-as (header-only manipulation without body buffering) - -## Source Material - -### FILE: examples/body/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - BufferTypeValues, - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - get_buffer_bytes, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - set_buffer_bytes, - set_property, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class HttpBodyRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug - return new HttpBody(context_id, this); - } -} - -class HttpBody extends Context { - constructor(context_id: u32, root_context: HttpBodyRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool - ): FilterHeadersStatusValues { - log(LogLevelValues.debug, "onRequestHeaders >>"); - // Remove the "content-length" header - stream_context.headers.request.remove("content-length"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool - ): FilterDataStatusValues { - log(LogLevelValues.debug, "onRequestBody >>"); - if (!end_of_stream) { - // Wait until the complete body is buffered - return FilterDataStatusValues.StopIterationAndBuffer; - } - - // Retrieve the body from the HttpRequestBody buffer - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpRequestBody, - 0, - body_buffer_length - ); - - if (bodyBytes.byteLength > 0) { - const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); - if (bodyStr.includes("Client")) { - const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; - set_buffer_bytes( - BufferTypeValues.HttpRequestBody, - 0, - body_buffer_length, - String.UTF8.encode(newBody) - ); - } - } - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.debug, "onResponseHeaders >>"); - - // Remove "content-length" header as the body size will change - stream_context.headers.response.remove("content-length"); - - // Set "transfer-encoding" to "chunked" - stream_context.headers.response.replace("transfer-encoding", "Chunked"); - - const contentType = stream_context.headers.response.get("content-type"); - if (contentType.length > 0) { - set_property("response.content_type", String.UTF8.encode(contentType)); - } - - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool - ): FilterDataStatusValues { - log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); - - if (!end_of_stream) { - // Wait until the complete body is buffered - return FilterDataStatusValues.StopIterationAndBuffer; - } - - log( - LogLevelValues.debug, - "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() - ); - - // Retrieve the request URL - const urlBytes = get_property("request.url"); - const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); - if (url !== "") { - log(LogLevelValues.info, `url=${url}`); - } - - // Retrieve the response content type stored in onResponseHeaders - const contentTypeBytes = get_property("response.content_type"); - const contentType = - contentTypeBytes.byteLength === 0 - ? "" - : String.UTF8.decode(contentTypeBytes); - if (contentType !== "") { - log(LogLevelValues.info, `contentType=${contentType}`); - } - - // Retrieve the body from the HttpRequestBody buffer - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpResponseBody, - 0, - body_buffer_length - ); - - if (bodyBytes.byteLength > 0) { - const bodyStr = String.UTF8.decode(bodyBytes); - log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); - } - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HttpBodyRoot(context_id); -}, "httpbody"); -``` - -### FILE: examples/body/package.json - -```json -{ - "name": "fastedge-as-example-body", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Body manipulation", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/body/README.md - -``` -[← Back to examples](../README.md) - -# Body - -This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. - -## What it does - -1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. - -2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. - -3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. - -4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. - -This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: - -- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). -- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| ----------------------- | -------------------------------------------------- | -| `build/body.wasm` | Optimised release binary — upload this to FastEdge | -| `build/body-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. -``` diff --git a/plugins/gcore-fastedge-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 baedbd0..cf31db4 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-08-17 + updated: 2026-08-20 --> # CDN Example: Convert Image (Rust) @@ -202,3 +202,300 @@ 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-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 613b6e1..090a82c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> # CORS — AssemblyScript (CDN) @@ -283,7 +283,6 @@ registerRootContext((context_id: u32) => { }, "cors"); ``` - ### FILE: examples/cors/package.json ```json @@ -306,7 +305,6 @@ registerRootContext((context_id: u32) => { } ``` - ### FILE: examples/cors/README.md ``` diff --git a/plugins/gcore-fastedge-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 9ec6651..bf58305 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -292,3 +292,241 @@ export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; - CDN app platform overview - host-services-rust reference (for Rust equivalent patterns) - best-practices reference (response body buffering, hook sequencing) + +## Source Material + +### FILE: examples/customErrorPages/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + BufferTypeValues, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class ErrorPagesRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new ErrorPagesContext(context_id, this); + } +} + +class ErrorPagesContext extends Context { + constructor(context_id: u32, root_context: ErrorPagesRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + if (statusBuf.byteLength < 2) { + return FilterHeadersStatusValues.Continue; + } + + const bytes = Uint8Array.wrap(statusBuf); + const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); + + if (code >= 400 && code < 600) { + stream_context.headers.response.replace("Content-Type", "text/html"); + stream_context.headers.response.remove("Content-Length"); + stream_context.headers.response.replace("Transfer-Encoding", "Chunked"); + + log(LogLevelValues.info, "Error response detected: " + code.toString()); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + // Read response status from property (no instance state between hooks) + const statusBuf = get_property("response.status"); + if (statusBuf.byteLength < 2) { + return FilterDataStatusValues.Continue; + } + + const bytes = Uint8Array.wrap(statusBuf); + const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); + + if (code < 400 || code >= 600) { + return FilterDataStatusValues.Continue; + } + + if (!end_of_stream) { + return FilterDataStatusValues.StopIterationAndBuffer; + } + const title = this.getErrorTitle(code); + const description = this.getErrorDescription(code); + const category = code >= 500 ? "Server Error" : "Client Error"; + + const html = + '' + + '' + + "" + + code.toString() + + " — " + + title + + "" + + "
" + + "

" + + code.toString() + + "

" + + "

" + + title + + "

" + + "

" + + description + + "

" + + "

" + + category + + "

" + + "
"; + + const body = String.UTF8.encode(html); + // Replace the entire original body — pass body_buffer_length as the length + // to replace, not body.byteLength. Otherwise any original bytes beyond the + // new body's length survive at the tail of the response. + set_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length as u32, + body, + ); + + return FilterDataStatusValues.Continue; + } + + private getErrorTitle(code: u32): string { + if (code == 400) return "Bad Request"; + if (code == 401) return "Unauthorized"; + if (code == 403) return "Forbidden"; + if (code == 404) return "Not Found"; + if (code == 405) return "Method Not Allowed"; + if (code == 408) return "Request Timeout"; + if (code == 429) return "Too Many Requests"; + if (code == 500) return "Internal Server Error"; + if (code == 502) return "Bad Gateway"; + if (code == 503) return "Service Unavailable"; + if (code == 504) return "Gateway Timeout"; + if (code >= 500) return "Server Error"; + return "Error"; + } + + private getErrorDescription(code: u32): string { + if (code == 400) + return "The server could not understand the request due to invalid syntax."; + if (code == 401) + return "You need to authenticate to access this resource."; + if (code == 403) + return "You do not have permission to access this resource."; + if (code == 404) + return "The requested page could not be found. It may have been moved or deleted."; + if (code == 405) + return "The request method is not supported for this resource."; + if (code == 408) + return "The server timed out waiting for the request."; + if (code == 429) + return "You have sent too many requests. Please try again later."; + if (code == 500) + return "The server encountered an unexpected condition that prevented it from fulfilling the request."; + if (code == 502) + return "The server received an invalid response from the upstream server."; + if (code == 503) + return "The server is temporarily unavailable. Please try again later."; + if (code == 504) + return "The server did not receive a timely response from the upstream server."; + if (code >= 500) + return "The server encountered an error processing your request."; + return "An error occurred processing your request."; + } +} + +registerRootContext((context_id: u32) => { + return new ErrorPagesRoot(context_id); +}, "customErrorPages"); +``` + + +### FILE: examples/customErrorPages/package.json + +```json +{ + "name": "fastedge-as-example-custom-error-pages", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Custom Error Pages — replace 4xx/5xx responses with branded HTML", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/customErrorPages/README.md + +``` +[← Back to examples](../README.md) + +# Custom Error Pages + +This application intercepts 4xx and 5xx error responses and replaces them with clean, branded HTML error pages. + +## What it does + +In `onResponseHeaders`, the app reads the `response.status` property. If it is in the 400-599 range, it sets the `Content-Type` to `text/html` and prepares for body replacement. + +In `onResponseBody`, the app buffers the full response, then replaces the body with a styled HTML page containing: + +- The numeric status code +- A human-readable error title (e.g. "Not Found", "Bad Gateway") +- A description explaining what went wrong +- An error category label ("Client Error" or "Server Error") + +Covers all common HTTP error codes (400, 401, 403, 404, 405, 408, 429, 500, 502, 503, 504) with specific messages and falls back to generic messages for other codes. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/customErrorPages.wasm` | Optimised release binary — upload this to FastEdge | +| `build/customErrorPages-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. +``` diff --git a/plugins/gcore-fastedge-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 ec7dc83..d049948 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-08-17 + updated: 2026-08-20 --> --- @@ -250,123 +250,3 @@ 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-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 541ac7c..2039ea1 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- type: example @@ -225,140 +225,3 @@ import { - examples-headers-cdn-as (request/response header manipulation patterns) - platform-overview (secret management and deployment configuration) - best-practices (logging levels and secret handling guidelines) - -## Source Material - -### FILE: examples/variablesAndSecrets/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class VariablesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new VariablesContext(context_id, this); - } -} - -class VariablesContext extends Context { - constructor(context_id: u32, root_context: VariablesRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const username = getEnv("USERNAME"); - const password = getSecret("PASSWORD"); - - log(LogLevelValues.info, "USERNAME: " + username); - log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); - - stream_context.headers.request.add("x-env-username", username); - stream_context.headers.request.add("x-env-password", password); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new VariablesRoot(context_id); -}, "variablesAndSecrets"); -``` - - -### FILE: examples/variablesAndSecrets/package.json - -```json -{ - "name": "fastedge-as-example-variables-and-secrets", - "version": "0.0.1", - "description": "FastEdge AssemblyScript example: Variables and Secrets", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/variablesAndSecrets/README.md - -``` -[← Back to examples](../README.md) - -# Variables and Secrets - -This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the `USERNAME` environment variable using `getEnv`. -2. Reads the `PASSWORD` secret using `getSecret`. -3. Logs that both values were retrieved (without logging the secret value itself). -4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. - -This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. - -> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -| ---------- | -------------------- | -------------------------------------- | -| `USERNAME` | Environment variable | The username value to forward upstream | -| `PASSWORD` | Secret | The password value to forward upstream | - -## Local testing - -The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. - -To test locally with the visual debugger, create `fixtures/.env`: - -``` -FASTEDGE_VAR_ENV_USERNAME=my-username -FASTEDGE_VAR_SECRET_PASSWORD=my-password -``` - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| -------------------------------------- | -------------------------------------------------- | -| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | -| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/variablesAndSecrets.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md index 7535b8a..03f0c0b 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -114,6 +114,8 @@ onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues All properties return `ArrayBuffer`. Decode with `String.UTF8.decode(buf)` before use. Check `buf.byteLength > 0` before decoding to detect missing values. +Routing to the selected origin is achieved by writing `request.url` — this is not an HTTP redirect. No `Location` header is set and no 3xx response is sent to the client. The upstream fetch target is rewritten transparently. + --- ## API Calls @@ -196,6 +198,13 @@ Build scripts (from `package.json`): | `asbuild:release` | `asc assembly/index.ts --target release` | | `asbuild` | Runs both debug and release | +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + --- ## Key Patterns @@ -225,6 +234,16 @@ const defaultOrigin = getEnv("DEFAULT"); if (!defaultOrigin) { /* handles both null and empty string */ } ``` +**Logging country code and matched origin for observability:** +```typescript +log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, +); +``` + --- ## Constraints and Gotchas @@ -236,6 +255,7 @@ if (!defaultOrigin) { /* handles both null and empty string */ } - Country code matching is case-sensitive and depends on the exact casing provided by `request.country` at runtime. - `getEnv` returns `""` (empty string) when a country-code variable is not set; the `=== ""` check must be used for country-specific origin fallback (not a null check). - The `DEFAULT` check uses `!defaultOrigin` (falsy), catching both null and empty string returns. +- The app logs at INFO level: country code with matched origin, host value (if present), and final request URL. These are visible in FastEdge application logs. --- diff --git a/plugins/gcore-fastedge-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 61f35b9..981cd2f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -66,14 +66,14 @@ registerRootContext((context_id: u32) => { All header operations are accessed via `stream_context.headers.request` or `stream_context.headers.response`. -| Method | Signature | Description | -| --------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ | -| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | -| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | -| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | -| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | -| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | -| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | +| Method | Signature | Description | +| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | +| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | +| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | +| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | +| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | +| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | ### `Headers` / `HeaderPair` type @@ -120,11 +120,11 @@ Operations performed in order: Expected post-mutation request headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Response Phase — `onResponseHeaders` @@ -144,11 +144,11 @@ Operations performed in order: Expected post-mutation response headers (new headers only): -| Header | Value(s) | -| --------------- | --------------------------- | -| `new-header-01` | `` (empty string) | -| `new-header-02` | `new-value-02` | -| `new-header-03` | `value-03`, `value-03-a` | +| Header | Value(s) | +| --------------- | ------------------------ | +| `new-header-01` | `` (empty string) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03`, `value-03-a` | ## Header Validation Pattern @@ -212,11 +212,11 @@ if (diff.missing.size > 0 || diff.extra.size > 0) { ## Error Response Codes Used -| Code | Meaning | Trigger | -| ---- | --------------------------------- | ----------------------------------------------------------- | -| 550 | No headers present | `get_headers()` returns empty collection | -| 551 | Host header present but empty | `get("host")` returns `""` | -| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | +| Code | Meaning | Trigger | +| ---- | ------------------------------ | ---------------------------------------------------------- | +| 550 | No headers present | `get_headers()` returns empty collection | +| 551 | Host header present but empty | `get("host")` returns `""` | +| 552 | Header mismatch after mutation | `validateHeaders()` returns non-empty `missing` or `extra` | All errors use `send_http_response(code, "internal server error", body, [])`. @@ -259,18 +259,18 @@ pnpm install pnpm run asbuild ``` -| Output file | Description | -| -------------------------- | ---------------------------------------------- | -| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | -| `build/headers-debug.wasm` | Debug binary with source maps | +| Output file | Description | +| -------------------------- | --------------------------------------------- | +| `build/headers.wasm` | Optimised release binary — deploy to FastEdge | +| `build/headers-debug.wasm` | Debug binary with source maps | Build scripts defined in `package.json`: -| Script | Command | -| ------------------- | ------------------------------------------ | -| `asbuild:debug` | `asc assembly/index.ts --target debug` | -| `asbuild:release` | `asc assembly/index.ts --target release` | -| `asbuild` | Runs both debug and release builds | +| Script | Command | +| ----------------- | ---------------------------------------- | +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | Runs both debug and release builds | ## Deployment diff --git a/plugins/gcore-fastedge-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 97aea43..bbfc088 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-08-17 + updated: 2026-08-20 --> --- @@ -230,356 +230,3 @@ 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-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 9509c21..bc289c7 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> ## Overview @@ -174,7 +174,7 @@ class HttpCallContext extends Context { onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { // FastEdge re-invokes this hook after the httpCall response is processed. // The latch gates re-dispatch so the second invocation returns Continue - // instead of firing another HTTP call. + // instead of firing another HTTP call. See SDK docs → Outbound HTTP. if (this.httpCallDispatched) { log( LogLevelValues.info, @@ -321,3 +321,202 @@ Dependencies (from `package.json`): - SDK API reference — full lifecycle hook signatures, `RootContext` and `Context` base class APIs, all enum values (`WasmResultValues`, `FilterHeadersStatusValues`, `BufferTypeValues`, `LogLevelValues`) - Platform overview — FastEdge CDN execution model, context lifecycle, how the runtime re-enters hooks after async callbacks - Best practices — timeout tuning, upstream cluster naming, error response patterns + +## Source Material + +### FILE: examples/httpCall/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BaseContext, + BufferTypeValues, + Context, + FilterHeadersStatusValues, + get_buffer_bytes, + HeaderPair, + log, + LogLevelValues, + makeHeaderPair, + registerRootContext, + RootContext, + send_http_response, + stream_context, + WasmResultValues, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const INTERNAL_SERVER_ERROR: u32 = 500; + +function handleHttpCallResponse( + ctx: BaseContext, + hdrs: u32, + bodySize: usize, + trls: u32, +): void { + if (hdrs == 0) { + log(LogLevelValues.error, "HTTP call failed — no response received"); + return; + } + + const userAgent = stream_context.headers.http_callback.get("user-agent"); + if (userAgent !== "") { + log(LogLevelValues.info, "User-Agent: " + userAgent); + } + + if (bodySize > 0) { + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpCallResponseBody, + 0, + bodySize as u32, + ); + const bodyStr = String.UTF8.decode(bodyBytes); + log( + LogLevelValues.info, + "Response body (" + bodySize.toString() + " bytes): " + bodyStr, + ); + } else { + log(LogLevelValues.info, "Response body: empty"); + } +} + +class HttpCallRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new HttpCallContext(context_id, this); + } +} + +class HttpCallContext extends Context { + httpCallDispatched: bool = false; + + constructor(context_id: u32, root_context: HttpCallRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // FastEdge re-invokes this hook after the httpCall response is processed. + // The latch gates re-dispatch so the second invocation returns Continue + // instead of firing another HTTP call. See SDK docs → Outbound HTTP. + if (this.httpCallDispatched) { + log( + LogLevelValues.info, + "HTTP call response received, resuming request.", + ); + return FilterHeadersStatusValues.Continue; + } + + log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); + + const headers = new Array(); + headers.push(makeHeaderPair(":scheme", "https")); + headers.push(makeHeaderPair(":authority", "httpbin.org")); + headers.push(makeHeaderPair(":path", "/ip")); + headers.push(makeHeaderPair(":method", "GET")); + headers.push(makeHeaderPair("User-Agent", "fastedge")); + + // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. + const result = (this.root_context as HttpCallRoot).httpCall( + "httpbin.org", + headers, + new ArrayBuffer(0), + new Array(), + 3000, + this, + handleHttpCallResponse, + ); + + if (result != WasmResultValues.Ok) { + log( + LogLevelValues.error, + "Failed to dispatch HTTP call: " + result.toString(), + ); + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Failed to dispatch HTTP call"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + this.httpCallDispatched = true; + log(LogLevelValues.info, "HTTP call dispatched, pausing request"); + + return FilterHeadersStatusValues.StopIteration; + } +} + +registerRootContext((context_id: u32) => { + return new HttpCallRoot(context_id); +}, "httpCall"); +``` + +### FILE: examples/httpCall/package.json + +```json +{ + "name": "fastedge-as-example-http-call", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: HTTP Call — async HTTP dispatch with callback", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/httpCall/README.md + +``` +[← Back to examples](../README.md) + +# HTTP Call + +This application makes an asynchronous HTTP call to an external service using the proxy-wasm HTTP dispatch API. + +## What it does + +In `onRequestHeaders`, the app dispatches an outbound HTTP GET request to `httpbin.org/ip` and pauses the hook (`StopIteration`) until the response arrives. Dispatch is latched on an instance field so the second invocation of the hook (after the response is processed) returns `Continue` instead of dispatching again. + +Inside the response callback passed to `httpCall`: + +1. Checks whether the call succeeded (a `headers` value of `0` indicates failure — timeout, DNS error, etc.). +2. Reads and logs the `User-Agent` response header via `stream_context.headers.http_callback`. +3. Reads and logs the response body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`. + +If the dispatch itself fails (e.g. invalid arguments), the app returns a `500` error to the client. + +## Key concepts + +- **`httpCall()`** on `RootContext` dispatches an async HTTP request. It accepts a cluster (host), headers, body, trailers, a timeout in milliseconds, the originating context, and a callback. +- **FastEdge resume model.** Returning `FilterHeadersStatusValues.StopIteration` pauses the hook. The runtime processes the HTTP response, invokes the callback, then **re-invokes `onRequestHeaders` on the same `Context` instance**. The `httpCallDispatched` latch gates re-dispatch so the hook returns `Continue` the second time. This differs from canonical proxy-wasm — calling `continueRequest()` is not required (and has no effect on FastEdge). +- **`stream_context.headers.http_callback`** provides access to the HTTP call response headers inside the callback. The SDK sets the effective context before the callback fires, so these lookups resolve against the originating request. +- **`get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`** reads the response body inside the callback. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | +| `build/httpCall-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/httpCall.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. +``` diff --git a/plugins/gcore-fastedge-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 3959d1b..35d0930 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-08-17 + updated: 2026-08-20 --> # HTTP Call — CDN (Rust) @@ -365,6 +365,7 @@ fn to_status_code(status: Status) -> u32 { - 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`. +- The callback may fire before or after response hooks depending on runtime timing — state tracking via struct fields is the correct synchronization mechanism. --- 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 dd4a9e0..0a8fd7b 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -165,12 +165,14 @@ if (!end_of_stream) { **Execution flow**: 1. Read query string: `get_property("request.query")` → `ArrayBuffer`, decode to string -2. Validate query params via `validateQueryParams(query)` → `Map` -3. Open store: `KvStore.open(store)` — send error response if `null` -4. Dispatch on `action` parameter -5. Serialize result map with `stringifyMap` → JSON string -6. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` -7. Returns `FilterDataStatusValues.Continue` +2. If query string is empty, send error response immediately +3. Validate query params via `validateQueryParams(query)` → `Map` +4. Check `params.has("error")` — send error response if validation failed +5. Open store: `KvStore.open(store)` — send error response if `null` +6. Dispatch on `action` parameter +7. Serialize result map with `stringifyMap` → JSON string +8. Replace response body: `set_buffer_bytes(BufferTypeValues.HttpResponseBody, 0, body_buffer_length, encoded)` +9. Returns `FilterDataStatusValues.Continue` **Error path**: calls `sendErrorResponse(msg, body_buffer_length)` which: - Sets `response.status` to `545` via `set_property("response.status", ...)` @@ -208,11 +210,13 @@ switch (action) { const storeArrBuff = myStore.get(key); // null → responseBodyMap.set("Response", "null (Not found)") // non-null → responseBodyMap.set("Response", String.UTF8.decode(storeArrBuff)) + break; } case "scan": { const match = params.get("match"); const keys = myStore.scan(match); // returns string[] — responseBodyMap.set("Response", keys.join(", ")) + break; } case "zrange": { const key = params.get("key"); @@ -220,18 +224,21 @@ switch (action) { const max = params.get("max"); const tuples = myStore.zrangeByScore(key, parseFloat(min), parseFloat(max)); // returns ValueScoreTuple[] + break; } case "zscan": { const key = params.get("key"); const match = params.get("match"); const tuples = myStore.zscan(key, match); // returns ValueScoreTuple[] + break; } case "bfExists": { const key = params.get("key"); const item = params.get("item"); const exists = myStore.bfExists(key, item); // returns bool → responseBodyMap.set("Response", exists ? "true" : "false") + break; } } ``` @@ -252,7 +259,7 @@ Error response: { "error": "" } ``` -Error HTTP status: `545` (set via `response.status` property). +Error HTTP status: `545` (set via `response.status` property — advisory only). --- @@ -274,6 +281,14 @@ Internal helper (not exported). Splits query string on `&`, handles `key=value` Decodes `%xx` hex sequences and converts `+` to space. If `%xx` is not a valid hex sequence, passes through literally. +### `stringifyMap(map: Map): string` + +Exported helper. Serializes a `Map` to a JSON object string. Iterates keys in insertion order, produces `{ "key": "value", ... }` format. + +### `stringifyValueScoreTuples(arr: Array): string` + +Exported helper. Serializes an array of `ValueScoreTuple` to a comma-separated string of `{ value: , score: }` entries. Decodes each `tuple.value` (`ArrayBuffer`) with `String.UTF8.decode`. + --- ## Logging @@ -323,6 +338,7 @@ npm run asbuild:release # release only - `response.status` set via `set_property` in `onResponseBody` is advisory only — the origin HTTP status passes through to the client; the JSON error body is the authoritative error signal - Empty query string is treated as an error — app responds with `545` and a JSON error body - `validateQueryParams` returns a map containing key `"error"` on failure; the caller must check `params.has("error")` before proceeding +- `ArrayBuffer` decoding: both `KvStore.get` return values and `ValueScoreTuple.value` fields must be decoded with `String.UTF8.decode` before use as strings --- diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md index c6d043a..56aad05 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -165,7 +165,7 @@ let result = match action { "zrange" => self.handle_zrange(&store, ¶ms), "zscan" => self.handle_zscan(&store, ¶ms), "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!("Invalid action '{}'", action)), + _ => Err(format!("Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", action)), }; ``` @@ -240,276 +240,3 @@ Sorted set entries shape: `[{"value": "", "score": }, ...]` - Host services reference — full KV store, secrets, and dictionary API documentation for CDN (proxy-wasm) apps - CDN apps reference — proxy-wasm lifecycle hooks, request properties, header and body manipulation patterns - SDK reference (Rust) — `fastedge` crate modules, feature flags, and component model vs. proxy-wasm differences - -## Source Material - -### FILE: examples/cdn/key_value/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating KV Store operations via the proxy-wasm 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 fastedge::proxywasm::key_value::Store; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde_json::json; -use std::collections::HashMap; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(KvStoreRoot) }); -}} - -struct KvStoreRoot; - -impl Context for KvStoreRoot {} - -impl RootContext for KvStoreRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(KvStoreContext)) - } -} - -struct KvStoreContext; - -impl Context for KvStoreContext {} - -impl HttpContext for KvStoreContext { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Remove content-length since we replace the body - self.set_http_response_header("content-length", None); - self.set_http_response_header("content-type", Some("application/json")); - self.set_http_response_header("transfer-encoding", Some("chunked")); - 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 query = self - .get_property(vec!["request", "query"]) - .and_then(|bytes| String::from_utf8(bytes).ok()) - .unwrap_or_default(); - - if query.is_empty() { - self.send_error("App must be called with query parameters", body_size); - return Action::Continue; - } - - let params: HashMap<&str, &str> = querystring::querify(&query).into_iter().collect(); - - let Some(store_name) = params.get("store") else { - self.send_error("Missing required param 'store'", body_size); - return Action::Continue; - }; - - let action = params.get("action").copied().unwrap_or("get"); - - let store = match Store::open(store_name) { - Ok(s) => s, - Err(e) => { - self.send_error(&format!("Failed to open KvStore '{}': {}", store_name, e), body_size); - return Action::Continue; - } - }; - - let result = match action { - "get" => self.handle_get(&store, ¶ms), - "scan" => self.handle_scan(&store, ¶ms), - "zrange" => self.handle_zrange(&store, ¶ms), - "zscan" => self.handle_zscan(&store, ¶ms), - "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!( - "Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", - action - )), - }; - - let body = match result { - Ok(json) => json, - Err(msg) => { - self.send_error(&msg, body_size); - return Action::Continue; - } - }; - - self.set_http_response_body(0, body_size, body.as_bytes()); - - Action::Continue - } -} - -impl KvStoreContext { - fn handle_get(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'get' action")?; - 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(format!("KV get error: {}", e)), - } - } - - fn handle_scan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'scan' action")?; - 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(format!("KV scan error: {}", e)), - } - } - - fn handle_zrange(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zrange' action")?; - let min: f64 = params - .get("min") - .ok_or("Missing required param 'min' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'min' value: must be a number".to_string())?; - let max: f64 = params - .get("max") - .ok_or("Missing required param 'max' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'max' value: must be a number".to_string())?; - - 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(format!("KV zrange error: {}", e)), - } - } - - fn handle_zscan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zscan' action")?; - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'zscan' action")?; - - 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(format!("KV zscan error: {}", e)), - } - } - - fn handle_bf_exists(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'bfExists' action")?; - let item = *params.get("item").ok_or("Missing required param 'item' for 'bfExists' action")?; - - 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(format!("KV bfExists error: {}", e)), - } - } - - fn send_error(&self, msg: &str, body_size: usize) { - println!("{}", msg); - self.set_property( - vec!["response", "status"], - Some(b"500"), - ); - let error_body = json!({"error": msg}).to_string(); - self.set_http_response_body(0, body_size, error_body.as_bytes()); - } -} -``` - - -### FILE: examples/cdn/key_value/Cargo.toml - -```toml -[workspace] - -[package] -name = "key_value" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -querystring = "1.1" -serde_json = "1" -``` - - -### FILE: examples/cdn/key_value/README.md - -``` -[← Back to examples](../../README.md) - -# Key Value (CDN) - -Implements KV store operations via query parameters — get, scan, zrange, zscan, and bfExists — using the proxy-wasm ABI. -``` 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 d1e2c6e..4c0c294 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-08-17 + updated: 2026-08-20 --> # A/B Testing — FastEdge Example @@ -208,9 +208,9 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **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 +## Source ```js import { getEnv } from 'fastedge::env'; @@ -232,8 +232,6 @@ async function eventHandler({ 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', { @@ -243,7 +241,6 @@ async function eventHandler({ request }) { 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', @@ -261,13 +258,10 @@ addEventListener('fetch', (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); @@ -277,7 +271,6 @@ const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { return [xid, headers]; } const randomXid = `${Math.random()}`.slice(1, 5); - // Request does not contain A/B cookie, return random number return [randomXid, headers]; }; @@ -307,21 +300,3 @@ const createAbTestHeaders = (reqHeaders, testConfig, xid) => { 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 d754a7e..6de33d5 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-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,114 @@ 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-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 21d90c6..d9377ed 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-08-17 + updated: 2026-08-20 --> --- @@ -150,162 +150,3 @@ 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-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md index 7032040..c3bc2f1 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> # FastEdge Cache — JavaScript Examples @@ -331,3 +331,279 @@ Validation errors (e.g., conflicting `WriteOptions`) are thrown synchronously. H - platform-overview (POP topology, request lifecycle) - deploy skill reference (fastedge-build CLI, binary upload) - best-practices (error handling patterns, response construction) + +## 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 { + // `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 { + // 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 { + 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 { + 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/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 2f17c5e..7fe9ca6 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-08-17 + updated: 2026-08-20 --> --- @@ -133,77 +133,3 @@ 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-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 3766914..99f207d 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-08-17 + updated: 2026-08-20 --> --- @@ -378,277 +378,3 @@ 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-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 f25c8e7..50f4454 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-08-17 + updated: 2026-08-20 --> --- @@ -186,120 +186,3 @@ 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-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 6be9762..83b2f9f 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-08-17 + updated: 2026-08-20 --> --- @@ -183,7 +183,7 @@ Ok(Response::builder() - wstd HTTP client documentation - serde_json crate documentation -## Source Material +## Full Source ### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md index a1963fe..e9a2822 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,3 +250,178 @@ Returns: `ArrayBuffer | null` — `null` if the key does not exist. - examples/kv-store — KV-backed caching patterns - platform-overview reference — plan limits, execution budget, and body size constraints - sdk-reference-js reference — full KvStore API + +## Source Material + +### FILE: docs/PROXY_PATTERNS.md + +``` + + +# Proxy and Response Transform Patterns + +FastEdge HTTP apps can act as a thin proxy in front of an origin or upstream API — fetching a response, transforming it, and returning the result. This document covers the common proxy and transform patterns. + +## Simple Proxy + +Fetch from an upstream and return the body unchanged: + +```typescript +async function handle(request) { + const url = new URL(request.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: request.method, + headers: request.headers, + body: request.method !== "GET" && request.method !== "HEAD" + ? await request.arrayBuffer() + : undefined, + }); + + return response; +} + +addEventListener("fetch", (event) => { + event.respondWith(handle(event.request)); +}); +``` + +`fetch()` returns a streamable `Response`; returning it directly forwards body chunks without buffering everything in memory. + +## Proxy with JSON Transform + +Modify the response body before returning it. This is the pattern from `examples/outbound-modify-response/`: + +```typescript +async function handle() { + const upstream = await fetch("https://jsonplaceholder.typicode.com/users"); + const users = await upstream.json(); + + const transformed = { + users: users.slice(0, 5), + total: 5, + skip: 0, + limit: 30, + }; + + return new Response(JSON.stringify(transformed), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +addEventListener("fetch", (event) => { + event.respondWith(handle()); +}); +``` + +Reading `await upstream.json()` consumes the body; you cannot read it again. Read the body once, transform what you need, then return. + +## Hono Proxy with Transform + +Inside a Hono app, use `c.req.raw.headers` to forward the inbound headers and `c.req.arrayBuffer()` for the body: + +```typescript +import { Hono } from "hono"; + +const app = new Hono(); + +app.all("/api/*", async (c) => { + const url = new URL(c.req.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: c.req.method, + headers: c.req.raw.headers, + body: c.req.method !== "GET" && c.req.method !== "HEAD" + ? await c.req.arrayBuffer() + : undefined, + }); + + const data = await response.json(); + data.processedAt = new Date().toISOString(); + return c.json(data, response.status); +}); + +addEventListener("fetch", (event) => { + event.respondWith(app.fetch(event.request)); +}); +``` + +## Header Manipulation in Proxies + +Strip hop-by-hop headers before forwarding to upstream, and add diagnostic headers on the way back: + +```typescript +async function handle(request) { + const upstreamHeaders = new Headers(request.headers); + // Hop-by-hop headers should not be forwarded + upstreamHeaders.delete("connection"); + upstreamHeaders.delete("keep-alive"); + upstreamHeaders.delete("transfer-encoding"); + + const upstream = await fetch("https://backend.example.com", { + method: request.method, + headers: upstreamHeaders, + }); + + const responseHeaders = new Headers(upstream.headers); + responseHeaders.set("X-Proxied-By", "FastEdge"); + + return new Response(upstream.body, { + status: upstream.status, + headers: responseHeaders, + }); +} +``` + +`new Response(upstream.body, ...)` streams the body through without reading it into memory — preferred for large responses. + +## Cache-aware Proxy with KV + +Cache upstream responses in the KV store to avoid repeated outbound calls. Note: KV is read-only from app code; writes happen via the portal/API: + +```typescript +import { KvStore } from "fastedge::kv"; + +async function handle(request) { + const url = new URL(request.url); + + try { + const cache = KvStore.open("api-cache"); + const cached = cache.get(url.pathname); + if (cached !== null) { + return new Response(cached, { + status: 200, + headers: { "content-type": "application/json", "x-cache": "hit" }, + }); + } + } catch { + // KV store unavailable — fall through to upstream fetch + } + + const upstream = await fetch(`https://backend.example.com${url.pathname}`); + return new Response(await upstream.arrayBuffer(), { + status: upstream.status, + headers: { ...Object.fromEntries(upstream.headers), "x-cache": "miss" }, + }); +} +``` + +`KvStore.open(name)` returns a `KvStoreInstance` (it does not return null) but can throw if the named store is not provisioned — wrap the open call in `try/catch`. `cache.get(key)` returns `ArrayBuffer | null`; check for `null`, not falsy, since an empty buffer is a valid value. + +## Operational Notes + +- **Outbound fetch budget.** Each invocation has a limited number of outbound requests (5 on Basic, 20 on Pro). Parallelise where possible with `Promise.all([...])` instead of sequential `await fetch(...)`. +- **Execution time budget.** Proxying upstream + transforming counts against the 50ms (Basic) / 200ms (Pro) execution budget. Slow upstreams will trip 532 timeouts. +- **Body size limits.** Inbound and outbound bodies are subject to the configured request/response size limits. Stream where possible rather than buffering with `arrayBuffer()` / `text()` / `json()`. + +## See Also + +- `examples/outbound-modify-response/` — JSON transform of upstream response +- `examples/outbound-fetch/` — basic outbound `fetch()` patterns +- `examples/headers/` — request/response header manipulation +- `examples/kv-store/` — KV-backed caching patterns + +``` 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 2f2a35a..f789dad 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-08-17 + updated: 2026-08-20 --> --- @@ -182,3 +182,236 @@ cargo build --release - sdk-reference-rust (`fastedge::send_request`, `Body`, `Request`, `Response`) - host-services-rust (outbound HTTP) - rusty-s3 crate documentation (external) + +## Source Material + +### FILE: examples/http/basic/s3upload/src/lib.rs + +```rust +use std::time::Duration; + +use fastedge::{ + body::Body, + http::{header, Error, Method, Request, Response, StatusCode}, +}; +use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle}; +use std::{collections::HashMap, env}; +use url::Url; + +#[fastedge::http] +fn main(req: Request) -> Result, Error> { + match req.method() { + // Allow only POST and PUT requests + &Method::POST | &Method::PUT => (), + + &Method::OPTIONS => { + return Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()); + } + + // Deny anything else + _ => { + return Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header(header::ALLOW, "PUT, POST") + .body(Body::from("This method is not allowed\n")); + } + }; + + /* get request params */ + let query_pairs = |q: &str| { + q.split('&') + .filter_map(|q| { + let mut i = q.splitn(2, '='); + let k = i.next()?; + let v = i.next()?; + Some((k, v)) + }) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>() + }; + let hash_query: HashMap = req.uri().query().map_or(HashMap::new(), query_pairs); + + let fname = match hash_query.get("name") { + None => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")) + } + Some(i) => i, + }; + if req.body().is_empty() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")); + } + let content_type = match req.headers().get("Content-Type") { + None => "application/octet-stream", + Some(v) => v.to_str().unwrap_or("application/octet-stream"), + }; + let content_type = content_type.to_owned(); + let content = req.into_body(); + + match env::var("MAX_FILE_SIZE").ok() { + None => {} + Some(l) => match l.parse::() { + Err(_) => {} + Ok(v) => { + if content.len() > v { + let msg = format!("File exceeds allowed limit of {} bytes\n", v); + return Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::from(msg.as_str().to_owned())); + } + } + }, + } + + let (signed_url, host) = match prepare_s3(fname) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("App misconfigured\n")) + } + Ok((u, h)) => (u, h), + }; + + /* build outgoing req */ + let out_req = Request::builder() + .method(Method::PUT) + .uri(signed_url.as_str()) + .header("Host", host) + .header("Accept-Encoding", "identity") + .header("Content-Length", content.len().to_string()) + .header("Content-Type", content_type); + + let Ok(req) = out_req.body(content) else { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Malformed request\n")); + }; + + let rsp = match fastedge::send_request(req) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + } + Ok(r) => r, + }; + let (parts, body) = rsp.into_parts(); + let body = if parts.status == StatusCode::OK { + let mut tmp_url = signed_url.clone(); + tmp_url.set_query(None); + Body::from(tmp_url.to_string()) + } else { + body + }; + Ok(Response::from_parts(parts, body)) +} + +fn prepare_s3(fname: &str) -> anyhow::Result<(Url, String)> { + /* read S3 access params from env */ + let access_key = env::var("ACCESS_KEY")?; + let secret_key = env::var("SECRET_KEY")?; + let region = env::var("REGION")?; + let base_hostname = env::var("BASE_HOSTNAME")?; + let bucket = env::var("BUCKET")?; + let scheme = env::var("SCHEME").unwrap_or_else(|_| "http".to_string()); + + /* set S3 request params */ + let host = region.clone() + "." + base_hostname.as_str(); + let upload_url = scheme + "://" + host.as_str(); + let parsed_url = upload_url.parse()?; + let bucket = Bucket::new(parsed_url, UrlStyle::Path, bucket, region)?; + + let creds = Credentials::new(access_key, secret_key); + let action = bucket.put_object(Some(&creds), fname); + let signed_url = action.sign(Duration::from_secs(60 * 60)); + + Ok((signed_url, host)) +} +``` + + +### FILE: examples/http/basic/s3upload/Cargo.toml + +```toml +[workspace] + +[package] +name = "s3upload" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +url = "2.3" +rusty-s3 = "0.5" +anyhow = "1" +``` + + +### FILE: examples/http/basic/s3upload/README.md + +``` +[← Back to examples](../../../README.md) + +# S3 Upload + +FastEdge edge function that accepts a file upload, signs an S3 PUT request on the fly, uploads the file directly to an S3-compatible bucket, and returns the clean object URL to the caller. + +> **Legacy handler:** Uses `#[fastedge::http]` (sync, `wasm32-wasip1`). For new apps prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +1. Accepts `POST` or `PUT` only — returns 405 for other methods +2. Requires `?name=` query parameter and a non-empty body — returns 400 otherwise +3. Enforces `MAX_FILE_SIZE` if set — returns 413 if exceeded +4. Calls `prepare_s3()` to build a 1-hour presigned `PUT` URL using `rusty_s3` +5. Forwards the file body to S3 via `fastedge::send_request` +6. On success (S3 returns 200): responds with the clean object URL (no query string) +7. On S3 error: forwards the S3 status and error body back to the caller + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ACCESS_KEY` | ✅ | S3 access key | +| `SECRET_KEY` | ✅ | S3 secret key | +| `REGION` | ✅ | S3 region (e.g. `s-ed1`) | +| `BASE_HOSTNAME` | ✅ | S3 base hostname (e.g. `cloud.gcore.lu`) | +| `BUCKET` | ✅ | S3 bucket name | +| `SCHEME` | optional | URL scheme — defaults to `http` | +| `MAX_FILE_SIZE` | optional | Maximum upload size in bytes — no limit if unset | + +The constructed endpoint is `://.//`. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/s3upload.wasm +``` + +## Usage + +``` +POST /upload?name=photo.jpg +Content-Type: image/jpeg + + +``` + +On success (200), the response body is the clean S3 object URL (presign query parameters stripped). + +## Notes + +- The `OPTIONS` method returns 204 but does **not** include CORS headers — add `Access-Control-Allow-*` headers if browser preflight support is needed. +- The presigned URL expires after 1 hour, but since the upload is performed server-side this has no practical impact. +- `MAX_FILE_SIZE` is silently ignored if set to a non-numeric value. +``` 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 3d9715b..d65296b 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-08-17 + updated: 2026-08-20 --> # Secret Access — Rust HTTP Example @@ -251,185 +251,3 @@ 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-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md index ac3b308..df75287 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> # Secret Rollover (WASI, Rust) @@ -124,28 +124,56 @@ Slots are ordered by slot number; any non-negative `u32` value is valid as a slo ## Implementation Pattern ```rust -use fastedge::secret; +use std::time::{SystemTime, UNIX_EPOCH}; -// Read slot from header, default to current unix timestamp -let slot: u32 = request - .headers() - .get("x-slot") - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.parse().ok()) - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as u32 +use anyhow::anyhow; +use fastedge::secret; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + // Read slot from header, default to current unix timestamp + let slot: u32 = request + .headers() + .get("x-slot") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as u32 + }); + + let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + + // Get current (latest) value + let current = secret::get(secret_name) + .map_err(|e| anyhow!("secret::get failed: {e}"))?; + + // Get 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}"))?; + + let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, }); -// Get current (latest) value -let current = secret::get(secret_name) - .map_err(|e| anyhow!("secret::get failed: {e}"))?; - -// Get 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}"))?; + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} ``` ## Constraints and Gotchas 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 3370f1b..5d19fe4 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-08-17 + updated: 2026-08-20 --> --- @@ -147,123 +147,3 @@ 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-cursor/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md index 4be6846..caea588 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -183,6 +183,7 @@ Build output: `target/wasm32-wasip1/release/smart_switch.wasm` - **`fastedge::send_request` errors map to `StatusCode`**: These are not `anyhow::Error` or `std::error::Error`; match on `fastedge::Error` variants explicitly. - **`"ACCEPTED"` string determines success**: The command result is compared as a string; only `"ACCEPTED"` maps to `204`. Any other value maps to `404`. - **Auth is a plain header match**: `Authorization` header value is compared directly to `PASSWORD` env var — no bearer prefix stripping, no hashing. +- **`serde` listed as dependency but not directly used in source**: `serde_json` handles all JSON parsing; `serde` is a transitive requirement. --- 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 b8e56f8..8a6e7c9 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-08-17 + updated: 2026-08-20 --> --- @@ -143,105 +143,3 @@ 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-cursor/skills/fastedge-docs/reference/templates/catalog.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md index 91bb0f1..45de29b 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/catalog.md @@ -4,7 +4,7 @@ - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 - updated: 2026-08-17 + updated: 2026-08-20 --> # FastEdge Bolt-On Templates @@ -41,6 +41,8 @@ Before writing authentication, cookie-hardening, or content-transform logic from **Attribute application:** `Secure` and `HttpOnly` are added only if not already present. `SameSite=Strict` is set unconditionally, overriding any existing `SameSite` value. +**Implementation notes:** Duplicate `Set-Cookie` headers are preserved — the host collapses them to one, so the filter clears the header and re-adds each cookie as its own occurrence. Empty segments from trailing semicolons are dropped when re-joining. Headers are only rewritten when at least one cookie value actually changes. + **Origin integration:** Configured entirely through environment variables (`COOKIE_NAME`, `SECURE`, `HTTPONLY`, `SAMESITE`) — no origin code required. --- @@ -63,9 +65,9 @@ Before writing authentication, cookie-hardening, or content-transform logic from **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). +**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). `AUTH_PREFIX` sets the path prefix reserved for auth routes (default: `/auth`). -**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. +**Origin integration:** This template has an integration reference. 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. --- @@ -88,7 +90,9 @@ Before writing authentication, cookie-hardening, or content-transform logic from - 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 +- The edge `mfa_session` is short-lived (8h, non-sliding) and not cross-PoP revocable +- CDN logs include the session subject (`sub`) and request path on each authorized request — review log retention policy if `sub` contains PII **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. +**Origin integration:** This template has an integration reference. 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-totp-integration.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/templates/edge-totp-integration.md index 4b3a82a..61b1457 100644 --- 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 @@ -4,7 +4,7 @@ - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 - updated: 2026-08-17 + updated: 2026-08-20 --> # edge-totp — Origin Integration @@ -107,7 +107,7 @@ Self-service enrollment (`{AUTH_PREFIX}/activate`) inherits the trust level of t **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 the edge-totp threat model reference (threat-model.md, risk R5) before relying on self-service enrollment for sensitive accounts. --- 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 d2b1159..695920d 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -222,233 +222,3 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide - -## Source Material - -### FILE: examples/abTesting/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - set_property, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; -import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; - -class AbTestingRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new AbTestingContext(context_id, this); - } -} - -class AbTestingContext extends Context { - constructor(context_id: u32, root_context: AbTestingRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const experimentName = getEnv("EXPERIMENT_NAME"); - if (experimentName === "") { - send_http_response( - 500, - "internal server error", - String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const variantAPath = getEnv("VARIANT_A_PATH"); - const variantBPath = getEnv("VARIANT_B_PATH"); - if (variantAPath === "" || variantBPath === "") { - send_http_response( - 500, - "internal server error", - String.UTF8.encode( - "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", - ), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // Check for existing experiment cookie - const cookieName = "fe_exp_" + experimentName; - const cookieHeader = stream_context.headers.request.get("Cookie"); - let assignedVariant = this.getCookieValue(cookieHeader, cookieName); - - // Assign variant if not already set - if (assignedVariant !== "A" && assignedVariant !== "B") { - // Use current time as a simple entropy source for 50/50 split - const now = getCurrentTime(); - assignedVariant = now % 2 == 0 ? "A" : "B"; - } - - // Rewrite the request path to the variant path - const pathArrBuf = get_property("request.path"); - if (pathArrBuf.byteLength === 0) { - return FilterHeadersStatusValues.Continue; - } - const originalPath = String.UTF8.decode(pathArrBuf); - const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; - const newPath = variantPath + originalPath; - - // Reconstruct request.url from its decomposed parts rather than splicing - // the path out of the full URL — splicing breaks when the path happens to - // appear inside the host, and it can silently lose the query string. - const schemeBuf = get_property("request.scheme"); - const hostBuf = get_property("request.host"); - if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { - const scheme = String.UTF8.decode(schemeBuf); - const host = String.UTF8.decode(hostBuf); - const queryBuf = get_property("request.query"); - const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; - const newUrl = - scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); - log(LogLevelValues.info, `A/B routing: ${newUrl}`); - set_property("request.url", String.UTF8.encode(newUrl)); - } - - // Add variant header for upstream visibility - stream_context.headers.request.add("X-Experiment", experimentName); - stream_context.headers.request.add("X-Variant", assignedVariant); - - log( - LogLevelValues.info, - `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, - ); - - return FilterHeadersStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Recover the assigned variant from the request header set in onRequestHeaders. - // Instance state (this.variant) does not survive the nginx -> core-proxy hop. - const variant = stream_context.headers.request.get("X-Variant"); - if (variant === "") { - return FilterHeadersStatusValues.Continue; - } - - const experimentName = getEnv("EXPERIMENT_NAME"); - const cookieName = "fe_exp_" + experimentName; - - // Set the experiment cookie so subsequent requests stick to the same variant - stream_context.headers.response.add( - "Set-Cookie", - cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", - ); - - // Add variant as response header for observability - stream_context.headers.response.add("X-Variant", variant); - - return FilterHeadersStatusValues.Continue; - } - - private getCookieValue(cookieHeader: string, name: string): string { - if (cookieHeader === "") return ""; - const pairs = cookieHeader.split(";"); - const prefix = name + "="; - for (let i = 0; i < pairs.length; i++) { - const pair = pairs[i].trim(); - if (pair.startsWith(prefix)) { - return pair.substring(prefix.length); - } - } - return ""; - } -} - -registerRootContext((context_id: u32) => { - return new AbTestingRoot(context_id); -}, "abTesting"); -``` - - -### FILE: examples/abTesting/package.json - -```json -{ - "name": "fastedge-as-example-ab-testing", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/abTesting/README.md - -``` -[← Back to examples](../README.md) - -# A/B Testing - -This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. - -## What it does - -In `onRequestHeaders`, the app: - -1. Checks for an existing experiment cookie (`fe_exp_`). -2. If no cookie is found, assigns the user to variant **A** or **B** (50/50 split). -3. Rewrites the request path by prepending the variant-specific path prefix (e.g. `/variant-a/original/path`). -4. Adds `X-Experiment` and `X-Variant` request headers for upstream visibility. - -In `onResponseHeaders`, the app sets a `Set-Cookie` header to persist the variant assignment for subsequent requests (24-hour TTL). - -> **Note on variant assignment entropy:** New-visitor assignment uses `getCurrentTime() % 2` as a simple 50/50 source. This is illustrative — it is not sticky across two requests that arrive in the same millisecond and is not reproducible in tests. Production A/B implementations typically hash a stable visitor identifier (e.g. client IP or session token) for deterministic, sticky pre-cookie assignment. - -## Configuration - -Set the following environment variables on your FastEdge application: - -| Variable | Example | Description | -|----------|---------|-------------| -| `EXPERIMENT_NAME` | `homepage-redesign` | Name of the experiment (required) | -| `VARIANT_A_PATH` | `/variant-a` | Path prefix for variant A (required) | -| `VARIANT_B_PATH` | `/variant-b` | Path prefix for variant B (required) | - -Your origin server should serve different content at each variant path prefix. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/abTesting.wasm` | Optimised release binary — upload this to FastEdge | -| `build/abTesting-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/abTesting.wasm` to the FastEdge portal and attach it to your CDN application. Configure the experiment environment variables in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md index 4e444bf..4295b4b 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-08-17 + updated: 2026-08-20 --> --- @@ -212,122 +212,3 @@ 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 { Box::new(ApiKeyRoot) }); -}} - -struct ApiKeyRoot; - -impl Context for ApiKeyRoot {} - -impl RootContext for ApiKeyRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - 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 ff3c689..07cdb77 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -267,206 +267,3 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint - -## Source Material - -### FILE: examples/jwt/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; - -const UNAUTHORIZED: u32 = 401; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class AuthRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes - return new Auth(context_id, this); - } -} - -class Auth extends Context { - constructor(context_id: u32, root_context: AuthRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const secret = getSecret("SECRET"); - if (!secret) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const authHeader = stream_context.headers.request.get("Authorization"); - if (!authHeader) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("No Authorization header"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - if (!authHeader.startsWith("Bearer ")) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Authorization header must use Bearer scheme"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const token = authHeader.slice(7); // strip "Bearer " prefix - if (!token) { - send_http_response( - UNAUTHORIZED, - "unauthorized", - String.UTF8.encode("Token not found"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - // Decode the JWT token - const jwtResult = jwtVerify(token, secret); - if (jwtResult !== JwtValidation.Ok) { - if (jwtResult === JwtValidation.Expired) { - log(LogLevelValues.info, "Token Expired"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Expired token"), - [], - ); - } else { - log(LogLevelValues.info, "Bad Token"); - send_http_response( - FORBIDDEN, - "forbidden", - String.UTF8.encode("Invalid token"), - [], - ); - } - return FilterHeadersStatusValues.StopIteration; - } - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new AuthRoot(context_id); -}, "auth"); -``` - - -### FILE: examples/jwt/package.json - -```json -{ - "name": "fastedge-as-example-jwt", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: JWT validation", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/as-jwt": "^1.0.3", - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", - "assemblyscript-json": "^1.1.0" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/jwt/README.md - -``` -[← Back to examples](../README.md) - -# JWT Validation - -This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. -2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. -3. Verifies the token signature and expiry using `jwtVerify()`. -4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. - -## Configuration - -Set the following secret variable on your FastEdge application: - -| Secret | Description | -| -------- | ------------------------------------------------------------------ | -| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | - -## Testing tokens - -**Expired token** (will return `403 Forbidden`): - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ -``` - -**Valid token** (expiry: 2035-01-01, will return `200 OK`): - -``` -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU -``` - -Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| ---------------------- | -------------------------------------------------- | -| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | -| `build/jwt-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/jwt.wasm` to the [FastEdge portal](https://portal.gcore.com) and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. - -For more on secrets and secret rotation slots, see the [FastEdge secrets documentation](https://gcore.com/docs/fastedge/secrets-manager/slots). -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md index 28c7eda..c3da37d 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-08-17 + updated: 2026-08-20 --> --- @@ -259,3 +259,142 @@ 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 { 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 {})) + } + + fn get_type(&self) -> Option { + 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::() 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::(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 8b560bd..3f82231 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -15,7 +15,7 @@ template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN AssemblyScript @@ -263,3 +263,118 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) + +## Source Material + +### FILE: examples/helloWorld/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; + +class HelloWorldRoot extends RootContext { + createContext(context_id: u32): Context { + return new HelloWorld(context_id, this); + } +} + +class HelloWorld extends Context { + constructor(context_id: u32, root_context: HelloWorldRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool, + ): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + log(LogLevelValues.info, "onRequestBody >> Hello World!"); + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool, + ): FilterDataStatusValues { + log(LogLevelValues.info, "onResponseBody >> Hello World!"); + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HelloWorldRoot(context_id); +}, "helloWorld"); +``` + + +### FILE: examples/helloWorld/package.json + +```json +{ + "name": "fastedge-as-example-hello-world", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/helloWorld/asconfig.json + +```json +{ + "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", + "targets": { + "debug": { + "outFile": "build/helloWorld-debug.wasm", + "textFile": "build/helloWorld-debug.wat", + "sourceMap": true, + "debug": true + }, + "release": { + "outFile": "build/helloWorld.wasm", + "textFile": "build/helloWorld.wat", + "sourceMap": true, + "optimizeLevel": 3, + "shrinkLevel": 0, + "converge": false, + "noAssert": false + } + }, + "options": { + "bindings": "esm", + "use": "abort=abort_proc_exit" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md index d4f417a..d3ad5df 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-08-17 + updated: 2026-08-20 --> --- @@ -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-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN Rust @@ -208,3 +208,77 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { Action::Continue } ``` + +## 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 { Box::new(HelloWorldRoot) }); +}} + +struct HelloWorldRoot; + +impl Context for HelloWorldRoot {} + +impl RootContext for HelloWorldRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(HelloWorld)) + } +} + +struct HelloWorld; + +impl Context for HelloWorld {} + +impl HttpContext for HelloWorld { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_headers"); + Action::Continue + } + + fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_body"); + Action::Continue + } + + 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 + } + + fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_response_body"); + 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 e3da1c6..837648f 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -237,7 +237,7 @@ onResponseBody(body_buffer_length: usize, end_of_stream: bool): FilterDataStatus ### `onLog(): void` -Called at the end of the request lifecycle. Not present in this example's source but inherited from `Context`. Used for final audit logging. +Called at the end of the request lifecycle. Inherited from `Context`. Used for final audit logging. Not implemented in this example's source. ## Registration diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md index e8b9ba2..4b49934 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-08-17 + updated: 2026-08-20 --> --- @@ -243,125 +243,3 @@ 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 { 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)) - } -} - -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 2987a96..f49e70c 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -187,191 +187,3 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) - -## Source Material - -### FILE: examples/cacheControl/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class CacheControlRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new CacheControlContext(context_id, this); - } -} - -class CacheControlContext extends Context { - constructor(context_id: u32, root_context: CacheControlRoot) { - super(context_id, root_context); - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const statusBuf = get_property("response.status"); - let statusCode: u32 = 200; - if (statusBuf.byteLength >= 2) { - const bytes = Uint8Array.wrap(statusBuf); - statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); - } - - // Only cache successful responses - if (statusCode < 200 || statusCode >= 400) { - stream_context.headers.response.replace( - "Cache-Control", - "no-store", - ); - return FilterHeadersStatusValues.Continue; - } - - // Determine cache policy based on content type - const contentType = stream_context.headers.response.get("Content-Type"); - - const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); - const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; - const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); - const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; - const rawApiMaxAge = getEnv("API_MAX_AGE"); - const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; - - let cacheControl: string; - - if (this.isStaticAsset(contentType)) { - // Static assets: long cache, immutable - cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; - } else if (contentType.includes("text/html")) { - // HTML: short cache, must revalidate - cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; - stream_context.headers.response.add("Vary", "Accept-Encoding"); - } else if ( - contentType.includes("application/json") || - contentType.includes("application/xml") - ) { - // API responses: configurable, private by default - if (apiMaxAge === "0") { - cacheControl = "no-cache, no-store, must-revalidate"; - } else { - cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; - } - stream_context.headers.response.add("Vary", "Accept, Authorization"); - } else { - // Default: moderate cache - cacheControl = "public, max-age=600"; - } - - stream_context.headers.response.replace("Cache-Control", cacheControl); - - log( - LogLevelValues.info, - "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", - ); - - return FilterHeadersStatusValues.Continue; - } - - private isStaticAsset(contentType: string): bool { - return ( - contentType.includes("image/") || - contentType.includes("font/") || - contentType.includes("application/javascript") || - contentType.includes("text/css") || - contentType.includes("text/javascript") || - contentType.includes("application/wasm") - ); - } -} - -registerRootContext((context_id: u32) => { - return new CacheControlRoot(context_id); -}, "cacheControl"); -``` - - -### FILE: examples/cacheControl/package.json - -```json -{ - "name": "fastedge-as-example-cache-control", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/cacheControl/README.md - -``` -[← Back to examples](../README.md) - -# Cache Control - -This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. - -## What it does - -In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: - -| Content Type | Cache Policy | Default Max-Age | -|---|---|---| -| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | -| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | -| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | -| Other | `public, max-age=600` | 10 minutes | -| Error responses (4xx/5xx) | `no-store` | — | - -Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). - -## Configuration - -All environment variables are optional — sensible defaults are applied when unset. - -| Variable | Default | Description | -|----------|---------|-------------| -| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | -| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | -| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | -| `build/cacheControl-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-rust.md index b49ed2a..0565961 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-08-17 + updated: 2026-08-20 --> --- @@ -219,3 +219,144 @@ 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 { Box::new(CacheControlRoot) }); +}} + +struct CacheControlRoot; + +impl Context for CacheControlRoot {} + +impl RootContext for CacheControlRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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 69d0e83..e156022 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-08-17 + updated: 2026-08-20 --> --- @@ -84,6 +84,9 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action **Logic:** 1. Always add `Image-Format: original` — serves as the cache key for the unconverted response. + ```rust + self.add_http_request_header("Image-Format", "original"); + ``` 2. Read `request.extension` property: ```rust let raw_ext = self.get_property(vec!["request.extension"]); @@ -284,300 +287,3 @@ 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 { 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-cursor/skills/scaffold/reference/cdn/custom-error-pages-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-as.md index 0bdc459..bb0b1ac 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -118,13 +118,34 @@ onResponseBody( const description = this.getErrorDescription(code); const category = code >= 500 ? "Server Error" : "Client Error"; - const html = /* ... build HTML string ... */; + const html = + '' + + '' + + "" + code.toString() + " — " + title + "" + + "
" + + "

" + code.toString() + "

" + + "

" + title + "

" + + "

" + description + "

" + + "

" + category + "

" + + "
"; const body = String.UTF8.encode(html); + // Replace the entire original body — pass body_buffer_length as the length + // to replace, not body.byteLength. Otherwise any original bytes beyond the + // new body's length survive at the tail of the response. set_buffer_bytes( BufferTypeValues.HttpResponseBody, 0, - body_buffer_length as u32, // use original length, NOT body.byteLength + body_buffer_length as u32, body, ); @@ -158,18 +179,30 @@ private getErrorTitle(code: u32): string { } private getErrorDescription(code: u32): string { - if (code == 400) return "The server could not understand the request due to invalid syntax."; - if (code == 401) return "You need to authenticate to access this resource."; - if (code == 403) return "You do not have permission to access this resource."; - if (code == 404) return "The requested page could not be found. It may have been moved or deleted."; - if (code == 405) return "The request method is not supported for this resource."; - if (code == 408) return "The server timed out waiting for the request."; - if (code == 429) return "You have sent too many requests. Please try again later."; - if (code == 500) return "The server encountered an unexpected condition that prevented it from fulfilling the request."; - if (code == 502) return "The server received an invalid response from the upstream server."; - if (code == 503) return "The server is temporarily unavailable. Please try again later."; - if (code == 504) return "The server did not receive a timely response from the upstream server."; - if (code >= 500) return "The server encountered an error processing your request."; + if (code == 400) + return "The server could not understand the request due to invalid syntax."; + if (code == 401) + return "You need to authenticate to access this resource."; + if (code == 403) + return "You do not have permission to access this resource."; + if (code == 404) + return "The requested page could not be found. It may have been moved or deleted."; + if (code == 405) + return "The request method is not supported for this resource."; + if (code == 408) + return "The server timed out waiting for the request."; + if (code == 429) + return "You have sent too many requests. Please try again later."; + if (code == 500) + return "The server encountered an unexpected condition that prevented it from fulfilling the request."; + if (code == 502) + return "The server received an invalid response from the upstream server."; + if (code == 503) + return "The server is temporarily unavailable. Please try again later."; + if (code == 504) + return "The server did not receive a timely response from the upstream server."; + if (code >= 500) + return "The server encountered an error processing your request."; return "An error occurred processing your request."; } ``` @@ -259,6 +292,14 @@ pnpm run asbuild | `build/customErrorPages.wasm` | Optimised release binary — upload to FastEdge | | `build/customErrorPages-debug.wasm` | Debug binary with source maps | +Build scripts defined in `package.json`: + +| Script | Command | +|---|---| +| `asbuild:debug` | `asc assembly/index.ts --target debug` | +| `asbuild:release` | `asc assembly/index.ts --target release` | +| `asbuild` | runs both debug and release | + --- ## Deploy @@ -273,6 +314,7 @@ Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to you - **Buffer entire body before replacing**: Return `FilterDataStatusValues.StopIterationAndBuffer` until `end_of_stream` is true, then replace. - **Replace length must be `body_buffer_length`**: Using the new body's byte length as the replacement length will leave original response bytes at the tail. - **Status buffer is big-endian 2 bytes**: Always check `byteLength >= 2` before decoding. +- **Status lookup methods must be private class methods**: No closures, no default args on nested functions. --- diff --git a/plugins/gcore-fastedge-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 d8d3907..eca26cd 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-08-17 + updated: 2026-08-20 --> --- @@ -310,207 +310,3 @@ 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 { 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)) - } -} - -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/.jpg` -2. Add a message file: `public/messages/.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 de7cb9f..a95115f 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-08-17 + updated: 2026-08-20 --> --- @@ -88,7 +88,7 @@ impl HttpContext for HttpHeaders { return Action::Pause; }; - // Trim leading '/' + //trim first '/' let path = if path.starts_with('/') { &path[1..] } else { @@ -260,123 +260,3 @@ 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 { 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-cursor/skills/scaffold/reference/cdn/env-secrets-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-as.md index 912df27..0a75003 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -179,138 +179,3 @@ Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to - cdn-base skeleton (base request/response handling structure) - fastedge-test reference (local WASM test runner and fixture format) - platform-overview reference (FastEdge application configuration and secret management) - -## Source Material - -### FILE: examples/variablesAndSecrets/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - getSecret, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class VariablesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new VariablesContext(context_id, this); - } -} - -class VariablesContext extends Context { - constructor(context_id: u32, root_context: VariablesRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const username = getEnv("USERNAME"); - const password = getSecret("PASSWORD"); - - log(LogLevelValues.info, "USERNAME: " + username); - log(LogLevelValues.info, "PASSWORD: [set, length " + password.length.toString() + "]"); - - stream_context.headers.request.add("x-env-username", username); - stream_context.headers.request.add("x-env-password", password); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new VariablesRoot(context_id); -}, "variablesAndSecrets"); -``` - -### FILE: examples/variablesAndSecrets/package.json - -```json -{ - "name": "fastedge-as-example-variables-and-secrets", - "version": "0.0.1", - "description": "FastEdge AssemblyScript example: Variables and Secrets", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/variablesAndSecrets/README.md - -``` -[← Back to examples](../README.md) - -# Variables and Secrets - -This application demonstrates reading environment variables and secrets, then forwarding their values as request headers to the upstream. - -## What it does - -In `onRequestHeaders`, the app: - -1. Reads the `USERNAME` environment variable using `getEnv`. -2. Reads the `PASSWORD` secret using `getSecret`. -3. Logs that both values were retrieved (without logging the secret value itself). -4. Injects them as `x-env-username` and `x-env-password` request headers so the upstream receives them. - -This is useful as a reference for understanding how to access environment variables and secrets within a FastEdge plugin. - -> **Security warning:** Never log secret values verbatim in production. Logs are often persisted and accessible to operators who should not see credential values. This example logs the secret's length rather than its content. Similarly, be deliberate about which upstream systems receive secret values via forwarded headers — limit forwarding to systems that need it. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -| ---------- | -------------------- | -------------------------------------- | -| `USERNAME` | Environment variable | The username value to forward upstream | -| `PASSWORD` | Secret | The password value to forward upstream | - -## Local testing - -The fixture at `fixtures/happy-path.test.json` uses `"dotenv": {"enabled": true}` to load values from `fixtures/.env`. The runner maps `FASTEDGE_VAR_ENV_` to `getEnv("NAME")` and `FASTEDGE_VAR_SECRET_` to `getSecret("NAME")`. - -To test locally with the visual debugger, create `fixtures/.env`: - -``` -FASTEDGE_VAR_ENV_USERNAME=my-username -FASTEDGE_VAR_SECRET_PASSWORD=my-password -``` - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -| -------------------------------------- | -------------------------------------------------- | -| `build/variablesAndSecrets.wasm` | Optimised release binary — upload this to FastEdge | -| `build/variablesAndSecrets-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/variablesAndSecrets.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `USERNAME` environment variable and the `PASSWORD` secret in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md index 9336819..e8a660a 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-08-17 + updated: 2026-08-20 --> --- @@ -175,104 +175,3 @@ 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 { Box::new(VariablesRoot) }); -}} - -struct VariablesRoot; - -impl Context for VariablesRoot {} - -impl RootContext for VariablesRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - 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 ae208d8..c505afe 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -266,185 +266,3 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) - -## Source Material - -### FILE: examples/geoRedirect/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - send_http_response, - set_property, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getEnv, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const BAD_GATEWAY: u32 = 502; -const INTERNAL_SERVER_ERROR: u32 = 500; - -class GeoRedirectRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new GeoRedirect(context_id, this); - } -} - -class GeoRedirect extends Context { - constructor(context_id: u32, root_context: GeoRedirectRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> "); - - const defaultOrigin = getEnv("DEFAULT"); - - if (!defaultOrigin) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("App misconfigured - DEFAULT must be set"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const countryArrBuf = get_property("request.country"); - if (countryArrBuf.byteLength === 0) { - send_http_response( - BAD_GATEWAY, - "bad gateway", - String.UTF8.encode("Missing country information"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - const countryCode = String.UTF8.decode(countryArrBuf); - const countrySpecificOrigin = getEnv(countryCode); - - log( - LogLevelValues.info, - `Country code: ( ${countryCode} ): ${ - countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin - }`, - ); - - const hostArrBuf = get_property("request.host"); - if (hostArrBuf.byteLength > 0) { - const host = String.UTF8.decode(hostArrBuf); - log(LogLevelValues.info, `Provided Host: ${host}`); - } - - const pathArrBuf = get_property("request.path"); - if (pathArrBuf.byteLength === 0) { - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("Internal server error - no request path"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - const path = String.UTF8.decode(pathArrBuf); - const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; - // remove trailing slashes from the origin - const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; - - const requestUrl = `${cleanedOrigin}${path}`; - - log(LogLevelValues.info, `request-url: ${requestUrl}`); - - set_property("request.url", String.UTF8.encode(requestUrl)); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new GeoRedirectRoot(context_id); -}, "geoRedirect"); -``` - - -### FILE: examples/geoRedirect/package.json - -```json -{ - "name": "fastedge-as-example-georedirect", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Geo Redirect", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/geoRedirect/README.md - -``` -[← Back to examples](../README.md) - -# Geo Redirect - -This application redirects requests to different origin URLs based on the client's country code. - -## What it does - -In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. - -- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. -- Otherwise it falls back to the `DEFAULT` origin. -- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. - -> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. - -> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. - -## Configuration - -Set the following environment variables on your FastEdge application: - -| Variable | Example | Description | -|----------|---------|-------------| -| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | -| `` | `DE=https://de.example.com` | Per-country origin URL (optional, one per country) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | -| `build/geoRedirect-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/geoRedirect.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `DEFAULT` environment variable and any per-country overrides in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-rust.md index dab63e7..a71fca0 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-08-17 + updated: 2026-08-20 --> --- @@ -152,3 +152,128 @@ proxy_wasm::main! {{ - FastEdge-sdk-rust CDN examples overview - proxy-wasm HttpContext trait reference - host-services-rust reference (property access, header manipulation) + +## Source Material + +### FILE: examples/cdn/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating geo-based origin routing. + +Routes requests to country-specific origins based on the request's +geo-IP country code. Falls back to a DEFAULT origin when no +country-specific mapping is configured. + +Required configuration: + - Environment variable: DEFAULT (fallback origin URL) + - Environment variable: (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use proxy_wasm::traits::*; +use std::env; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoRedirectRoot) }); +}} + +struct GeoRedirectRoot; + +impl Context for GeoRedirectRoot {} + +impl RootContext for GeoRedirectRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(GeoRedirectContext)) + } +} + +struct GeoRedirectContext; + +impl Context for GeoRedirectContext {} + +impl HttpContext for GeoRedirectContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(default_origin) = env::var("DEFAULT") else { + self.send_http_response(500, vec![], Some(b"App misconfigured - DEFAULT must be set")); + return Action::Pause; + }; + + let country_code = self + .get_property(vec!["request.country"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(); + + if country_code.is_empty() { + self.send_http_response(502, vec![], Some(b"Missing country information")); + return Action::Pause; + } + + let origin = env::var(&country_code).unwrap_or(default_origin); + let origin = origin.trim_end_matches('/'); + + let path = self + .get_property(vec!["request.path"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| "/".to_string()); + + // Preserve the Host header if present + if let Some(host) = self + .get_property(vec!["request.host"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + { + self.set_http_request_header("Host", Some(&host)); + } + + let request_url = format!("{}{}", origin, path); + + println!("Redirecting to: {}", request_url); + + self.set_property(vec!["request.url"], Some(request_url.as_bytes())); + + Action::Continue + } +} +``` + + +### FILE: examples/cdn/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + + +### FILE: examples/cdn/geo_redirect/README.md + +``` +[← Back to examples](../../README.md) + +# Geo Redirect (CDN) + +Routes CDN requests to country-specific origins based on the geoIP country code using the proxy-wasm ABI. + +## Configuration + +- Environment variable: `DEFAULT` — fallback origin URL +- Environment variable: `` — optional per-country origin URLs (e.g. `US`, `DE`, `GB`) +``` 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 189abf0..1f39fe8 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-08-17 + updated: 2026-08-20 --> --- @@ -206,112 +206,3 @@ Output binary: `target/wasm32-wasip1/release/.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 { Box::new(GeoblockRoot) }); -}} - -struct GeoblockRoot; - -impl Context for GeoblockRoot {} - -impl RootContext for GeoblockRoot { - fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(GeoblockContext {})) - } - - fn get_type(&self) -> Option { - 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::() else { - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - - let Ok(tw_end) = tw_end.parse::() 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-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-as.md index bf8dc73..0c7d16c 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 + updated: 2026-08-20 --> --- @@ -280,6 +280,32 @@ Resulting effective request headers after mutation: The same add/remove/replace sequence is repeated identically in `onResponseHeaders` for response headers. +## Complete Mutation Sequence (Response Phase) + +```typescript +// Add headers +stream_context.headers.response.add("new-header-01", "value-01"); +stream_context.headers.response.add("new-header-02", "value-02"); +stream_context.headers.response.add("new-header-03", "value-03"); + +// Remove (sets to empty string in nginx — does not delete) +stream_context.headers.response.remove("new-header-01"); + +// Replace value +stream_context.headers.response.replace("new-header-02", "new-value-02"); + +// Add a second value for the same header name +stream_context.headers.response.add("new-header-03", "value-03-a"); +``` + +Resulting effective response headers after mutation: + +| Header | Value | +|---|---| +| `new-header-01` | `""` (empty — nginx cannot delete) | +| `new-header-02` | `new-value-02` | +| `new-header-03` | `value-03` and `value-03-a` (multi-value) | + ## Class Structure ```typescript diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md index f2b6798..0fb98ea 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-08-17 + updated: 2026-08-20 --> --- @@ -327,3 +327,356 @@ Called after the request/response cycle completes. Use for per-request logging o - proxy-wasm Rust SDK reference (sdk-reference-rust) - FastEdge CDN app examples index (examples CDN) - Host services Rust reference (host-services-rust) + +## 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-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md index 8d280e2..aaa3722 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 @@ -4,7 +4,7 @@ - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -138,3 +138,129 @@ No additional dependencies beyond the cdn-base skeleton. - cdn-base skeleton reference - proxy-wasm-sdk-as SDK reference (AssemblyScript) - platform-overview reference for environment variable configuration + +## Source Material + +### FILE: examples/largeDictionary/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getDictionary, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class LargeDictionaryRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new LargeDictionaryContext(context_id, this); + } +} + +class LargeDictionaryContext extends Context { + constructor(context_id: u32, root_context: LargeDictionaryRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. + const config = getDictionary("LARGE_CONFIG"); + + const size = config.length; + log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); + + stream_context.headers.request.add("x-config-size", size.toString()); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new LargeDictionaryRoot(context_id); +}, "largeDictionary"); +``` + + +### FILE: examples/largeDictionary/package.json + +```json +{ + "name": "fastedge-as-example-large-dictionary", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/largeDictionary/README.md + +``` +[← Back to examples](../README.md) + +# Large Dictionary + +This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. + +## When to use `getDictionary` vs `getEnv` + +| Function | Use when | +|----------|----------| +| `getEnv(name)` | Variable value is under 64 KB (most cases) | +| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | + +The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. + +For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. + +## What it does + +In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | +| `build/largeDictionary-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-rust.md index a8334a3..dda7cb2 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-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,111 @@ 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 { Box::new(LargeEnvRoot) }); +}} + +struct LargeEnvRoot; + +impl Context for LargeEnvRoot {} + +impl RootContext for LargeEnvRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + 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/http/ab-testing-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/ab-testing-wasi-rust.md index 9f1a6a0..0edcedd 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-08-17 + updated: 2026-08-20 --> --- @@ -217,3 +217,214 @@ 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-` +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) -> anyhow::Result> { + 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-` 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::>() + .join("; ") +} + +fn is_valid_xid(xid: &str) -> bool { + matches!(xid.parse::(), 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 f2b3419..671c3e6 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-08-17 + updated: 2026-08-20 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-17 +updated: 2026-08-20 --- @@ -194,10 +194,8 @@ async fn main(request: Request) -> anyhow::Result> { "Hello, you made a wasi request to {url}" )))?) } - ``` - ### FILE: examples/http/wasi/hello_world/Cargo.toml ```toml @@ -214,5 +212,4 @@ 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 f81acb2..ba67275 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-08-17 + updated: 2026-08-20 --> --- @@ -176,67 +176,3 @@ 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 ad66102..c90f65f 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-08-17 + updated: 2026-08-20 --> --- @@ -163,6 +163,9 @@ async fn main(req: Request) -> anyhow::Result> { .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 })); } @@ -190,117 +193,3 @@ fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result) -> 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" -``` 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 8e33d7f..914ed37 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-08-17 + updated: 2026-08-20 --> --- @@ -231,117 +231,3 @@ 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 (UTF-8) - // entry.json() -> Promise (parsed JSON) - // entry.arrayBuffer() -> Promise - 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 5579117..4a59ac4 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-08-17 + updated: 2026-08-20 --> --- @@ -315,281 +315,3 @@ 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 { - // `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 { - // 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 { - 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 { - 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 20e88de..78af466 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-08-17 + updated: 2026-08-20 --> --- @@ -176,6 +176,18 @@ let body_bytes = body.contents().await?.to_vec(); Must call `.into_body()` before reading; `.contents().await?` buffers the full response body. +### Collect Upstream Headers + +```rust +let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); +``` + +Headers must be collected before consuming the response body via `.into_body()`. + ### Conditional Cache Write ```rust @@ -215,6 +227,7 @@ Purge all example: ```rust if path_and_query == "/purge" { let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); return Ok(Response::builder().status(204).body(Body::empty())?); } ``` @@ -225,6 +238,7 @@ Purge prefix example: 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())?); } ``` @@ -235,6 +249,7 @@ Delete single key example: 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())?); } ``` @@ -273,3 +288,201 @@ 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/ — 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 + +```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>)` +- `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 3331544..10afb0e 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-08-17 + updated: 2026-08-20 --> --- @@ -260,100 +260,3 @@ Configure via the FastEdge secrets API or the manage skill before deploying. - WebCrypto API (crypto.subtle) — available as a global in the FastEdge JS runtime - http-base skeleton (base event listener and Response patterns) - FastEdge deploy skill (building and uploading WASM, configuring secrets) - -## Source Material - -### FILE: examples/crypto-hmac-jwt/src/index.js - -```js -import { getSecret } from 'fastedge::secret'; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - -function base64urlToBytes(str) { - const padded = str.replace(/-/gu, '+').replace(/_/gu, '/') + '='.repeat((4 - (str.length % 4)) % 4); - const binary = atob(padded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.codePointAt(i); - } - return bytes; -} - -async function verifyJwtHs256(token, secret) { - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('malformed token: expected three segments'); - } - const [encodedHeader, encodedPayload, encodedSignature] = parts; - - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(secret), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['verify'], - ); - - const signature = base64urlToBytes(encodedSignature); - const signedData = encoder.encode(`${encodedHeader}.${encodedPayload}`); - - const valid = await crypto.subtle.verify('HMAC', key, signature, signedData); - if (!valid) { - throw new Error('invalid signature'); - } - - const claims = JSON.parse(decoder.decode(base64urlToBytes(encodedPayload))); - - if (typeof claims.exp === 'number' && Math.floor(Date.now() / 1000) >= claims.exp) { - throw new Error('token expired'); - } - - return claims; -} - -async function app(event) { - const auth = event.request.headers.get('authorization') ?? ''; - const match = auth.match(/^Bearer\s+(.+)$/iu); - if (!match) { - return Response.json( - { ok: false, error: 'missing or malformed Authorization header' }, - { status: 401 }, - ); - } - - const secret = getSecret('JWT_SECRET'); - if (!secret) { - return Response.json({ ok: false, error: 'JWT_SECRET is not configured' }, { status: 500 }); - } - - try { - const claims = await verifyJwtHs256(match[1], secret); - return Response.json({ ok: true, claims }); - } catch (error) { - return Response.json({ ok: false, error: error.message }, { status: 401 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/crypto-hmac-jwt/package.json - -```json -{ - "name": "fastedge-example-crypto-hmac-jwt", - "version": "1.0.0", - "description": "FastEdge JS example: verify HS256 JWTs with the Web Crypto API", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/crypto-hmac-jwt.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` 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 64f2d27..c4395fb 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-08-17 + updated: 2026-08-20 --> --- @@ -44,8 +44,8 @@ crate-type = ["cdylib"] ## Required Configuration -| Variable | Type | Required | Description | -|--------------|--------|----------|--------------------------------| +| Variable | Type | Required | Description | +|--------------|--------|----------|---------------------------------| | `ORIGIN_URL` | string | yes | Origin URL to proxy requests to | ## API @@ -190,103 +190,3 @@ async fn main(req: Request) -> anyhow::Result> { - 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= 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" -``` 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 85ad7d9..4554a90 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-08-17 + updated: 2026-08-20 --> --- @@ -154,3 +154,119 @@ 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: (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-cursor/skills/scaffold/reference/http/kv-store-basic-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-basic-ts.md index a21b2d4..54b3f87 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-basic-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-basic-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -127,47 +127,3 @@ Build script from `package.json`: - fastedge-sdk-js SDK reference - FastEdge app configuration (store attachment) - BUILD_CLI reference (fastedge-build options) - -## Source Material - -### FILE: examples/kv-store-basic/src/index.js - -```js -import { KvStore } from 'fastedge::kv'; - -async function eventHandler(event) { - try { - const myStore = KvStore.open('kv-store-name-as-defined-on-app'); - const entry = await myStore.getEntry('key'); - - if (entry === null) { - return new Response('Key not found', { status: 404 }); - } - - return new Response(`The KV Store responded with: ${await entry.text()}`); - } catch (error) { - return Response.json({ error: error.message }, { status: 500 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store-basic/package.json - -```json -{ - "name": "fastedge-example-kv-store-basic", - "version": "1.0.0", - "description": "FastEdge JS example: simple KV Store get operation", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/kv-store-basic.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-wasi-rust.md index 02ee4c4..a23be87 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -250,10 +250,10 @@ The app accepts these query parameters: | Parameter | Required | Description | |-----------|----------|-------------| -| `store` | Yes | Name of the KV store to open | -| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | -| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | -| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | +| `store` | Yes | Name of the KV store to open | +| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | +| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | +| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | | `min` | Yes for `zrange` | Minimum score bound (float); no default — must be provided | | `max` | Yes for `zrange` | Maximum score bound (float); no default — must be provided | | `item` | Yes for `bfExists` | Item to check in Bloom Filter | @@ -350,3 +350,218 @@ target = "wasm32-wasip1" - fastedge-sdk-rust key_value module documentation - http-base skeleton reference - wstd HTTP server documentation + +## 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" +``` 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 54b0eef..8a36751 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-08-17 + updated: 2026-08-20 --> --- @@ -400,282 +400,3 @@ 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(url: string): Promise { - 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(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(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(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 c1a13e7..aeaede1 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-08-17 + updated: 2026-08-20 --> --- @@ -127,3 +127,90 @@ 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) -> 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}"))?; + + // 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 f802a94..8c88e7f 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-08-17 + updated: 2026-08-20 --> --- @@ -145,49 +145,3 @@ 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 9d4a66e..c33ea71 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-08-17 + updated: 2026-08-20 --> --- @@ -216,3 +216,82 @@ async fn main(_request: Request) -> anyhow::Result> { - 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) -> 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" +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rotation-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rotation-ts.md index 61b3fbb..5e3fdf9 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rotation-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/secret-rotation-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -45,13 +45,14 @@ import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; #### `getSecretEffectiveAt(name, slot)` -| Parameter | Type | Description | -|-----------|--------|----------------------------------------------| -| `name` | string | Secret name (key) | -| `slot` | number | Slot index or unix timestamp (non-negative integer) | +| Parameter | Type | Description | +|-----------|--------|---------------------------------------------------------------------| +| `name` | string | Secret name (key) | +| `slot` | number | Slot index or unix timestamp (non-negative integer) | - **Returns**: `string | null` — the value from the highest slot `<= slot`, or `null` if no slot satisfies the constraint. - **Slot model**: slots are interpreted as either indices (`0, 1, 2, …`) or unix timestamps. The host resolves by returning the value stored at the highest slot number that is less than or equal to the supplied slot number. +- Both functions are synchronous — no `await` required. --- @@ -207,3 +208,65 @@ This shape is the diagnostic surface for verifying that rotation is working corr - http-base skeleton - deploy skill (for uploading secrets via the API before deploying) - manage skill (`secrets` subcommand for secret CRUD operations) + +## Source Material + +### FILE: examples/secret-rotation/src/index.js + +```js +import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; + +function app(event) { + const { request } = event; + + // Read the slot from the x-slot header, defaulting to the current unix timestamp. + // Slots can be interpreted either as indices (0, 1, 2...) or as unix timestamps; + // the host returns the value from the highest slot <= this number. + const slotHeader = request.headers.get('x-slot'); + const slot = + slotHeader !== null ? Number.parseInt(slotHeader, 10) : Math.floor(Date.now() / 1000); + + if (!Number.isFinite(slot) || slot < 0) { + return new Response('x-slot header must be a non-negative integer', { status: 400 }); + } + + const secretName = request.headers.get('x-secret-name') ?? 'TOKEN_SECRET'; + + const current = getSecret(secretName); + const effective = getSecretEffectiveAt(secretName, slot); + + const body = JSON.stringify({ + secret_name: secretName, + slot, + current, + effective_at_slot: effective, + is_same: current === effective, + }); + + return new Response(body, { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/secret-rotation/package.json + +```json +{ + "name": "fastedge-example-secret-rotation", + "version": "1.0.0", + "description": "FastEdge JS example: slot-based secret retrieval for rotation", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/secret-rotation.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/simple-fetch-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/simple-fetch-wasi-rust.md index f9fc9d2..cf34807 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -131,6 +131,19 @@ use wstd::http::{Client, Request, Response}; | Missing `x-fetch-url` header | Falls back to `https://httpbin.org/get` | | Non-UTF-8 header value | `to_str().ok()` returns `None`; fallback default applied | +## Build + +```bash +cargo component build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Example Usage + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + ## Source Material ### FILE: examples/http/wasi/simple_fetch/src/lib.rs diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-wasi-rust.md index e859b3e..1eecd03 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + updated: 2026-08-20 --> --- @@ -32,6 +32,7 @@ include_str!("../assets/filename.ext") - Embeds a UTF-8 text file into the binary at compile time. - Path is relative to the source file containing the macro. - Returns `&'static str`. +- Build fails if the target file does not exist at compile time. ```rust include_bytes!("../assets/filename.ext") @@ -127,6 +128,13 @@ async fn main(req: Request) -> anyhow::Result> { } ``` +## Imports + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response, StatusCode}; +``` + ## Cargo.toml ```toml @@ -165,6 +173,7 @@ All files in `assets/` must exist at compile time; the build will fail if `inclu - Asset paths in `include_str!` are relative to the `.rs` source file, not the crate root. - All assets are compiled into the WASM binary — large assets increase binary size proportionally. - No dynamic file loading is possible at runtime; adding an asset requires a rebuild. +- The WASM runtime has no filesystem; there is no alternative to compile-time embedding. ## See Also 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 8155e9b..053da59 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-08-17 + updated: 2026-08-20 --> --- @@ -115,3 +115,50 @@ 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 e011ba1..956f361 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-08-17 + updated: 2026-08-20 --> --- @@ -141,3 +141,67 @@ async fn main(_request: Request) -> anyhow::Result> { - 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:///` (`-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" +``` 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 5d7002e..36b8b19 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-08-17 + updated: 2026-08-20 --> --- @@ -202,3 +202,86 @@ 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 c23693c..00d44d9 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-08-17 + updated: 2026-08-20 --> --- @@ -135,3 +135,89 @@ 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) -> anyhow::Result> { + 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: , Password: +``` + +## 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/.claude-plugin/plugin.json b/plugins/gcore-fastedge/.claude-plugin/plugin.json index eeb19f1..4b0388d 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.6", + "version": "0.2.7", "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 90eeb93..7549dcd 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-08-18T07:33:54.938Z", + "generated_at": "2026-08-20T15:14:07.190Z", "source": { "repo": "fastedge-plugin", - "commit": "067d44e", + "commit": "deaf77e", "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "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-08-17", + "2026-08-20", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "884a6d522e8ab8459d1662eba50c58a0d1d6c02c56f2dd0bb413c6eb782166ea", + "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -60,7 +60,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "rust", "sdk", @@ -250,7 +250,9 @@ "---------------------------------------------------", "on_http_request_headers", "num_headers", + "usize", "end_of_stream", + "bool", "action", "request", "headers", @@ -258,8 +260,7 @@ "modify", "forwarding", "on_http_request_body", - "body_size", - "end_of_strea" + "body_size" ] }, { @@ -532,7 +533,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md", "skill": "fastedge-docs", "category": "examples", @@ -556,7 +557,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "testing", "cookie-based", "traffic", @@ -566,7 +567,7 @@ "requests", "different" ], - "content_sha256": "9546c33b6d3c8657538a75f442e7738541cdad753fc7b88a1cd759cc036629f0", + "content_sha256": "a8a65c5b6a1c0cfb93650966caa7539474e92037e1a305816ef0944e3e09a1fc", "sections": [ { "id": "cdn-examples-ab-testing-as#introduction", @@ -585,7 +586,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "testing", "assemblyscript", "cdn", @@ -1000,7 +1001,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1024,7 +1025,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1034,7 +1035,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "5357796dddeeaaeabe4ac218bb4833c9d2ef35a8980a2b6991dcea977557a27a", + "content_sha256": "02a0716c80b4d8908f4996e26cf4a36904a49faf5c7a78d99d5abacf7512e387", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1053,7 +1054,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1650,63 +1651,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1730,7 +1681,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -1740,7 +1691,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "ad7a92a128d2e079ab2a101e17374409ab4aae0ad4bc6df4161eba26a0f5a4a8", + "content_sha256": "d47decc23a11899c2cdd3360355fb8c5e9db6fb48d6b2ca5c90bee4cc5c8ae2b", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1759,7 +1710,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -2114,183 +2065,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2314,7 +2095,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "api", "key", "validation", @@ -2324,7 +2105,7 @@ "requests", "against" ], - "content_sha256": "bbaa97e8bc532d48176cd6b5f6c44e90f38200072425c6f32426bada8a682099", + "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2343,7 +2124,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "api", "key", "validation", @@ -2588,13 +2369,63 @@ "traits", "types" ] + }, + { + "id": "cdn-examples-api-key-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 163, + "line_end": 279, + "keywords": [ + "source", + "material", + "file", + "examples", + "cdn", + "api_key", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "api", + "key", + "validation.", + "validates", + "requests", + "using", + "x-api-key", + "header", + "checked", + "against", + "stored", + "secret.", + "simpler", + "alternative", + "jwt", + "token", + "expiry", + "claims", + "needed.", + "required", + "configuration", + "secret", + "use" + ] } ] }, { "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- 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-20 --> --- 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", @@ -2620,7 +2451,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "jwt-auth", @@ -2631,7 +2462,7 @@ "example", "app_type" ], - "content_sha256": "c9792a91f6b66ee092a92fc63a8c0a961358dc9ee9cd2b42f8e0d5a9791869d5", + "content_sha256": "b1fb0a577490fc4af605c215e8ece9ba673631e87fe3755435cda8df374760f4", "sections": [ { "id": "cdn-examples-auth-jwt-as#introduction", @@ -2650,7 +2481,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "jwt-auth", @@ -3096,7 +2927,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -3120,7 +2951,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -3130,7 +2961,7 @@ "auth", "jwt" ], - "content_sha256": "35c20c709fe8bd2a6ae069467c5a1da66bf3f28d8e251c6ce2b1c692532d625a", + "content_sha256": "fab044cd88079e758ffa7433aa100275b9466d2b19457fe94f40539fca11ec06", "sections": [ { "id": "cdn-examples-auth-jwt-rust#introduction", @@ -3149,7 +2980,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -3652,7 +3483,7 @@ { "id": "cdn-examples-body-as", "title": "Body Manipulation — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3676,7 +3507,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -3686,7 +3517,7 @@ "app_type", "languages" ], - "content_sha256": "5f9174505c2fc0670b007154e21adacd423171efef52d35918203abca63de32a", + "content_sha256": "94226f2f7783dd5978e697ebea97008427e93dbddfd4b033a8c05d74268e9a34", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3705,7 +3536,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "title", "body", @@ -4132,146 +3963,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -4295,7 +3993,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -4305,7 +4003,7 @@ "body-inspection", "body-modification" ], - "content_sha256": "24fb25e4211d794f9ead55478f050a7b3f88d2a46128c145112ab22b167617c0", + "content_sha256": "8ae1b1e52a1c6d5700b08e8892ae6cda7aad01471796e76d73ec79dcf32e297b", "sections": [ { "id": "cdn-examples-body-rust#introduction", @@ -4324,7 +4022,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -4727,7 +4425,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md", "skill": "fastedge-docs", "category": "examples", @@ -4753,7 +4451,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -4763,7 +4461,7 @@ "cache-control", "response-headers" ], - "content_sha256": "1addef442e8b046f2689030c6ded25b7bd7ce8c303a7d46d5249db12d9955e1f", + "content_sha256": "3b3e6161bccadfc2c7ed913237cf413f6147239e7386715b566aa6893587902d", "sections": [ { "id": "cdn-examples-cache-control-as#introduction", @@ -4782,7 +4480,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -5357,7 +5055,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5381,7 +5079,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -5391,7 +5089,7 @@ "cache-control", "content-type-aware-caching" ], - "content_sha256": "52b2a31bb8aad5cdca314ece4bd67e6404091a413f20cd8198f3449d3d5b6d0d", + "content_sha256": "b0fabdfa84d4803e073c0d36e6b51424537f4051ef1a9e6ba50b56a4e184e169", "sections": [ { "id": "cdn-examples-cache-control-rust#introduction", @@ -5410,7 +5108,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -5943,7 +5641,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5967,7 +5665,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "example", "convert", "image", @@ -5977,7 +5675,7 @@ "avif", "format" ], - "content_sha256": "f2e97f2b27b3fbaa6b71ed3ad2536dc88cc4c915cd44e74236ab77efaf64d310", + "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5996,7 +5694,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "cdn", "example", "convert", @@ -6461,13 +6159,129 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6491,7 +6305,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "response-header", "injection", @@ -6501,7 +6315,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "8b31b60143149b04ddeb68d836976ec36d26c63d4d4a72c1076fa16704e259fe", + "content_sha256": "dcfb3c1c7467c4e745a43d3b8821e1ef8725789583876c7d2ffafd9955cc8efa", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6520,7 +6334,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cors", "assemblyscript", "cdn", @@ -6907,7 +6721,7 @@ "level": 2, "anchor": "source-material", "line_start": 197, - "line_end": 318, + "line_end": 316, "keywords": [ "source", "material", @@ -6945,8 +6759,8 @@ "heading": "What it does", "level": 2, "anchor": "what-it-does", - "line_start": 319, - "line_end": 324, + "line_start": 317, + "line_end": 322, "keywords": [ "onresponseheaders", "requests", @@ -6993,8 +6807,8 @@ "heading": "Configuration", "level": 2, "anchor": "configuration", - "line_start": 325, - "line_end": 333, + "line_start": 323, + "line_end": 331, "keywords": [ "configuration", "set", @@ -7033,8 +6847,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 334, - "line_end": 347, + "line_start": 332, + "line_end": 345, "keywords": [ "build", "pnpm", @@ -7063,8 +6877,8 @@ "heading": "Deploy", "level": 2, "anchor": "deploy", - "line_start": 348, - "line_end": 352, + "line_start": 346, + "line_end": 350, "keywords": [ "deploy", "upload", @@ -7089,7 +6903,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -7113,7 +6927,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -7123,7 +6937,7 @@ "cors", "preflight" ], - "content_sha256": "a47598b96dd1c4106627c991a847554a508c930b05c892327d99ac7d7b11620d", + "content_sha256": "0fb3e333f7c0f3374c1253af8febc8847afb1ee90ffeabcc90ca5cdd75762c5b", "sections": [ { "id": "cdn-examples-cors-rust#introduction", @@ -7142,7 +6956,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -7643,7 +7457,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7667,7 +7481,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -7677,7 +7491,7 @@ "response-headers", "response-body" ], - "content_sha256": "d12c096f8a9390f29e88401819bd8ca1502c7362abdeffa7b16da73df9318ddc", + "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7696,7 +7510,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8126,13 +7940,154 @@ "hook", "sequencing" ] + }, + { + "id": "cdn-examples-custom-error-pages-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 296, + "line_end": 501, + "keywords": [ + "source", + "material", + "file", + "examples", + "customerrorpages", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterdatastatusvalues", + "filterheadersstatusvalues", + "get_property", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "set_buffer_bytes", + "buffertypevalues", + "stream_context", + "setloglevel", + "fastedge", + "class", + "errorpagesroot" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 502, + "line_end": 514, + "keywords": [ + "onresponseheaders", + "app", + "reads", + "response.status", + "property.", + "400-599", + "range", + "sets", + "content-type", + "text", + "html", + "prepares", + "body", + "replacement.", + "onresponsebody", + "buffers", + "full", + "response", + "replaces", + "styled", + "page", + "containing", + "numeric", + "status", + "code", + "human-readable", + "error", + "title", + "e.g.", + "found", + "bad", + "gateway", + "description", + "explaining", + "went", + "wrong", + "category", + "label", + "client", + "server" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 515, + "line_end": 528, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "customerrorpages.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "customerrorpages-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-custom-error-pages-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 529, + "line_end": 533, + "keywords": [ + "deploy", + "upload", + "build", + "customerrorpages.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -8156,7 +8111,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "custom", "error", "pages", @@ -8166,7 +8121,7 @@ "5xx", "http" ], - "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", + "content_sha256": "6ca59f2c7881b6cfa873c1aa90ca38a3135eeee3546ba894a62801be63e0266e", "sections": [ { "id": "cdn-examples-custom-error-pages-rust#introduction", @@ -8185,7 +8140,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -8243,7 +8198,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -8267,7 +8222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8277,7 +8232,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "60d439619ff3c15cf82c3dbfcb386f45049a357377e8905e512be1dd58b7c52c", + "content_sha256": "a73e35e2bec26a8caef88e750c20eec94b8a4e22bd45622922f8ab9f586d554a", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -8296,7 +8251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8678,59 +8633,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 --> --- 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-20 --> --- 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", @@ -8754,7 +8663,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -8765,7 +8674,7 @@ "secrets", "environment" ], - "content_sha256": "208942b748224cc193b939e8296ec03eb3c7ac4177134ecf33e65558d2169d4c", + "content_sha256": "0c73ed18f10ce8d8e06a88f54b4d3f23ac0b67968d5c25e974066c47408cfacb", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8784,7 +8693,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9195,228 +9104,13 @@ "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-08-17 --> --- 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-20 --> --- 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", @@ -9440,7 +9134,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9450,7 +9144,7 @@ "env-vars", "secrets" ], - "content_sha256": "626204a284720f5610349657a81d03da8d6fd6ae3440ed314183318d50bf3e8e", + "content_sha256": "c649f355de98cfbff02eb0209ff30aec29948b20dda1e664b62b81a49e8a518a", "sections": [ { "id": "cdn-examples-env-secrets-rust#introduction", @@ -9469,7 +9163,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9872,7 +9566,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md", "skill": "fastedge-docs", "category": "examples", @@ -9896,7 +9590,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -9906,7 +9600,7 @@ "geo-routing", "header-manipulation" ], - "content_sha256": "067cd6a598690aec364ccea7550edc1cf38e963244f81bc53a1f9e51521a20e5", + "content_sha256": "77bc8d7bb75d5073d55729d167f95732aae1c14e0d2cc8f5dbd17a584fe83bdf", "sections": [ { "id": "cdn-examples-geo-redirect-as#introduction", @@ -9925,7 +9619,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -10132,7 +9826,7 @@ "level": 2, "anchor": "runtime-properties-used", "line_start": 106, - "line_end": 118, + "line_end": 120, "keywords": [ "runtime", "properties", @@ -10181,8 +9875,8 @@ "heading": "API Calls", "level": 2, "anchor": "api-calls", - "line_start": 119, - "line_end": 150, + "line_start": 121, + "line_end": 152, "keywords": [ "api", "calls", @@ -10222,8 +9916,8 @@ "heading": "Environment Variables", "level": 2, "anchor": "environment-variables", - "line_start": 151, - "line_end": 161, + "line_start": 153, + "line_end": 163, "keywords": [ "environment", "variables", @@ -10272,8 +9966,8 @@ "heading": "Error Responses", "level": 2, "anchor": "error-responses", - "line_start": 162, - "line_end": 171, + "line_start": 164, + "line_end": 173, "keywords": [ "error", "responses", @@ -10312,8 +10006,8 @@ "heading": "Registration", "level": 2, "anchor": "registration", - "line_start": 172, - "line_end": 183, + "line_start": 174, + "line_end": 185, "keywords": [ "registration", "typescript", @@ -10335,8 +10029,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 184, - "line_end": 200, + "line_start": 186, + "line_end": 209, "keywords": [ "build", "pnpm", @@ -10356,7 +10050,17 @@ "--target", "release", "runs", - "---" + "output", + "file", + "description", + "------", + "-------------", + "georedirect.wasm", + "optimised", + "binary", + "upload", + "fastedge", + "georedirect-debug.wasm" ] }, { @@ -10364,8 +10068,8 @@ "heading": "Key Patterns", "level": 2, "anchor": "key-patterns", - "line_start": 201, - "line_end": 229, + "line_start": 210, + "line_end": 248, "keywords": [ "key", "patterns", @@ -10401,8 +10105,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 230, - "line_end": 241, + "line_start": 249, + "line_end": 261, "keywords": [ "constraints", "gotchas", @@ -10450,8 +10154,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 242, - "line_end": 248, + "line_start": 262, + "line_end": 268, "keywords": [ "see", "geoblock", @@ -10487,7 +10191,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -10511,7 +10215,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -10521,7 +10225,7 @@ "geo-routing", "origin-rewrite" ], - "content_sha256": "310cec0f54b004867a013a8705658059764759351d317237fa99619f4369db8d", + "content_sha256": "e261c562cb83f75dba70f28b364fbf919b2f5013ba58235119ad284f266ad0d9", "sections": [ { "id": "cdn-examples-geo-redirect-rust#introduction", @@ -10540,7 +10244,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -11043,7 +10747,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11067,7 +10771,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -11077,7 +10781,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "45f4aed05034630ae0428c51417368006589a428d93125905ff42c54a819deda", + "content_sha256": "84af0ea22b79918d77cf3d4bfb495acc76a3246c5535c41b1555f7052fcc183d", "sections": [ { "id": "cdn-examples-geoblock-as#introduction", @@ -11096,7 +10800,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -11572,7 +11276,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -11596,7 +11300,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -11606,7 +11310,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "9284d14217f3925fd3de6aa8a69a6e0ab2de9bf4abdbe7a4a4ca23298e1bbe66", + "content_sha256": "a7c062df70dea7d93851c4767e7e29ebd0ea6f820171bab1417c892702ee748e", "sections": [ { "id": "cdn-examples-geoblock-rust#introduction", @@ -11625,7 +11329,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12102,7 +11806,7 @@ { "id": "cdn-examples-headers-as", "title": "CDN Headers Manipulation — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12128,7 +11832,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12138,7 +11842,7 @@ "headers", "request-headers" ], - "content_sha256": "1d592a880b8687d8ba11ee3f27be25ebadbfb596ba9d1df160c4a44164afc63f", + "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -12157,7 +11861,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12295,11 +11999,9 @@ "method", "signature", "description", - "---------------------------------------", - "----------------------------------------------------", - "------------------------------------------------------------------------", - "get", - "name" + "-------------------", + "-----------------------------------------------", + "--------------------------------------------------------------------------------------------------------" ] }, { @@ -12549,8 +12251,8 @@ "meaning", "trigger", "----", - "---------------------------------", - "-----------------------------------------------------------", + "------------------------------", + "----------------------------------------------------------", "550", "headers", "present", @@ -12564,7 +12266,8 @@ "get", "552", "mismatch", - "mutation" + "mutation", + "validateheaders" ] }, { @@ -12667,7 +12370,7 @@ "file", "description", "--------------------------", - "----------------------------------------------", + "---------------------------------------------", "headers.wasm", "optimised", "release", @@ -12683,7 +12386,8 @@ "package.json", "script", "command", - "---------------" + "-----------------", + "---" ] }, { @@ -12735,7 +12439,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12759,7 +12463,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -12769,7 +12473,7 @@ "headers", "request-headers" ], - "content_sha256": "bc58685771c9c5b448fded9a2de2d0bbe99fd6dab638d94f24a864345a8ddd79", + "content_sha256": "26db929462e18b733d0d8ff6392d1a509520adb603ee47545516c24043493a11", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -12788,7 +12492,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -13128,58 +12832,13 @@ "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "title": "HTTP Call", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13205,7 +12864,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "overview", "httpcall", "dispatches", @@ -13216,7 +12875,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "2729e49aa567e87d7df75087c34af90c5df2235ba17803b5d48858361abb4615", + "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -13235,7 +12894,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -13507,13 +13166,209 @@ "response", "patterns" ] + }, + { + "id": "cdn-examples-http-call-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 325, + "line_end": 485, + "keywords": [ + "source", + "material", + "file", + "examples", + "httpcall", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "exports", + "required", + "functions", + "interact", + "us.", + "import", + "basecontext", + "buffertypevalues", + "context", + "filterheadersstatusvalues", + "get_buffer_bytes", + "headerpair", + "log", + "loglevelvalues", + "makeheaderpair", + "registerrootcontext", + "rootcontext", + "send_http_response", + "stream_context", + "wasmresultvalues", + "proxy-wasm-sdk-a" + ] + }, + { + "id": "cdn-examples-http-call-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 486, + "line_end": 497, + "keywords": [ + "onrequestheaders", + "app", + "dispatches", + "outbound", + "http", + "get", + "request", + "httpbin.org", + "pauses", + "hook", + "stopiteration", + "until", + "response", + "arrives.", + "dispatch", + "latched", + "instance", + "field", + "second", + "invocation", + "processed", + "returns", + "continue", + "instead", + "dispatching", + "again.", + "inside", + "callback", + "passed", + "httpcall", + "checks", + "whether", + "call", + "succeeded", + "headers", + "value", + "indicates", + "failure", + "timeout", + "dns" + ] + }, + { + "id": "cdn-examples-http-call-as#key-concepts", + "heading": "Key concepts", + "level": 2, + "anchor": "key-concepts", + "line_start": 498, + "line_end": 504, + "keywords": [ + "key", + "concepts", + "httpcall", + "rootcontext", + "dispatches", + "async", + "http", + "request.", + "accepts", + "cluster", + "host", + "headers", + "body", + "trailers", + "timeout", + "milliseconds", + "originating", + "context", + "callback.", + "fastedge", + "resume", + "model.", + "returning", + "filterheadersstatusvalues.stopiteration", + "pauses", + "hook.", + "runtime", + "processes", + "response", + "invokes", + "callback", + "re-invokes", + "onrequestheaders", + "same", + "instance", + "httpcalldispatched", + "latch", + "gates", + "re-dispatch", + "hoo" + ] + }, + { + "id": "cdn-examples-http-call-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 505, + "line_end": 518, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "httpcall.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "httpcall-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-http-call-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 519, + "line_end": 523, + "keywords": [ + "deploy", + "upload", + "build", + "httpcall.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "environment", + "variables", + "secrets", + "required." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13537,7 +13392,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "makes", @@ -13547,7 +13402,7 @@ "services", "timeout" ], - "content_sha256": "94cc819b5d52f649c645084f23ad0fd9959aa5d1843708d649f3afec7aa84cf3", + "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13566,7 +13421,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "call", "cdn", @@ -14009,7 +13864,7 @@ "level": 2, "anchor": "gotchas", "line_start": 360, - "line_end": 370, + "line_end": 371, "keywords": [ "gotchas", "action", @@ -14058,8 +13913,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 371, - "line_end": 377, + "line_start": 372, + "line_end": 378, "keywords": [ "see", "proxy-wasm", @@ -14086,7 +13941,7 @@ { "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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14112,7 +13967,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14122,7 +13977,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "63367c278dec3bfa24f1e1ded3e653715e5fb713f3b50acec6f83c3d09ba93c0", + "content_sha256": "7c82be09762a859504b8de768a520016b4dd63da93c478964e74af2344eb76b3", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -14141,7 +13996,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -14373,7 +14228,7 @@ "level": 2, "anchor": "lifecycle-hooks", "line_start": 143, - "line_end": 182, + "line_end": 184, "keywords": [ "lifecycle", "hooks", @@ -14416,8 +14271,8 @@ "heading": "Query Parameters", "level": 2, "anchor": "query-parameters", - "line_start": 183, - "line_end": 201, + "line_start": 185, + "line_end": 203, "keywords": [ "query", "parameters", @@ -14451,8 +14306,8 @@ "heading": "Action Dispatch", "level": 2, "anchor": "action-dispatch", - "line_start": 202, - "line_end": 240, + "line_start": 204, + "line_end": 247, "keywords": [ "action", "dispatch", @@ -14471,6 +14326,7 @@ "found", "non-null", "string.utf8.decode", + "break", "scan", "match", "keys", @@ -14478,8 +14334,7 @@ "returns", "string", "keys.join", - "zrange", - "cons" + "cas" ] }, { @@ -14487,8 +14342,8 @@ "heading": "Response Body Format", "level": 2, "anchor": "response-body-format", - "line_start": 241, - "line_end": 258, + "line_start": 248, + "line_end": 265, "keywords": [ "response", "body", @@ -14514,6 +14369,8 @@ "via", "response.status", "property", + "advisory", + "only", "---" ] }, @@ -14522,8 +14379,8 @@ "heading": "Query Param Validation", "level": 2, "anchor": "query-param-validation", - "line_start": 259, - "line_end": 278, + "line_start": 266, + "line_end": 293, "keywords": [ "query", "param", @@ -14572,8 +14429,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 279, - "line_end": 288, + "line_start": 294, + "line_end": 303, "keywords": [ "logging", "typescript", @@ -14596,8 +14453,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 289, - "line_end": 303, + "line_start": 304, + "line_end": 318, "keywords": [ "build", "npm", @@ -14630,8 +14487,8 @@ "heading": "Deployment Requirements", "level": 2, "anchor": "deployment-requirements", - "line_start": 304, - "line_end": 315, + "line_start": 319, + "line_end": 330, "keywords": [ "deployment", "requirements", @@ -14679,8 +14536,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 316, - "line_end": 328, + "line_start": 331, + "line_end": 344, "keywords": [ "gotchas", "kvstore.get", @@ -14724,8 +14581,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 329, - "line_end": 334, + "line_start": 345, + "line_end": 350, "keywords": [ "see", "kvstore", @@ -14758,7 +14615,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14782,7 +14639,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -14793,7 +14650,7 @@ "example", "app_type" ], - "content_sha256": "b56c8120fcb573549cbddbd0fdf3a443307eed1688172532a2abc64f3e513366", + "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14812,7 +14669,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -15054,62 +14911,13 @@ "vs.", "differences" ] - }, - { - "id": "cdn-examples-kv-store-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 244, - "line_end": 516, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "key_value", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "store", - "operations", - "via", - "proxy-wasm", - "interface.", - "supports", - "query", - "parameters", - "name", - "action", - "get", - "key", - "scan", - "match", - "pattern", - "zrange", - "min", - "f64", - "max", - "zscan", - "bfexists", - "ite" - ] } ] }, { "id": "cdn-examples-large-dictionary-as", "title": "Large Dictionary — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15133,7 +14941,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "large", "dictionary", "overview", @@ -15142,7 +14950,7 @@ "environment", "variables" ], - "content_sha256": "a936577a2b3efa9cfcff9339d604f7a0272e309e4918a08568fdae87213a259a", + "content_sha256": "5c0712f113a19fa2bcc1667657ca33586d5a538706d60bc80ad976fc408270c3", "sections": [ { "id": "cdn-examples-large-dictionary-as#introduction", @@ -15161,7 +14969,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "large", "dictionary", "assemblyscript", @@ -15471,7 +15279,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15495,7 +15303,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "large-env-var", @@ -15505,7 +15313,7 @@ "app_type", "languages" ], - "content_sha256": "9f6288861599a4bb46e1693f7e6f6e2d598bd3a0228c090806a292f2bbec154b", + "content_sha256": "1eb33add15b946d5567ac85d662c67d674c0322a60fd9aa28e0930be81001b54", "sections": [ { "id": "cdn-examples-large-dictionary-rust#introduction", @@ -15524,7 +15332,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "large-env-var", @@ -15591,7 +15399,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: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15617,7 +15425,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -15627,7 +15435,7 @@ "logging", "time" ], - "content_sha256": "fe63b52ab054457ceacd13f3722b33ea5b1f0aceb1f6b55d2c1801be3cd946da", + "content_sha256": "0cd462da5aca1fa0b6b8f81e6775b0dea2879447776f153e4cc8522280774585", "sections": [ { "id": "cdn-examples-log-time-as#introduction", @@ -15646,7 +15454,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -15990,7 +15798,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -16014,7 +15822,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -16024,7 +15832,7 @@ "logging", "timing" ], - "content_sha256": "8dcf8bf7448143db11991ddddaeb01b7a161fe81224189a2b4631a49d9060f4d", + "content_sha256": "20055b088c670a075eae455b07e6b9a38488a69a6c3199d9566cab1f18d3beaf", "sections": [ { "id": "cdn-examples-log-time-rust#introduction", @@ -16043,7 +15851,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -16381,7 +16189,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -16405,7 +16213,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -16415,7 +16223,7 @@ "response-body-transform", "request-path-rewrite" ], - "content_sha256": "796fc091efb4397c28b107d9b27eda72793c58776c4ecb1f23300362f6b68f86", + "content_sha256": "1b26d02c4a91ae4a45b55116506d088b7e878c81183e345f40fb5d7a4e9c6f95", "sections": [ { "id": "cdn-examples-md2html-rust#introduction", @@ -16434,7 +16242,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -16863,7 +16671,7 @@ { "id": "cdn-examples-properties-as", "title": "CDN Runtime Properties — AssemblyScript", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md", "skill": "fastedge-docs", "category": "examples", @@ -16887,7 +16695,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "runtime", "properties", "language", @@ -16897,7 +16705,7 @@ "type", "overview" ], - "content_sha256": "487273a2f72dca3787758f1b60af9110a2c173879ba02842d23c21af59930534", + "content_sha256": "1a7cb1c6e4a4a24400ab5988ca4ec6145ff264706a8559555d2c0a9c37c4273c", "sections": [ { "id": "cdn-examples-properties-as#introduction", @@ -16916,7 +16724,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "cdn", "runtime", "properties", @@ -17414,7 +17222,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -17438,7 +17246,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -17448,7 +17256,7 @@ "request-properties", "geo-data" ], - "content_sha256": "82d09fea9ce746cfca7d504e9c7904e0154f9c4ee7f47da800421a3be9b5066d", + "content_sha256": "eb4c151ead93e097ac9ac3e38b0515c3c08db7a6c4eaf57ed6516291ea67ae49", "sections": [ { "id": "cdn-examples-properties-rust#introduction", @@ -17467,7 +17275,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -18140,7 +17948,7 @@ { "id": "dotenv", "title": "Dotenv — Runtime Secrets and Environment Variables", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/dotenv.md", "skill": "test", "category": "testing", @@ -18159,9 +17967,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "dotenv", "runtime", "secrets", @@ -18172,7 +17980,7 @@ "debugger", "inject" ], - "content_sha256": "c7c39cf06d60e6c2cfb502b636db8a85f3ad05d79ed5acbcc9ded2e54390e567", + "content_sha256": "dd108104b49d289c76e6b1d0f11806564dc84135fbc78acdda8afd8fa457bfb5", "sections": [ { "id": "dotenv#introduction", @@ -18189,9 +17997,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "dotenv", "runtime", "secrets", @@ -18436,7 +18244,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -18457,7 +18265,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "host", "services", "reference", @@ -18468,7 +18276,7 @@ "storage", "secret" ], - "content_sha256": "8f3541160c886153298e2cd2f0fd7c056bf959ecef2da28e70bd11b9788400c3", + "content_sha256": "815f0408c27342ac5a6ee1babbd38b3bf41d35228b85062807f8172c7ef86788", "sections": [ { "id": "host-services-rust#introduction", @@ -18487,7 +18295,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "rust", "host", "services", @@ -18757,7 +18565,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -18781,7 +18589,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example", @@ -18792,7 +18600,7 @@ "visitors", "test" ], - "content_sha256": "cadcf9541b49c1be76dfde3f5df754d6ffe54e3bc0e9348d353f735c97d9776d", + "content_sha256": "25feb56e225dc3c2de096f569836c7a02371f3ad66f47c95f09b1d43cae9d0af", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -18811,7 +18619,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "testing", "fastedge", "example" @@ -19235,7 +19043,7 @@ "level": 2, "anchor": "gotchas", "line_start": 200, - "line_end": 210, + "line_end": 212, "keywords": [ "gotchas", "headers", @@ -19278,20 +19086,14 @@ ] }, { - "id": "http-examples-ab-testing-js#source-material", - "heading": "Source Material", + "id": "http-examples-ab-testing-js#source", + "heading": "Source", "level": 2, - "anchor": "source-material", - "line_start": 211, - "line_end": 328, + "anchor": "source", + "line_start": 213, + "line_end": 303, "keywords": [ "source", - "material", - "file", - "examples", - "ab-testing", - "src", - "index.js", "import", "getenv", "fastedge", @@ -19315,7 +19117,8 @@ "slicedheaders", "sliceabtestidfromcookie", "headers", - "createabtesthe" + "createabtestheaders", + "outbo" ] } ] @@ -19323,7 +19126,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19347,7 +19150,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -19357,7 +19160,7 @@ "ab-testing", "cookie-parsing" ], - "content_sha256": "b245afb81d6b40b72fe551491d375c288dc9d18b10c65ce15d85b513144abdcb", + "content_sha256": "21cff74a5cb7a1216109a412f205162be95a99647d070c31cfb71ff8c06626fb", "sections": [ { "id": "http-examples-ab-testing-wasi-rust#introduction", @@ -19376,7 +19179,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -19851,7 +19654,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19875,7 +19678,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "examples-api-wrapper-basic-rust", "type", "example", @@ -19885,7 +19688,7 @@ "language", "app" ], - "content_sha256": "74f225e315847ffb3cd2a02a6b9ec467cff01874a624faa9a71908d68c9948f1", + "content_sha256": "eb2a0df93df00172d7020dc806527d1f98e18809247273240ede8f6952fccbf6", "sections": [ { "id": "http-examples-api-wrapper-basic-rust#introduction", @@ -19904,7 +19707,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "examples-api-wrapper-basic-rust", "type", "http", @@ -20434,7 +20237,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md", "skill": "fastedge-docs", "category": "examples", @@ -20458,7 +20261,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "authentication", "patterns", "fastedge", @@ -20469,7 +20272,7 @@ "shared", "secrets" ], - "content_sha256": "55de1130ba0160fafee05133cfcbf66aa1309fff2174117f714f31a5ddb01782", + "content_sha256": "2e7f44f68db995e99a39e614304ec39247aa583cb6ccb04e1674dd9b579b49e7", "sections": [ { "id": "http-examples-auth-js#introduction", @@ -20488,7 +20291,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "authentication", "patterns", "javascript", @@ -20785,7 +20588,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20809,7 +20612,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -20819,7 +20622,7 @@ "outbound-http", "query-params" ], - "content_sha256": "5601522eba883714e3b242af7b6cf8a33827aae05b302144136ffd1aa2d6111d", + "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20838,7 +20641,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21260,13 +21063,207 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21290,7 +21287,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21300,7 +21297,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "a1664580f19b8b9f966b764ebd0246d202ab6f8253cbdfc82c091aefd4bd15a8", + "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21319,7 +21316,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21772,195 +21769,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21984,7 +21799,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -21994,7 +21809,7 @@ "cache", "basic" ], - "content_sha256": "d9497b9de89acf7daf64068b5f9abe4e6118b9e5de4bd8b62f77282b88d34b3f", + "content_sha256": "51fe5b585b86d0b39142ab2d5313ea57a260b1d8830ad7d38bce3b839f9f9961", "sections": [ { "id": "http-examples-cache-basic-rust#introduction", @@ -22013,7 +21828,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -22412,7 +22227,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22438,7 +22253,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "cache", "module", @@ -22448,7 +22263,7 @@ "2.3.0", "app" ], - "content_sha256": "cf67b907345117e9955805dd7a38a16f9e8fed0fe9600d3df6a5b9713e2577a2", + "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22467,7 +22282,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "cache", "javascript", @@ -22796,13 +22611,63 @@ "response", "construction" ] + }, + { + "id": "http-examples-cache-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 335, + "line_end": 610, + "keywords": [ + "source", + "material", + "file", + "examples", + "cache", + "src", + "index.ts", + "fastedge", + "flagship", + "patterns", + "example", + "demonstrates", + "three", + "highest-value", + "uses", + "module", + "per-ip", + "rate", + "limiting", + "atomic", + "counters", + "origin-cache", + "proxy", + "manual", + "get", + "set", + "conditional", + "caching", + "json", + "memoisation", + "getorset", + "computed", + "populator", + "rely", + "strongly", + "consistent", + "within", + "pop", + "incr", + "returns" + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22826,7 +22691,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -22836,7 +22701,7 @@ "cache", "outbound-http" ], - "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", + "content_sha256": "31cfe4b83115feeb78ff407e5c37207e21a522d87f06aeb8d7607aa6ad31cd67", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -22855,7 +22720,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -23398,7 +23263,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -23422,7 +23287,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "diagnostic-logging", @@ -23432,7 +23297,7 @@ "languages", "diagnostic" ], - "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", + "content_sha256": "b55cd1e2b402fa75e701e2336c62aa6314ec4be802409333a130cb1ffcbfe91c", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -23451,7 +23316,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "diagnostic-logging", @@ -24175,7 +24040,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md", "skill": "fastedge-docs", "category": "examples", @@ -24199,7 +24064,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fetch", "outbound", "requests", @@ -24210,7 +24075,7 @@ "standard", "url" ], - "content_sha256": "49ef6239614d42258573902b5711bde7807b8c31eea93cf6df351d25de5702f2", + "content_sha256": "032adc2beba6b9b9dade30fcf609e1febf5a9b7ddc47a88c37889d1eec835870", "sections": [ { "id": "http-examples-fetch-js#introduction", @@ -24229,7 +24094,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -24286,7 +24151,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", @@ -24310,7 +24175,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "example", "geo-redirect", "redirects", @@ -24322,7 +24187,7 @@ "visitor", "country" ], - "content_sha256": "0bfad27c4682481b14340d4a70e53c0738d79b48c729219df0e5b2f92fec8c9c", + "content_sha256": "07208f0d1266ed5eecd1ac212b47f0a27cb432220881b2f7fac6a6d4aee0bbd4", "sections": [ { "id": "http-examples-geo-redirect-js#introduction", @@ -24341,7 +24206,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -24399,7 +24264,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24423,7 +24288,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -24433,7 +24298,7 @@ "geo-redirect", "headers" ], - "content_sha256": "5c4595ed117a31ae93362494563368610e2f957a6ae0f6fcdd2e9d9ca9570e2a", + "content_sha256": "92d1d6dcd29aa84e8c9a383b7f31e2a4c3550686a7586e2c164187596b5df709", "sections": [ { "id": "http-examples-geo-redirect-wasi-rust#introduction", @@ -24452,7 +24317,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -25025,7 +24890,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md", "skill": "fastedge-docs", "category": "examples", @@ -25049,7 +24914,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "headers", "example", "fastedge", @@ -25061,7 +24926,7 @@ "environment", "variable" ], - "content_sha256": "632e383ad4e7a30633f7e55a82768bfe1de9f44f53b8918113de23c41a1748db", + "content_sha256": "d4a5d1b949bfe5116aee65279e796b023d55d59544854876eaf493c110eef90c", "sections": [ { "id": "http-examples-headers-js#introduction", @@ -25080,7 +24945,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -25132,7 +24997,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25156,7 +25021,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -25166,7 +25031,7 @@ "headers", "env-vars" ], - "content_sha256": "8e03eeb304c1cd911ea0f30ecb0d686d6e6af33163b0a7b665d41162785fc059", + "content_sha256": "b6142b5b461c8cc47c3422fa749ff3d7ca808ac8618aa481eaecd952f6b80483", "sections": [ { "id": "http-examples-headers-wasi-rust#introduction", @@ -25185,7 +25050,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -25781,7 +25646,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25805,7 +25670,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -25815,7 +25680,7 @@ "hello-world", "basic-handler" ], - "content_sha256": "8a3f946fbee1451f81cbcef5859a2efd998d661f3f6af16fca2ec48839ea7fee", + "content_sha256": "c3aac88e44de0d555bc462d6d70424ab34d4f0dec25606a2625ceb5e39b67400", "sections": [ { "id": "http-examples-hello-world-basic-rust#introduction", @@ -25834,7 +25699,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26213,7 +26078,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26237,7 +26102,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26247,7 +26112,7 @@ "hello-world", "wasi" ], - "content_sha256": "d9923d42c68fc22ba2e4de5640ba05027fd7c95a4f06ff5c4f34ac2177c5397b", + "content_sha256": "b15ce34c19d4cf004bf6f602e6bf370c34f33abd962e77ce72f4bc13969016fe", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -26266,7 +26131,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -26659,134 +26524,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md", "skill": "fastedge-docs", "category": "examples", @@ -26812,7 +26556,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "hono", "patterns", "fastedge", @@ -26821,7 +26565,7 @@ "apps.", "small" ], - "content_sha256": "7e3e27ca00488b97dfe98e96516cb4dd19a222664008cfa50d17898b59b5cf30", + "content_sha256": "4c75f73bbc6d7fbc5686f1d701a49ba1af7af0c650a910e3d47ae72272afdf1e", "sections": [ { "id": "http-examples-hono-js#introduction", @@ -26840,7 +26584,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "hono", "patterns", "fastedge", @@ -27067,7 +26811,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md", "skill": "fastedge-docs", "category": "examples", @@ -27091,7 +26835,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "store", "example", "reference", @@ -27103,7 +26847,7 @@ "fastedge", "edge" ], - "content_sha256": "d3c4dbab0c624edfd7e3590c6413091cc12881c8a06e43b9a74fe7008ce7e515", + "content_sha256": "bbee208b19ac9c128c58c2b3a483f133bf01d56becf4d62a328e664fb466c646", "sections": [ { "id": "http-examples-kv-store-js#introduction", @@ -27122,7 +26866,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17" + "2026-08-20" ] }, { @@ -27178,7 +26922,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27202,7 +26946,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27212,7 +26956,7 @@ "languages", "store" ], - "content_sha256": "8c92de423216bc2e4fae7db3faea29d4ef524be954287c86bf2242e13a7a9193", + "content_sha256": "315969c42f8aa2c0325e094018cc3d59e5f68550acd5744a480b2302a15d0860", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -27231,7 +26975,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "capabilities", "kv-store", @@ -27658,229 +27402,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27904,7 +27432,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "large", "environment", "variable", @@ -27914,7 +27442,7 @@ "demonstrates", "read" ], - "content_sha256": "37b6892618f7fddf856bd9cf38f8d86dab1740c449c61cb78869f309c9c94d9e", + "content_sha256": "ab40bc5e1a189d77f212f789961be555b81ceead639067c4ccfbdc9f2e4e1d6c", "sections": [ { "id": "http-examples-large-env-variable-wasi-rust#introduction", @@ -27933,7 +27461,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "large", "environment", "variable", @@ -28269,7 +27797,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28293,7 +27821,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -28303,7 +27831,7 @@ "outbound-fetch", "env-vars" ], - "content_sha256": "2c31f5e301b8fd9c5eede29a285d26452eeb1606d1d009ebb4bbe5d5bc1fafc9", + "content_sha256": "e170431974501dcb609073b8be6b0f0c93a73841a33255b961077dec00c66f68", "sections": [ { "id": "http-examples-markdown-render-basic-rust#introduction", @@ -28322,7 +27850,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -28795,7 +28323,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28819,7 +28347,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -28829,7 +28357,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "1dd08bab3d3be3a3369b1bf4ecd325fe72ffc3c530b3285cefd260df6685b077", + "content_sha256": "7d9b8470977ba554efeff699feeee8f078a5b682b5d396d070f0589f49f8044f", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -28848,7 +28376,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29216,178 +28744,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29411,7 +28774,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "outbound", "fetch", "wasi", @@ -29421,7 +28784,7 @@ "upstream", "origin" ], - "content_sha256": "924c9269ecb0377ada1ee5ef156d290297e9b92ca1a918b15c0ed328c38b2e31", + "content_sha256": "c5a6c3a406e12b4a82281f715d7f268d84b732cc76ec9376b34314302cea12dc", "sections": [ { "id": "http-examples-outbound-fetch-wasi-rust#introduction", @@ -29440,7 +28803,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "outbound", "fetch", "wasi", @@ -29837,7 +29200,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29861,7 +29224,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -29871,7 +29234,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "2f116f94e55b44886de6b1d87621bb56da6b66f50e7dfb95ee4330f228ced3be", + "content_sha256": "f6509335e7b9957b364049295ea3a84967dce13ae1e03c5ebbd8412f6e0dfb05", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -29890,7 +29253,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -30208,15 +29571,15 @@ ] }, { - "id": "http-examples-outbound-modify-response-wasi-rust#source-material", - "heading": "Source Material", + "id": "http-examples-outbound-modify-response-wasi-rust#full-source", + "heading": "Full Source", "level": 2, - "anchor": "source-material", + "anchor": "full-source", "line_start": 186, "line_end": 277, "keywords": [ + "full", "source", - "material", "file", "examples", "http", @@ -30284,7 +29647,7 @@ { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -30308,7 +29671,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -30318,7 +29681,7 @@ "request-inspection", "header-iteration" ], - "content_sha256": "b2851a2b3a8f9ff251ca478362e8120186a3863d09a4570781c3c28bd518e650", + "content_sha256": "7c8720cc88a63e99432da519839a4f5b8438520df5ac92432b3c524afbaa1f85", "sections": [ { "id": "http-examples-print-basic-rust#introduction", @@ -30337,7 +29700,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -30936,7 +30299,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30962,7 +30325,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "proxy", "response", "transform", @@ -30971,7 +30334,7 @@ "fastedge", "apps" ], - "content_sha256": "4640d7da83d11810dadbfff726f51712a64847b66562b908cd5b1d0f1beec95a", + "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30990,7 +30353,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "proxy", "response", "transform", @@ -31424,13 +30787,356 @@ "kvstore", "api" ] + }, + { + "id": "http-examples-proxy-js#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 254, + "line_end": 264, + "keywords": [ + "source", + "material", + "file", + "docs", + "proxy_patterns.md", + "maintenance", + "hand-authored.", + "produced", + "fastedge-plugin-source", + "generate-docs.sh.", + "edit", + "directly.", + "proxy", + "response", + "transform", + "patterns", + "fastedge", + "http", + "apps", + "act", + "thin", + "front", + "origin", + "upstream", + "api", + "fetching", + "transforming", + "returning", + "result.", + "document", + "covers", + "common", + "patterns." + ] + }, + { + "id": "http-examples-proxy-js#simple-proxy", + "heading": "Simple Proxy", + "level": 2, + "anchor": "simple-proxy", + "line_start": 265, + "line_end": 291, + "keywords": [ + "simple", + "proxy", + "fetch", + "upstream", + "return", + "body", + "unchanged", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "method", + "request.method", + "headers", + "request.headers", + "get", + "head", + "request.arraybuffer", + "undefined", + "addeventlistener" + ] + }, + { + "id": "http-examples-proxy-js#proxy-with-json-transform", + "heading": "Proxy with JSON Transform", + "level": 2, + "anchor": "proxy-with-json-transform", + "line_start": 292, + "line_end": 320, + "keywords": [ + "proxy", + "json", + "transform", + "modify", + "response", + "body", + "returning", + "it.", + "pattern", + "examples", + "outbound-modify-response", + "typescript", + "async", + "function", + "handle", + "const", + "upstream", + "await", + "fetch", + "https", + "jsonplaceholder.typicode.com", + "users", + "upstream.json", + "transformed", + "users.slice", + "total", + "skip", + "limit", + "return", + "new", + "json.stringify", + "status", + "200", + "headers", + "content-type" + ] + }, + { + "id": "http-examples-proxy-js#hono-proxy-with-transform", + "heading": "Hono Proxy with Transform", + "level": 2, + "anchor": "hono-proxy-with-transform", + "line_start": 321, + "line_end": 351, + "keywords": [ + "hono", + "proxy", + "transform", + "inside", + "app", + "use", + "c.req.raw.headers", + "forward", + "inbound", + "headers", + "c.req.arraybuffer", + "body", + "typescript", + "import", + "const", + "new", + "app.all", + "api", + "async", + "url", + "c.req.url", + "upstream", + "https", + "backend.example.com", + "url.pathname", + "url.search", + "response", + "await", + "fetch", + "method", + "c.req.method", + "get", + "c.r" + ] + }, + { + "id": "http-examples-proxy-js#header-manipulation-in-proxies", + "heading": "Header Manipulation in Proxies", + "level": 2, + "anchor": "header-manipulation-in-proxies", + "line_start": 352, + "line_end": 380, + "keywords": [ + "header", + "manipulation", + "proxies", + "strip", + "hop-by-hop", + "headers", + "forwarding", + "upstream", + "add", + "diagnostic", + "way", + "back", + "typescript", + "async", + "function", + "handle", + "request", + "const", + "upstreamheaders", + "new", + "request.headers", + "forwarded", + "upstreamheaders.delete", + "connection", + "keep-alive", + "transfer-encoding", + "await", + "fetch", + "https", + "backend.example.com", + "method", + "request.m" + ] + }, + { + "id": "http-examples-proxy-js#cache-aware-proxy-with-kv", + "heading": "Cache-aware Proxy with KV", + "level": 2, + "anchor": "cache-aware-proxy-with-kv", + "line_start": 381, + "line_end": 413, + "keywords": [ + "cache-aware", + "proxy", + "cache", + "upstream", + "responses", + "store", + "avoid", + "repeated", + "outbound", + "calls.", + "note", + "read-only", + "app", + "code", + "writes", + "happen", + "via", + "portal", + "api", + "typescript", + "import", + "kvstore", + "fastedge", + "async", + "function", + "handle", + "request", + "const", + "url", + "new", + "request.url", + "try", + "kvstore.open", + "api-cache", + "cached", + "cache.get", + "url.pathname", + "null", + "return", + "response" + ] + }, + { + "id": "http-examples-proxy-js#operational-notes", + "heading": "Operational Notes", + "level": 2, + "anchor": "operational-notes", + "line_start": 414, + "line_end": 419, + "keywords": [ + "operational", + "notes", + "outbound", + "fetch", + "budget.", + "invocation", + "limited", + "number", + "requests", + "basic", + "pro", + "parallelise", + "possible", + "promise.all", + "...", + "instead", + "sequential", + "await", + "execution", + "time", + "proxying", + "upstream", + "transforming", + "counts", + "against", + "50ms", + "200ms", + "slow", + "upstreams", + "trip", + "532", + "timeouts.", + "body", + "size", + "limits.", + "inbound", + "bodies", + "subject", + "configured", + "request" + ] + }, + { + "id": "http-examples-proxy-js#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 420, + "line_end": 428, + "keywords": [ + "see", + "examples", + "outbound-modify-response", + "json", + "transform", + "upstream", + "response", + "outbound-fetch", + "basic", + "outbound", + "fetch", + "patterns", + "headers", + "request", + "header", + "manipulation", + "kv-store", + "kv-backed", + "caching" + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31454,7 +31160,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31464,7 +31170,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "58e28ee56342730d02fcb2249e9c06c5b66248e3d248c353702b0b1f5f876521", + "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31483,7 +31189,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -31922,13 +31628,247 @@ "documentation", "external" ] + }, + { + "id": "http-examples-s3upload-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 186, + "line_end": 369, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "s3upload", + "src", + "lib.rs", + "rust", + "use", + "std", + "time", + "duration", + "fastedge", + "body", + "header", + "error", + "method", + "request", + "response", + "statuscode", + "rusty_s3", + "bucket", + "credentials", + "s3action", + "urlstyle", + "collections", + "hashmap", + "env", + "url", + "main", + "req", + "result", + "match", + "req.method", + "allow", + "only", + "post", + "put" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 370, + "line_end": 379, + "keywords": [ + "accepts", + "post", + "put", + "only", + "returns", + "405", + "methods", + "requires", + "name", + "filename", + "query", + "parameter", + "non-empty", + "body", + "400", + "otherwise", + "enforces", + "max_file_size", + "set", + "413", + "exceeded", + "calls", + "prepare_s3", + "build", + "1-hour", + "presigned", + "url", + "using", + "rusty_s3", + "forwards", + "file", + "via", + "fastedge", + "send_request", + "success", + "200", + "responds", + "clean", + "object", + "string" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 380, + "line_end": 393, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "access_key", + "access", + "key", + "secret_key", + "secret", + "region", + "e.g.", + "s-ed1", + "base_hostname", + "base", + "hostname", + "cloud.gcore.lu", + "bucket", + "name", + "scheme", + "optional", + "url", + "defaults", + "http", + "max_file_size", + "maximum", + "upload", + "size", + "bytes", + "limit", + "unset", + "constructed", + "endpoint", + "bucke" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 394, + "line_end": 400, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "s3upload.wasm" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#usage", + "heading": "Usage", + "level": 2, + "anchor": "usage", + "line_start": 401, + "line_end": 411, + "keywords": [ + "usage", + "post", + "upload", + "name", + "photo.jpg", + "content-type", + "image", + "jpeg", + "file", + "bytes", + "success", + "200", + "response", + "body", + "clean", + "object", + "url", + "presign", + "query", + "parameters", + "stripped" + ] + }, + { + "id": "http-examples-s3upload-basic-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 412, + "line_end": 418, + "keywords": [ + "notes", + "options", + "method", + "returns", + "204", + "include", + "cors", + "headers", + "add", + "access-control-allow-", + "browser", + "preflight", + "support", + "needed.", + "presigned", + "url", + "expires", + "hour", + "since", + "upload", + "performed", + "server-side", + "practical", + "impact.", + "max_file_size", + "silently", + "ignored", + "set", + "non-numeric", + "value." + ] } ] }, { "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31952,7 +31892,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "example", @@ -31962,7 +31902,7 @@ "fastedge", "platform" ], - "content_sha256": "132c3f3a1a47128eda646068a038b904750dc8a1bbe65bf3208069c53c81b086", + "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31981,7 +31921,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "access", "rust", @@ -32371,284 +32311,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32672,7 +32341,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "rollover", "wasi", @@ -32683,7 +32352,7 @@ "rotation", "using" ], - "content_sha256": "b1e9ad960518a8d3660beb3cbce5070ea6eb16c44ced1266fcc6af599c5eb272", + "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32702,7 +32371,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "secret", "rollover", "wasi", @@ -32958,14 +32627,30 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 150, + "line_end": 178, "keywords": [ "implementation", "pattern", "rust", "use", + "std", + "time", + "systemtime", + "unix_epoch", + "anyhow", "fastedge", "secret", + "serde_json", + "json", + "wstd", + "http", + "body", + "request", + "response", + "http_server", + "async", + "main", + "result", "read", "slot", "header", @@ -32975,29 +32660,13 @@ "timestamp", "let", "u32", - "request", ".headers", ".get", "x-slot", ".and_then", "v.to_str", ".ok", - "v.parse", - ".unwrap_or_else", - "systemtime", - "now", - ".duration_since", - "unix_epoch", - ".expect", - "time", - "went", - "backwards", - ".as_secs", - "get", - "latest", - "value", - "secret_name", - ".map_err" + "v.p" ] }, { @@ -33005,8 +32674,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 151, - "line_end": 158, + "line_start": 179, + "line_end": 186, "keywords": [ "constraints", "gotchas", @@ -33055,8 +32724,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 159, - "line_end": 165, + "line_start": 187, + "line_end": 193, "keywords": [ "build", "cargo", @@ -33073,8 +32742,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 166, - "line_end": 172, + "line_start": 194, + "line_end": 200, "keywords": [ "see", "fastedge", @@ -33105,7 +32774,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33129,7 +32798,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33139,7 +32808,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "843e00d8b278c3bc8d98512f028bde253162bedba500212666b992f9369ee18d", + "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -33158,7 +32827,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33523,193 +33192,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33733,7 +33222,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -33743,7 +33232,7 @@ "outbound-http", "env-vars" ], - "content_sha256": "3b6d38e23605762f4da281035dedd49a989d71608c1a70e4258efce6a57dc746", + "content_sha256": "3195fb4f9d583f55e2834ef95604d838f310817302a02705bec7c99b51685485", "sections": [ { "id": "http-examples-smart-switch-basic-rust#introduction", @@ -33762,7 +33251,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34064,7 +33553,7 @@ "level": 2, "anchor": "key-gotchas", "line_start": 179, - "line_end": 188, + "line_end": 189, "keywords": [ "key", "gotchas", @@ -34113,8 +33602,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 189, - "line_end": 197, + "line_start": 190, + "line_end": 198, "keywords": [ "build", "cargo", @@ -34133,8 +33622,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 198, - "line_end": 204, + "line_start": 199, + "line_end": 205, "keywords": [ "see", "api_wrapper", @@ -34176,7 +33665,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34202,7 +33691,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34212,7 +33701,7 @@ "static-assets", "routing" ], - "content_sha256": "3bd6b8863a1735b55457f71936fd446400c805558a41a5f1afc836c80da1d69d", + "content_sha256": "dcefd9ae20154d629c0a4b74b11bb152c2e0ddd0712407cb4ead1a03002ede52", "sections": [ { "id": "http-examples-static-assets-wasi-rust#introduction", @@ -34231,7 +33720,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34778,7 +34267,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34802,7 +34291,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -34812,7 +34301,7 @@ "streaming", "async" ], - "content_sha256": "f45c5da0c3641528c49fa0d4c003631252658f5ef281ce74184f1653a3fff676", + "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34831,7 +34320,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -35160,151 +34649,13 @@ "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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -35328,7 +34679,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -35338,7 +34689,7 @@ "env-vars", "secrets" ], - "content_sha256": "ae6dbe416dacde5f3693ca7811601acccf63b3e59ee8dd549a1151f03782078f", + "content_sha256": "9d8807dc93609399f430a94d714047bb9d9458bc07f16c18e6a0814c194ffb94", "sections": [ { "id": "http-examples-variables-and-secrets-wasi-rust#introduction", @@ -35357,7 +34708,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "---", "type", "example", @@ -35814,7 +35165,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -35838,7 +35189,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "example", "watermark", "basic", @@ -35848,7 +35199,7 @@ "handler", "fastedge" ], - "content_sha256": "df0d2b23a77c3575ea0b55b0217dfb97f259ea286c4c78126390ddab63db305f", + "content_sha256": "1fee467e8245bcae3adf9bcb0e06e0dda564403fccf9374939f5a72c90358ab3", "sections": [ { "id": "http-examples-watermark-basic-rust#introduction", @@ -35867,7 +35218,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "http", "example", "watermark", @@ -36389,7 +35740,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md", "skill": "fastedge-docs", "category": "platform", @@ -36407,7 +35758,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "runtime", "constraints", @@ -36419,7 +35770,7 @@ "particularly", "affecting" ], - "content_sha256": "a06ba95b1d679971a8bbccad222c8e3892781a57fe0dd89318488e4f45142132", + "content_sha256": "cac0e481a35b2a82d139b5878410a53c91f3f39c30c0190dd1a4ae6a1b229904", "sections": [ { "id": "js-runtime#introduction", @@ -36438,7 +35789,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "runtime", "constraints", @@ -39582,7 +38933,7 @@ { "id": "quickstart-as", "title": "Quickstart: AssemblyScript CDN Apps on FastEdge", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md", "skill": "fastedge-docs", "category": "reference", @@ -39605,7 +38956,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "quickstart", "cdn", "apps", @@ -39616,7 +38967,7 @@ "compile", "webassembly" ], - "content_sha256": "41ad91b4703ac00f166601df66080f0e3bd82de89c6e9fd8abfe5f3cf36abc80", + "content_sha256": "fff52212e2f6238e7ed758a19a80e19a19974e5089f08928df7abac1819c1ada", "sections": [ { "id": "quickstart-as#introduction", @@ -39635,7 +38986,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "quickstart", "assemblyscript", "cdn", @@ -40016,7 +39367,7 @@ { "id": "quickstart-js", "title": "FastEdge JavaScript Quickstart", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md", "skill": "fastedge-docs", "category": "reference", @@ -40039,7 +39390,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "quickstart", "prerequisites", @@ -40050,7 +39401,7 @@ "install", "bash" ], - "content_sha256": "8a824dda2cf16aed9d754c8e1f6e56580ad28f51b38337244715900bcd08442b", + "content_sha256": "51cef432b6305a0da282efc422dcd4b0a9f385cb40f047aee77022cb9258e771", "sections": [ { "id": "quickstart-js#introduction", @@ -40069,7 +39420,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "javascript", "quickstart" @@ -40357,7 +39708,7 @@ { "id": "quickstart-rust", "title": "FastEdge Rust SDK — Quickstart", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -40378,7 +39729,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "sdk", "quickstart", @@ -40388,7 +39739,7 @@ "wasm32-wasip1", "target" ], - "content_sha256": "d2adca4401b83156057de1dc4163c81eab082a24bf4063bc2f2b8ba4ef2cbfbd", + "content_sha256": "4e1d3d5a01a3e8aaf9f864c197899fbbc06cc30f86aba6a34d8207f67a629c25", "sections": [ { "id": "quickstart-rust#introduction", @@ -40407,7 +39758,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "rust", "sdk", @@ -40707,7 +40058,7 @@ { "id": "runner-internals", "title": "Runner Internals — Low-Level Runner API", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/runner-internals.md", "skill": "test", "category": "testing", @@ -40723,9 +40074,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "runner", "internals", "low-level", @@ -40737,7 +40088,7 @@ "use", "you" ], - "content_sha256": "3ece5464164e143468e6f52883fffde960a434dddb1352dc3f9b65f188f0a4f8", + "content_sha256": "abcb6943073feb071640d4e2fdbd1df4f815e41279a360bb65711d1935b48711", "sections": [ { "id": "runner-internals#introduction", @@ -40754,9 +40105,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "runner", "internals", "low-level", @@ -41102,7 +40453,7 @@ { "id": "sdk-reference-as", "title": "AssemblyScript Proxy-Wasm SDK Reference", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md", "skill": "fastedge-docs", "category": "sdk", @@ -41125,7 +40476,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "proxy-wasm", "reference", "gcoredev", @@ -41135,7 +40486,7 @@ "writing", "cdn" ], - "content_sha256": "353effa70cde17d46a928071309fca138aa211b6c6768542059770adcf6cc021", + "content_sha256": "76f2b7665b04f19b479f02f492977afaf6fffe062287c53fcfb4f387c5012e89", "sections": [ { "id": "sdk-reference-as#introduction", @@ -41154,7 +40505,7 @@ "commit", "8e3bb621bc013a0aed7e52122066b417ad62a207", "updated", - "2026-08-17", + "2026-08-20", "assemblyscript", "proxy-wasm", "sdk", @@ -41649,7 +41000,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md", "skill": "fastedge-docs", "category": "sdk", @@ -41670,7 +41021,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "reference", "gcoredev", "source", @@ -41680,7 +41031,7 @@ "authoritative", "api" ], - "content_sha256": "1875e18221368a4e9d95a8215be2efa52d35634b23916b74e51d0b51dbeef789", + "content_sha256": "b24418a0122025cc0863b8fa10c1295844a91aeb97a941495530e3cd5bcbcf1b", "sections": [ { "id": "sdk-reference-js#introduction", @@ -41699,7 +41050,7 @@ "commit", "81145a9a43ec499240c687bd49376ab20c72b11c", "updated", - "2026-08-17", + "2026-08-20", "javascript", "sdk", "reference", @@ -42244,7 +41595,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-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -42265,7 +41616,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "reference", "fastedge", "crate", @@ -42275,7 +41626,7 @@ "start", "cargo.toml" ], - "content_sha256": "1aae120746bea8e110025874bbf4432f20b571af4f8a713875fec01c7e9ad8de", + "content_sha256": "e3598e9cb99f2f28beb8527ea3284480bee8c63132151da4c8874089bf9bf01b", "sections": [ { "id": "sdk-reference-rust#introduction", @@ -42294,7 +41645,7 @@ "commit", "6347a7c2fda0d03e66f1214db5eec041c16801b7", "updated", - "2026-08-17", + "2026-08-20", "rust", "sdk", "reference", @@ -42651,7 +42002,7 @@ { "id": "server-api", "title": "Server API — REST and WebSocket Endpoints", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/server-api.md", "skill": "test", "category": "testing", @@ -42667,9 +42018,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "server", "api", "rest", @@ -42681,7 +42032,7 @@ "interfaces", "loading" ], - "content_sha256": "96fce5e89649293442cc67e66685fffc2282f052c1d92ac935c72b815ae71822", + "content_sha256": "7b0851d7b108a2c4d80330ca3c3f2510160d389f34850e62a985d48a73dc38e7", "sections": [ { "id": "server-api#introduction", @@ -42698,9 +42049,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "server", "api", "rest", @@ -42906,7 +42257,7 @@ { "id": "templates-catalog", "title": "FastEdge Bolt-On Templates", - "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/catalog.md", "skill": "fastedge-docs", "category": "reference", @@ -42924,7 +42275,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -42936,7 +42287,7 @@ "scratch", "check" ], - "content_sha256": "8163fdfd77d2320dabac43027905b69b13d14eabbff5f40c16ad4ac358d04c4a", + "content_sha256": "910cf26789610f4f959d807e57f149e1cf0af1cb1bcdcc089f45792bb595ff83", "sections": [ { "id": "templates-catalog#introduction", @@ -42955,7 +42306,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "bolt-on", "templates", @@ -43040,7 +42391,7 @@ "level": 2, "anchor": "harden-cookies-—-cookie-security-hardening-secure-httponly-samesite-attributes-set-cookie-rewriting", "line_start": 32, - "line_end": 47, + "line_end": 49, "keywords": [ "harden-cookies", "cookie", @@ -43089,8 +42440,8 @@ "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, + "line_start": 50, + "line_end": 73, "keywords": [ "edge-sso", "sso", @@ -43139,8 +42490,8 @@ "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, + "line_start": 74, + "line_end": 99, "keywords": [ "edge-totp", "totp", @@ -43189,7 +42540,7 @@ { "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 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/edge-sso-integration.md", "skill": "fastedge-docs", "category": "reference", @@ -43207,7 +42558,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-sso", "origin", "integration", @@ -43218,7 +42569,7 @@ "pair", "auth-app" ], - "content_sha256": "7301aea1c160744e3ee14e1d9dbe28aa1fb33e22fd28c5a4bc99be69b5bbc361", + "content_sha256": "6b3e417969b4c786f86226a022f2180d50d870c6b9092c5a00870b2af82aadf3", "sections": [ { "id": "templates-edge-sso-integration#introduction", @@ -43237,7 +42588,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-sso", "origin", "integration", @@ -43574,7 +42925,7 @@ { "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 -->", + "description": "auto-updated: true sources: - id: fastedge-templates ref: main commit: 87b7dc143db5e74cf0e7eb52f67484f6abc51c43 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/templates/edge-totp-integration.md", "skill": "fastedge-docs", "category": "reference", @@ -43592,7 +42943,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", @@ -43603,7 +42954,7 @@ "existing", "password" ], - "content_sha256": "dbdc4544faf42a8d644ce3978dd3096ecec9f07a4590b0f45d9e16f1d44388ae", + "content_sha256": "2f2493935877ac046f328513d03575666424c44043fa8f4d319e59f96b93dccb", "sections": [ { "id": "templates-edge-totp-integration#introduction", @@ -43622,7 +42973,7 @@ "commit", "87b7dc143db5e74cf0e7eb52f67484f6abc51c43", "updated", - "2026-08-17", + "2026-08-20", "edge-totp", "origin", "integration", @@ -43951,7 +43302,7 @@ { "id": "test-config", "title": "test-config Reference", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/test-config.md", "skill": "test", "category": "testing", @@ -43967,9 +43318,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "test-config", "reference", "fastedge-config.test.json", @@ -43981,7 +43332,7 @@ "request", "cdn" ], - "content_sha256": "cf3e921e482468b97114a9494e3123f97e275060da3afffe2206862b7ffe0469", + "content_sha256": "0c69141a01fc2bd5ea12fe232d0388bdf67a31eb1ed5b40cff5fbacc2f452993", "sections": [ { "id": "test-config#introduction", @@ -43998,9 +43349,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "test-config", "reference", "fastedge-config.test.json", @@ -44277,7 +43628,7 @@ { "id": "test-framework", "title": "FastEdge Test Framework API", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/test-framework.md", "skill": "test", "category": "testing", @@ -44296,9 +43647,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "framework", "api", @@ -44309,7 +43660,7 @@ "runner", "apps." ], - "content_sha256": "e125a779b09525ed8048d05e09b9979c2ceea4c1c90bbc4a988c1ce98a9fcda1", + "content_sha256": "e78d8b55b9e4ea9c9245dda8d2962141185f3b9331c85d6b54b6d13120f5ab1d", "sections": [ { "id": "test-framework#introduction", @@ -44326,9 +43677,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "test", "framework", @@ -44693,7 +44044,7 @@ { "id": "vscode-debugger", "title": "FastEdge Visual Debugger", - "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: 61497eca6ead033ac810165bc1e20e1d6dd4678f updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-test ref: main commit: c79583ecbec74b0d36ae664cd3a148ae34059a75 updated: 2026-08-20 -->", "path": "plugins/gcore-fastedge/skills/test/reference/vscode-debugger.md", "skill": "test", "category": "testing", @@ -44709,9 +44060,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "visual", "debugger", @@ -44723,7 +44074,7 @@ "gcoredev", "same" ], - "content_sha256": "627d9b7700750420febf279bfb44b3cea3ea212faa41800f660fa1f74a5965e5", + "content_sha256": "1df0bfe2966e48ff0e99a252fb29c8e29b60eeb55b2a2f618d37c472e9062d41", "sections": [ { "id": "vscode-debugger#introduction", @@ -44740,9 +44091,9 @@ "ref", "main", "commit", - "61497eca6ead033ac810165bc1e20e1d6dd4678f", + "c79583ecbec74b0d36ae664cd3a148ae34059a75", "updated", - "2026-08-17", + "2026-08-20", "fastedge", "visual", "debugger",