diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b1a53a8..20b0281 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.10" + "version": "0.2.11" } ] } diff --git a/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json b/plugins/gcore-fastedge-codex/.codex-plugin/plugin.json index 4e6d497..7329354 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.10", + "version": "0.2.11", "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 5eec19e..c340e5f 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-09-09T13:28:16.298Z", + "generated_at": "2026-09-24T09:24:50.728Z", "source": { "repo": "fastedge-plugin", - "commit": "99dc55a", + "commit": "503a8fc", "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn-apps-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -29,9 +29,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", + "content_sha256": "6a79f809ecedc76ed7617ab078604096a5933da07a44a2059849f2042a42910a", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -58,9 +58,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "rust", "sdk", @@ -150,9 +150,9 @@ "0.2", "log", "0.4", + "cache", "secrets", - "dictionary", - "depend" + "dictionary" ] }, { @@ -245,7 +245,7 @@ "callback", "phase", "description", - "-----------------------------------------------------------------", + "-----------------------------------------------------------------------------------------------", "----------------", "---------------------------------------------------", "on_http_request_headers", @@ -256,11 +256,7 @@ "action", "request", "headers", - "inspect", - "modify", - "forwarding", - "on_http_request_body", - "body_size" + "inspect" ] }, { @@ -431,7 +427,7 @@ "level": 2, "anchor": "host-services-for-cdn-apps", "line_start": 423, - "line_end": 733, + "line_end": 818, "keywords": [ "host", "services", @@ -477,8 +473,8 @@ "heading": "API Comparison: HTTP vs CDN", "level": 2, "anchor": "api-comparison-http-vs-cdn", - "line_start": 734, - "line_end": 746, + "line_start": 819, + "line_end": 832, "keywords": [ "api", "comparison", @@ -491,12 +487,11 @@ "proxywasm", "-------------", "-------------------------------------------------------------------", - "--------------------------------------------------------", + "-----------------------------------------------------------------", "key-value", "fastedge", "key_value", - "store", - "secrets" + "store" ] }, { @@ -504,8 +499,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 747, - "line_end": 751, + "line_start": 833, + "line_end": 837, "keywords": [ "see", "sdk", @@ -2071,7 +2066,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2093,9 +2088,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2105,7 +2100,7 @@ "requests", "against" ], - "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", + "content_sha256": "6c28a1664d639ebeb12397dea7294bdd293848795b65c88c2f5aab81e2bee614", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2122,9 +2117,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2369,56 +2364,6 @@ "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" - ] } ] }, @@ -5638,10 +5583,416 @@ } ] }, + { + "id": "cdn-examples-cache-rust", + "title": "CDN Cache — Rust Example", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cache", + "example", + "overview", + "app", + "demonstrating", + "operations" + ], + "content_sha256": "4c6cf165f6d058b5f6d401c04abf933162f423e82a02a2b81d3a9f1ada41c285", + "sections": [ + { + "id": "cdn-examples-cache-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 11, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cdn", + "cache", + "rust", + "example" + ] + }, + { + "id": "cdn-examples-cache-rust#overview", + "heading": "Overview", + "level": 2, + "anchor": "overview", + "line_start": 12, + "line_end": 17, + "keywords": [ + "overview", + "cdn", + "app", + "demonstrating", + "cache", + "operations", + "via", + "proxy-wasm", + "abi.", + "scoped", + "calling", + "application", + "addressed", + "key", + "alone", + "there", + "named", + "stores", + "handles", + "unlike", + "store", + "dispatched", + "query", + "parameters", + "incoming", + "request.", + "responses", + "always", + "json.", + "errors", + "return", + "http", + "500", + "error", + "message" + ] + }, + { + "id": "cdn-examples-cache-rust#app-type", + "heading": "App Type", + "level": 2, + "anchor": "app-type", + "line_start": 18, + "line_end": 23, + "keywords": [ + "app", + "type", + "interface", + "proxy-wasm", + "fastedge", + "proxywasm", + "cache", + "cdn", + "language", + "rust" + ] + }, + { + "id": "cdn-examples-cache-rust#request-interface", + "heading": "Request Interface", + "level": 2, + "anchor": "request-interface", + "line_start": 24, + "line_end": 38, + "keywords": [ + "request", + "interface", + "operations", + "specified", + "via", + "query", + "parameters.", + "action", + "parameter", + "selects", + "operation", + "get", + "default", + "omitted.", + "string", + "---", + "key", + "read", + "cached", + "value.", + "response", + "null", + "absent.", + "set", + "value", + "ttl", + "store", + "omitting", + "stores", + "expiry.", + "delete", + "key.", + "no-op", + "exists" + ] + }, + { + "id": "cdn-examples-cache-rust#api-reference", + "heading": "API Reference", + "level": 2, + "anchor": "api-reference", + "line_start": 39, + "line_end": 195, + "keywords": [ + "api", + "reference", + "functions", + "fastedge", + "proxywasm", + "cache", + "get", + "rust", + "pub", + "key", + "str", + "result", + "option", + "vec", + "retrieve", + "cached", + "bytes", + "key.", + "parameters", + "string", + "returns", + "exists", + "none", + "absent", + "err", + "failure", + "json", + "response", + "shape", + "action", + "value", + "null", + "---", + "set" + ] + }, + { + "id": "cdn-examples-cache-rust#error-handling", + "heading": "Error Handling", + "level": 2, + "anchor": "error-handling", + "line_start": 196, + "line_end": 202, + "keywords": [ + "error", + "handling", + "errors", + "return", + "http", + "500", + "body", + "message", + "missing", + "required", + "query", + "parameters", + "produce", + "descriptive", + "messages", + "e.g.", + "param", + "key", + "get", + "action", + "invalid", + "parameter", + "values", + "non-integer", + "ttl", + "delta", + "parse", + "messages.", + "unknown", + "listing", + "supported", + "actions." + ] + }, + { + "id": "cdn-examples-cache-rust#dependencies", + "heading": "Dependencies", + "level": 2, + "anchor": "dependencies", + "line_start": 203, + "line_end": 211, + "keywords": [ + "dependencies", + "toml", + "proxy-wasm", + "0.2", + "fastedge", + "path", + "...", + "features", + "proxywasm", + "querystring", + "1.1", + "serde_json" + ] + }, + { + "id": "cdn-examples-cache-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 212, + "line_end": 218, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache.wasm" + ] + }, + { + "id": "cdn-examples-cache-rust#lifecycle-notes", + "heading": "Lifecycle Notes", + "level": 2, + "anchor": "lifecycle-notes", + "line_start": 219, + "line_end": 225, + "keywords": [ + "lifecycle", + "notes", + "app", + "uses", + "proxy-wasm", + "rootcontext", + "httpcontext", + "pattern.", + "logic", + "executes", + "on_http_response_body", + "waits", + "end_of_stream", + "on_http_response_headers", + "strips", + "content-length", + "sets", + "content-type", + "application", + "json", + "transfer-encoding", + "chunked", + "body", + "processing.", + "query", + "string", + "read", + "request.query", + "via", + "get_property" + ] + }, + { + "id": "cdn-examples-cache-rust#key-constraints", + "heading": "Key Constraints", + "level": 2, + "anchor": "key-constraints", + "line_start": 226, + "line_end": 232, + "keywords": [ + "key", + "constraints", + "cache", + "operations", + "scoped", + "calling", + "application", + "keys", + "one", + "app", + "accessible", + "another.", + "there", + "named", + "stores", + "handles", + "sole", + "address.", + "ttl", + "values", + "always", + "milliseconds", + "delta", + "incr", + "signed", + "64-bit", + "integer", + "i64", + "missing", + "treated" + ] + }, + { + "id": "cdn-examples-cache-rust#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 233, + "line_end": 238, + "keywords": [ + "see", + "fastedge-sdk-rust", + "cdn", + "key_value", + "example", + "named", + "store", + "handles", + "contrast", + "cache", + "api", + "examples", + "overview", + "proxy-wasm", + "httpcontext", + "rootcontext", + "trait", + "documentation" + ] + } + ] + }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5663,9 +6014,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "example", "convert", "image", @@ -5675,7 +6026,7 @@ "avif", "format" ], - "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", + "content_sha256": "c9e876d444727919f19871887b51f11a25b8653ddc28e5f19f241a707cadd786", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5692,9 +6043,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "cdn", "example", "convert", @@ -6159,122 +6510,6 @@ "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" - ] } ] }, @@ -7457,7 +7692,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7479,9 +7714,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7491,7 +7726,7 @@ "response-headers", "response-body" ], - "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", + "content_sha256": "db4273297015e84837b705d18e071b4684410000b7bc9fcf363fe82c1c58ea15", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7508,9 +7743,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7940,266 +8175,125 @@ "hook", "sequencing" ] - }, + } + ] + }, + { + "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 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "updated", + "2026-08-17", + "custom", + "error", + "pages", + "overview", + "intercepts", + "4xx", + "5xx", + "http" + ], + "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", + "sections": [ { - "id": "cdn-examples-custom-error-pages-as#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 296, - "line_end": 501, + "id": "cdn-examples-custom-error-pages-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, "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" + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "updated", + "2026-08-17" ] }, { - "id": "cdn-examples-custom-error-pages-as#what-it-does", - "heading": "What it does", + "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", + "heading": "Custom Error Pages — CDN (Rust)", "level": 2, - "anchor": "what-it-does", - "line_start": 502, - "line_end": 514, + "anchor": "custom-error-pages-—-cdn-rust", + "line_start": 10, + "line_end": 297, "keywords": [ - "onresponseheaders", - "app", - "reads", - "response.status", - "property.", - "400-599", - "range", - "sets", - "content-type", - "text", - "html", - "prepares", - "body", - "replacement.", - "onresponsebody", - "buffers", - "full", - "response", + "custom", + "error", + "pages", + "cdn", + "rust", + "overview", + "intercepts", + "4xx", + "5xx", + "http", + "responses", + "edge", "replaces", + "their", + "bodies", + "branded", + "html", + "rendered", + "via", + "handlebars", + "templates.", + "use", + "pattern", + "you", + "consistent", "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", + "served", + "regardless", + "origin", + "returns.", "build", - "customerrorpages.wasm", - "fastedge", - "portal", - "attach", - "your", - "cdn", - "application.", - "environment", - "variables", - "secrets", - "required." + "script", + "build.rs", + "runs", + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] }, { - "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 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "cdn" - ], - "languages": [ - "rust" - ], - "tags": [ - "examples", - "fastedge-docs", - "cdn", - "rust", - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-17", - "custom", - "error", - "pages", - "overview", - "intercepts", - "4xx", - "5xx", - "http" - ], - "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", - "sections": [ - { - "id": "cdn-examples-custom-error-pages-rust#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, - "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-17" - ] - }, - { - "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", - "heading": "Custom Error Pages — CDN (Rust)", - "level": 2, - "anchor": "custom-error-pages-—-cdn-rust", - "line_start": 10, - "line_end": 297, - "keywords": [ - "custom", - "error", - "pages", - "cdn", - "rust", - "overview", - "intercepts", - "4xx", - "5xx", - "http", - "responses", - "edge", - "replaces", - "their", - "bodies", - "branded", - "html", - "rendered", - "via", - "handlebars", - "templates.", - "use", - "pattern", - "you", - "consistent", - "styled", - "served", - "regardless", - "origin", - "returns.", - "build", - "script", - "build.rs", - "runs", - "compile", - "time", - "embed", - "images", - "messages", - "public" - ] - } - ] - }, - { - "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-20 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", + "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-20 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -11806,7 +11900,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11830,9 +11924,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11842,7 +11936,7 @@ "headers", "request-headers" ], - "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", + "content_sha256": "eb268cb0cde0417bb082dbadb140da4f26f837195f2c6d93a907bc1643fc243e", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11859,9 +11953,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11999,9 +12093,11 @@ "method", "signature", "description", - "-------------------", - "-----------------------------------------------", - "--------------------------------------------------------------------------------------------------------" + "--------------------------", + "---------------------------------------", + "-----------------------------------------------------------------------------------------------------", + "get", + "nam" ] }, { @@ -12837,8 +12933,8 @@ }, { "id": "cdn-examples-http-call-as", - "title": "HTTP Call", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", + "title": "cdn-examples-http-call-as", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12862,9 +12958,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "overview", "httpcall", "dispatches", @@ -12875,7 +12971,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", + "content_sha256": "5fd3877e09d4896bd6c69ed4c0cb9cc56dc6cd0d8fadb01c02391285f4b8bce6", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12892,9 +12988,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -13166,209 +13262,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13390,9 +13290,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "makes", @@ -13402,7 +13302,7 @@ "services", "timeout" ], - "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", + "content_sha256": "5e665b5b4838fd593db0ee7e992ade82f31afd9c62cf39aa13075f9ffdf2b6ee", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13419,9 +13319,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "cdn", @@ -13823,7 +13723,7 @@ "level": 2, "anchor": "complete-example", "line_start": 247, - "line_end": 359, + "line_end": 360, "keywords": [ "complete", "example", @@ -13863,8 +13763,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 360, - "line_end": 371, + "line_start": 361, + "line_end": 372, "keywords": [ "gotchas", "action", @@ -13913,8 +13813,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 372, - "line_end": 378, + "line_start": 373, + "line_end": 379, "keywords": [ "see", "proxy-wasm", @@ -14615,7 +14515,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14637,9 +14537,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14650,7 +14550,7 @@ "example", "app_type" ], - "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", + "content_sha256": "23c7f8c100db8a05f960e1382c3a9933d52661da3f5ad5a253cfd75e1d720c90", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14667,9 +14567,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14826,7 +14726,7 @@ "level": 2, "anchor": "gotchas", "line_start": 223, - "line_end": 237, + "line_end": 239, "keywords": [ "gotchas", "get", @@ -14875,8 +14775,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 238, - "line_end": 243, + "line_start": 240, + "line_end": 245, "keywords": [ "related", "host", @@ -14911,13 +14811,250 @@ "vs.", "differences" ] + }, + { + "id": "cdn-examples-kv-store-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 246, + "line_end": 518, + "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-kv-store-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 519, + "line_end": 535, + "keywords": [ + "app", + "supports", + "following", + "actions", + "get", + "fetch", + "one", + "key", + "scan", + "list", + "keys", + "matching", + "pattern", + "zrange", + "read", + "sorted-set", + "entries", + "score", + "range", + "zscan", + "bfexists", + "check", + "whether", + "bloom", + "filter", + "contains", + "item", + "request", + "include", + "least", + "store", + "name", + "action", + "operation", + "optional", + "defaults", + "validates", + "required", + "parameters", + "returns" + ] + }, + { + "id": "cdn-examples-kv-store-rust#supported-query-examples", + "heading": "Supported query examples", + "level": 2, + "anchor": "supported-query-examples", + "line_start": 536, + "line_end": 567, + "keywords": [ + "supported", + "query", + "examples", + "get", + "key", + "text", + "store", + "my_store", + "action", + "user", + "list", + "keys", + "pattern", + "scan", + "match", + "read", + "sorted", + "set", + "score", + "range", + "zrange", + "leaderboard", + "min", + "max", + "100", + "search", + "sorted-set", + "members", + "zscan", + "check", + "bloom", + "filter", + "item", + "bfexists", + "visitors", + "alice", + "examp" + ] + }, + { + "id": "cdn-examples-kv-store-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 568, + "line_end": 576, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "key_value.wasm", + "example", + "cdn", + "app", + "targets" + ] + }, + { + "id": "cdn-examples-kv-store-rust#response-format", + "heading": "Response format", + "level": 2, + "anchor": "response-format", + "line_start": 577, + "line_end": 598, + "keywords": [ + "response", + "format", + "app", + "replaces", + "body", + "json", + "sets", + "content-type", + "application", + "successful", + "looks", + "like", + "store", + "my_store", + "action", + "get", + "key", + "user", + "alice", + "required", + "parameter", + "missing", + "operation", + "fails", + "responds", + "error", + "payload", + "param" + ] + }, + { + "id": "cdn-examples-kv-store-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 599, + "line_end": 605, + "keywords": [ + "notes", + "app", + "requires", + "query", + "parameters", + "run.", + "action", + "omitted", + "defaults", + "get", + "code", + "uses", + "fastedge", + "proxywasm", + "key_value", + "store", + "proxy_wasm", + "lifecycle", + "hooks", + "operate", + "store." + ] } ] }, { "id": "cdn-examples-large-dictionary-as", "title": "Large Dictionary — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14939,9 +15076,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-05-20", + "2026-09-22", "large", "dictionary", "overview", @@ -14950,7 +15087,7 @@ "environment", "variables" ], - "content_sha256": "6d6681e4b50286f8686c5abb48674711db1f35f9090ae69be2f77d3fbce085f7", + "content_sha256": "06c3e8963a2738ac48fbbfbf6217cded6950009199039727a0ed53963ab8474d", "sections": [ { "id": "cdn-examples-large-dictionary-as#introduction", @@ -14967,9 +15104,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-05-20", + "2026-09-22", "large", "dictionary", "assemblyscript", @@ -15273,6 +15410,205 @@ "patterns", "fastedge-docs" ] + }, + { + "id": "cdn-examples-large-dictionary-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 140, + "line_end": 223, + "keywords": [ + "source", + "material", + "file", + "examples", + "largedictionary", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getdictionary", + "setloglevel", + "fastedge", + "class", + "largedictionaryroot", + "extends", + "createcontext", + "context_id", + "u32", + "con" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#when-to-use-getdictionary-vs-getenv", + "heading": "When to use `getDictionary` vs `getEnv`", + "level": 2, + "anchor": "when-to-use-getdictionary-vs-getenv", + "line_start": 224, + "line_end": 234, + "keywords": [ + "use", + "getdictionary", + "getenv", + "function", + "----------", + "name", + "variable", + "value", + "under", + "cases", + "exceed", + "wasi", + "env", + "var", + "size", + "limit", + "environment", + "interface", + "per", + "variable.", + "your", + "app", + "needs", + "read", + "larger", + "values", + "e.g.", + "large", + "json", + "configs", + "pem", + "certificates", + "policy", + "documents", + "calls", + "proxy_dictionary_get", + "bypasses", + "thi" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 235, + "line_end": 238, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "large_config", + "environment", + "variable", + "using", + "getdictionary", + "logs", + "size", + "adds", + "x-config-size", + "request", + "header", + "upstream", + "see." + ] + }, + { + "id": "cdn-examples-large-dictionary-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 239, + "line_end": 246, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "large_config", + "environment", + "variable", + "large", + "payload", + "e.g.", + "json", + "pem", + "certificate" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 247, + "line_end": 260, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "largedictionary.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "largedictionary-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 261, + "line_end": 265, + "keywords": [ + "deploy", + "upload", + "build", + "largedictionary.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "large_config", + "environment", + "variable", + "application", + "settings." + ] } ] }, @@ -18244,7 +18580,7 @@ { "id": "host-services-rust", "title": "Rust Host Services Reference", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/host-services-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -18263,9 +18599,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "host", "services", "reference", @@ -18276,7 +18612,7 @@ "storage", "secret" ], - "content_sha256": "702508848c5aadcd4ee98b9fa6c0ead4ed2085d84e0c321143f5bc626f376ece", + "content_sha256": "3c1895152a7a969acdb7c1f5e670bd6441f6b3d44fadf5a8640adab15cc3185e", "sections": [ { "id": "host-services-rust#introduction", @@ -18293,9 +18629,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "rust", "host", "services", @@ -18443,6 +18779,13 @@ "change", "lifetime", "deployment.", + "config", + "key_value", + "persistent", + "data", + "secret", + "encrypted", + "credentials.", "api", "rust", "pub", @@ -18462,15 +18805,7 @@ "decoded", "utf-8.", "environment", - "variables", - "set", - "deployment", - "time", - "via", - "platform", - "same", - "management", - "mechanism" + "vari" ] }, { @@ -20588,7 +20923,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20610,9 +20945,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -20622,7 +20957,7 @@ "outbound-http", "query-params" ], - "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", + "content_sha256": "653bed0caf43133e7cf62e0930264bec8ced4e99d9db75ad00188b1f618c266d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20639,9 +20974,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21063,207 +21398,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21285,9 +21426,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21297,7 +21438,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", + "content_sha256": "a447a165a864934a9567ab24a9258a36cc441050690b08e0afe96a7a9523a505", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21314,9 +21455,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21769,13 +21910,195 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 304, + "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": 305, + "line_end": 309, + "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": 310, + "line_end": 314, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "bloom-filter-denylist" + ] } ] }, { "id": "http-examples-cache-basic-rust", "title": "Cache Basic (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21797,9 +22120,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -21809,7 +22132,7 @@ "cache", "basic" ], - "content_sha256": "fcc0c42a053e0104b03a420b039b53f6e58b97cb14c7f0f77c1dfb5fad31d840", + "content_sha256": "58aa39343f1d956e9ab8ab5ea82eba707a1033fac18ca3f195c40b96b7b3016b", "sections": [ { "id": "http-examples-cache-basic-rust#introduction", @@ -21826,9 +22149,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -22221,13 +22544,199 @@ "selection", "guidance" ] + }, + { + "id": "http-examples-cache-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 244, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "cache-aside", + "pattern", + "via", + "interface.", + "request", + "builds", + "key", + "path.", + "returns", + "cached", + "body", + "immediately", + "hit", + "x-cache", + "miss", + "generates", + "response", + "stores", + "environment", + "variables", + "cache_ttl_ms", + "long" + ] + }, + { + "id": "http-examples-cache-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 245, + "line_end": 250, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "cache_ttl_ms", + "long", + "cache", + "response", + "milliseconds.", + "default", + "30000" + ] + }, + { + "id": "http-examples-cache-basic-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 251, + "line_end": 259, + "keywords": [ + "works", + "get", + "api", + "data", + "cache", + "miss", + "generate", + "body", + "store", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "page", + "request-path", + "unique", + "path", + "gets", + "own", + "entry.", + "response", + "simple", + "html", + "includes", + "request", + "stand", + "expensive", + "computation", + "template", + "render." + ] + }, + { + "id": "http-examples-cache-basic-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 260, + "line_end": 269, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "html", + "x-cache", + "hit", + "miss", + "doctype", + "..." + ] + }, + { + "id": "http-examples-cache-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 270, + "line_end": 276, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache_basic.wasm" + ] + }, + { + "id": "http-examples-cache-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 277, + "line_end": 282, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry" + ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22251,9 +22760,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "module", @@ -22263,7 +22772,7 @@ "2.3.0", "app" ], - "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", + "content_sha256": "c53a54daf9307b32c0aa69a72792c852be660acff331a69288bf9425b6a194a6", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22280,9 +22789,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "javascript", @@ -22611,63 +23120,13 @@ "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: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22689,9 +23148,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "type", "example", @@ -22701,7 +23160,7 @@ "cache", "outbound-http" ], - "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", + "content_sha256": "7c58be673c4adce9e4ac6ad2928c95a42a645cf231c1f09d901ed10534e327cc", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -22718,9 +23177,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "type", "example", @@ -23069,201 +23528,13 @@ "workflow", "skill" ] - }, - { - "id": "http-examples-cache-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 192, - "line_end": 359, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "cache", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "response", - "caching", - "purging", - "via", - "interface.", - "reads", - "origin_host", - "environment", - "forwards", - "incoming", - "request", - "origin", - "caches", - "body", - "keyed", - "path.", - "subsequent", - "requests", - "same", - "path", - "cached", - "returned" - ] - }, - { - "id": "http-examples-cache-wasi-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 360, - "line_end": 366, - "keywords": [ - "configuration", - "env", - "var", - "required", - "description", - "---", - "origin_host", - "yes", - "base", - "url", - "upstream", - "origin", - "e.g.", - "https", - "api.example.com", - "returns", - "500", - "unset.", - "cache_ttl_ms", - "long", - "cache", - "responses", - "milliseconds.", - "default", - "60000" - ] - }, - { - "id": "http-examples-cache-wasi-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 367, - "line_end": 373, - "keywords": [ - "works", - "get", - "data", - "cache", - "miss", - "forward", - "origin_host", - "2xx", - "body", - "200", - "x-cache", - "hit", - "return", - "cached", - "key", - "path", - "query", - "only", - "responses", - "origin", - "error", - "pass", - "without", - "stored.", - "response", - "headers", - "replayed", - "cache-hit", - "use", - "content-type", - "application", - "octet-stream", - "sinc" - ] - }, - { - "id": "http-examples-cache-wasi-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 374, - "line_end": 378, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip2", - "release", - "cache_wasi.wasm" - ] - }, - { - "id": "http-examples-cache-wasi-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 379, - "line_end": 385, - "keywords": [ - "apis", - "used", - "fastedge", - "cache", - "get", - "key", - "retrieve", - "cached", - "bytes", - "returns", - "option", - "vec", - "set", - "ttl_ms", - "store", - "optional", - "ttl", - "milliseconds", - "none", - "means", - "expiry", - "wstd", - "http", - "client", - "new", - ".send", - "req", - ".await", - "async", - "outbound", - "request", - "origin" - ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -23285,9 +23556,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23297,7 +23568,7 @@ "languages", "diagnostic" ], - "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", + "content_sha256": "e0b1cfb99fd9375902a7ab410213218de42cf836f17460e19e3cef595ebc872b", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -23314,9 +23585,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23817,342 +24088,125 @@ "store", "usage" ] - }, + } + ] + }, + { + "id": "http-examples-fetch-js", + "title": "http-examples-fetch-js", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-fetch-js.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "http" + ], + "languages": [ + "javascript" + ], + "tags": [ + "examples", + "fastedge-docs", + "http", + "javascript", + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23", + "fetch", + "outbound", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url" + ], + "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", + "sections": [ { - "id": "http-examples-diagnostic-logging-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 194, - "line_end": 307, + "id": "http-examples-fetch-js#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "diagnostic_logging", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "diagnostic", - "logging", - "example.", - "tiny", - "pass-through", - "proxy", - "writes", - "single", - "set_user_diag", - "tag", - "per", - "request", - "summarising", - "outcome", - "config_error", - "origin_unreachable", - "proxied", - "appears", - "fastedge", - "platform", - "per-request", - "log", - "viewer", - "distinct", - "stdout" + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23" ] }, { - "id": "http-examples-diagnostic-logging-wasi-rust#configuration", - "heading": "Configuration", + "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", + "heading": "fetch — Outbound HTTP Requests", "level": 2, - "anchor": "configuration", - "line_start": 308, - "line_end": 311, + "anchor": "fetch-—-outbound-http-requests", + "line_start": 10, + "line_end": 93, "keywords": [ - "configuration", - "origin_url", - "environment", - "variable", - "origin", + "fetch", + "outbound", + "http", "requests", - "proxied", - "to." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", - "heading": "Outcomes", - "level": 2, - "anchor": "outcomes", - "line_start": 312, - "line_end": 321, - "keywords": [ - "outcomes", - "request", - "writes", - "exactly", - "one", - "condition", - "tag", - "---", - "origin_url", - "missing", - "outcome", - "config_error", - "reason", - "origin_missing", - "origin", - "unreachable", - "origin_unreachable", - "method", - "path", - "err", - "proxied", - "status" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", - "heading": "`set_user_diag` vs `println!`", - "level": 2, - "anchor": "set_user_diag-vs-println", - "line_start": 322, - "line_end": 330, - "keywords": [ - "set_user_diag", - "println", - "---", - "channel", - "stdout", - "general", - "application", - "logs", - "per-request", - "structured", - "tag", - "platform", - "log", - "viewer", - "cardinality", - "many", - "per", - "request", - "one", - "multiple", - "calls", - "leave", - "only", - "last", - "concatenated", - "undefined", - "best", - "verbose", - "traces", - "debug", - "details", - "single", - "filterable", - "outcome", - "label", - "forbidden", - "secrets", - "pii", - "tags", - "appear" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#convention", - "heading": "Convention", - "level": 2, - "anchor": "convention", - "line_start": 331, - "line_end": 336, - "keywords": [ - "convention", - "call", - "set_user_diag", - "once", - "branch", - "late", - "enough", - "handler", - "know", - "outcome.", - "logfmt", - "-ish", - "format", - "outcome", - "verb", - "key", - "value", - "easy", - "slice", - "log", - "search", - "tooling", - "keep", - "keys", - "short", - "stable", - "they", - "make", - "good", - "filter", - "terms." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#related", - "heading": "Related", - "level": 2, - "anchor": "related", - "line_start": 337, - "line_end": 342, - "keywords": [ - "related", - "cdn", - "proxy-wasm", - "variant", + "overview", "fastedge", - "proxywasm", - "utils", - "set_user_diag", - "same", - "semantics", - "different", - "module", - "path.", - "see", - "docs", - "cdn_apps.md" + "supports", + "via", + "standard", + "url", + "options", + "api.", + "runtime", + "exposes", + "subset", + "browser", + "api", + "sufficient", + "downstream", + "service", + "calls.", + "---", + "signature", + "string", + "requestinit", + "promise", + "response", + "parameters", + "parameter", + "type", + "required", + "description", + "-----------", + "------", + "----------", + "-------------", + "yes", + "absolute", + "resou" ] } ] }, { - "id": "http-examples-fetch-js", - "title": "http-examples-fetch-js", + "id": "http-examples-geo-redirect-js", + "title": "http-examples-geo-redirect-js", "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-fetch-js.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "http" - ], - "languages": [ - "javascript" - ], - "tags": [ - "examples", - "fastedge-docs", - "http", - "javascript", - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23", - "fetch", - "outbound", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url" - ], - "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", - "sections": [ - { - "id": "http-examples-fetch-js#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, - "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23" - ] - }, - { - "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", - "heading": "fetch — Outbound HTTP Requests", - "level": 2, - "anchor": "fetch-—-outbound-http-requests", - "line_start": 10, - "line_end": 93, - "keywords": [ - "fetch", - "outbound", - "http", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url", - "options", - "api.", - "runtime", - "exposes", - "subset", - "browser", - "api", - "sufficient", - "downstream", - "service", - "calls.", - "---", - "signature", - "string", - "requestinit", - "promise", - "response", - "parameters", - "parameter", - "type", - "required", - "description", - "-----------", - "------", - "----------", - "-------------", - "yes", - "absolute", - "resou" - ] - } - ] - }, - { - "id": "http-examples-geo-redirect-js", - "title": "http-examples-geo-redirect-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -29647,7 +29701,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-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29669,9 +29723,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -29681,7 +29735,7 @@ "request-inspection", "header-iteration" ], - "content_sha256": "2301b014adb12b2ba4442aef724edf0a892cda4df9240c2959f515a28095883b", + "content_sha256": "b33f1a09f2dca08a64f23ac6419b228e8de755a66bc4de6a66fffadee231998c", "sections": [ { "id": "http-examples-print-basic-rust#introduction", @@ -29698,9 +29752,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -30097,209 +30151,13 @@ "building", "patterns" ] - }, - { - "id": "http-examples-print-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 151, - "line_end": 214, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "print", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "result", - "fastedge", - "body", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "mut", - "string", - "method", - ".to_string", - "body.push_str", - "req.method", - ".as_str", - "nurl", - "req.uri", - "nheaders" - ] - }, - { - "id": "http-examples-print-basic-rust#what-it-demonstrates", - "heading": "What it demonstrates", - "level": 2, - "anchor": "what-it-demonstrates", - "line_start": 215, - "line_end": 222, - "keywords": [ - "demonstrates", - "reading", - "request", - "method", - "via", - "req.method", - ".as_str", - "uri", - "req.uri", - ".to_string", - "iterating", - "headers", - "req.headers", - "handling", - "non-utf-8", - "header", - "values", - "gracefully", - "match", - "v.to_str", - "building", - "plain-text", - "response", - "builder", - "body", - "..." - ] - }, - { - "id": "http-examples-print-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 223, - "line_end": 233, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "-----", - "---------", - "req.method", - ".as_str", - "http", - "method", - "string", - "slice", - "req.uri", - ".to_string", - "full", - "request", - "uri", - "req.headers", - "iterator", - "over", - "headername", - "headervalue", - "pairs", - "v.to_str", - "decode", - "header", - "value", - "str", - "returns", - "err", - "non-utf-8", - "response", - "builder", - ".status", - "...", - ".body", - "build", - "body", - "create" - ] - }, - { - "id": "http-examples-print-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 234, - "line_end": 240, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "print.wasm" - ] - }, - { - "id": "http-examples-print-basic-rust#expected-behaviour", - "heading": "Expected behaviour", - "level": 2, - "anchor": "expected-behaviour", - "line_start": 241, - "line_end": 259, - "keywords": [ - "expected", - "behaviour", - "request", - "response", - "body", - "plain-text", - "dump", - "details", - "method", - "get", - "url", - "path", - "query", - "value", - "headers", - "host", - "example.com", - "accept", - "...", - "status", - "200", - "content", - "plain", - "text", - "content-type", - "header", - "set", - "explicitly", - "platform", - "add", - "one", - "appears", - "own", - "line", - "indented", - "four", - "spaces", - "non-utf-8", - "values", - "replaced" - ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30323,9 +30181,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30334,7 +30192,7 @@ "fastedge", "apps" ], - "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", + "content_sha256": "08a57d5d0098acb645f51c29d3f5850d1dad67ec7d808d89c78453d0323ec904", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30351,9 +30209,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30787,356 +30645,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31158,9 +30673,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31170,7 +30685,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", + "content_sha256": "72cff95ae062275b261acaf37d03d72affe87facf10960cd16531237ff6bd4cf", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31187,9 +30702,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31628,247 +31143,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31890,9 +31171,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "example", @@ -31902,7 +31183,7 @@ "fastedge", "platform" ], - "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", + "content_sha256": "1d1ae1d9f636c28df70129574cad04baf9b39ec87e640e902ed814f97a07d13b", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31919,9 +31200,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "rust", @@ -32311,13 +31592,284 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 368, + "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": 369, + "line_end": 378, + "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": 379, + "line_end": 389, + "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": 390, + "line_end": 399, + "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": 400, + "line_end": 406, + "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": 407, + "line_end": 416, + "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": 417, + "line_end": 434, + "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32339,9 +31891,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32352,7 +31904,7 @@ "rotation", "using" ], - "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", + "content_sha256": "2cd00d58e3f6047c79cb57ea352f8e2684f86dec50a7357dadfe62d4bf0d8151", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32369,9 +31921,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32627,7 +32179,7 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 178, + "line_end": 177, "keywords": [ "implementation", "pattern", @@ -32653,20 +32205,19 @@ "result", "read", "slot", + "x-slot", "header", - "default", + "defaulting", "current", - "unix", "timestamp", "let", "u32", ".headers", ".get", - "x-slot", ".and_then", "v.to_str", ".ok", - "v.p" + ".and" ] }, { @@ -32674,8 +32225,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 179, - "line_end": 186, + "line_start": 178, + "line_end": 185, "keywords": [ "constraints", "gotchas", @@ -32724,8 +32275,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 187, - "line_end": 193, + "line_start": 186, + "line_end": 192, "keywords": [ "build", "cargo", @@ -32742,8 +32293,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 194, - "line_end": 200, + "line_start": 193, + "line_end": 199, "keywords": [ "see", "fastedge", @@ -32774,7 +32325,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32796,9 +32347,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32808,7 +32359,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", + "content_sha256": "ff728af48b322c2e67a2a06dd8e555818382d4535f559f6cf83b2f14e6732db5", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -32825,9 +32376,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32901,7 +32452,7 @@ "level": 2, "anchor": "request-headers", "line_start": 30, - "line_end": 35, + "line_end": 36, "keywords": [ "request", "headers", @@ -32923,7 +32474,14 @@ "httpbin.org", "get", "fetch", - "outbound" + "outbound", + "x-fetch-header", + "key", + "value", + "additional", + "forward", + "parsed", + "separator" ] }, { @@ -32931,8 +32489,8 @@ "heading": "Behavior", "level": 2, "anchor": "behavior", - "line_start": 36, - "line_end": 42, + "line_start": 37, + "line_end": 45, "keywords": [ "behavior", "read", @@ -32947,27 +32505,33 @@ "get", "absent", "unparseable.", - "build", + "begin", + "building", "outbound", "url", "accept", "application", "json", "header.", + "x-fetch-header", + "present", + "parseable", + "key", + "value", + "split", + "first", + "add", + "builder.", + "finalize", + "builder", + ".body", + "body", + "empty", "send", "via", "client", "new", - ".send", - "req", - ".await", - "return", - "upstream", - "response", - "body", - "directly", - "caller", - "decomposition." + ".send" ] }, { @@ -32975,8 +32539,8 @@ "heading": "Key API Patterns", "level": 2, "anchor": "key-api-patterns", - "line_start": 43, - "line_end": 99, + "line_start": 46, + "line_end": 122, "keywords": [ "key", "api", @@ -33025,8 +32589,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 100, - "line_end": 109, + "line_start": 123, + "line_end": 132, "keywords": [ "logging", "rust", @@ -33049,8 +32613,8 @@ "heading": "Cargo.toml Requirements", "level": 2, "anchor": "cargotoml-requirements", - "line_start": 110, - "line_end": 126, + "line_start": 133, + "line_end": 149, "keywords": [ "cargo.toml", "requirements", @@ -33083,8 +32647,8 @@ "heading": "WASI vs Basic HTTP Comparison", "level": 2, "anchor": "wasi-vs-basic-http-comparison", - "line_start": 127, - "line_end": 135, + "line_start": 150, + "line_end": 158, "keywords": [ "wasi", "basic", @@ -33121,8 +32685,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 136, - "line_end": 143, + "line_start": 159, + "line_end": 168, "keywords": [ "gotchas", "url", @@ -33151,19 +32715,17 @@ "values", "silently", "discarded", - "anyhow", - "declared", - "dependency", - "use", - "macro.", - "examples", - "http", - "wasi", - "same", - "async", - "main", - "wstd", - "http_server" + "x-fetch-header", + "parsed", + ".split_once", + "only", + "first", + "used", + "separator", + "containing", + "preserved", + "correctly.", + "build" ] }, { @@ -33171,8 +32733,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 144, - "line_end": 150, + "line_start": 169, + "line_end": 175, "keywords": [ "see", "fastedge-docs", @@ -33192,6 +32754,186 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 176, + "line_end": 271, + "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": 272, + "line_end": 277, + "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": 278, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 306, + "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" + ] } ] }, @@ -34267,7 +34009,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34289,9 +34031,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34301,7 +34043,7 @@ "streaming", "async" ], - "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", + "content_sha256": "fcd87ce4100ea3343d0f2b4543cce45409c480b856f3ded1838b4d752a43e3ea", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34318,9 +34060,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34649,6 +34391,144 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, @@ -37079,6 +36959,348 @@ } ] }, + { + "id": "platform-cdn-filter-runtime", + "title": "CDN Filter Runtime — Capabilities and Traps", + "description": "What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a dep", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items" + ], + "content_sha256": "6279754b693e44807306ef152f9b733bb0a575ece2390a0bccb5755b2be9799e", + "sections": [ + { + "id": "platform-cdn-filter-runtime#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items", + "here", + "invisible", + "local", + "tests", + "only", + "surface", + "deployed", + "app.", + "claim", + "carries", + "provenance", + "tag", + "meaning", + "---", + "live", + "measured", + "against", + "real", + "resource", + "source" + ] + }, + { + "id": "platform-cdn-filter-runtime#capability-boundary-—-filter-vs-http-app", + "heading": "Capability boundary — filter vs HTTP app", + "level": 2, + "anchor": "capability-boundary-—-filter-vs-http-app", + "line_start": 16, + "line_end": 41, + "keywords": [ + "capability", + "boundary", + "filter", + "http", + "app", + "core", + "wasm", + "module", + "linked", + "against", + "proxy-wasm", + "env", + "abi.", + "different", + "execution", + "world", + "subset", + "it.", + "designs", + "assume", + "symmetry", + "http-app", + "sdk", + "fail", + "deploy.", + "cdn", + "---", + "read", + "same", + "six", + "operations", + "open", + "get", + "scan", + "zrange_by_score", + "zscan", + "bf_exists", + "source", + "write", + "api" + ] + }, + { + "id": "platform-cdn-filter-runtime#reading-the-request-—-use-properties-not-headers", + "heading": "Reading the request — use properties, not headers", + "level": 2, + "anchor": "reading-the-request-—-use-properties-not-headers", + "line_start": 42, + "line_end": 69, + "keywords": [ + "reading", + "request", + "use", + "properties", + "headers", + "several", + "proxy-wasm", + "developer", + "reach", + "mangled", + "absent.", + "failures", + "silent", + "live", + "read", + "via", + "result", + "---", + "property", + "request.host", + "clean", + "client-requested", + "host", + "origin", + "rewrite", + "edge", + "tier", + "shield", + "nodes", + "carry", + "shield_", + "prefix", + "request.x_real_ip", + "true", + "client", + "ipv4", + "ipv6", + "request.path", + "includes", + "query" + ] + }, + { + "id": "platform-cdn-filter-runtime#security-—-gating-filters", + "heading": "Security — gating filters", + "level": 2, + "anchor": "security-—-gating-filters", + "line_start": 70, + "line_end": 105, + "keywords": [ + "security", + "gating", + "filters", + "cdn", + "normalise", + "path", + "filter", + "runs", + "live", + "traversal", + "separator", + "sequences", + "arrive", + "verbatim", + "bash", + "curl", + "--path-as-is", + "...", + "app", + "test", + "request.path", + "bypasses", + "auth", + "prefix", + "public", + "wave", + "admin", + "origin", + "back", + "authentication", + "bypass" + ] + }, + { + "id": "platform-cdn-filter-runtime#synthetic-responses", + "heading": "Synthetic responses", + "level": 2, + "anchor": "synthetic-responses", + "line_start": 106, + "line_end": 111, + "keywords": [ + "synthetic", + "responses", + "live", + "filter", + "serve", + "complete", + "response", + "via", + "send_http_response", + "status", + "arbitrary", + "headers", + "multiple", + "set-cookie", + "one", + "bodies", + "least", + "without", + "truncation.", + "single-app", + "designs", + "challenge", + "pages", + "interstitials", + "error", + "second", + "http", + "app.", + "---" + ] + }, + { + "id": "platform-cdn-filter-runtime#what-local-testing-cannot-prove", + "heading": "What local testing cannot prove", + "level": 2, + "anchor": "what-local-testing-cannot-prove", + "line_start": 112, + "line_end": 117, + "keywords": [ + "local", + "testing", + "cannot", + "prove", + "gcoredev", + "fastedge-test", + "runs", + "proxy-wasm", + "filters", + "locally", + "proves", + "flow", + "logic", + "routing", + "header", + "manipulation", + "gate", + "decisions.", + "host", + "behaviour", + "property", + "semantics", + "mangling", + "path", + "non-normalisation", + "instantiation", + "failures", + "cache", + "interaction.", + "trap", + "page", + "passes", + "suite", + "green.", + "confirm", + "host-dependent", + "deployed", + "app", + "attached", + "real" + ] + }, + { + "id": "platform-cdn-filter-runtime#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 118, + "line_end": 123, + "keywords": [ + "see", + "storage.md", + "cache", + "semantics", + "limits", + "measured", + "performance", + "cdn-integration.md", + "attaching", + "filters", + "cdn", + "resources", + "error-codes.md", + "530", + "diagnosis" + ] + } + ] + }, { "id": "platform-cdn-integration", "title": "CDN Integration — Attaching FastEdge Apps to CDN Resources", @@ -37117,7 +37339,7 @@ "effect", "app" ], - "content_sha256": "6cb942fb6413ec2d1faf71699c0e79ea10e04ccc089ed9e49aee100e7a6623fe", + "content_sha256": "e36fbf654e7eb8d595bd084a7fa6577ef61114287351099e7ccaa99cdf0f9eb6", "sections": [ { "id": "platform-cdn-integration#introduction", @@ -37272,7 +37494,7 @@ "level": 2, "anchor": "reading-and-updating", "line_start": 157, - "line_end": 185, + "line_end": 202, "keywords": [ "reading", "updating", @@ -37310,8 +37532,8 @@ "heading": "Operational Notes", "level": 2, "anchor": "operational-notes", - "line_start": 186, - "line_end": 195, + "line_start": 203, + "line_end": 212, "keywords": [ "operational", "notes", @@ -37358,8 +37580,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 196, - "line_end": 203, + "line_start": 213, + "line_end": 220, "keywords": [ "see", "rust", @@ -37436,7 +37658,7 @@ "distinguish", "edge" ], - "content_sha256": "d47de15e41e07d9b842b6690558f41b840a4121f7ec91a85c4d86d1dc966a436", + "content_sha256": "e0561d0b112db5f5f509a1ce16da82dedfa2cea834283fcf1b6102a118136876", "sections": [ { "id": "platform-error-codes#introduction", @@ -37489,7 +37711,7 @@ "level": 2, "anchor": "530-—-app-initialization-failed", "line_start": 9, - "line_end": 29, + "line_end": 38, "keywords": [ "530", "app", @@ -37501,35 +37723,36 @@ "start", "processing", "request.", + "signal", + "cdn", + "fails", + "instantiate", + "returns", + "x-cdn-internal-status", + "3100", + "header", + "distinguishes", + "ran", + "errored", + "531", "common", "causes", - "missing", - "required", - "environment", - "variables", - "reads", - "corrupted", - "invalid", + "unresolved", + "imports", + "apps", "binary", - "compiled", - "wrong", - "target", - "wasm32-wasip1", - "large", - "platform", - "rejects", - "uploads", - "beyond", - "50mb", - "investigate", - "bundled", - "debugging", - "steps", - "verify", - "built", - "correctly", - "fastedge-build", - "src" + "host", + "function", + "proxy-wasm", + "provide", + "commonly", + "http-app", + "component-model", + "api", + "used", + "filter", + "e.g.", + "top-leve" ] }, { @@ -37537,8 +37760,8 @@ "heading": "531 — Runtime Error", "level": 2, "anchor": "531-—-runtime-error", - "line_start": 30, - "line_end": 61, + "line_start": 39, + "line_end": 71, "keywords": [ "531", "runtime", @@ -37554,6 +37777,19 @@ "processing.", "common", "causes", + "calling", + "getenv", + "getsecret", + "module", + "top", + "level", + "they", + "request-time", + "only", + "call", + "them", + "inside", + "handler", "uncaught", "javascript", "typeerror", @@ -37566,20 +37802,7 @@ "err", "failed", "fetch", - "call", - "without", - "handling", - "json", - "parse", - "malformed", - "body", - "accessing", - "undefined", - "properties", - "debugging", - "steps", - "test", - "locally" + "without" ] }, { @@ -37587,8 +37810,8 @@ "heading": "532 — Timeout Exceeded", "level": 2, "anchor": "532-—-timeout-exceeded", - "line_start": 62, - "line_end": 93, + "line_start": 72, + "line_end": 103, "keywords": [ "532", "timeout", @@ -37637,8 +37860,8 @@ "heading": "533 — Memory Limit Exceeded", "level": 2, "anchor": "533-—-memory-limit-exceeded", - "line_start": 94, - "line_end": 122, + "line_start": 104, + "line_end": 132, "keywords": [ "533", "memory", @@ -37687,8 +37910,8 @@ "heading": "General Debugging Strategy", "level": 2, "anchor": "general-debugging-strategy", - "line_start": 123, - "line_end": 131, + "line_start": 133, + "line_end": 141, "keywords": [ "general", "debugging", @@ -38283,7 +38506,7 @@ "instantiates", "app" ], - "content_sha256": "98c4a8456ad2cedcb63fe13b4f4556e3b0a8b09ab2a621f0111a59b942d65cb8", + "content_sha256": "350d18b69668668c510056232b304d8bb3d9d3354bfc1493438b059d93d586b6", "sections": [ { "id": "platform-overview#introduction", @@ -38485,7 +38708,7 @@ "level": 2, "anchor": "request-lifecycle", "line_start": 93, - "line_end": 101, + "line_end": 109, "keywords": [ "request", "lifecycle", @@ -38518,7 +38741,15 @@ "instance", "destroyed", "memory", - "freed" + "freed", + "scheduled", + "deferred", + "work", + "there", + "scheduler", + "durable", + "background", + "execution." ] }, { @@ -38526,8 +38757,8 @@ "heading": "Resource Limits", "level": 2, "anchor": "resource-limits", - "line_start": 102, - "line_end": 115, + "line_start": 110, + "line_end": 123, "keywords": [ "resource", "limits", @@ -38576,8 +38807,8 @@ "heading": "Plans", "level": 2, "anchor": "plans", - "line_start": 116, - "line_end": 120, + "line_start": 124, + "line_end": 128, "keywords": [ "plans", "basic", @@ -38599,8 +38830,8 @@ "heading": "App Statuses", "level": 2, "anchor": "app-statuses", - "line_start": 121, - "line_end": 128, + "line_start": 129, + "line_end": 136, "keywords": [ "app", "statuses", @@ -38620,8 +38851,8 @@ "heading": "Networking", "level": 2, "anchor": "networking", - "line_start": 129, - "line_end": 135, + "line_start": 137, + "line_end": 143, "keywords": [ "networking", "apps", @@ -38652,8 +38883,8 @@ "heading": "Logging & Monitoring", "level": 2, "anchor": "logging-&-monitoring", - "line_start": 136, - "line_end": 141, + "line_start": 144, + "line_end": 149, "keywords": [ "logging", "monitoring", @@ -38679,6 +38910,261 @@ } ] }, + { + "id": "platform-storage", + "title": "Storage Semantics — KV Store and Cache", + "description": "Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your lan", + "path": "plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/storage.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance." + ], + "content_sha256": "65116f9b1f97e5e14b18ff2a6af86b58e926d192ad3101290e37fd23e8db5d3e", + "sections": [ + { + "id": "platform-storage#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance.", + "applies", + "http", + "apps", + "cdn", + "filters", + "unless", + "noted.", + "per-language", + "api", + "see", + "reference", + "your", + "language.", + "---", + "scope", + "global", + "account-level", + "assigned", + "per-pop", + "shared" + ] + }, + { + "id": "platform-storage#kv-store", + "heading": "KV Store", + "level": 2, + "anchor": "kv-store", + "line_start": 16, + "line_end": 58, + "keywords": [ + "store", + "get", + "cached", + "read", + "per", + "node", + "live", + "production", + "key", + "populates", + "per-node", + "cache", + "entry", + "lives", + "whether", + "existed", + "until", + "expires", + "keeps", + "returning", + "old", + "result", + "value", + "absent", + "now", + "exists.", + "sorted-set", + "reads", + "zrangebyscore", + "reflect", + "writes", + "write", + "visible", + "---", + "none", + "create", + "heavy", + "pol" + ] + }, + { + "id": "platform-storage#cache", + "heading": "Cache", + "level": 2, + "anchor": "cache", + "line_start": 59, + "line_end": 82, + "keywords": [ + "cache", + "interface", + "atomicity", + "source", + "get", + "set", + "delete", + "exists", + "incr", + "expire", + "purge", + "purge_prefix", + "only", + "atomic", + "primitive", + "compare-and-set", + "setnx", + "scripting", + "multi-key", + "operations", + "scan", + "coordination", + "expressible", + "increment", + "compare", + "returned", + "value", + "ttl", + "rules", + "live", + "ttls", + "silently", + "clamped", + "deployment", + "ceiling", + "default", + "days", + "rust", + "sdk", + "references" + ] + }, + { + "id": "platform-storage#measured-performance", + "heading": "Measured performance", + "level": 2, + "anchor": "measured-performance", + "line_start": 83, + "line_end": 103, + "keywords": [ + "measured", + "performance", + "live", + "production", + "point-in-time", + "one", + "account", + "mostly", + "pop", + "evidence", + "guarantees.", + "metric", + "value", + "---", + "cache.incr", + "p50", + "p95", + "max", + "102", + "cache.expire", + "write", + "visible", + "prior", + "get", + "polling", + "107", + "min", + "rate", + "client", + "sustained", + "writes", + "sorted-set", + "ingest", + "450", + "members", + "knee", + "concurrent", + "writers", + "largest", + "single" + ] + }, + { + "id": "platform-storage#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 104, + "line_end": 108, + "keywords": [ + "see", + "cdn-filter-runtime.md", + "cdn", + "filter", + "access", + "overview.md", + "account-level", + "stores", + "resource", + "limits" + ] + } + ] + }, { "id": "quickstart", "title": "FastEdge Quickstart", 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 bdad5fe..42303a5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) @@ -46,7 +46,7 @@ proxy-wasm = "0.2" log = "0.4" ``` -**Tier 2 — CDN app with FastEdge host services** (KV, secrets, dictionary): +**Tier 2 — CDN app with FastEdge host services** (KV, cache, secrets, dictionary): ```toml [package] @@ -195,12 +195,12 @@ impl HttpContext for HelloWorld { ## Lifecycle Callbacks -| Callback | Phase | Description | -| ----------------------------------------------------------------- | ---------------- | --------------------------------------------------- | -| `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 | +| Callback | Phase | Description | +| ----------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------- | +| `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. @@ -508,6 +508,91 @@ impl HttpContext for RateLimitFilter { } ``` +### Cache (`fastedge::proxywasm::cache`) + +Provides ephemeral cache storage, implemented by the host. Unlike `key_value::Store`, cache operations are not scoped to a named store or handle — every function is a free function keyed directly, and every entry is scoped to the calling application. + +```rust,ignore +pub fn get(key: &str) -> Result>, Error> +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> +pub fn delete(key: &str) -> Result<(), Error> +pub fn exists(key: &str) -> Result +pub fn incr(key: &str, delta: i64) -> Result +pub fn expire(key: &str, ttl_ms: u64) -> Result +pub fn purge() -> Result +pub fn purge_prefix(prefix: &str) -> Result +``` + +| Function | Return Type | Description | +| ---------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------- | +| `get(key: &str)` | `Result>, Error>` | Get the value for a key; `None` if the key does not exist | +| `set(key: &str, value: &[u8], ttl_ms: Option)` | `Result<(), Error>` | Set a value with an optional expiry; `None` means no expiry | +| `delete(key: &str)` | `Result<(), Error>` | Delete a key; a no-op if the key does not exist | +| `exists(key: &str)` | `Result` | Test whether a key exists | +| `incr(key: &str, delta: i64)` | `Result` | Atomically increment (or decrement) an integer value; returns the new value | +| `expire(key: &str, ttl_ms: u64)` | `Result` | Set or update a key's expiry; `false` if the key does not exist | +| `purge()` | `Result` | Delete all cache entries owned by the calling application | +| `purge_prefix(prefix: &str)` | `Result` | Delete all cache entries whose key begins with `prefix` | + +`purge()` and `purge_prefix()` both return the number of keys that were deleted. + +#### `Error` + +```rust,ignore +pub enum Error { + AccessDenied, + InternalError, + Other(String), +} +``` + +| Variant | Description | +| --------------- | ----------------------------------------------------------- | +| `AccessDenied` | The application does not have access to the specified cache | +| `InternalError` | An unexpected internal error occurred | +| `Other(String)` | An implementation-specific error (e.g., I/O failure) | + +#### Example — cache a computed value in the response headers phase + +```rust,no_run +use fastedge::proxywasm::cache; +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(CacheRoot) }); +}} + +struct CacheRoot; +impl Context for CacheRoot {} +impl RootContext for CacheRoot { + fn get_type(&self) -> Option { Some(ContextType::HttpContext) } + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CacheFilter)) + } +} + +struct CacheFilter; +impl Context for CacheFilter {} + +impl HttpContext for CacheFilter { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + match cache::get("key-3338664") { + Ok(Some(_cached)) => { + // reuse the cached value + } + Ok(None) => { + // store the value for 5 minutes + let _ = cache::set("key-3338664", b"value", Some(300_000)); + } + Err(_) => {} + } + Action::Continue + } +} +``` + ### Secret Management (`fastedge::proxywasm::secret`) Provides access to encrypted secrets stored in the FastEdge platform. @@ -733,16 +818,17 @@ The `log` crate macros (`info!`, `warn!`, `error!`, etc.) work when `proxy_wasm: ## API Comparison: HTTP vs CDN -| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | -| ------------- | ------------------------------------------------------------------- | -------------------------------------------------------- | -| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | -| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | -| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | -| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | -| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value) | -| Cargo feature | None required | `features = ["proxywasm"]` | -| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | -| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | +| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | +| ------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | +| Cache | `fastedge::cache` | `fastedge::proxywasm::cache` | +| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | +| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | +| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | +| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value, cache) | +| Cargo feature | None required | `features = ["proxywasm"]` | +| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | +| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | ## See Also 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 5aeb1a4..a001d60 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # API Key Validation — CDN (Rust) @@ -159,120 +159,3 @@ 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-cache-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-rust.md new file mode 100644 index 0000000..8a65925 --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-cache-rust.md @@ -0,0 +1,237 @@ + + +# CDN Cache — Rust Example + +## Overview + +CDN app demonstrating cache operations via the proxy-wasm ABI. All operations are scoped to the calling application and addressed by key alone — there are no named stores or handles (unlike the KV store). + +Operations are dispatched via query parameters on the incoming request. Responses are always JSON. Errors return HTTP 500 with `{"error": ""}`. + +## App Type + +- **Interface**: proxy-wasm (`fastedge::proxywasm::cache`) +- **App type**: CDN +- **Language**: Rust + +## Request Interface + +All operations are specified via query parameters. The `action` parameter selects the operation; `action=get` is the default when omitted. + +| Query string | Operation | +|---|---| +| `?action=get&key=` | Read a cached value. `response` is `null` when the key is absent. | +| `?action=set&key=&value=[&ttl=]` | Store a value. Omitting `ttl` stores with no expiry. | +| `?action=delete&key=` | Delete a key. No-op when the key is absent. | +| `?action=exists&key=` | Test key membership. | +| `?action=incr&key=&delta=` | Atomic increment. `delta` may be negative. A missing key starts at `0`. | +| `?action=expire&key=&ttl=` | Set or update a key's TTL. `response` is `false` when the key is absent. | +| `?action=purge` | Delete every key owned by this app. Returns count of deleted keys. | +| `?action=purgePrefix&prefix=` | Delete app-owned keys starting with `prefix`. Returns count deleted. | + +## API Reference + +All functions are in `fastedge::proxywasm::cache`. + +### `cache::get` + +```rust +pub fn get(key: &str) -> Result>, _> +``` + +Retrieve cached bytes by key. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(Some(Vec))` if the key exists, `Ok(None)` if absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "get", "key": "", "response": "" } +{ "action": "get", "key": "", "response": null } +``` + +--- + +### `cache::set` + +```rust +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), _> +``` + +Store bytes under a key with an optional TTL. + +- **Parameters**: + - `key` — cache key string + - `value` — byte slice to store + - `ttl_ms` — TTL in milliseconds; `None` means no expiry +- **Returns**: `Ok(())` on success, `Err` on failure +- **Constraint**: `ttl_ms` must be a positive integer when provided; invalid values produce an error response + +**JSON response shape:** +```json +{ "action": "set", "key": "", "value": "", "ttlMs": |null, "response": true } +``` + +--- + +### `cache::delete` + +```rust +pub fn delete(key: &str) -> Result<(), _> +``` + +Remove a key from the cache. No-op if the key does not exist. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(())` on success, `Err` on failure + +**JSON response shape:** +```json +{ "action": "delete", "key": "", "response": true } +``` + +--- + +### `cache::exists` + +```rust +pub fn exists(key: &str) -> Result +``` + +Test whether a key is present in the cache. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(true)` if present, `Ok(false)` if absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "exists", "key": "", "response": true|false } +``` + +--- + +### `cache::incr` + +```rust +pub fn incr(key: &str, delta: i64) -> Result +``` + +Atomically increment (or decrement) a cached counter. If the key does not exist, it is treated as `0` before applying the delta. + +- **Parameters**: + - `key` — cache key string + - `delta` — signed 64-bit integer; may be negative for decrements +- **Returns**: `Ok(new_value)` — the value after increment, `Err` on failure + +**JSON response shape:** +```json +{ "action": "incr", "key": "", "delta": , "response": } +``` + +--- + +### `cache::expire` + +```rust +pub fn expire(key: &str, ttl_ms: u64) -> Result +``` + +Set or update the TTL on an existing key. + +- **Parameters**: + - `key` — cache key string + - `ttl_ms` — TTL in milliseconds; must be a positive integer +- **Returns**: `Ok(true)` if the key existed and was updated, `Ok(false)` if the key was absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "expire", "key": "", "ttlMs": , "response": true|false } +``` + +--- + +### `cache::purge` + +```rust +pub fn purge() -> Result +``` + +Delete all keys owned by the calling application. + +- **Parameters**: none +- **Returns**: `Ok(count)` — number of keys deleted, `Err` on failure + +**JSON response shape:** +```json +{ "action": "purge", "response": } +``` + +--- + +### `cache::purge_prefix` + +```rust +pub fn purge_prefix(prefix: &str) -> Result +``` + +Delete all app-owned keys whose names start with the given prefix. + +- **Parameters**: `prefix` — key prefix string +- **Returns**: `Ok(count)` — number of keys deleted, `Err` on failure + +**JSON response shape:** +```json +{ "action": "purgePrefix", "prefix": "", "response": } +``` + +--- + +## Error Handling + +- All errors return HTTP 500 with body `{"error": ""}`. +- Missing required query parameters produce descriptive error messages, e.g. `"Missing required param 'key' for 'get' action"`. +- Invalid parameter values (e.g. non-integer `ttl`, non-integer `delta`) produce descriptive parse error messages. +- Unknown `action` values produce an error listing all supported actions. + +## Dependencies + +```toml +proxy-wasm = "0.2" +fastedge = { path = "...", features = ["proxywasm"] } +querystring = "1.1" +serde_json = "1" +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache.wasm +``` + +## Lifecycle Notes + +- The app uses the proxy-wasm `RootContext` + `HttpContext` pattern. +- Logic executes in `on_http_response_body` (waits for `end_of_stream`). +- `on_http_response_headers` strips `content-length`, sets `content-type: application/json`, and sets `transfer-encoding: chunked` before body processing. +- Query string is read from `request.query` via `get_property`. + +## Key Constraints + +- Cache operations are scoped to the calling application — keys from one app are not accessible to another. +- There are no named stores or handles; the key is the sole address. +- `ttl` values are always in **milliseconds**. +- `delta` for `incr` is a signed 64-bit integer (`i64`); a missing key is treated as `0`. + +## See Also + +- fastedge-sdk-rust CDN key_value example (named KV store with handles, contrast to this cache API) +- fastedge-sdk-rust CDN examples overview +- proxy-wasm `HttpContext` and `RootContext` trait documentation 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 cf31db4..9f4fdd9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # CDN Example: Convert Image (Rust) @@ -202,300 +202,3 @@ Transformation is skipped (returning `Action::Continue` without modifying the re - FastEdge CDN app platform overview - FastEdge SDK Rust reference - FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/convert_image/src/lib.rs - -```rust -use image::*; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::{env, env::VarError, io::Cursor, str::from_utf8}; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); -}} - -struct ConvertImageRoot; - -impl Context for ConvertImageRoot {} - -impl RootContext for ConvertImageRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(ConvertImageContext)) - } -} - -struct ConvertImageContext; - -impl Context for ConvertImageContext {} - -impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // this header is used to select correct image version from cache - self.add_http_request_header("Image-Format", "original"); - - // get extension - let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); - println!("request.path={path:?}"); - let raw_ext = self.get_property(vec!["request.extension"]); - println!("request.extension={raw_ext:?}"); - let Some(ext) = raw_ext else { - println!("No extension in request path, not transforming"); - return Action::Continue; - }; - let Ok(ext) = from_utf8(&ext) else { - println!("Invalid UTF-8 in request extension, not transforming"); - return Action::Continue; - }; - if ext.is_empty() { - println!("No extension in request path, not transforming"); - return Action::Continue; - } - - // FORMATS_TO_TRANSFORM contains list of file extensions to transfor - // note that jpg and jpeg are different extensions - let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { - println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); - return Action::Continue; - }; - if !image_list.split(',').any(|entry| entry == ext) { - println!( - "extension {} is not in the list of formats to transform: {}, not transforming", - ext, image_list - ); - return Action::Continue; - } - - // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed - let Some(ua) = self.get_http_request_header("User-Agent") else { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - }; - if ua.is_empty() { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - } - if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { - if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { - println!("User-Agent is in ignore list, not transforming"); - return Action::Continue; - } - } - - // indicator for on_response_headers and for cache key - self.set_http_request_header("Image-Format", Some("image/avif")); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // only process 200 responses - if let Some(status) = self.rsp_status() { - if status != 200 { - println!( - "Response status is {} instead of expected 200, not transforming", - status - ); - return Action::Continue; - } - } else { - println!("Response status is not set, not transforming"); - return Action::Continue; - } - - // if "Image-Format" request header is not set, don't convert the image - let Some(content_type) = self.get_http_request_header("Image-Format") else { - return Action::Continue; - }; - // instruct cache to vary by this header so "original" and "image/avif" are cached separately - self.add_http_response_header("Vary", "Image-Format"); - - if content_type == "original" { - return Action::Continue; - }; - - // image to be transformed, set headers accordingly - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some(content_type.as_str())); - - // indicate to on_http_response_body that transformation is needed - self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // wait till we get complete body - return Action::Pause; - } - - let Some(content_type) = self.get_property(vec!["response.content-type"]) else { - return Action::Continue; - }; - - let Ok(content_type) = from_utf8(&content_type) else { - // should never happen - println!("Invalid UTF-8 in Content-Type"); - self.send_http_response(500, vec![], None); - return Action::Pause; - }; - - if content_type != "image/avif" { - // should never happen - println!( - "Content-Type {} is not supported, not transforming", - content_type - ); - return Action::Continue; - } - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let buf = body_bytes.as_bytes(); - let img = match load_from_memory(buf) { - Ok(i) => i, - Err(e) => { - println!("cannot load image to memory {}, not converting", e); - return Action::Continue; - } - }; - - let mut out = Vec::new(); - let mut c = Cursor::new(&mut out); - let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( - &mut c, - u8_param("AVIF_SPEED", 1, 10, 5), - u8_param("AVIF_QUALITY", 1, 100, 70), - )); - - match res { - Ok(_) => { - println!( - "{} bytes -> {} bytes {}", - body_size, - out.len(), - content_type - ); - self.set_http_response_body(0, body_size, &out) - } - Err(e) => println!("cannot store transformed image {}", e), - } - } else { - println!("No response body to transform"); - } - - Action::Continue - } -} - -impl ConvertImageContext { - fn rsp_status(&mut self) -> Option { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() != 2 { - println!("HTTP status property is not 2 bytes"); - return None; - } - return Some(u16::from_be_bytes([status[0], status[1]])); - } - None - } -} - -fn str_param(name: &str) -> Result { - let val = env::var(name)?; - if val.is_empty() { - return Err(VarError::NotPresent); - } - - Ok(val) -} - -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { - let Ok(val) = env::var(name) else { - println!("Param {} is not set, using default value {}", name, default); - return default; - }; - if val.is_empty() { - println!("Param {} is not set, using default value {}", name, default); - return default; - } - - let val = match val.parse() { - Err(_) => { - println!( - "Param {} is not a valid number, using default value {}", - name, default - ); - return default; - } - Ok(v) => v, - }; - if val < min { - println!( - "Param {} is below minimum {}, using default value {}", - name, min, default - ); - return default; - } - if val > max { - println!( - "Param {} is above maximum {}, using default value {}", - name, max, default - ); - return default; - } - - val -} -``` - - -### FILE: examples/cdn/convert_image/Cargo.toml - -```toml -[workspace] - -[package] -name = "convert_image" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -image = "0.25" -``` - - -### FILE: examples/cdn/convert_image/README.md - -``` -[← Back to examples](../../README.md) - -# Convert Image (CDN) - -Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. - -## Configuration - -- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) -- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip -- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) -- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) - -## How it works - -1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation -2. **on_response_headers** — sets response headers for AVIF content type on 200 responses -3. **on_response_body** — decodes the original image and re-encodes it as AVIF -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md index bf58305..ce71e32 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -292,241 +292,3 @@ export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; - CDN app platform overview - host-services-rust reference (for Rust equivalent patterns) - best-practices reference (response body buffering, hook sequencing) - -## Source Material - -### FILE: examples/customErrorPages/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - set_buffer_bytes, - BufferTypeValues, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class ErrorPagesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new ErrorPagesContext(context_id, this); - } -} - -class ErrorPagesContext extends Context { - constructor(context_id: u32, root_context: ErrorPagesRoot) { - super(context_id, root_context); - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterHeadersStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code >= 400 && code < 600) { - stream_context.headers.response.replace("Content-Type", "text/html"); - stream_context.headers.response.remove("Content-Length"); - stream_context.headers.response.replace("Transfer-Encoding", "Chunked"); - - log(LogLevelValues.info, "Error response detected: " + code.toString()); - } - - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - // Read response status from property (no instance state between hooks) - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterDataStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code < 400 || code >= 600) { - return FilterDataStatusValues.Continue; - } - - if (!end_of_stream) { - return FilterDataStatusValues.StopIterationAndBuffer; - } - const title = this.getErrorTitle(code); - const description = this.getErrorDescription(code); - const category = code >= 500 ? "Server Error" : "Client Error"; - - const html = - '' + - '' + - "" + - code.toString() + - " — " + - title + - "" + - "
" + - "

" + - code.toString() + - "

" + - "

" + - title + - "

" + - "

" + - description + - "

" + - "

" + - category + - "

" + - "
"; - - const body = String.UTF8.encode(html); - // Replace the entire original body — pass body_buffer_length as the length - // to replace, not body.byteLength. Otherwise any original bytes beyond the - // new body's length survive at the tail of the response. - set_buffer_bytes( - BufferTypeValues.HttpResponseBody, - 0, - body_buffer_length as u32, - body, - ); - - return FilterDataStatusValues.Continue; - } - - private getErrorTitle(code: u32): string { - if (code == 400) return "Bad Request"; - if (code == 401) return "Unauthorized"; - if (code == 403) return "Forbidden"; - if (code == 404) return "Not Found"; - if (code == 405) return "Method Not Allowed"; - if (code == 408) return "Request Timeout"; - if (code == 429) return "Too Many Requests"; - if (code == 500) return "Internal Server Error"; - if (code == 502) return "Bad Gateway"; - if (code == 503) return "Service Unavailable"; - if (code == 504) return "Gateway Timeout"; - if (code >= 500) return "Server Error"; - return "Error"; - } - - private getErrorDescription(code: u32): string { - if (code == 400) - return "The server could not understand the request due to invalid syntax."; - if (code == 401) - return "You need to authenticate to access this resource."; - if (code == 403) - return "You do not have permission to access this resource."; - if (code == 404) - return "The requested page could not be found. It may have been moved or deleted."; - if (code == 405) - return "The request method is not supported for this resource."; - if (code == 408) - return "The server timed out waiting for the request."; - if (code == 429) - return "You have sent too many requests. Please try again later."; - if (code == 500) - return "The server encountered an unexpected condition that prevented it from fulfilling the request."; - if (code == 502) - return "The server received an invalid response from the upstream server."; - if (code == 503) - return "The server is temporarily unavailable. Please try again later."; - if (code == 504) - return "The server did not receive a timely response from the upstream server."; - if (code >= 500) - return "The server encountered an error processing your request."; - return "An error occurred processing your request."; - } -} - -registerRootContext((context_id: u32) => { - return new ErrorPagesRoot(context_id); -}, "customErrorPages"); -``` - - -### FILE: examples/customErrorPages/package.json - -```json -{ - "name": "fastedge-as-example-custom-error-pages", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Custom Error Pages — replace 4xx/5xx responses with branded HTML", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/customErrorPages/README.md - -``` -[← Back to examples](../README.md) - -# Custom Error Pages - -This application intercepts 4xx and 5xx error responses and replaces them with clean, branded HTML error pages. - -## What it does - -In `onResponseHeaders`, the app reads the `response.status` property. If it is in the 400-599 range, it sets the `Content-Type` to `text/html` and prepares for body replacement. - -In `onResponseBody`, the app buffers the full response, then replaces the body with a styled HTML page containing: - -- The numeric status code -- A human-readable error title (e.g. "Not Found", "Bad Gateway") -- A description explaining what went wrong -- An error category label ("Client Error" or "Server Error") - -Covers all common HTTP error codes (400, 401, 403, 404, 405, 408, 429, 500, 502, 503, 504) with specific messages and falls back to generic messages for other codes. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/customErrorPages.wasm` | Optimised release binary — upload this to FastEdge | -| `build/customErrorPages-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` diff --git a/plugins/gcore-fastedge-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 981cd2f..7a52ea5 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -66,14 +66,14 @@ registerRootContext((context_id: u32) => { All header operations are accessed via `stream_context.headers.request` or `stream_context.headers.response`. -| Method | Signature | Description | -| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | -| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | -| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | -| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | -| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | -| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | +| Method | Signature | Description | +| -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | +| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | +| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | +| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | +| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | +| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | ### `Headers` / `HeaderPair` type diff --git a/plugins/gcore-fastedge-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 bc289c7..95ba36c 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> ## Overview @@ -321,202 +321,3 @@ Dependencies (from `package.json`): - SDK API reference — full lifecycle hook signatures, `RootContext` and `Context` base class APIs, all enum values (`WasmResultValues`, `FilterHeadersStatusValues`, `BufferTypeValues`, `LogLevelValues`) - Platform overview — FastEdge CDN execution model, context lifecycle, how the runtime re-enters hooks after async callbacks - Best practices — timeout tuning, upstream cluster naming, error response patterns - -## Source Material - -### FILE: examples/httpCall/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - BaseContext, - BufferTypeValues, - Context, - FilterHeadersStatusValues, - get_buffer_bytes, - HeaderPair, - log, - LogLevelValues, - makeHeaderPair, - registerRootContext, - RootContext, - send_http_response, - stream_context, - WasmResultValues, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const INTERNAL_SERVER_ERROR: u32 = 500; - -function handleHttpCallResponse( - ctx: BaseContext, - hdrs: u32, - bodySize: usize, - trls: u32, -): void { - if (hdrs == 0) { - log(LogLevelValues.error, "HTTP call failed — no response received"); - return; - } - - const userAgent = stream_context.headers.http_callback.get("user-agent"); - if (userAgent !== "") { - log(LogLevelValues.info, "User-Agent: " + userAgent); - } - - if (bodySize > 0) { - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpCallResponseBody, - 0, - bodySize as u32, - ); - const bodyStr = String.UTF8.decode(bodyBytes); - log( - LogLevelValues.info, - "Response body (" + bodySize.toString() + " bytes): " + bodyStr, - ); - } else { - log(LogLevelValues.info, "Response body: empty"); - } -} - -class HttpCallRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new HttpCallContext(context_id, this); - } -} - -class HttpCallContext extends Context { - httpCallDispatched: bool = false; - - constructor(context_id: u32, root_context: HttpCallRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // FastEdge re-invokes this hook after the httpCall response is processed. - // The latch gates re-dispatch so the second invocation returns Continue - // instead of firing another HTTP call. See SDK docs → Outbound HTTP. - if (this.httpCallDispatched) { - log( - LogLevelValues.info, - "HTTP call response received, resuming request.", - ); - return FilterHeadersStatusValues.Continue; - } - - log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); - - const headers = new Array(); - headers.push(makeHeaderPair(":scheme", "https")); - headers.push(makeHeaderPair(":authority", "httpbin.org")); - headers.push(makeHeaderPair(":path", "/ip")); - headers.push(makeHeaderPair(":method", "GET")); - headers.push(makeHeaderPair("User-Agent", "fastedge")); - - // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. - const result = (this.root_context as HttpCallRoot).httpCall( - "httpbin.org", - headers, - new ArrayBuffer(0), - new Array(), - 3000, - this, - handleHttpCallResponse, - ); - - if (result != WasmResultValues.Ok) { - log( - LogLevelValues.error, - "Failed to dispatch HTTP call: " + result.toString(), - ); - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("Failed to dispatch HTTP call"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - this.httpCallDispatched = true; - log(LogLevelValues.info, "HTTP call dispatched, pausing request"); - - return FilterHeadersStatusValues.StopIteration; - } -} - -registerRootContext((context_id: u32) => { - return new HttpCallRoot(context_id); -}, "httpCall"); -``` - -### FILE: examples/httpCall/package.json - -```json -{ - "name": "fastedge-as-example-http-call", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: HTTP Call — async HTTP dispatch with callback", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/httpCall/README.md - -``` -[← Back to examples](../README.md) - -# HTTP Call - -This application makes an asynchronous HTTP call to an external service using the proxy-wasm HTTP dispatch API. - -## What it does - -In `onRequestHeaders`, the app dispatches an outbound HTTP GET request to `httpbin.org/ip` and pauses the hook (`StopIteration`) until the response arrives. Dispatch is latched on an instance field so the second invocation of the hook (after the response is processed) returns `Continue` instead of dispatching again. - -Inside the response callback passed to `httpCall`: - -1. Checks whether the call succeeded (a `headers` value of `0` indicates failure — timeout, DNS error, etc.). -2. Reads and logs the `User-Agent` response header via `stream_context.headers.http_callback`. -3. Reads and logs the response body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`. - -If the dispatch itself fails (e.g. invalid arguments), the app returns a `500` error to the client. - -## Key concepts - -- **`httpCall()`** on `RootContext` dispatches an async HTTP request. It accepts a cluster (host), headers, body, trailers, a timeout in milliseconds, the originating context, and a callback. -- **FastEdge resume model.** Returning `FilterHeadersStatusValues.StopIteration` pauses the hook. The runtime processes the HTTP response, invokes the callback, then **re-invokes `onRequestHeaders` on the same `Context` instance**. The `httpCallDispatched` latch gates re-dispatch so the hook returns `Continue` the second time. This differs from canonical proxy-wasm — calling `continueRequest()` is not required (and has no effect on FastEdge). -- **`stream_context.headers.http_callback`** provides access to the HTTP call response headers inside the callback. The SDK sets the effective context before the callback fires, so these lookups resolve against the originating request. -- **`get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`** reads the response body inside the callback. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | -| `build/httpCall-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/httpCall.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` diff --git a/plugins/gcore-fastedge-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 35d0930..2cefbe5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # HTTP Call — CDN (Rust) @@ -285,6 +285,7 @@ impl Context for HttpHeaders { println!( "Received http call response with token id: {token_id}, num_headers: {num_headers}" ); + // If num_headers is 0, then the HTTP call failed. if num_headers != 0 { let headers = self.get_http_call_response_headers(); let headers_str = headers @@ -294,7 +295,7 @@ impl Context for HttpHeaders { .join(","); println!("Response headers: [{}]", headers_str); - self.state = 1; + self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. self.resume_http_request(); } else { 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 56aad05..620ee62 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -64,7 +64,7 @@ serde_json = "1" ### Proxy-wasm lifecycle hooks - **`on_http_response_headers`** — set response `content-type` and remove `content-length` before replacing the body -- **`on_http_response_body`** — read request query parameters via `get_property(vec!["request", "query"])`, open the store, dispatch to operations, and replace the body via `set_http_response_body` +- **`on_http_response_body`** — read request query parameters via `get_property(vec!["request.query"])`, open the store, dispatch to operations, and replace the body via `set_http_response_body` Query parameters are only available in `on_http_response_body` via `get_property` because request data is accessible throughout the response phase. @@ -151,7 +151,7 @@ match store.zrange_by_score("leaderboard", 0.0, 100.0) { ```rust let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); @@ -189,7 +189,7 @@ The complete example supports these query parameter combinations: fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); self.set_property( - vec!["response", "status"], + vec!["response.status"], Some(b"500"), ); let error_body = serde_json::json!({"error": msg}).to_string(); @@ -228,7 +228,9 @@ Sorted set entries shape: `[{"value": "", "score": }, ...]` - **`Store::open` takes a platform-configured name** — the store name must match a KV store resource attached to the app in the FastEdge platform. Passing an unknown name returns `Err`. - **`content-length` must be removed before body replacement** — when replacing the response body, always remove `content-length` in `on_http_response_headers` (set to `None`). Note: on the FastEdge CDN platform, passing `None` sets the header to an empty string rather than truly removing it; set `transfer-encoding: chunked` as a workaround. - **`on_http_response_body` must wait for end of stream** — return `Action::Pause` until `end_of_stream` is `true` before reading query parameters or replacing the body. -- **`query` property is available in the response phase** — `get_property(vec!["request", "query"])` works in `on_http_response_body` because request metadata is accessible throughout the response lifecycle. +- **`get_property` uses dot-notation path** — the correct call is `get_property(vec!["request.query"])` (a single dot-separated string), not `get_property(vec!["request", "query"])` (two separate path segments). The source uses `vec!["request.query"]`. +- **`query` property is available in the response phase** — `get_property(vec!["request.query"])` works in `on_http_response_body` because request metadata is accessible throughout the response lifecycle. +- **`set_property` uses dot-notation path** — the correct call is `set_property(vec!["response.status"], Some(b"500"))` (a single dot-separated string). - **Error type is a typed `Error` enum** (not a raw `u32` as with secrets). Match on the `Err(e)` variant and format with `format!("{}", e)` for human-readable messages. - **`store` query parameter is always required** — the example reads `store` from query params and passes it to `Store::open`. An absent `store` param returns a 500 error before any store operations are attempted. - **Empty query string is rejected** — if no query parameters are provided at all, the app returns a 500 error with the message `"App must be called with query parameters"`. @@ -240,3 +242,363 @@ 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) + +This example shows how to read and write data from a FastEdge KV store from a CDN app. +It intercepts the HTTP response, reads the request query string, and executes a KV operation against a named store. + +## What it does + +The app supports the following actions: + +- `get` — fetch one key +- `scan` — list keys matching a pattern +- `zrange` — read sorted-set entries by score range +- `zscan` — list sorted-set entries matching a pattern +- `bfExists` — check whether a Bloom filter contains an item + +The request must include at least: + +- `store=` — KV store name +- `action=` — optional, defaults to `get` + +For each action, the app validates required parameters and returns a JSON response body. + +## Supported query examples + +### Get a key + +```text +?store=my_store&action=get&key=user:42 +``` + +### List keys by pattern + +```text +?store=my_store&action=scan&match=user:* +``` + +### Read a sorted set by score range + +```text +?store=my_store&action=zrange&key=leaderboard&min=0&max=100 +``` + +### Search sorted-set members by pattern + +```text +?store=my_store&action=zscan&key=leaderboard&match=user:* +``` + +### Check a Bloom filter item + +```text +?store=my_store&action=bfExists&key=visitors&item=alice@example.com +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/key_value.wasm +``` + +This example is a CDN app, so it targets `wasm32-wasip1`. + +## Response format + +The app replaces the response body with JSON and sets `content-type: application/json`. +A successful response looks like this: + +```json +{ + "store": "my_store", + "action": "get", + "key": "user:42", + "response": "Alice" +} +``` + +If a required parameter is missing or the KV operation fails, the app responds with an error payload: + +```json +{ + "error": "Missing required param 'key' for 'get' action" +} +``` + +## Notes + +- The app requires query parameters to run. +- If `action` is omitted, it defaults to `get`. +- The code uses `fastedge::proxywasm::key_value::Store` and `proxy_wasm` lifecycle hooks to operate on the KV store. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md index 0961c7e..fe756b2 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # Large Dictionary — AssemblyScript (CDN) @@ -136,3 +136,129 @@ Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your - `getEnv` — standard WASI environment variable access (under 64 KB) - proxy-wasm-sdk-as SDK reference — full list of imports from `@gcoredev/proxy-wasm-sdk-as/assembly` and `@gcoredev/proxy-wasm-sdk-as/assembly/fastedge` - FastEdge CDN app examples (other concepts) — additional CDN app patterns in the fastedge-docs reference + +## Source Material + +### FILE: examples/largeDictionary/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getDictionary, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class LargeDictionaryRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new LargeDictionaryContext(context_id, this); + } +} + +class LargeDictionaryContext extends Context { + constructor(context_id: u32, root_context: LargeDictionaryRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. + const config = getDictionary("LARGE_CONFIG"); + + const size = config.length; + log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); + + stream_context.headers.request.add("x-config-size", size.toString()); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new LargeDictionaryRoot(context_id); +}, "largeDictionary"); +``` + + +### FILE: examples/largeDictionary/package.json + +```json +{ + "name": "fastedge-as-example-large-dictionary", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/largeDictionary/README.md + +``` +[← Back to examples](../README.md) + +# Large Dictionary + +This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. + +## When to use `getDictionary` vs `getEnv` + +| Function | Use when | +|----------|----------| +| `getEnv(name)` | Variable value is under 64 KB (most cases) | +| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | + +The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. + +For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. + +## What it does + +In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | +| `build/largeDictionary-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/host-services-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/host-services-rust.md index ac21c92..3214224 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/host-services-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/host-services-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Rust Host Services Reference @@ -260,7 +260,7 @@ async fn main(_request: Request) -> anyhow::Result> { Module: `fastedge::dictionary` -Provides fast, read-only lookups for configuration values that do not change during the lifetime of a deployment. +Provides fast, read-only lookups for configuration values that do not change during the lifetime of a deployment. Dictionary = read-only config, key_value = persistent data, secret = encrypted credentials. ### API 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 6de33d5..eeb5488 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -153,114 +153,3 @@ cargo build --release --target wasm32-wasip1 - platform-overview (FastEdge request lifecycle, outbound request capabilities) - sdk-reference-rust (`fastedge::send_request`, `Body`, HTTP types) - best-practices (body buffering considerations, error handling patterns) - -## Source Material - -### FILE: examples/http/basic/backend/src/lib.rs - -```rust -use anyhow::{anyhow, Error, Result}; -use fastedge::body::Body; -use fastedge::http::{Method, Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request) -> Result> { - let (parts, body) = req.into_parts(); - let query = parts - .uri - .query() - .ok_or(anyhow!("missing uri query parameter"))?; - let params = querystring::querify(query); - let url = params - .iter() - .find(|(k, _)| k == &"url") - .ok_or(anyhow!("missing url parameter"))?; - let url = urlencoding::decode(url.1)?.to_string(); - println!("url = {:?}", url); - let request = Request::builder().uri(url).method(Method::GET).body(body)?; - - let response = fastedge::send_request(request).map_err(Error::msg)?; - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "len = {}, content-type = {:?}", - response.body().len(), - response.headers().get("Content-Type") - ))) - .map_err(Error::msg) -} -``` - - -### FILE: examples/http/basic/backend/Cargo.toml - -```toml -[workspace] - -[package] -name = "backend" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -querystring = "1.1" -urlencoding = "2.1" -``` - - -### FILE: examples/http/basic/backend/README.md - -``` -[← Back to examples](../../../README.md) - -# Backend (URL Proxy) - -A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. - -> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). - -## What it does - -1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). -2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. -3. Returns HTTP 200 with a plain-text body: - ``` - len = , content-type = - ``` -4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::send_request(request)` | Blocking outbound HTTP request | -| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | -| `fastedge::body::Body` | Request and response bodies | -| `querystring::querify` | Parse query string into key-value pairs | -| `urlencoding::decode` | Percent-decode the `?url=` value | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/backend.wasm -``` - -## Expected behavior - -| Request | Response status | Response body | -|---|---|---| -| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = , content-type = Some("")` | -| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | -| `GET /` (no query string) | 500 | `missing uri query parameter` | - -The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option` debug string (e.g. `Some("application/json")`). -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md index d9377ed..7acebb4 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -150,3 +150,164 @@ 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-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md index ab82a99..a1e64c6 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -133,3 +133,149 @@ use fastedge::http::{Request, Response, StatusCode}; - examples-cache-basic-rust (this file) pairs with the HTTP app deploy reference for upload and wiring steps - platform-overview for cache subsystem lifecycle and eviction behavior - best-practices for cache key namespacing and TTL selection guidance + +## Source Material + +### FILE: examples/http/basic/cache/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Example app demonstrating cache-aside pattern via the cache interface. + +On each request the app: + 1. Builds a cache key from the request path. + 2. Returns the cached body immediately on a hit (x-cache: hit). + 3. On a miss, generates a response body, stores it in the cache, and + returns it (x-cache: miss). + +Environment variables: + CACHE_TTL_MS How long to cache the generated body in milliseconds + (default: 30000) + +Build: + cargo build --release +*/ + +use std::env; + +use fastedge::body::Body; +use fastedge::cache; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> anyhow::Result> { + let ttl_ms: u64 = env::var("CACHE_TTL_MS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(30_000); + + let path = req.uri().path().to_string(); + let cache_key = format!("page:{path}"); + + // Cache hit — return stored body + if let Some(cached) = cache::get(&cache_key)? { + println!("cache hit: {cache_key}"); + return Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html") + .header("x-cache", "hit") + .body(Body::from(cached))?); + } + + // Cache miss — generate the response body + println!("cache miss: {cache_key}"); + let body = generate_body(&path); + + // Store in cache with TTL + cache::set(&cache_key, body.as_bytes(), Some(ttl_ms))?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html") + .header("x-cache", "miss") + .body(Body::from(body))?) +} + +/// Simulates an expensive computation or template render. +fn generate_body(path: &str) -> String { + format!( + "\ + FastEdge Cache Demo\ + \ +

Hello from FastEdge

\ +

Path: {path}

\ +

This response was generated and is now cached.

\ + " + ) +} +``` + + +### FILE: examples/http/basic/cache/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_basic" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/basic/cache/README.md + +``` +[← Back to examples](../../../README.md) + +# Cache (Basic) + +Demonstrates the cache-aside pattern using `fastedge::cache` — store a generated response body with a TTL and serve it directly on subsequent requests without re-computing it. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `CACHE_TTL_MS` | No | How long to cache each response in milliseconds. Default: `30000` (30 s). | + +## How it works + +``` +GET /api/data → cache miss → generate body → store in cache → 200 (x-cache: miss) +GET /api/data → cache hit → return cached body → 200 (x-cache: hit) +``` + +The cache key is `page:`. Each unique path gets its own cache entry. The response body is a simple HTML page that includes the request path — stand in for any expensive computation or template render. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/html +x-cache: hit | miss + +... +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache_basic.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 +``` 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 c3bc2f1..46d3c9c 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # FastEdge Cache — JavaScript Examples @@ -331,279 +331,3 @@ 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-cache-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md index 4193334..b8d5f3c 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -188,197 +188,3 @@ cargo build --release - HTTP outbound requests via wstd (sdk-reference-rust) - Environment variable configuration (platform-overview) - HTTP app deploy workflow (deploy skill) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/ — purge keys whose cache key starts with cache:/ - -Environment variables: - ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com - CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) - -Build: - cargo build --release -*/ - -use std::env; - -use anyhow::anyhow; -use fastedge::cache; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let origin = env::var("ORIGIN_HOST") - .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; - - let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse().ok()) - .unwrap_or_else(|| { - env::var("CACHE_TTL_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(60_000) - }); - - // Build cache key from the request path (and query string if present) - let path_and_query = req - .uri() - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - - - // Handle purge requests before any cache/origin logic - if path_and_query == "/purge" { - let deleted = cache::purge()?; - println!("purge all: {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - if let Some(prefix) = path_and_query.strip_prefix("/purge/") { - let prefix = format!("cache:/{prefix}"); - let deleted = cache::purge_prefix(&prefix)?; - println!("purge prefix '{prefix}': {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - if let Some(prefix) = path_and_query.strip_prefix("/delete/") { - let prefix = format!("cache:/{prefix}"); - cache::delete(&prefix)?; - println!("prefix '{prefix}': removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - let cache_key = format!("cache:{path_and_query}"); - - // Return cached response if available - if let Some(cached) = cache::get(&cache_key)? { - println!("cache hit: {cache_key}"); - return Ok(Response::builder() - .status(200) - .header("content-type", "application/octet-stream") - .header("x-cache", "hit") - .body(Body::from(cached))?); - } - - // Cache miss — forward request to origin - let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); - println!("cache miss: {cache_key} → {upstream_url}"); - - let upstream_req = Request::get(&upstream_url) - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("upstream request failed: {e}"))?; - - let status = upstream_resp.status(); - let headers: Vec<(String, String)> = upstream_resp - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - // Read body bytes - let mut body = upstream_resp.into_body(); - let body_bytes = body.contents().await?.to_vec(); - - // Only cache successful responses - if status.is_success() { - cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; - } - - // Replay original response - let mut builder = Response::builder() - .status(status) - .header("x-cache", "miss"); - for (k, v) in &headers { - builder = builder.header(k, v); - } - Ok(builder.body(Body::from(body_bytes))?) -} -``` - - -### FILE: examples/http/wasi/cache/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/cache/README.md - -``` -[← Back to examples](../../../README.md) - -# Cache (WASI) - -Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | -| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | - -## How it works - -GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) -GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) - -Cache key is `cache:?`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. - -## Build - -cargo build --release -# Output: target/wasm32-wasip2/release/cache_wasi.wasm - -## APIs used - -- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` -- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry -- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md index fe37566..da2a970 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -190,152 +190,3 @@ Use `key=value` pairs separated by spaces (`logfmt`-ish format). Benefits: - host-services-rust (other host service integrations available to Rust WASI apps) - CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, different module path (see CDN apps reference) - examples-kv-store-wasi-rust (another Rust WASI HTTP example showing KV Store usage) - -## Source Material - -### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Diagnostic logging example. - -Tiny pass-through proxy that writes a single `set_user_diag` tag per request -summarising the outcome (config_error, origin_unreachable, or proxied). The -tag appears in the FastEdge platform's per-request log viewer and is distinct -from stdout — it's intended for filterable outcome labels, not verbose -traces. - -Required configuration: - - Environment variable: ORIGIN_URL (origin to proxy to) - -Uses `logfmt`-ish formatting (`outcome= key=value ...`) so the tag is -easy to slice in log search tooling. -*/ - -use std::env; - -use fastedge::utils::set_user_diag; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let method = req.method().as_str().to_string(); - let path = req.uri().path().to_string(); - - let origin = match env::var("ORIGIN_URL") { - Ok(u) if !u.trim().is_empty() => u, - _ => { - set_user_diag("outcome=config_error reason=origin_missing"); - return Ok(Response::builder() - .status(500) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("ORIGIN_URL is not configured"))?); - } - }; - - let outbound = Request::get(&origin).body(Body::empty())?; - let resp = match Client::new().send(outbound).await { - Ok(r) => r, - Err(e) => { - set_user_diag(&format!( - "outcome=origin_unreachable method={method} path={path} err={e}" - )); - return Ok(Response::builder() - .status(502) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("origin unreachable"))?); - } - }; - - let status = resp.status().as_u16(); - set_user_diag(&format!( - "outcome=proxied method={method} path={path} status={status}" - )); - - let (parts, mut body) = resp.into_parts(); - let bytes = body.contents().await?; - let content_type = parts - .headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream") - .to_string(); - - Ok(Response::builder() - .status(parts.status) - .header("content-type", content_type) - .body(Body::from(bytes))?) -} -``` - - -### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml - -```toml -[workspace] - -[package] -name = "diagnostic_logging_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/diagnostic_logging/README.md - -``` -[← Back to examples](../../../README.md) - -# Diagnostic Logging (WASI) - -Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request -summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, -distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at -per-request outcomes. - -## Configuration - -- `ORIGIN_URL` environment variable — the origin that requests are proxied to. - -## Outcomes - -Each request writes exactly one of: - -| Condition | Tag | -| --- | --- | -| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | -| Origin unreachable | `outcome=origin_unreachable method= path=

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

status=` | - -## `set_user_diag` vs `println!` - -| | `println!` | `set_user_diag` | -| --- | --- | --- | -| Channel | stdout — general application logs | per-request structured tag in platform log viewer | -| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | -| Best for | verbose traces, debug details | a single filterable outcome label | -| Forbidden | — | secrets and PII (tags appear in platform logs) | - -## Convention - -Call `set_user_diag` **once**, on every branch, late enough in the handler to know the -outcome. The `logfmt`-ish format (`outcome= key=value key=value …`) is easy to slice in -log search tooling — keep keys short and stable so they make good filter terms. - -## Related - -CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, -different module path. See `docs/CDN_APPS.md`. -``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-print-basic-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-print-basic-rust.md index 0b6ff0c..0c80045 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-print-basic-rust.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-print-basic-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -147,112 +147,3 @@ cargo build --release - sdk-reference-rust (full API reference for `fastedge` crate) - platform-overview (request lifecycle, header forwarding behaviour) - best-practices (response building patterns) - -## Source Material - -### FILE: examples/http/basic/print/src/lib.rs - -```rust -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request) -> Result> { - let mut body: String = "Method: ".to_string(); - body.push_str(req.method().as_str()); - - body.push_str("\nURL: "); - body.push_str(req.uri().to_string().as_str()); - - body.push_str("\nHeaders:"); - for (h, v) in req.headers() { - body.push_str("\n "); - body.push_str(h.as_str()); - body.push_str(": "); - match v.to_str() { - Err(_) => body.push_str("not a valid text"), - Ok(a) => body.push_str(a), - } - } - let res = Response::builder() - .status(StatusCode::OK) - .body(Body::from(body))?; - Ok(res) -} -``` - -### FILE: examples/http/basic/print/Cargo.toml - -```toml -[workspace] - -[package] -name = "print" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -``` - -### FILE: examples/http/basic/print/README.md - -``` -[← Back to examples](../../../README.md) - -# Print - -Echoes the incoming request's method, URL, and all headers back in the response body as plain text. Useful for debugging and inspecting what a FastEdge app receives from clients and the platform. - -> **Note:** This example uses the legacy `#[fastedge::http]` sync handler (`wasm32-wasip1`). For new apps, prefer `#[wstd::http_server]` (async, `wasm32-wasip2`) — see [`examples/http/wasi/`](../../wasi/). - -## What it demonstrates - -- Reading request method via `req.method().as_str()` -- Reading the request URI via `req.uri().to_string()` -- Iterating all request headers via `req.headers()` -- Handling non-UTF-8 header values gracefully with a `match` on `v.to_str()` -- Building a plain-text response with `Response::builder()` and `Body::from(...)` - -## APIs used - -| API | Purpose | -|-----|---------| -| `req.method().as_str()` | HTTP method as a string slice | -| `req.uri().to_string()` | Full request URI as a `String` | -| `req.headers()` | Iterator over `(HeaderName, HeaderValue)` pairs | -| `v.to_str()` | Decode a header value to `&str` (returns `Err` for non-UTF-8) | -| `Response::builder().status(...).body(...)` | Build the HTTP response | -| `Body::from(string)` | Create a response body from a `String` | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/print.wasm -``` - -## Expected behaviour - -For any request, the response body is a plain-text dump of the request details: - -``` -Method: GET -URL: /some/path?query=value -Headers: - host: example.com - accept: */* - ... -``` - -- Status: `200 OK` -- Content: plain text (no `content-type` header is set explicitly; the platform may add one) -- Each header appears on its own line, indented with four spaces -- Non-UTF-8 header values are replaced with `not a valid text` -``` 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 e9a2822..b61ea98 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,178 +250,3 @@ 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 f789dad..f523acd 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -182,236 +182,3 @@ 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 d65296b..44ff104 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Secret Access — Rust HTTP Example @@ -251,3 +251,183 @@ 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 df75287..70416dc 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Secret Rollover (WASI, Rust) @@ -134,7 +134,7 @@ use wstd::http::{Request, Response}; #[wstd::http_server] async fn main(request: Request) -> anyhow::Result> { - // Read slot from header, default to current unix timestamp + // Read the slot from the x-slot header, defaulting to current timestamp let slot: u32 = request .headers() .get("x-slot") @@ -153,11 +153,10 @@ async fn main(request: Request) -> anyhow::Result> { .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 the current secret value (latest slot) + let current = secret::get(secret_name).map_err(|e| anyhow!("secret::get failed: {e}"))?; - // Get value effective at the given slot + // Get the secret effective at the requested slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; 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 5d19fe4..56c7e10 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -16,7 +16,7 @@ capabilities: [outbound-fetch, header-read, async] # Example: Simple Fetch (WASI HTTP, Rust) -Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` crate. Reads a target URL from an incoming request header and proxies the response back to the caller. +Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` crate. Reads a target URL from an incoming request header and proxies the response back to the caller. Optionally forwards a custom header to the outbound request. ## Crate and Handler @@ -32,13 +32,16 @@ Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` | Header | Required | Type | Default | Description | |--------|----------|------|---------|-------------| | `x-fetch-url` | No | String (fully-qualified URL) | `https://httpbin.org/get` | URL to fetch outbound | +| `x-fetch-header` | No | String (`key: value`) | — | Additional header to forward to the outbound request; parsed on `:` separator | ## Behavior 1. Read `x-fetch-url` header from the incoming request; fall back to `https://httpbin.org/get` if absent or unparseable. -2. Build an outbound `GET` request to that URL with `accept: application/json` header. -3. Send via `Client::new().send(req).await`. -4. Return the upstream `Response` directly to the caller — no decomposition. +2. Begin building an outbound `GET` request to that URL with `accept: application/json` header. +3. If `x-fetch-header` is present and parseable as `key: value` (split on first `:`), add that header to the outbound request builder. +4. Finalize the builder with `.body(Body::empty())`. +5. Send via `Client::new().send(req).await`. +6. Return the upstream `Response` directly to the caller — no decomposition. ## Key API Patterns @@ -58,14 +61,34 @@ let target_url = request - `.unwrap_or(default)` provides a safe fallback - `.to_string()` required — `Request::get` takes a `&str` or `String`; ensure the URL is owned before use +### Conditionally Adding an Outbound Header + +```rust +let mut builder = Request::get(&target_url).header("accept", "application/json"); + +if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) +{ + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } +} +``` + +- The builder is `mut` to allow conditional chaining. +- `.split_once(':')` splits on the first `:`, returning `Option<(&str, &str)>`. +- `.trim()` removes surrounding whitespace from key and value before passing to `.header()`. +- If `x-fetch-header` is absent or unparseable, the builder proceeds without the extra header. + ### Building an Outbound Request ```rust use wstd::http::{Client, Request}; use wstd::http::body::Body; -let upstream_req = Request::get(&target_url) - .header("accept", "application/json") +let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; ``` @@ -138,6 +161,8 @@ package = "component:simple_fetch" - The URL extracted from the header must be converted to an owned `String` with `.to_string()` before passing to `Request::get`. - The header parsing chain (`.get` → `.to_str()` → `.ok()`) returns `Option` — always provide a fallback via `.unwrap_or`. - Non-UTF-8 header values are silently discarded by `.to_str().ok()`. +- `x-fetch-header` is parsed with `.split_once(':')` — only the first `:` is used as the separator; values containing `:` are preserved correctly. +- The request builder must be declared `mut` when conditionally adding headers after initial construction. - `anyhow` must be declared as a dependency to use the `anyhow!()` macro. - All examples in `examples/http/wasi/` use the same `async fn main` + `#[wstd::http_server]` pattern. @@ -147,3 +172,134 @@ 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 mut builder = Request::get(&target_url).header("accept", "application/json"); + + if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) + { + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } + } + + let upstream_req = builder + .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.1" +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 interface via the `wstd` crate. + +Uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). The same async pattern is used by all examples in `examples/http/wasi/`. + +## How it works + +The app receives an incoming request, reads the target URL from the `x-fetch-url` header, makes an outbound GET request to that URL, and streams the response back to the caller. + +If the `x-fetch-url` header is absent, it defaults to `https://httpbin.org/get`. + +## Request headers + +| Header | Required | Description | +|--------|----------|-------------| +| `x-fetch-url` | No | URL to fetch. Defaults to `https://httpbin.org/get` | + +## Example + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + +## Build + +```bash +cargo build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Key differences from basic HTTP examples + +| | Basic HTTP (`fastedge` crate) | WASI HTTP (`wstd` crate) | +|---|---|---| +| Handler | `fn main(req)` — sync | `async fn main(req)` — async | +| Macro | `#[fastedge::http]` | `#[wstd::http_server]` | +| Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | +| Build target | `wasm32-wasip1` | `wasm32-wasip2` | +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md index 8a6e7c9..f972648 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -143,3 +143,105 @@ chunk 4 - examples-streaming-js reference (JavaScript mirror of this example) - wstd HTTP body reference - FastEdge SDK Rust reference + +## Source Material + +### FILE: examples/http/wasi/streaming/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https:///` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} +``` + + +### FILE: examples/http/wasi/streaming/Cargo.toml + +```toml +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" +``` + + +### FILE: examples/http/wasi/streaming/README.md + +``` +[← Back to examples](../../../README.md) + +# Streaming Response (WASI) + +Generates a response body on the fly — five text chunks, one every 200 ms — using +`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it +is produced, not all at once at the end. + +Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async +stream generation, and `wstd::time::Timer` for per-chunk delays. + +## Testing the streaming behaviour + +```sh +curl -N https://.fastedge.cdn.gc.onl/ +``` + +`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a +time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. + +## Other streaming patterns + +- **Pass-through streaming** — return an upstream response's body directly. See + [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. +- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, + then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. +- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where + `chunks` is any iterable of `Into`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/streaming/`. +``` diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md new file mode 100644 index 0000000..91b5ad4 --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md @@ -0,0 +1,122 @@ +# CDN Filter Runtime — Capabilities and Traps + +What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a deployed app. + +Every claim carries its provenance: + +| Tag | Meaning | +|---|---| +| `[live]` | Measured against a deployed app on a real CDN resource | +| `[source]` | Read from the FastEdge runtime or SDK source | + +Last verified: 2026-08-25 (SDK surface re-checked 2026-09-23). + +--- + +## Capability boundary — filter vs HTTP app + +A filter is a core wasm module linked against the proxy-wasm `env` ABI. It is a different execution world from an HTTP app, not a subset of it. Designs that assume symmetry with the HTTP-app SDK fail at deploy. + +| Capability | HTTP app | CDN filter | +|---|---|---| +| KV read | ✅ | ✅ same six operations: `open · get · scan · zrange_by_score · zscan · bf_exists` `[source]` | +| KV write | ❌ (API only — see [storage.md](./storage.md)) | ❌ (API only) | +| Cache (`get/set/delete/exists/incr/expire/purge/purgePrefix`) | ✅ | ✅ host-supported; **SDK support varies — check the SDK reference for your language** | +| Secrets / dictionary | ✅ | ✅ via the SDK's proxy-wasm module | +| Outbound `fetch` | ✅ | proxy-wasm `http_call` only | +| Deferred / background work | JS `waitUntil` only (not a timer) | ❌ none | +| State between requests | ❌ | ❌ — consecutive requests hit different nodes `[live]` | + +### Use only the SDK's proxy-wasm modules + +HTTP-app host APIs are component-model (WIT) imports. A proxy-wasm host cannot resolve them, so **one reference anywhere in a filter makes the app fail to instantiate — HTTP 530 on every path**, including paths that never reach the call `[live]`. It compiles and links with no warning. + +- **Rust:** in a filter import only from `fastedge::proxywasm::*`. Never use a top-level `fastedge::` module (`fastedge::cache`, `fastedge::secret`, …) — each has a `fastedge::proxywasm::` counterpart. +- **Check before deploying** — any `gcore:fastedge/...` import in a filter build means a 530 at runtime: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` + +--- + +## Reading the request — use properties, not headers + +Several headers a proxy-wasm developer would reach for are mangled or absent. **All failures are silent** `[live]`. + +| Read via | Result | +|---|---| +| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) on the edge tier; on shield nodes it may carry a `shield_` prefix | +| property `request.x_real_ip` | ✅ true client IP, IPv4 and IPv6 | +| property `request.path` | ⚠️ **includes the query string** — see below | +| property `request.query` | ✅ query without `?`; not set by the host when there is no query (how "not set" surfaces — `None`, empty buffer — depends on the SDK) | +| property `request.asn` / `.country` / `.city` / `.region` / `.continent` | ✅ | +| property `source.address` | ❌ does not exist (Envoy's name) — returns nothing | +| header `host` | ❌ mangled to `_cache_sharded` | +| headers `:path`, `:authority` | ❌ absent | +| headers `x-real-ip`, `x-forwarded-for`, `cookie`, `user-agent` | ✅ intact (`cookie` is visible even with `ignore_cookie: true`) | + +### `request.path` includes the query string + +``` +GET /x?a=1&b=2 +request.path = "/x?a=1&b=2" +request.query = "a=1&b=2" +``` + +Prefix checks are unaffected; **equality checks silently fail on any request with a query** — auth callbacks are exactly where equality checks live. Split on `?` first. Corollary: because it is path+query, `request.path` works directly as a relative `Location` for a same-origin redirect, which avoids the mangled `host` header. + +--- + +## Security — gating filters + +### The CDN does not normalise the path before the filter runs + +`[live]` Traversal and separator sequences arrive verbatim: + +```bash +curl --path-as-is ".../app/x/../test" # request.path = "/app/x/../test" +curl --path-as-is ".../app//test" # request.path = "/app//test" +``` + +A filter that bypasses auth on a prefix (`/auth/`, `/public/`) will wave through `/auth/../admin`, which the origin may normalise back to `/admin`. **That is an authentication bypass**, and it is the natural way to write the code. + +Reject suspicious sequences before any prefix bypass, and **fail closed** — fall through to the auth check. Never normalise the path yourself and proceed; you will not match the origin's rules. + +```rust +fn has_suspicious_sequence(path: &str) -> bool { + if path.contains("..") || path.contains("//") || path.contains('\\') { return true; } + let lower = path.to_ascii_lowercase(); + lower.contains("%2e") || lower.contains("%2f") + || lower.contains("%5c") || lower.contains("%25") // %25 catches double-encoding +} +``` + +### Filters run before the CDN cache lookup + +`[live, edge tier only]` A request-phase filter runs on **every** request, including cache hits. + +- **Security:** a cached response cannot bypass a request-phase gate. +- **Cost:** every request pays for whatever the filter does — KV reads, cache ops — even on cache hits. +- A `no-store` response from a filter is not cached. Varying the cache key on a session cookie fragments the cache per visitor. + +Only `execute_on_edge` was measured; ordering on the shield tier is unverified. + +--- + +## Synthetic responses + +`[live]` A filter can serve a complete response via `send_http_response`: any status, arbitrary headers, multiple `Set-Cookie` headers on one response, and bodies up to at least 1 MB without truncation. Single-app designs (challenge pages, interstitials, error pages) do not need a second HTTP app. + +--- + +## What local testing cannot prove + +`@gcoredev/fastedge-test` runs proxy-wasm filters locally and proves **flow logic** — routing, header manipulation, gate decisions. It cannot prove **host behaviour**: property semantics, header mangling, path non-normalisation, instantiation failures, cache interaction. Every trap on this page passes a local suite green. Confirm host-dependent behaviour on a deployed app attached to a real CDN resource. + +--- + +## See Also + +- [storage.md](./storage.md) — KV and Cache semantics, limits, measured performance +- [cdn-integration.md](./cdn-integration.md) — attaching filters to CDN resources +- [error-codes.md](./error-codes.md) — 530 diagnosis diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-integration.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-integration.md index 2024764..49aa783 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-integration.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/cdn-integration.md @@ -181,8 +181,25 @@ curl -s -X PATCH "https://api.gcore.com/cdn/resources/" \ }' ``` +**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. Do not confuse this with the rule-vs-resource replace semantics above, which govern request-time override, not the API. + Rules are managed through their own endpoints under `/cdn/resources/{resource_id}/rules`. The full rule API is in the Gcore CDN documentation. +### Cached content after a routing change + +Request-phase filters run before the cache lookup (see [cdn-filter-runtime.md](./cdn-filter-runtime.md)), so attaching or changing an `on_request_headers` filter takes effect on cached paths without a purge. What does **not** change on its own is content already cached from the previous configuration — e.g. after a rule that points a path at a different origin, or a change to response-phase output. That content keeps being served until it expires (observed ~15 min). + +In that case, purge only the affected paths, then verify: + +```bash +curl -s -X POST "https://api.gcore.com/cdn/resources//purge" \ + -H "Authorization: APIKey $GCORE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"paths": ["/affected/path/*"]}' +``` + +`{"paths": []}` purges **the entire resource** and sends all traffic back to origin — use it only with explicit user confirmation. The FastEdge MCP server's default access policy blocks this endpoint; when it is blocked, give the user the command to run themselves. + ## Operational Notes - **`app_id` is a string.** The FastEdge apps API (`GET /fastedge/v1/apps`) returns `id` as an integer, but in the `options.fastedge..app_id` field it must be quoted as a string. Mixing types is a common source of validation errors. @@ -191,7 +208,7 @@ Rules are managed through their own endpoints under `/cdn/resources/{resource_id - **Hook absence vs. `enabled: false`.** Both result in the hook not running. Removing the key entirely is cleaner; toggling `enabled: false` preserves the hook's other config for easy re-enable. - **`execute_on_shield` only matters when origin shielding is on.** If `shielded: false` on the resource, the shield-layer setting has no effect — the hook only ever runs at the edge. - **Rule weight ordering.** When multiple rules match a request path, only the rule with the lowest weight applies. Rules don't compose. -- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. +- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Content cached under the old configuration may also need a path-scoped purge (above). ## See Also diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/error-codes.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/error-codes.md index 23b24f5..fb81210 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/error-codes.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/error-codes.md @@ -10,7 +10,10 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The Wasm module failed to start up before processing the request. +**Signal:** a CDN app that fails to instantiate also returns `x-cdn-internal-status: 3100`. That header distinguishes "failed to start" from "the app ran and errored" (531). + **Common Causes:** +- **Unresolved imports (CDN apps)** — the binary imports a host function the proxy-wasm host does not provide, most commonly an HTTP-app (component-model) API used in a filter, e.g. top-level `fastedge::cache` in Rust instead of `fastedge::proxywasm::cache`. **Every path returns 530**, including paths that never reach the call, and the build gives no warning. See [cdn-filter-runtime.md](./cdn-filter-runtime.md). - Missing required environment variables that the app reads during initialization - Corrupted or invalid Wasm binary - Binary was compiled for the wrong target (not `wasm32-wasip1`) @@ -21,9 +24,15 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco - JS: `fastedge-build ./src/index.js ./.wasm` completed without errors - Rust: `cargo build --release --target wasm32-wasip1` succeeded 2. Check binary size: `ls -la *.wasm` or `ls -la target/wasm32-wasip1/release/*.wasm` -3. Test locally: `fastedge-run http -w ./app.wasm --port 8080` -4. Verify all required env vars are set via API or portal -5. Re-upload the binary and update the app +3. **CDN apps:** inspect the import table — any `gcore:fastedge/...` import in a proxy-wasm build is unresolvable: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` +4. Run locally: + - HTTP apps: `fastedge-run http -w ./app.wasm --port 8080` + - CDN apps: `fastedge-run` does not run proxy-wasm — use `@gcoredev/fastedge-test`. Note a local run does not reproduce host instantiation, so a clean local run does not rule out step 3. +5. Verify all required env vars are set via API or portal +6. Re-upload the binary and update the app --- @@ -32,6 +41,7 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The app started successfully but threw an unhandled exception during request processing. **Common Causes:** +- Calling `getEnv()` / `getSecret()` at module top level (JS) — they are request-time only; call them inside the handler - Uncaught JavaScript exception (TypeError, ReferenceError, etc.) - Rust panic (`unwrap()` on `None` or `Err`) - Failed `fetch()` call without error handling diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/overview.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/overview.md index 415dc14..78300ee 100644 --- a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/overview.md +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/overview.md @@ -99,6 +99,14 @@ What is appropriate: per-end-user or per-session key prefixes within your app's 5. **Response Delivery** — Response sent back to client 6. **Cleanup** — Wasm instance destroyed, memory freed +### No scheduled or deferred work + +There is no scheduler or durable background execution. Timers exist within a request (e.g. JS `setTimeout`) but live only as long as that instance. JS `event.waitUntil()` extends the instance until a promise settles, starting immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no post-response work API. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron. + +### PoP identity + +`getEnv("dc")` returns the PoP's lowercase short code (e.g. `ls1`, `am3`) — the only PoP identifier available to an app. **It can be empty**; an empty-string fallback silently merges every PoP's keys into one namespace. Fail loudly instead. Node count per PoP varies, and the number of active PoPs is not discoverable from inside an app. + ## Resource Limits | Resource | Basic Plan | Pro Plan | diff --git a/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/storage.md b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/storage.md new file mode 100644 index 0000000..e55ac0c --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/fastedge-docs/reference/platform/storage.md @@ -0,0 +1,107 @@ +# Storage Semantics — KV Store and Cache + +Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your language. + +| | KV Store | Cache | +|---|---|---| +| Scope | Global, account-level, assigned to apps | Per-PoP, shared by every node in the PoP `[source + told]` | +| Consistency | Eventually consistent; **reads cached per node** (below) | Strong within a PoP | +| Writes from app code | ❌ API only | ✅ | +| Atomic primitive | — | `incr` only | + +Provenance tags: `[live]` measured on a deployed app · `[source]` read from the runtime source · `[told]` stated by the FastEdge team · `[unverified]` not yet confirmed. Last verified: 2026-08-27. + +--- + +## KV Store + +### `get()` is a cached read — ~30 s per node + +`[live, production]` Each `get(key)` populates a per-node cache entry that lives ~30 s, **whether or not the key existed**. Until it expires, that node keeps returning the old result — the old value, or "absent" for a key that now exists. Sorted-set reads (`zrangeByScore`) are **not** cached and reflect writes in 0–1 s. + +| Reads before the write | Write | Visible after | +|---|---|---| +| none | create `kv` key | 1 s | +| heavy `get()` polling of the absent key | create `kv` key | **31 s** | +| heavy `get()` polling of the existing key | update its value | **30 s, then flapped** | +| heavy `zrangeByScore` polling | add `sorted_set` member | 0 s | + +- **Values flap.** Warm and expired nodes answer differently at the same instant, so a polled `kv` flag can turn on, off, and on again during the window. For access gating this is worse than a delay. +- **Polling makes it worse** — every poll refreshes the staleness hiding the change. +- **Absence is not a safe signal.** "Key missing ⇒ not started" fails in the direction of "my change hasn't happened". + +**Rule:** use `kv` + `get()` for values written once and read as configuration. For anything polled that must see changes promptly (a "has this happened yet?" flag), use a **sorted set with one member whose score is the timestamp** — uncached and consistent across nodes. + +### Writes go through the API only + +`[live]` `PUT /fastedge/v1/kv/{store_id}/data` — never from inside an app. Many entries can be batched in one call. + +```jsonc +[{ "op": "add", // add | del_key | del_entries — REQUIRED + "key": "…", + "datatype": "kv", // kv | sorted_set | bloom_filter + "payload": { "value": "…", "encoding": "plain" } }] // encoding: plain | base64 | sha256 +``` + +- **`sorted_set` nests differently:** its `payload` *is* the member array. The 400 only says `doesn't match any schema from "anyOf"`. To learn a payload shape, write one entry by hand and read it back with `GET /fastedge/v1/kv/{store}/data/{key}` — the read shape mirrors the write shape. +- **`datatype` scopes `del_key`.** Deleting a `kv` key with `datatype: "sorted_set"` returns **200 with `del_count: 0`** and removes nothing; a later `zrangeByScore` on that name then throws a type error. **Check `del_count`** on cleanup writes, especially when migrating a key between datatypes. +- The response includes `revision`, a store-wide monotonic counter — a freshness signal independent of clocks. +- Attaching a store to an app: `stores: {"": {"id": }}`; omit the inner `name` (read-only). + +### Missing operations + +`[source]` The read surface is exactly `open · get · scan · zrange_by_score · zscan · bf_exists`. There is **no** rank-based `ZRANGE`, `ZCOUNT`, `ZCARD` or `ZSCORE`. Do not emulate them in app code: `zrange_by_score(-inf, +inf)` transfers every member with its score into the wasm instance on every call. + +`zrange_by_score` on a missing key returns an empty list, not an error. + +--- + +## Cache + +### Interface and atomicity + +`[source]` `get · set · delete · exists · incr · expire · purge · purge_prefix`. **`incr` is the only atomic primitive** — no compare-and-set, `SETNX`, scripting, multi-key operations, or `SCAN`. Coordination must be expressible as "increment and compare the returned value". + +### TTL rules + +`[source + live]` + +- **TTLs are silently clamped** to a deployment ceiling (default 4 days). The JS and Rust SDK references describe `set` without a TTL as "no expiry"; the host actually applies the default ceiling (runtime source, and a no-TTL `set` key measured gone within 22 days). **Treat the host behaviour as authoritative** — do not rely on any entry outliving ~4 days. There is no TTL-read primitive, so the clamp is invisible. +- **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. Every `incr` key needs an `expire`, but *when* depends on the key shape — `expire` sets a deadline relative to now, so calling it again pushes the deadline out: + - **Fixed key** (e.g. `rl:`): call `expire` only when `incr` returns `1`, and retry that `expire` within the request budget. Expiring on every call resets the window and keeps the key alive under continuous traffic. If the one `expire` fails for good, the key never expires — there is no TTL-read to detect it. + - **Time-bucketed key** (window id in the name, e.g. `rl::`): calling `expire` on every call is safe and self-healing — writes stop when the window passes, so the key expires one TTL after its last write even if earlier `expire` calls failed. Prefer this shape when a permanently leaked key is costly. +- `expire` returns `false` for an absent key and `Err` for a failure. Do not collapse the two. + +### Failures are real — retry + +`[live, production]` Cache operations fail outright with explicit errors (not timeouts) at ~0.44% overall, bursty and PoP-specific (one PoP showed 1.5%). Plan for retries rather than assuming success, but bound them by the remaining execution budget — a single op can take ~100 ms at the tail, beyond the Basic plan's 50 ms limit. Decide deliberately whether an exhausted retry fails open or closed. + +> **CDN filters:** the figures and TTL behaviour on this page were measured through HTTP apps. Proxy-wasm cache support is newer and is `[unverified]` to share the same backend behaviour. + +--- + +## Measured performance + +`[live, production]` Point-in-time, one account, mostly one PoP — evidence, not guarantees. + +| Metric | Value | +|---|---| +| `Cache.incr` | p50 1 ms · p95 27 ms · max 102 ms | +| `Cache.expire` | p50 2 ms · p95 6 ms · max 21 ms | +| KV write → visible (no prior `get()` polling) | 61 / 80 / 107 ms (min / p50 / max) | +| KV write rate, one client | sustained ~94 writes/s | +| KV sorted-set ingest | ~2,450 members/s; knee at 32–64 concurrent writers | +| Largest single batched `PUT` | ~1,560 entries — bounded by a 30 s gateway timeout, not payload size | +| Sorted-set storage | ~31 bytes/member | +| Intra-PoP clock skew | ≤5 ms, probably ≤1 ms | + +### Preprod is not production + +Preprod KV propagation was bimodal — typically ~210 ms, but ~1 in 8–15 writes invisible for over 45 s. Production did not do this. Do not benchmark or size designs on preprod. Before attributing a ~30 s delay to either, check the cached-read behaviour above: if something was polling the key with `get()`, or the value flaps once it appears, it is the per-node read cache. + +--- + +## See Also + +- [cdn-filter-runtime.md](./cdn-filter-runtime.md) — what a CDN filter can access +- [overview.md](./overview.md) — account-level KV stores, resource limits 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 695920d..2393c2e 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -222,3 +222,233 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide + +## Source Material + +### FILE: examples/abTesting/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; + +class AbTestingRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new AbTestingContext(context_id, this); + } +} + +class AbTestingContext extends Context { + constructor(context_id: u32, root_context: AbTestingRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const experimentName = getEnv("EXPERIMENT_NAME"); + if (experimentName === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const variantAPath = getEnv("VARIANT_A_PATH"); + const variantBPath = getEnv("VARIANT_B_PATH"); + if (variantAPath === "" || variantBPath === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode( + "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", + ), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Check for existing experiment cookie + const cookieName = "fe_exp_" + experimentName; + const cookieHeader = stream_context.headers.request.get("Cookie"); + let assignedVariant = this.getCookieValue(cookieHeader, cookieName); + + // Assign variant if not already set + if (assignedVariant !== "A" && assignedVariant !== "B") { + // Use current time as a simple entropy source for 50/50 split + const now = getCurrentTime(); + assignedVariant = now % 2 == 0 ? "A" : "B"; + } + + // Rewrite the request path to the variant path + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + return FilterHeadersStatusValues.Continue; + } + const originalPath = String.UTF8.decode(pathArrBuf); + const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; + const newPath = variantPath + originalPath; + + // Reconstruct request.url from its decomposed parts rather than splicing + // the path out of the full URL — splicing breaks when the path happens to + // appear inside the host, and it can silently lose the query string. + const schemeBuf = get_property("request.scheme"); + const hostBuf = get_property("request.host"); + if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { + const scheme = String.UTF8.decode(schemeBuf); + const host = String.UTF8.decode(hostBuf); + const queryBuf = get_property("request.query"); + const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; + const newUrl = + scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); + log(LogLevelValues.info, `A/B routing: ${newUrl}`); + set_property("request.url", String.UTF8.encode(newUrl)); + } + + // Add variant header for upstream visibility + stream_context.headers.request.add("X-Experiment", experimentName); + stream_context.headers.request.add("X-Variant", assignedVariant); + + log( + LogLevelValues.info, + `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, + ); + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Recover the assigned variant from the request header set in onRequestHeaders. + // Instance state (this.variant) does not survive the nginx -> core-proxy hop. + const variant = stream_context.headers.request.get("X-Variant"); + if (variant === "") { + return FilterHeadersStatusValues.Continue; + } + + const experimentName = getEnv("EXPERIMENT_NAME"); + const cookieName = "fe_exp_" + experimentName; + + // Set the experiment cookie so subsequent requests stick to the same variant + stream_context.headers.response.add( + "Set-Cookie", + cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", + ); + + // Add variant as response header for observability + stream_context.headers.response.add("X-Variant", variant); + + return FilterHeadersStatusValues.Continue; + } + + private getCookieValue(cookieHeader: string, name: string): string { + if (cookieHeader === "") return ""; + const pairs = cookieHeader.split(";"); + const prefix = name + "="; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].trim(); + if (pair.startsWith(prefix)) { + return pair.substring(prefix.length); + } + } + return ""; + } +} + +registerRootContext((context_id: u32) => { + return new AbTestingRoot(context_id); +}, "abTesting"); +``` + + +### FILE: examples/abTesting/package.json + +```json +{ + "name": "fastedge-as-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/abTesting/README.md + +``` +[← Back to examples](../README.md) + +# A/B Testing + +This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. + +## What it does + +In `onRequestHeaders`, the app: + +1. Checks for an existing experiment cookie (`fe_exp_`). +2. If no cookie is found, assigns the user to variant **A** or **B** (50/50 split). +3. Rewrites the request path by prepending the variant-specific path prefix (e.g. `/variant-a/original/path`). +4. Adds `X-Experiment` and `X-Variant` request headers for upstream visibility. + +In `onResponseHeaders`, the app sets a `Set-Cookie` header to persist the variant assignment for subsequent requests (24-hour TTL). + +> **Note on variant assignment entropy:** New-visitor assignment uses `getCurrentTime() % 2` as a simple 50/50 source. This is illustrative — it is not sticky across two requests that arrive in the same millisecond and is not reproducible in tests. Production A/B implementations typically hash a stable visitor identifier (e.g. client IP or session token) for deterministic, sticky pre-cookie assignment. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `EXPERIMENT_NAME` | `homepage-redesign` | Name of the experiment (required) | +| `VARIANT_A_PATH` | `/variant-a` | Path prefix for variant A (required) | +| `VARIANT_B_PATH` | `/variant-b` | Path prefix for variant B (required) | + +Your origin server should serve different content at each variant path prefix. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/abTesting.wasm` | Optimised release binary — upload this to FastEdge | +| `build/abTesting-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/abTesting.wasm` to the FastEdge portal and attach it to your CDN application. Configure the experiment environment variables in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/api-key-rust.md index 4295b4b..f97e840 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -212,3 +212,122 @@ target = "wasm32-wasip1" - FastEdge secrets configuration (dashboard secret variable setup) - auth-jwt-rust reference (JWT-based authentication — use when token expiry and claims are needed) - platform-overview reference (CDN vs HTTP app type distinction) + +## Source Material + +### FILE: examples/cdn/api_key/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating API key validation. + +Validates requests using an X-API-Key header checked against a stored +secret. Simpler alternative to JWT when token expiry and claims are +not needed. + +Required configuration: + - Secret: API_KEY +*/ + +use fastedge::proxywasm::secret; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { 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 07cdb77..9da636d 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -267,3 +267,204 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint + +## Source Material + +### FILE: examples/jwt/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class AuthRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes + return new Auth(context_id, this); + } +} + +class Auth extends Context { + constructor(context_id: u32, root_context: AuthRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const secret = getSecret("SECRET"); + if (!secret) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const authHeader = stream_context.headers.request.get("Authorization"); + if (!authHeader) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("No Authorization header"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (!authHeader.startsWith("Bearer ")) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Authorization header must use Bearer scheme"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const token = authHeader.slice(7); // strip "Bearer " prefix + if (!token) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Token not found"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Decode the JWT token + const jwtResult = jwtVerify(token, secret); + if (jwtResult !== JwtValidation.Ok) { + if (jwtResult === JwtValidation.Expired) { + log(LogLevelValues.info, "Token Expired"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Expired token"), + [], + ); + } else { + log(LogLevelValues.info, "Bad Token"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid token"), + [], + ); + } + return FilterHeadersStatusValues.StopIteration; + } + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new AuthRoot(context_id); +}, "auth"); +``` + +### FILE: examples/jwt/package.json + +```json +{ + "name": "fastedge-as-example-jwt", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: JWT validation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/as-jwt": "^1.0.3", + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", + "assemblyscript-json": "^1.1.0" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/jwt/README.md + +``` +[← Back to examples](../README.md) + +# JWT Validation + +This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. +2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. +3. Verifies the token signature and expiry using `jwtVerify()`. +4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. + +## Configuration + +Set the following secret variable on your FastEdge application: + +| Secret | Description | +| -------- | ------------------------------------------------------------------ | +| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | + +## Testing tokens + +**Expired token** (will return `403 Forbidden`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, will return `200 OK`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ---------------------- | -------------------------------------------------- | +| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | +| `build/jwt-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/jwt.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. + +For more on secrets and secret rotation slots, see the FastEdge secrets documentation. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/auth-jwt-rust.md index c3da37d..a1badbb 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -259,142 +259,3 @@ target = "wasm32-wasip1" - FastEdge SDK Rust reference (proxywasm module, secret API) - FastEdge secrets configuration (dashboard secret variable setup) - platform-overview reference (CDN vs HTTP app type distinction) - -## Source Material - -### FILE: examples/cdn/jwt/src/lib.rs - -```rust -use std::time::{SystemTime, UNIX_EPOCH}; -use headers::HeaderValue; -use headers::authorization::{Bearer, Credentials}; - -use fastedge::proxywasm::secret; -use jsonwebtoken::{decode, DecodingKey, Validation}; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde::Deserialize; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { 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 3f82231..6a43237 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -14,8 +14,8 @@ languages: [assemblyscript] template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as -source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-20 +source_ref: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 +updated: 2026-09-22 --- # Base Skeleton: CDN AssemblyScript @@ -263,118 +263,3 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) - -## Source Material - -### FILE: examples/helloWorld/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; - -class HelloWorldRoot extends RootContext { - createContext(context_id: u32): Context { - return new HelloWorld(context_id, this); - } -} - -class HelloWorld extends Context { - constructor(context_id: u32, root_context: HelloWorldRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool, - ): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onRequestBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onResponseBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HelloWorldRoot(context_id); -}, "helloWorld"); -``` - - -### FILE: examples/helloWorld/package.json - -```json -{ - "name": "fastedge-as-example-hello-world", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/helloWorld/asconfig.json - -```json -{ - "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", - "targets": { - "debug": { - "outFile": "build/helloWorld-debug.wasm", - "textFile": "build/helloWorld-debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/helloWorld.wasm", - "textFile": "build/helloWorld.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "use": "abort=abort_proc_exit" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/base-rust.md index d3ad5df..c74dfa9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -13,8 +13,8 @@ app_type: cdn languages: [rust] template_origin: cdn-base source_repo: https://github.com/G-Core/FastEdge-sdk-rust -source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-20 +source_ref: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 +updated: 2026-09-22 --- # Base Skeleton: CDN Rust @@ -208,77 +208,3 @@ 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 837648f..89784fd 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -360,3 +360,218 @@ Upload `build/body.wasm` via the FastEdge portal and attach it to your CDN appli - cdn-base skeleton - FastEdge portal deployment guide - fastedge-test reference (for testing body manipulation logic) + +## Source Material + +### FILE: examples/body/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BufferTypeValues, + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_buffer_bytes, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class HttpBodyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug + return new HttpBody(context_id, this); + } +} + +class HttpBody extends Context { + constructor(context_id: u32, root_context: HttpBodyRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool + ): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); + // Remove the "content-length" header + stream_context.headers.request.remove("content-length"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); + if (bodyStr.includes("Client")) { + const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; + set_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length, + String.UTF8.encode(newBody) + ); + } + } + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); + + // Remove "content-length" header as the body size will change + stream_context.headers.response.remove("content-length"); + + // Set "transfer-encoding" to "chunked" + stream_context.headers.response.replace("transfer-encoding", "Chunked"); + + const contentType = stream_context.headers.response.get("content-type"); + if (contentType.length > 0) { + set_property("response.content_type", String.UTF8.encode(contentType)); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + + // Retrieve the request URL + const urlBytes = get_property("request.url"); + const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } + + // Retrieve the response content type stored in onResponseHeaders + const contentTypeBytes = get_property("response.content_type"); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); + } + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HttpBodyRoot(context_id); +}, "httpbody"); +``` + + +### FILE: examples/body/package.json + +```json +{ + "name": "fastedge-as-example-body", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Body manipulation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/body/README.md + +``` +[← Back to examples](../README.md) + +# Body + +This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. + +## What it does + +1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. + +2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. + +3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. + +4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. + +This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: + +- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). +- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ----------------------- | -------------------------------------------------- | +| `build/body.wasm` | Optimised release binary — upload this to FastEdge | +| `build/body-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/body-rust.md index 4b49934..47fbc4a 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -243,3 +243,125 @@ Requires `log = "0.4"` in `Cargo.toml`. Log level is set to `Trace` at startup v - proxy-wasm HttpContext trait reference - host-services-rust reference (property API, logging) - platform-overview reference (CDN app lifecycle) + +## Source Material + +### FILE: examples/cdn/body/src/lib.rs + +```rust +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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 f49e70c..63a5c49 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -187,3 +187,191 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | +| Other | `public, max-age=600` | 10 minutes | +| Error responses (4xx/5xx) | `no-store` | — | + +Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). + +## Configuration + +All environment variables are optional — sensible defaults are applied when unset. + +| Variable | Default | Description | +|----------|---------|-------------| +| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | +| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | +| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cacheControl-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-rust.md new file mode 100644 index 0000000..7b6e966 --- /dev/null +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cache-rust.md @@ -0,0 +1,228 @@ + + +# CDN Cache — Rust Example + +## Overview + +Demonstrates cache operations in a CDN app using the proxy-wasm ABI via `fastedge::proxywasm::cache`. The cache has no named stores or handles — every operation is scoped to the calling application and addressed by key alone. This distinguishes it from the KV store, which uses named stores. + +Operations are selected via the `action` query parameter. All responses are JSON. Errors return HTTP 500 with `{"error": "..."}`. + +## App Type + +- **App type**: CDN (proxy-wasm) +- **Language**: Rust +- **Crate type**: `cdylib` +- **Entry point**: `on_http_response_body` — operations run during the response phase + +## Dependencies + +```toml +proxy-wasm = "0.2" +fastedge = { path = "...", features = ["proxywasm"] } +querystring = "1.1" +serde_json = "1" +``` + +Feature flag `proxywasm` must be enabled on the `fastedge` crate. + +## API Reference + +All functions are in the `fastedge::proxywasm::cache` module. + +### `cache::get` + +```rust +pub fn get(key: &str) -> Result>, Error> +``` + +Retrieves cached bytes by key. + +- Returns `Ok(Some(Vec))` when the key exists. +- Returns `Ok(None)` when the key is absent. +- Returns `Err` on host error. + +### `cache::set` + +```rust +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> +``` + +Stores bytes under a key with an optional TTL. + +- `ttl_ms`: milliseconds until expiry. Pass `None` for no expiry. +- Overwrites any existing value for the key. + +### `cache::delete` + +```rust +pub fn delete(key: &str) -> Result<(), Error> +``` + +Removes a key. No-op when the key is absent. + +### `cache::exists` + +```rust +pub fn exists(key: &str) -> Result +``` + +Tests whether a key is present. Returns `true` if the key exists, `false` otherwise. + +### `cache::incr` + +```rust +pub fn incr(key: &str, delta: i64) -> Result +``` + +Atomically increments (or decrements) a numeric counter stored under `key`. + +- `delta` may be negative. +- A missing key is treated as starting at `0`. +- Returns the new value after applying `delta`. + +### `cache::expire` + +```rust +pub fn expire(key: &str, ttl_ms: u64) -> Result +``` + +Sets or updates the expiry of an existing key. + +- `ttl_ms`: milliseconds from now until the key expires. +- Returns `true` if the key existed and was updated, `false` if the key was absent. + +### `cache::purge` + +```rust +pub fn purge() -> Result +``` + +Deletes every key owned by the calling application. + +- Returns the number of keys deleted. + +### `cache::purge_prefix` + +```rust +pub fn purge_prefix(prefix: &str) -> Result +``` + +Deletes all keys owned by the calling application whose names start with `prefix`. + +- Returns the number of keys deleted. + +## Query Parameter Interface + +The example app exposes all cache operations via HTTP query parameters. + +| Query string | Operation | +|---|---| +| `?action=get&key=` | Read a value. `response` is `null` when absent. | +| `?action=set&key=&value=[&ttl=]` | Store a value. Omit `ttl` for no expiry. | +| `?action=delete&key=` | Delete a key. No-op when absent. | +| `?action=exists&key=` | Key membership check. | +| `?action=incr&key=&delta=` | Atomic increment/decrement. Missing key starts at `0`. | +| `?action=expire&key=&ttl=` | Set or update expiry. `response` is `false` when key absent. | +| `?action=purge` | Delete all keys for this app; returns count deleted. | +| `?action=purgePrefix&prefix=` | Delete keys with prefix; returns count deleted. | + +Default action when `action` is omitted: `get`. + +## Response Format + +Successful responses are JSON objects. Shape varies by action: + +**get** +```json +{ "action": "get", "key": "", "response": "" | null } +``` + +**set** +```json +{ "action": "set", "key": "", "value": "", "ttlMs": | null, "response": true } +``` + +**delete** +```json +{ "action": "delete", "key": "", "response": true } +``` + +**exists** +```json +{ "action": "exists", "key": "", "response": true | false } +``` + +**incr** +```json +{ "action": "incr", "key": "", "delta": , "response": } +``` + +**expire** +```json +{ "action": "expire", "key": "", "ttlMs": , "response": true | false } +``` + +**purge** +```json +{ "action": "purge", "response": } +``` + +**purgePrefix** +```json +{ "action": "purgePrefix", "prefix": "", "response": } +``` + +**Error (HTTP 500)** +```json +{ "error": "" } +``` + +## Proxy-wasm Integration + +The app uses the proxy-wasm `HttpContext` trait. Key implementation details: + +- Operations execute in `on_http_response_body`, triggered when `end_of_stream` is `true`. +- The request query string is read via `self.get_property(vec!["request.query"])`. +- Response headers are set in `on_http_response_headers`: removes `content-length`, sets `content-type: application/json`, sets `transfer-encoding: chunked`. +- The upstream response body is replaced entirely using `set_http_response_body`. +- Error status is set via `self.set_property(vec!["response.status"], Some(b"500"))`. +- The app pauses body accumulation (`Action::Pause`) until `end_of_stream` is received. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache.wasm +``` + +## Usage Examples + +```sh +curl 'https:///?action=set&key=hits&value=0&ttl=60000' +curl 'https:///?action=incr&key=hits&delta=1' +curl 'https:///?action=get&key=hits' +``` + +## Constraints and Behavior Notes + +- Cache scope is per-application. Two apps cannot share cache entries. +- `set` with no `ttl` parameter stores the value with no expiry. +- `incr` operates atomically. The key must store a numeric value; if the key is absent it is initialized to `0` before applying `delta`. +- `expire` returns `false` (not an error) when the target key does not exist. +- `purge` and `purge_prefix` return the count of deleted keys, not an error, when no matching keys exist. +- All values are stored and retrieved as bytes (`&[u8]` / `Vec`). String conversion is the caller's responsibility. + +## See Also + +- CDN KV store example (key_value) — named stores, per-store handles, similar operation set +- `fastedge::proxywasm` module — proxy-wasm host service bindings +- proxy-wasm `HttpContext` trait — lifecycle hooks used by CDN apps +- FastEdge CDN app platform overview — app type selection, deployment model 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 e156022..1fa5481 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -38,12 +38,12 @@ image = "0.25" ## Environment Variables (Configuration) -| Variable | Required | Type | Constraints | Default | Description | -|-----------------------|----------|-----------------------------|-----------------|---------|------------------------------------------------------------------| -| `FORMATS_TO_TRANSFORM`| Yes | Comma-separated strings | Non-empty | — | File extensions to convert (e.g., `jpg,jpeg,png`). Note: `jpg` and `jpeg` are distinct entries. | -| `IGNORED_UA_LIST` | No | Comma-separated strings | — | — | User-Agent substrings; requests matching any entry are skipped. | -| `AVIF_SPEED` | No | u8 | 1–10 inclusive | 5 | AVIF encoding speed. Values outside range fall back to default. | -| `AVIF_QUALITY` | No | u8 | 1–100 inclusive | 70 | AVIF encoding quality. Values outside range fall back to default.| +| Variable | Required | Type | Constraints | Default | Description | +|------------------------|----------|-------------------------|------------------|---------|---------------------------------------------------------------------------------------------------| +| `FORMATS_TO_TRANSFORM` | Yes | Comma-separated strings | Non-empty | — | File extensions to convert (e.g., `jpg,jpeg,png`). Note: `jpg` and `jpeg` are distinct entries. | +| `IGNORED_UA_LIST` | No | Comma-separated strings | — | — | User-Agent substrings; requests matching any entry are skipped. | +| `AVIF_SPEED` | No | u8 | 1–10 inclusive | 5 | AVIF encoding speed. Values outside range fall back to default. | +| `AVIF_QUALITY` | No | u8 | 1–100 inclusive | 70 | AVIF encoding quality. Values outside range fall back to default. | Parameter helper behavior: if a variable is absent, empty, non-numeric, or out of range, the default is used and a trace log is emitted. @@ -95,7 +95,7 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action let Ok(ext) = from_utf8(&ext) else { return Action::Continue; }; if ext.is_empty() { return Action::Continue; } ``` - Safe decoding chain: `Option>` → check `None` → decode UTF-8 → check empty. + Safe decoding chain: `Option>` → check `None` → decode UTF-8 → check empty. This extracts the file extension without manual path splitting. 3. Check `FORMATS_TO_TRANSFORM` env var — if the extension is not in the comma-separated list, pass through unchanged. 4. Read `User-Agent` request header — if absent or empty, pass through. 5. Check `IGNORED_UA_LIST` env var — if the UA contains any listed substring, pass through. @@ -243,12 +243,12 @@ Returns `Err(VarError::NotPresent)` if the variable is absent or empty. ## Cross-Hook Signalling Summary -| Signal | Set by | Read by | Mechanism | -|-------------------------|----------------------|---------------------------|------------------------------------------------| -| `Image-Format: original`| request hook (always)| response-headers hook | request header | -| `Image-Format: image/avif` | request hook (on match) | response-headers hook | request header | -| `response.content-type` | response-headers hook| response-body hook | `set_property` / `get_property` | -| `Vary: Image-Format` | response-headers hook| CDN cache | response header | +| Signal | Set by | Read by | Mechanism | +|----------------------------|------------------------------|---------------------------|------------------------------------------| +| `Image-Format: original` | request hook (always) | response-headers hook | request header | +| `Image-Format: image/avif` | request hook (on match) | response-headers hook | request header | +| `response.content-type` | response-headers hook | response-body hook | `set_property` / `get_property` | +| `Vary: Image-Format` | response-headers hook | CDN cache | response header | --- @@ -265,19 +265,19 @@ proxy_wasm::main! {{ ## Error Conditions -| Condition | Behavior | -|----------------------------------------|---------------------------------------------------| -| No file extension in request path | Pass through without transformation | -| Extension not in `FORMATS_TO_TRANSFORM`| Pass through without transformation | -| `FORMATS_TO_TRANSFORM` not set | Pass through without transformation | -| No or empty `User-Agent` header | Pass through without transformation | -| UA matches `IGNORED_UA_LIST` entry | Pass through without transformation | -| Response status not 200 | Pass through without transformation | -| Response status property not exactly 2 bytes | Treat as missing status, pass through | -| No response body (`get_http_response_body` returns None) | Log and continue (original served) | -| Image decode failure (`load_from_memory`) | Log error, serve original body | -| AVIF encode failure | Log error, serve original body | -| `response.content-type` invalid UTF-8 | Send HTTP 500, return `Action::Pause` | +| Condition | Behavior | +|-------------------------------------------------------------------|-----------------------------------------------------| +| No file extension in request path | Pass through without transformation | +| Extension not in `FORMATS_TO_TRANSFORM` | Pass through without transformation | +| `FORMATS_TO_TRANSFORM` not set | Pass through without transformation | +| No or empty `User-Agent` header | Pass through without transformation | +| UA matches `IGNORED_UA_LIST` entry | Pass through without transformation | +| Response status not 200 | Pass through without transformation | +| Response status property not exactly 2 bytes | Treat as missing status, pass through | +| No response body (`get_http_response_body` returns None) | Log and continue (original served) | +| Image decode failure (`load_from_memory`) | Log error, serve original body | +| AVIF encode failure | Log error, serve original body | +| `response.content-type` invalid UTF-8 | Send HTTP 500, return `Action::Pause` | --- @@ -287,3 +287,300 @@ proxy_wasm::main! {{ - scaffold reference for CDN app type - FastEdge SDK Rust host services reference (proxy-wasm ABI, `get_property`, `set_property`) - platform overview (CDN app lifecycle, cache variation with `Vary` headers) + +## Source Material + +### FILE: examples/cdn/convert_image/src/lib.rs + +```rust +use image::*; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::{env, env::VarError, io::Cursor, str::from_utf8}; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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/cors-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-rust.md index 2ae30f6..172455f 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/cors-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -267,3 +267,178 @@ Output binary: `target/wasm32-wasip1/release/cors.wasm` - headers-rust reference (simpler single-hook CDN filter for header manipulation) - deploy skill reference (uploading and registering the compiled WASM binary) - Platform overview reference (environment variable configuration) + +## Source Material + +### FILE: examples/cdn/cors/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating CORS header management. + +Handles preflight OPTIONS requests and adds CORS response headers +for allowed origins. Supports configurable origin allow-lists, +methods, and exposed headers. + +Configuration: + - Environment variable: ALLOWED_ORIGINS (comma-separated origins or "*") + When unset or empty the filter is dormant — requests pass through + without CORS headers, so browsers will block cross-origin access. + - Environment variable: ALLOWED_METHODS (default: "GET, POST, PUT, DELETE, OPTIONS") + - Environment variable: MAX_AGE (default: "86400") + - Environment variable: EXPOSE_HEADERS (response headers to expose) +*/ + +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(CorsRoot) }); +}} + +struct CorsRoot; + +impl Context for CorsRoot {} + +impl RootContext for CorsRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CorsContext)) + } +} + +struct CorsContext; + +impl Context for CorsContext {} + +impl HttpContext for CorsContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let origin = match self.get_http_request_header("Origin") { + Some(o) if !o.is_empty() => o, + _ => return Action::Continue, + }; + + let allowed_origins = env::var("ALLOWED_ORIGINS").unwrap_or_default(); + if !is_origin_allowed(&origin, &allowed_origins) { + return Action::Continue; + } + + // Handle preflight OPTIONS request + let method = self + .get_http_request_header(":method") + .unwrap_or_default(); + + if method == "OPTIONS" { + let allow_methods = env::var("ALLOWED_METHODS") + .unwrap_or_else(|_| "GET, POST, PUT, DELETE, OPTIONS".to_string()); + let allow_headers = self + .get_http_request_header("Access-Control-Request-Headers") + .unwrap_or_else(|| "Content-Type, Authorization".to_string()); + let max_age = env::var("MAX_AGE").unwrap_or_else(|_| "86400".to_string()); + + let effective_origin = if allowed_origins == "*" { + "*".to_string() + } else { + origin + }; + + let mut headers = vec![ + ("Access-Control-Allow-Origin", effective_origin.as_str()), + ("Access-Control-Allow-Methods", allow_methods.as_str()), + ("Access-Control-Allow-Headers", allow_headers.as_str()), + ("Access-Control-Max-Age", max_age.as_str()), + ("Content-Length", "0"), + ]; + + // Vary: Origin is needed when the response varies by origin, + // so shared caches don't serve a cached response for a different origin. + // Not needed when Allow-Origin is "*" (response is the same for all origins). + if effective_origin != "*" { + headers.push(("Vary", "Origin")); + } + + self.send_http_response(204, headers, None); + + return Action::Pause; + } + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let origin = match self.get_http_request_header("Origin") { + Some(o) if !o.is_empty() => o, + _ => return Action::Continue, + }; + + let allowed_origins = env::var("ALLOWED_ORIGINS").unwrap_or_default(); + if !is_origin_allowed(&origin, &allowed_origins) { + return Action::Continue; + } + + let effective_origin = if allowed_origins == "*" { + "*".to_string() + } else { + origin + }; + + self.add_http_response_header("Access-Control-Allow-Origin", &effective_origin); + if effective_origin != "*" { + self.add_http_response_header("Vary", "Origin"); + } + + if let Ok(expose) = env::var("EXPOSE_HEADERS") { + if !expose.is_empty() { + self.add_http_response_header("Access-Control-Expose-Headers", &expose); + } + } + + Action::Continue + } +} + +fn is_origin_allowed(origin: &str, allowed: &str) -> bool { + if allowed.is_empty() { + return false; + } + if allowed == "*" { + return true; + } + allowed.split(',').any(|o| o.trim() == origin) +} +``` + +### FILE: examples/cdn/cors/Cargo.toml + +```toml +[workspace] + +[package] +name = "cors" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/cors/README.md + +``` +[← Back to examples](../../README.md) + +# CORS (CDN) + +Handles Cross-Origin Resource Sharing: preflight OPTIONS responses and CORS headers on all responses for allowed origins. +``` 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 eca26cd..a3463d6 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -310,3 +310,207 @@ Edit `public/styles.css` directly. No build tools required. Styles are embedded - host-services-rust reference (property API) - platform-overview reference (CDN app lifecycle) - body-rust blueprint (general body manipulation pattern) + +## Source Material + +### FILE: examples/cdn/custom_error_pages/src/lib.rs + +```rust +use handlebars::Handlebars; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde_json::json; +use std::collections::HashMap; +use std::env; + +// Include the generated image map +include!(concat!(env!("OUT_DIR"), "/image_map.rs")); +include!(concat!(env!("OUT_DIR"), "/message_map.rs")); + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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/env-secrets-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/env-secrets-rust.md index e8a660a..3da5935 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -175,3 +175,104 @@ println!("PASSWORD: {}", password); - FastEdge app secrets management (platform docs) - proxy-wasm HttpContext trait reference - fastedge crate proxywasm feature documentation + +## Source Material + +### FILE: examples/cdn/variables_and_secrets/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating environment variables and secrets access. + +Reads USERNAME from environment variables and PASSWORD from secrets, +then forwards both as request headers to the upstream origin. + +Required configuration: + - Environment variable: USERNAME + - Secret: PASSWORD +*/ + +use fastedge::proxywasm::secret; +use std::env; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { 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 c505afe..35f839a 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -266,3 +266,185 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) + +## Source Material + +### FILE: examples/geoRedirect/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const BAD_GATEWAY: u32 = 502; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class GeoRedirectRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new GeoRedirect(context_id, this); + } +} + +class GeoRedirect extends Context { + constructor(context_id: u32, root_context: GeoRedirectRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> "); + + const defaultOrigin = getEnv("DEFAULT"); + + if (!defaultOrigin) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured - DEFAULT must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const countryArrBuf = get_property("request.country"); + if (countryArrBuf.byteLength === 0) { + send_http_response( + BAD_GATEWAY, + "bad gateway", + String.UTF8.encode("Missing country information"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + const countryCode = String.UTF8.decode(countryArrBuf); + const countrySpecificOrigin = getEnv(countryCode); + + log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, + ); + + const hostArrBuf = get_property("request.host"); + if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); + } + + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Internal server error - no request path"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const path = String.UTF8.decode(pathArrBuf); + const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; + // remove trailing slashes from the origin + const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; + + const requestUrl = `${cleanedOrigin}${path}`; + + log(LogLevelValues.info, `request-url: ${requestUrl}`); + + set_property("request.url", String.UTF8.encode(requestUrl)); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new GeoRedirectRoot(context_id); +}, "geoRedirect"); +``` + + +### FILE: examples/geoRedirect/package.json + +```json +{ + "name": "fastedge-as-example-georedirect", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Geo Redirect", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/geoRedirect/README.md + +``` +[← Back to examples](../README.md) + +# Geo Redirect + +This application redirects requests to different origin URLs based on the client's country code. + +## What it does + +In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. + +- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. +- Otherwise it falls back to the `DEFAULT` origin. +- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. + +> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. + +> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | +| `` | `DE=https://de.example.com` | Per-country origin URL (optional, one per country) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/geoRedirect.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `DEFAULT` environment variable and any per-country overrides in the application settings. +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md index c0544ac..2b9a311 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -158,10 +158,10 @@ registerRootContext((context_id: u32) => { Both blocked and allowed requests are logged at `INFO` level, providing an audit trail for all traffic decisions. -| Event | Log message | -| ---------------- | ------------------------------------------------ | -| Request blocked | `"geoBlock: blocked request from " + countryStr` | -| Request allowed | `"geoBlock: allowed request from " + countryStr` | +| Event | Log message | +| --------------- | ------------------------------------------------ | +| Request blocked | `"geoBlock: blocked request from " + countryStr` | +| Request allowed | `"geoBlock: allowed request from " + countryStr` | Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/headers-rust.md index 0fb98ea..c275404 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -87,7 +87,6 @@ impl Context for HttpHeaders {} impl HttpContext for HttpHeaders { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { ... } fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { ... } - fn on_log(&mut self) { ... } } ``` @@ -296,18 +295,6 @@ Return `Action::Continue` when processing should proceed normally. --- -## Lifecycle Hook - -```rust -fn on_log(&mut self) { - println!("#{} completed.", self.context_id); -} -``` - -Called after the request/response cycle completes. Use for per-request logging or cleanup. - ---- - ## Constraints - `add_http_request_header` / `add_http_response_header` append new entries; existing entries with the same name are preserved (duplicates allowed). @@ -327,356 +314,3 @@ 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/kv-store-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-rust.md index 451f32a..e705d21 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/kv-store-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -111,11 +111,11 @@ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Ac ## Query Parameter Parsing -Query string is retrieved from the request property path `["request", "query"]`. +Query string is retrieved from the request property path `["request.query"]` (dot-notation single string). ```rust let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); @@ -251,12 +251,12 @@ Replaces the full upstream response body. First argument is offset (always `0`), ## Error Handling -Errors are logged via `println!` and returned as `{"error": ""}` JSON. Response status is set to `500` via the `response.status` property. +Errors are logged via `println!` and returned as `{"error": ""}` JSON. Response status is set to `500` via the `response.status` property (dot-notation single string). ```rust fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); - self.set_property(vec!["response", "status"], Some(b"500")); + 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()); } @@ -292,6 +292,7 @@ fn send_error(&self, msg: &str, body_size: usize) { - `min` and `max` for `zrange` must be parseable as `f64`; any non-numeric string returns an error. - The store name must correspond to a KV store provisioned and bound to the FastEdge app. Attempting to open an unbound store returns an error. - `send_error` uses `println!` for logging (not `proxy_wasm::hostcalls::log`). +- Property paths use dot-notation single strings: `"request.query"` and `"response.status"` (not separate vector elements). ## See Also @@ -299,3 +300,363 @@ fn send_error(&self, msg: &str, body_size: usize) { - proxy-wasm HttpContext trait (on_http_response_headers, on_http_response_body, set_http_response_body) - cdn-base skeleton (RootContext and HttpContext wiring) - platform-overview reference (KV store provisioning and binding) + +## 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) + +This example shows how to read and write data from a FastEdge KV store from a CDN app. +It intercepts the HTTP response, reads the request query string, and executes a KV operation against a named store. + +## What it does + +The app supports the following actions: + +- `get` — fetch one key +- `scan` — list keys matching a pattern +- `zrange` — read sorted-set entries by score range +- `zscan` — list sorted-set entries matching a pattern +- `bfExists` — check whether a Bloom filter contains an item + +The request must include at least: + +- `store=` — KV store name +- `action=` — optional, defaults to `get` + +For each action, the app validates required parameters and returns a JSON response body. + +## Supported query examples + +### Get a key + +```text +?store=my_store&action=get&key=user:42 +``` + +### List keys by pattern + +```text +?store=my_store&action=scan&match=user:* +``` + +### Read a sorted set by score range + +```text +?store=my_store&action=zrange&key=leaderboard&min=0&max=100 +``` + +### Search sorted-set members by pattern + +```text +?store=my_store&action=zscan&key=leaderboard&match=user:* +``` + +### Check a Bloom filter item + +```text +?store=my_store&action=bfExists&key=visitors&item=alice@example.com +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/key_value.wasm +``` + +This example is a CDN app, so it targets `wasm32-wasip1`. + +## Response format + +The app replaces the response body with JSON and sets `content-type: application/json`. +A successful response looks like this: + +```json +{ + "store": "my_store", + "action": "get", + "key": "user:42", + "response": "Alice" +} +``` + +If a required parameter is missing or the KV operation fails, the app responds with an error payload: + +```json +{ + "error": "Missing required param 'key' for 'get' action" +} +``` + +## Notes + +- The app requires query parameters to run. +- If `action` is omitted, it defaults to `get`. +- The code uses `fastedge::proxywasm::key_value::Store` and `proxy_wasm` lifecycle hooks to operate on the KV store. +``` 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 aaa3722..bb51254 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -138,129 +138,3 @@ No additional dependencies beyond the cdn-base skeleton. - cdn-base skeleton reference - proxy-wasm-sdk-as SDK reference (AssemblyScript) - platform-overview reference for environment variable configuration - -## Source Material - -### FILE: examples/largeDictionary/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getDictionary, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class LargeDictionaryRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new LargeDictionaryContext(context_id, this); - } -} - -class LargeDictionaryContext extends Context { - constructor(context_id: u32, root_context: LargeDictionaryRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Use getDictionary for environment variables that may exceed 64KB. - // For normal-sized env vars (< 64KB), use getEnv instead. - const config = getDictionary("LARGE_CONFIG"); - - const size = config.length; - log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); - - stream_context.headers.request.add("x-config-size", size.toString()); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new LargeDictionaryRoot(context_id); -}, "largeDictionary"); -``` - - -### FILE: examples/largeDictionary/package.json - -```json -{ - "name": "fastedge-as-example-large-dictionary", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/largeDictionary/README.md - -``` -[← Back to examples](../README.md) - -# Large Dictionary - -This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. - -## When to use `getDictionary` vs `getEnv` - -| Function | Use when | -|----------|----------| -| `getEnv(name)` | Variable value is under 64 KB (most cases) | -| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | - -The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. - -For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. - -## What it does - -In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -|------|------|-------------| -| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | -| `build/largeDictionary-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md index f6472f8..416db62 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -302,9 +302,12 @@ class Properties extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Error codes 551–559 identify the absent property via the HTTP response status: + // 551=uri 552=host 553=path 554=scheme 555=extension 556=query 557=x_real_ip 558=country 559=city if (!this.handleProperty(REQUEST_URI, 551, "uri", "request-uri")) { return FilterHeadersStatusValues.StopIteration; } + // host must be present for upstream routing; validated but not logged or exposed as a response header if (!this.handleProperty(REQUEST_HOST, 552)) { return FilterHeadersStatusValues.StopIteration; } diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-rust.md index a007b08..116d81c 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/cdn/properties-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -416,19 +416,31 @@ impl HttpContext for PropertiesContext { let params = querystring::querify(query_str); if let Some(url) = params.iter().find_map(|(k, v)| { - if "url".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "url".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_URI], Some(url.as_bytes())); } if let Some(host) = params.iter().find_map(|(k, v)| { - if "host".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "host".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_HOST], Some(host.as_bytes())); } if let Some(path) = params.iter().find_map(|(k, v)| { - if "path".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "path".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_PATH], Some(path.as_bytes())); } 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 0edcedd..6fc190d 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -217,214 +217,3 @@ Response::builder() - fastedge-sdk-rust platform overview - host-services-rust reference (outbound HTTP, environment variables) - best-practices reference (cookie security, entropy sources) - -## Source Material - -### FILE: examples/http/wasi/ab_testing/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Cookie-based A/B testing example. - -Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically -assign the visitor to weighted variants of each configured test, then proxies the -request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-` -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/bloom-filter-denylist-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-ts.md index ba67275..eaa4402 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -176,3 +176,67 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide + +## Source Material + +### FILE: examples/bloom-filter-denylist/src/index.js + +```js +import { getEnv } from 'fastedge::env'; +import { KvStore } from 'fastedge::kv'; + +const BLOOM_KEY = 'blocked-ips'; + +function app(event) { + const storeName = getEnv('DENYLIST_STORE'); + if (!storeName) { + return Response.json( + { error: 'DENYLIST_STORE environment variable is not configured' }, + { status: 500 }, + ); + } + + const ip = event.client.address; + if (!ip) { + return Response.json({ error: 'client address unavailable' }, { status: 500 }); + } + + let blocked; + try { + const store = KvStore.open(storeName); + blocked = store.bfExists(BLOOM_KEY, ip); + } catch (error) { + return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); + } + + if (blocked) { + // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. + // Acceptable for a denylist (you over-block some legitimate users); not acceptable for + // allowlists or anything requiring exact membership — use KvStore.get() for that. + return Response.json({ allowed: false, ip }, { status: 403 }); + } + + return Response.json({ allowed: true, ip }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/bloom-filter-denylist/package.json + +```json +{ + "name": "fastedge-example-bloom-filter-denylist", + "version": "1.0.0", + "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md index c90f65f..0ca1b79 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -193,3 +193,117 @@ 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 914ed37..c10ff23 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -227,7 +227,121 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` ## See Also -- `fastedge::kv` reference — globally replicated key/value storage (use when cross-POP visibility is required) -- `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface +- fastedge::kv reference — globally replicated key/value storage (use when cross-POP visibility is required) +- 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 4a59ac4..82141f5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -315,3 +315,281 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise { + // `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 78af466..93b6a95 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -288,201 +288,3 @@ cargo build --release - http-base skeleton - outbound-http feature blueprint - platform-overview (environment variable configuration) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/ — 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 10afb0e..643e5bd 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -260,3 +260,100 @@ 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 c4395fb..fb3c0c9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -190,3 +190,104 @@ 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-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-ts.md index b4ffbb2..1fba134 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/geo-redirect-ts.md @@ -3,14 +3,14 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- type: feature app_type: http languages: [typescript, javascript] -capabilities: [geo-routing, geo-redirect] +capabilities: [geo-routing] base_skeleton: http-base source_example: FastEdge-sdk-js/examples/geo-redirect --- @@ -113,7 +113,7 @@ addEventListener('fetch', (event) => { - Environment variables are set in the Gcore dashboard or via the API when creating or updating the FastEdge app. - Country-specific origins are configured as environment variables using ISO 3166-1 alpha-2 codes. If no matching variable exists, `BASE_ORIGIN` is used as the fallback. - If `BASE_ORIGIN` is not set, the handler returns HTTP 500 with a descriptive error message. -- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `81145a9a43ec499240c687bd49376ab20c72b11c`). +- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `9c8c7886f0d1ec5ac2296b4080805966a96ca817`). ## Source Material 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 4554a90..b36860f 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -154,119 +154,3 @@ cargo build --release - http-base skeleton reference - FastEdge platform overview (geoip-country-code header injection) - deploy skill reference (uploading WASM binary and setting environment variables) - -## Source Material - -### FILE: examples/http/wasi/geo_redirect/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example WASI-HTTP app demonstrating geo-based redirects. - -Reads the country code from the geoip-country-code request header -and redirects to a country-specific origin URL. Falls back to -BASE_ORIGIN when no country-specific mapping is configured. - -Required configuration: - - Environment variable: BASE_ORIGIN (fallback origin URL) - - Environment variable: (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 54b3f87..108a855 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -127,3 +127,47 @@ 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 a23be87..b6fde53 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -350,218 +350,3 @@ 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/large-env-variable-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/large-env-variable-wasi-rust.md index 0b2a974..11bc794 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/large-env-variable-wasi-rust.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/large-env-variable-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -131,3 +131,92 @@ The crate type must be `cdylib` for WASM/WASI compilation. - http-base reference (base skeleton for WASI HTTP apps) - fastedge-sdk-rust SDK reference (full `fastedge` crate API) - platform-overview reference (environment variable limits and configuration) + +## Source Material + +### FILE: examples/http/wasi/large_env_variable/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example WASI-HTTP app demonstrating access to large environment variables. + +Uses `fastedge::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::dictionary; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + // 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(); + + Ok(Response::builder() + .status(200) + .body(Body::from(format!( + "LARGE_CONFIG loaded: {} bytes", + size + )))?) +} +``` + + +### FILE: examples/http/wasi/large_env_variable/Cargo.toml + +```toml +[workspace] + +[package] +name = "large_env_variable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/large_env_variable/README.md + +``` +[← Back to examples](../../../README.md) + +# Large Environment Variable (WASI) + +Demonstrates how to read **large environment variables** (> 64KB) using `fastedge::dictionary`. + +## When to use `dictionary` vs `std::env` + +| Method | Use when | +|--------|----------| +| `std::env::var("KEY")` | Variable value is under 64KB (most cases) | +| `fastedge::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/mcp-server-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/mcp-server-ts.md index 8a36751..055d200 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -291,7 +291,7 @@ Include verbatim: "@gcoredev/fastedge-sdk-js": "^2.3.0", "@hono/mcp": "^0.2.5", "@modelcontextprotocol/sdk": "^1.29.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "zod": "^4.3.6" ``` 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 aeaede1..6f84bac 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -127,90 +127,3 @@ Both errors propagate via `?` and result in a 500-class response from the FastEd - `outbound-modify-response` (WASI, Rust) — fetches upstream and reshapes the body into a new JSON response - `streaming` (WASI, Rust) — handler that generates its own streaming response without an upstream fetch - `outbound-fetch` (JavaScript) — mirror of this example in the FastEdge SDK JS - -## Source Material - -### FILE: examples/http/wasi/outbound_fetch/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Minimal outbound fetch example. - -Makes a GET request to an upstream HTTP origin and returns the upstream -response verbatim — status, headers, and body pass through unchanged. - -For a variant that reads and transforms the upstream body, see -`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. - -Mirror of the FastEdge-sdk-js `outbound-fetch` example. -*/ - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request) -> 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-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md index c33ea71..eb8fd8f 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -216,82 +216,3 @@ 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/react-with-hono-server-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/react-with-hono-server-ts.md index 3882cd6..50dc4c2 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/react-with-hono-server-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/react-with-hono-server-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -35,7 +35,7 @@ Over `http-base`, add the following to `package.json`. **Runtime dependencies:** ```json "@gcoredev/fastedge-sdk-js": "^2.3.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" ``` @@ -43,7 +43,7 @@ Over `http-base`, add the following to `package.json`. **Dev dependencies:** ```json "@eslint/js": "^9.36.0", -"@hono/node-server": "^1.19.14", +"@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -125,13 +125,13 @@ Script semantics: }, "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25", + "hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" }, "devDependencies": { "@eslint/js": "^9.36.0", - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", 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 5e3fdf9..4da7354 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -208,65 +208,3 @@ 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 cf34807..2ea2f92 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -18,11 +18,11 @@ source_example: FastEdge-sdk-rust/examples/http/wasi/simple_fetch # Simple Fetch (WASI HTTP, Rust) -Outbound HTTP request to a caller-supplied URL, returning the upstream response directly. Uses the WASI-HTTP interface via the `wstd` crate with an async handler. +Outbound HTTP request to a caller-supplied URL, returning the upstream response directly. Supports optional header forwarding from the incoming request to the upstream request. Uses the WASI-HTTP interface via the `wstd` crate with an async handler. ## When to Use -Use this pattern when the app must make an outbound HTTP request to a URL specified by the incoming request (via the `x-fetch-url` header) and return the upstream response directly to the caller without decomposing it. +Use this pattern when the app must make an outbound HTTP request to a URL specified by the incoming request (via the `x-fetch-url` header) and return the upstream response directly to the caller without decomposing it. Optionally forwards a caller-specified header to the upstream request via `x-fetch-header`. ## Crate Dependencies @@ -69,17 +69,30 @@ let target_url = request - `.and_then(|v| v.to_str().ok())` — converts to `Option<&str>`, discarding invalid UTF-8 - `.unwrap_or(default)` — fallback value when header is absent or invalid -### Build Outbound Request +### Build Outbound Request with Conditional Header Forwarding ```rust -let upstream_req = Request::get(&target_url) - .header("accept", "application/json") +let mut builder = Request::get(&target_url).header("accept", "application/json"); + +if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) +{ + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } +} + +let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; ``` - `Request::get(url)` — initiates a GET request builder -- `.header(key, value)` — appends a request header +- `.header(key, value)` — appends a request header; chainable +- `mut builder` — builder is held as a mutable variable to allow conditional header addition before finalizing +- `fetch_header.split_once(':')` — splits `"Key: Value"` into `("Key", " Value")`; `.trim()` strips whitespace - `.body(Body::empty())` — finalizes with an empty body; returns `Result` - `.map_err(...)` — converts builder error into `anyhow::Error` @@ -104,6 +117,7 @@ Ok(response) | Header | Required | Default | Description | |--------|----------|---------|-------------| | `x-fetch-url` | No | `https://httpbin.org/get` | URL to fetch outbound | +| `x-fetch-header` | No | — | A single header to forward to the upstream request, in `Key: Value` format | ## Imports @@ -130,6 +144,7 @@ use wstd::http::{Client, Request, Response}; | Outbound send failure | `map_err` converts to `anyhow::Error`; propagated via `?` | | 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 | +| `x-fetch-header` missing `:` separator | `split_once(':')` returns `None`; header is silently ignored | ## Build @@ -142,6 +157,7 @@ cargo component build --release ```bash curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +curl -H "x-fetch-url: https://httpbin.org/get" -H "x-fetch-header: x-custom-key: myvalue" https:/// ``` ## Source Material @@ -177,8 +193,19 @@ async fn main(request: Request) -> anyhow::Result> { println!("Fetching: {target_url}"); - let upstream_req = Request::get(&target_url) - .header("accept", "application/json") + let mut builder = Request::get(&target_url).header("accept", "application/json"); + + if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) + { + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } + } + + let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; @@ -201,7 +228,7 @@ async fn main(request: Request) -> anyhow::Result> { [package] name = "simple_fetch" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false @@ -266,6 +293,7 @@ cargo build --release - http-base skeleton (base_skeleton for this feature) - outbound-fetch capability reference +- header-driven-routing capability reference - wstd crate (crates.io/crates/wstd) - WASI-HTTP interface specification (WebAssembly/wasi-http) - FastEdge-sdk-rust examples/http/wasi/ for other WASI async patterns diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-ts.md index 9141628..dbd2a9b 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/static-assets-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -312,7 +312,7 @@ footer a { "license": "ISC", "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25" + "hono": "^4.13.5" }, "devDependencies": { "npm-run-all2": "^9.0.2" @@ -348,7 +348,7 @@ Additions over `http-base`: | Package | Type | Version | |---|---|---| | `@gcoredev/fastedge-sdk-js` | runtime | `^2.3.0` | -| `hono` | runtime | `^4.12.25` | +| `hono` | runtime | `^4.13.5` | | `npm-run-all2` | devDependency | `^9.0.2` | ## Key API Patterns 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 053da59..4d314c9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -115,50 +115,3 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference - -## Source Material - -### FILE: examples/streaming/src/index.js - -```js -function app(event) { - const encoder = new TextEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { setTimeout(resolve, 200); }); - controller.enqueue(encoder.encode(`chunk ${i}\n`)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/streaming/package.json - -```json -{ - "name": "fastedge-example-streaming", - "version": "1.0.0", - "description": "FastEdge JS example: streaming response with ReadableStream", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/streaming.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/streaming-wasi-rust.md index 956f361..ca4dcdc 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -141,67 +141,3 @@ 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-ab-testing-ts.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md index ee50bab..eb66618 100644 --- a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md +++ b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,91 +202,3 @@ addEventListener('fetch', (event) => { - http-base skeleton (base event handler structure and build setup) - fastedge-build CLI reference (WASM compilation) - Handlebars documentation (template syntax and compilation API) - -## Source Material - -### FILE: examples/template-invoice-ab-testing/src/index.js - -```javascript -import Handlebars from 'handlebars'; - -import { getStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { getLogoBrand } 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({ request }) { - const isAbTestLogo = request.headers.get('ab-test-logo') === 'bottle'; - const logo = getLogoBrand(isAbTestLogo); - - const abTestFont = request.headers.get('ab-test-font'); - const cssStyles = getStyles(abTestFont); - - const rawHtmlTemplate = htmlTemplate(abTestFont); - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logo, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/template-invoice-ab-testing/package.json - -```json -{ - "name": "fastedge-example-template-invoice-ab-testing", - "version": "1.0.0", - "description": "FastEdge JS example: Handlebars invoice with A/B test header variants", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice-ab-testing.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` 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 36b8b19..8538296 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,86 +202,3 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference - -## Source Material - -### FILE: examples/template-invoice/src/index.js - -```js -import Handlebars from 'handlebars'; - -import { cssStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { logoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler() { - const rawHtmlTemplate = htmlTemplate(); - - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logoBrand, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler()); -}); -``` - -### FILE: examples/template-invoice/package.json - -```json -{ - "name": "fastedge-example-template-invoice", - "version": "1.0.0", - "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge-codex/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md index 00d44d9..df8a8c7 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -135,89 +135,3 @@ cargo build --release - deploy skill reference (uploading the compiled `.wasm` binary) - manage skill reference (setting environment variables and secrets on an app) - FastEdge platform overview (secret storage model) - -## Source Material - -### FILE: examples/http/wasi/variables_and_secrets/src/lib.rs - -```rust -use fastedge::secret; -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request) -> 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 7e558fc..bca0623 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.10", + "version": "0.2.11", "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 fe30668..9845658 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-09-09T13:28:16.418Z", + "generated_at": "2026-09-24T09:24:50.834Z", "source": { "repo": "fastedge-plugin", - "commit": "99dc55a", + "commit": "503a8fc", "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn-apps-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -29,9 +29,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", + "content_sha256": "6a79f809ecedc76ed7617ab078604096a5933da07a44a2059849f2042a42910a", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -58,9 +58,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "rust", "sdk", @@ -150,9 +150,9 @@ "0.2", "log", "0.4", + "cache", "secrets", - "dictionary", - "depend" + "dictionary" ] }, { @@ -245,7 +245,7 @@ "callback", "phase", "description", - "-----------------------------------------------------------------", + "-----------------------------------------------------------------------------------------------", "----------------", "---------------------------------------------------", "on_http_request_headers", @@ -256,11 +256,7 @@ "action", "request", "headers", - "inspect", - "modify", - "forwarding", - "on_http_request_body", - "body_size" + "inspect" ] }, { @@ -431,7 +427,7 @@ "level": 2, "anchor": "host-services-for-cdn-apps", "line_start": 423, - "line_end": 733, + "line_end": 818, "keywords": [ "host", "services", @@ -477,8 +473,8 @@ "heading": "API Comparison: HTTP vs CDN", "level": 2, "anchor": "api-comparison-http-vs-cdn", - "line_start": 734, - "line_end": 746, + "line_start": 819, + "line_end": 832, "keywords": [ "api", "comparison", @@ -491,12 +487,11 @@ "proxywasm", "-------------", "-------------------------------------------------------------------", - "--------------------------------------------------------", + "-----------------------------------------------------------------", "key-value", "fastedge", "key_value", - "store", - "secrets" + "store" ] }, { @@ -504,8 +499,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 747, - "line_end": 751, + "line_start": 833, + "line_end": 837, "keywords": [ "see", "sdk", @@ -2071,7 +2066,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2093,9 +2088,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2105,7 +2100,7 @@ "requests", "against" ], - "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", + "content_sha256": "6c28a1664d639ebeb12397dea7294bdd293848795b65c88c2f5aab81e2bee614", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2122,9 +2117,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2369,56 +2364,6 @@ "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" - ] } ] }, @@ -5638,10 +5583,416 @@ } ] }, + { + "id": "cdn-examples-cache-rust", + "title": "CDN Cache — Rust Example", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cache", + "example", + "overview", + "app", + "demonstrating", + "operations" + ], + "content_sha256": "4c6cf165f6d058b5f6d401c04abf933162f423e82a02a2b81d3a9f1ada41c285", + "sections": [ + { + "id": "cdn-examples-cache-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 11, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cdn", + "cache", + "rust", + "example" + ] + }, + { + "id": "cdn-examples-cache-rust#overview", + "heading": "Overview", + "level": 2, + "anchor": "overview", + "line_start": 12, + "line_end": 17, + "keywords": [ + "overview", + "cdn", + "app", + "demonstrating", + "cache", + "operations", + "via", + "proxy-wasm", + "abi.", + "scoped", + "calling", + "application", + "addressed", + "key", + "alone", + "there", + "named", + "stores", + "handles", + "unlike", + "store", + "dispatched", + "query", + "parameters", + "incoming", + "request.", + "responses", + "always", + "json.", + "errors", + "return", + "http", + "500", + "error", + "message" + ] + }, + { + "id": "cdn-examples-cache-rust#app-type", + "heading": "App Type", + "level": 2, + "anchor": "app-type", + "line_start": 18, + "line_end": 23, + "keywords": [ + "app", + "type", + "interface", + "proxy-wasm", + "fastedge", + "proxywasm", + "cache", + "cdn", + "language", + "rust" + ] + }, + { + "id": "cdn-examples-cache-rust#request-interface", + "heading": "Request Interface", + "level": 2, + "anchor": "request-interface", + "line_start": 24, + "line_end": 38, + "keywords": [ + "request", + "interface", + "operations", + "specified", + "via", + "query", + "parameters.", + "action", + "parameter", + "selects", + "operation", + "get", + "default", + "omitted.", + "string", + "---", + "key", + "read", + "cached", + "value.", + "response", + "null", + "absent.", + "set", + "value", + "ttl", + "store", + "omitting", + "stores", + "expiry.", + "delete", + "key.", + "no-op", + "exists" + ] + }, + { + "id": "cdn-examples-cache-rust#api-reference", + "heading": "API Reference", + "level": 2, + "anchor": "api-reference", + "line_start": 39, + "line_end": 195, + "keywords": [ + "api", + "reference", + "functions", + "fastedge", + "proxywasm", + "cache", + "get", + "rust", + "pub", + "key", + "str", + "result", + "option", + "vec", + "retrieve", + "cached", + "bytes", + "key.", + "parameters", + "string", + "returns", + "exists", + "none", + "absent", + "err", + "failure", + "json", + "response", + "shape", + "action", + "value", + "null", + "---", + "set" + ] + }, + { + "id": "cdn-examples-cache-rust#error-handling", + "heading": "Error Handling", + "level": 2, + "anchor": "error-handling", + "line_start": 196, + "line_end": 202, + "keywords": [ + "error", + "handling", + "errors", + "return", + "http", + "500", + "body", + "message", + "missing", + "required", + "query", + "parameters", + "produce", + "descriptive", + "messages", + "e.g.", + "param", + "key", + "get", + "action", + "invalid", + "parameter", + "values", + "non-integer", + "ttl", + "delta", + "parse", + "messages.", + "unknown", + "listing", + "supported", + "actions." + ] + }, + { + "id": "cdn-examples-cache-rust#dependencies", + "heading": "Dependencies", + "level": 2, + "anchor": "dependencies", + "line_start": 203, + "line_end": 211, + "keywords": [ + "dependencies", + "toml", + "proxy-wasm", + "0.2", + "fastedge", + "path", + "...", + "features", + "proxywasm", + "querystring", + "1.1", + "serde_json" + ] + }, + { + "id": "cdn-examples-cache-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 212, + "line_end": 218, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache.wasm" + ] + }, + { + "id": "cdn-examples-cache-rust#lifecycle-notes", + "heading": "Lifecycle Notes", + "level": 2, + "anchor": "lifecycle-notes", + "line_start": 219, + "line_end": 225, + "keywords": [ + "lifecycle", + "notes", + "app", + "uses", + "proxy-wasm", + "rootcontext", + "httpcontext", + "pattern.", + "logic", + "executes", + "on_http_response_body", + "waits", + "end_of_stream", + "on_http_response_headers", + "strips", + "content-length", + "sets", + "content-type", + "application", + "json", + "transfer-encoding", + "chunked", + "body", + "processing.", + "query", + "string", + "read", + "request.query", + "via", + "get_property" + ] + }, + { + "id": "cdn-examples-cache-rust#key-constraints", + "heading": "Key Constraints", + "level": 2, + "anchor": "key-constraints", + "line_start": 226, + "line_end": 232, + "keywords": [ + "key", + "constraints", + "cache", + "operations", + "scoped", + "calling", + "application", + "keys", + "one", + "app", + "accessible", + "another.", + "there", + "named", + "stores", + "handles", + "sole", + "address.", + "ttl", + "values", + "always", + "milliseconds", + "delta", + "incr", + "signed", + "64-bit", + "integer", + "i64", + "missing", + "treated" + ] + }, + { + "id": "cdn-examples-cache-rust#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 233, + "line_end": 238, + "keywords": [ + "see", + "fastedge-sdk-rust", + "cdn", + "key_value", + "example", + "named", + "store", + "handles", + "contrast", + "cache", + "api", + "examples", + "overview", + "proxy-wasm", + "httpcontext", + "rootcontext", + "trait", + "documentation" + ] + } + ] + }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5663,9 +6014,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "example", "convert", "image", @@ -5675,7 +6026,7 @@ "avif", "format" ], - "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", + "content_sha256": "c9e876d444727919f19871887b51f11a25b8653ddc28e5f19f241a707cadd786", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5692,9 +6043,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "cdn", "example", "convert", @@ -6159,122 +6510,6 @@ "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" - ] } ] }, @@ -7457,7 +7692,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7479,9 +7714,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7491,7 +7726,7 @@ "response-headers", "response-body" ], - "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", + "content_sha256": "db4273297015e84837b705d18e071b4684410000b7bc9fcf363fe82c1c58ea15", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7508,9 +7743,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7940,266 +8175,125 @@ "hook", "sequencing" ] - }, + } + ] + }, + { + "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 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "updated", + "2026-08-17", + "custom", + "error", + "pages", + "overview", + "intercepts", + "4xx", + "5xx", + "http" + ], + "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", + "sections": [ { - "id": "cdn-examples-custom-error-pages-as#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 296, - "line_end": 501, + "id": "cdn-examples-custom-error-pages-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, "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" + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "updated", + "2026-08-17" ] }, { - "id": "cdn-examples-custom-error-pages-as#what-it-does", - "heading": "What it does", + "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", + "heading": "Custom Error Pages — CDN (Rust)", "level": 2, - "anchor": "what-it-does", - "line_start": 502, - "line_end": 514, + "anchor": "custom-error-pages-—-cdn-rust", + "line_start": 10, + "line_end": 297, "keywords": [ - "onresponseheaders", - "app", - "reads", - "response.status", - "property.", - "400-599", - "range", - "sets", - "content-type", - "text", - "html", - "prepares", - "body", - "replacement.", - "onresponsebody", - "buffers", - "full", - "response", + "custom", + "error", + "pages", + "cdn", + "rust", + "overview", + "intercepts", + "4xx", + "5xx", + "http", + "responses", + "edge", "replaces", + "their", + "bodies", + "branded", + "html", + "rendered", + "via", + "handlebars", + "templates.", + "use", + "pattern", + "you", + "consistent", "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", + "served", + "regardless", + "origin", + "returns.", "build", - "customerrorpages.wasm", - "fastedge", - "portal", - "attach", - "your", - "cdn", - "application.", - "environment", - "variables", - "secrets", - "required." + "script", + "build.rs", + "runs", + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] }, { - "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 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "cdn" - ], - "languages": [ - "rust" - ], - "tags": [ - "examples", - "fastedge-docs", - "cdn", - "rust", - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-17", - "custom", - "error", - "pages", - "overview", - "intercepts", - "4xx", - "5xx", - "http" - ], - "content_sha256": "e6824b6d988f328a00704b636f67d6243f930c7954b0c5cdeda3e795ff9166b0", - "sections": [ - { - "id": "cdn-examples-custom-error-pages-rust#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, - "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-17" - ] - }, - { - "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", - "heading": "Custom Error Pages — CDN (Rust)", - "level": 2, - "anchor": "custom-error-pages-—-cdn-rust", - "line_start": 10, - "line_end": 297, - "keywords": [ - "custom", - "error", - "pages", - "cdn", - "rust", - "overview", - "intercepts", - "4xx", - "5xx", - "http", - "responses", - "edge", - "replaces", - "their", - "bodies", - "branded", - "html", - "rendered", - "via", - "handlebars", - "templates.", - "use", - "pattern", - "you", - "consistent", - "styled", - "served", - "regardless", - "origin", - "returns.", - "build", - "script", - "build.rs", - "runs", - "compile", - "time", - "embed", - "images", - "messages", - "public" - ] - } - ] - }, - { - "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-20 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", + "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-20 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -11806,7 +11900,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11830,9 +11924,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11842,7 +11936,7 @@ "headers", "request-headers" ], - "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", + "content_sha256": "eb268cb0cde0417bb082dbadb140da4f26f837195f2c6d93a907bc1643fc243e", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11859,9 +11953,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11999,9 +12093,11 @@ "method", "signature", "description", - "-------------------", - "-----------------------------------------------", - "--------------------------------------------------------------------------------------------------------" + "--------------------------", + "---------------------------------------", + "-----------------------------------------------------------------------------------------------------", + "get", + "nam" ] }, { @@ -12837,8 +12933,8 @@ }, { "id": "cdn-examples-http-call-as", - "title": "HTTP Call", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", + "title": "cdn-examples-http-call-as", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12862,9 +12958,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "overview", "httpcall", "dispatches", @@ -12875,7 +12971,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", + "content_sha256": "5fd3877e09d4896bd6c69ed4c0cb9cc56dc6cd0d8fadb01c02391285f4b8bce6", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12892,9 +12988,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -13166,209 +13262,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13390,9 +13290,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "makes", @@ -13402,7 +13302,7 @@ "services", "timeout" ], - "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", + "content_sha256": "5e665b5b4838fd593db0ee7e992ade82f31afd9c62cf39aa13075f9ffdf2b6ee", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13419,9 +13319,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "cdn", @@ -13823,7 +13723,7 @@ "level": 2, "anchor": "complete-example", "line_start": 247, - "line_end": 359, + "line_end": 360, "keywords": [ "complete", "example", @@ -13863,8 +13763,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 360, - "line_end": 371, + "line_start": 361, + "line_end": 372, "keywords": [ "gotchas", "action", @@ -13913,8 +13813,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 372, - "line_end": 378, + "line_start": 373, + "line_end": 379, "keywords": [ "see", "proxy-wasm", @@ -14615,7 +14515,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14637,9 +14537,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14650,7 +14550,7 @@ "example", "app_type" ], - "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", + "content_sha256": "23c7f8c100db8a05f960e1382c3a9933d52661da3f5ad5a253cfd75e1d720c90", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14667,9 +14567,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14826,7 +14726,7 @@ "level": 2, "anchor": "gotchas", "line_start": 223, - "line_end": 237, + "line_end": 239, "keywords": [ "gotchas", "get", @@ -14875,8 +14775,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 238, - "line_end": 243, + "line_start": 240, + "line_end": 245, "keywords": [ "related", "host", @@ -14911,13 +14811,250 @@ "vs.", "differences" ] + }, + { + "id": "cdn-examples-kv-store-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 246, + "line_end": 518, + "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-kv-store-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 519, + "line_end": 535, + "keywords": [ + "app", + "supports", + "following", + "actions", + "get", + "fetch", + "one", + "key", + "scan", + "list", + "keys", + "matching", + "pattern", + "zrange", + "read", + "sorted-set", + "entries", + "score", + "range", + "zscan", + "bfexists", + "check", + "whether", + "bloom", + "filter", + "contains", + "item", + "request", + "include", + "least", + "store", + "name", + "action", + "operation", + "optional", + "defaults", + "validates", + "required", + "parameters", + "returns" + ] + }, + { + "id": "cdn-examples-kv-store-rust#supported-query-examples", + "heading": "Supported query examples", + "level": 2, + "anchor": "supported-query-examples", + "line_start": 536, + "line_end": 567, + "keywords": [ + "supported", + "query", + "examples", + "get", + "key", + "text", + "store", + "my_store", + "action", + "user", + "list", + "keys", + "pattern", + "scan", + "match", + "read", + "sorted", + "set", + "score", + "range", + "zrange", + "leaderboard", + "min", + "max", + "100", + "search", + "sorted-set", + "members", + "zscan", + "check", + "bloom", + "filter", + "item", + "bfexists", + "visitors", + "alice", + "examp" + ] + }, + { + "id": "cdn-examples-kv-store-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 568, + "line_end": 576, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "key_value.wasm", + "example", + "cdn", + "app", + "targets" + ] + }, + { + "id": "cdn-examples-kv-store-rust#response-format", + "heading": "Response format", + "level": 2, + "anchor": "response-format", + "line_start": 577, + "line_end": 598, + "keywords": [ + "response", + "format", + "app", + "replaces", + "body", + "json", + "sets", + "content-type", + "application", + "successful", + "looks", + "like", + "store", + "my_store", + "action", + "get", + "key", + "user", + "alice", + "required", + "parameter", + "missing", + "operation", + "fails", + "responds", + "error", + "payload", + "param" + ] + }, + { + "id": "cdn-examples-kv-store-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 599, + "line_end": 605, + "keywords": [ + "notes", + "app", + "requires", + "query", + "parameters", + "run.", + "action", + "omitted", + "defaults", + "get", + "code", + "uses", + "fastedge", + "proxywasm", + "key_value", + "store", + "proxy_wasm", + "lifecycle", + "hooks", + "operate", + "store." + ] } ] }, { "id": "cdn-examples-large-dictionary-as", "title": "Large Dictionary — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 updated: 2026-05-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14939,9 +15076,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-05-20", + "2026-09-22", "large", "dictionary", "overview", @@ -14950,7 +15087,7 @@ "environment", "variables" ], - "content_sha256": "6d6681e4b50286f8686c5abb48674711db1f35f9090ae69be2f77d3fbce085f7", + "content_sha256": "06c3e8963a2738ac48fbbfbf6217cded6950009199039727a0ed53963ab8474d", "sections": [ { "id": "cdn-examples-large-dictionary-as#introduction", @@ -14967,9 +15104,9 @@ "ref", "master", "commit", - "60f25c7bd35564e5bafb421be7f37aa4acf1bf81", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-05-20", + "2026-09-22", "large", "dictionary", "assemblyscript", @@ -15273,6 +15410,205 @@ "patterns", "fastedge-docs" ] + }, + { + "id": "cdn-examples-large-dictionary-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 140, + "line_end": 223, + "keywords": [ + "source", + "material", + "file", + "examples", + "largedictionary", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getdictionary", + "setloglevel", + "fastedge", + "class", + "largedictionaryroot", + "extends", + "createcontext", + "context_id", + "u32", + "con" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#when-to-use-getdictionary-vs-getenv", + "heading": "When to use `getDictionary` vs `getEnv`", + "level": 2, + "anchor": "when-to-use-getdictionary-vs-getenv", + "line_start": 224, + "line_end": 234, + "keywords": [ + "use", + "getdictionary", + "getenv", + "function", + "----------", + "name", + "variable", + "value", + "under", + "cases", + "exceed", + "wasi", + "env", + "var", + "size", + "limit", + "environment", + "interface", + "per", + "variable.", + "your", + "app", + "needs", + "read", + "larger", + "values", + "e.g.", + "large", + "json", + "configs", + "pem", + "certificates", + "policy", + "documents", + "calls", + "proxy_dictionary_get", + "bypasses", + "thi" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 235, + "line_end": 238, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "large_config", + "environment", + "variable", + "using", + "getdictionary", + "logs", + "size", + "adds", + "x-config-size", + "request", + "header", + "upstream", + "see." + ] + }, + { + "id": "cdn-examples-large-dictionary-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 239, + "line_end": 246, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "large_config", + "environment", + "variable", + "large", + "payload", + "e.g.", + "json", + "pem", + "certificate" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 247, + "line_end": 260, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "largedictionary.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "largedictionary-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 261, + "line_end": 265, + "keywords": [ + "deploy", + "upload", + "build", + "largedictionary.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "large_config", + "environment", + "variable", + "application", + "settings." + ] } ] }, @@ -18244,7 +18580,7 @@ { "id": "host-services-rust", "title": "Rust Host Services Reference", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/host-services-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -18263,9 +18599,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "host", "services", "reference", @@ -18276,7 +18612,7 @@ "storage", "secret" ], - "content_sha256": "702508848c5aadcd4ee98b9fa6c0ead4ed2085d84e0c321143f5bc626f376ece", + "content_sha256": "3c1895152a7a969acdb7c1f5e670bd6441f6b3d44fadf5a8640adab15cc3185e", "sections": [ { "id": "host-services-rust#introduction", @@ -18293,9 +18629,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "rust", "host", "services", @@ -18443,6 +18779,13 @@ "change", "lifetime", "deployment.", + "config", + "key_value", + "persistent", + "data", + "secret", + "encrypted", + "credentials.", "api", "rust", "pub", @@ -18462,15 +18805,7 @@ "decoded", "utf-8.", "environment", - "variables", - "set", - "deployment", - "time", - "via", - "platform", - "same", - "management", - "mechanism" + "vari" ] }, { @@ -20588,7 +20923,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20610,9 +20945,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -20622,7 +20957,7 @@ "outbound-http", "query-params" ], - "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", + "content_sha256": "653bed0caf43133e7cf62e0930264bec8ced4e99d9db75ad00188b1f618c266d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20639,9 +20974,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21063,207 +21398,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21285,9 +21426,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21297,7 +21438,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", + "content_sha256": "a447a165a864934a9567ab24a9258a36cc441050690b08e0afe96a7a9523a505", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21314,9 +21455,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21769,13 +21910,195 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 304, + "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": 305, + "line_end": 309, + "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": 310, + "line_end": 314, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "bloom-filter-denylist" + ] } ] }, { "id": "http-examples-cache-basic-rust", "title": "Cache Basic (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21797,9 +22120,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -21809,7 +22132,7 @@ "cache", "basic" ], - "content_sha256": "fcc0c42a053e0104b03a420b039b53f6e58b97cb14c7f0f77c1dfb5fad31d840", + "content_sha256": "58aa39343f1d956e9ab8ab5ea82eba707a1033fac18ca3f195c40b96b7b3016b", "sections": [ { "id": "http-examples-cache-basic-rust#introduction", @@ -21826,9 +22149,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -22221,13 +22544,199 @@ "selection", "guidance" ] + }, + { + "id": "http-examples-cache-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 244, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "cache-aside", + "pattern", + "via", + "interface.", + "request", + "builds", + "key", + "path.", + "returns", + "cached", + "body", + "immediately", + "hit", + "x-cache", + "miss", + "generates", + "response", + "stores", + "environment", + "variables", + "cache_ttl_ms", + "long" + ] + }, + { + "id": "http-examples-cache-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 245, + "line_end": 250, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "cache_ttl_ms", + "long", + "cache", + "response", + "milliseconds.", + "default", + "30000" + ] + }, + { + "id": "http-examples-cache-basic-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 251, + "line_end": 259, + "keywords": [ + "works", + "get", + "api", + "data", + "cache", + "miss", + "generate", + "body", + "store", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "page", + "request-path", + "unique", + "path", + "gets", + "own", + "entry.", + "response", + "simple", + "html", + "includes", + "request", + "stand", + "expensive", + "computation", + "template", + "render." + ] + }, + { + "id": "http-examples-cache-basic-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 260, + "line_end": 269, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "html", + "x-cache", + "hit", + "miss", + "doctype", + "..." + ] + }, + { + "id": "http-examples-cache-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 270, + "line_end": 276, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache_basic.wasm" + ] + }, + { + "id": "http-examples-cache-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 277, + "line_end": 282, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry" + ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22251,9 +22760,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "module", @@ -22263,7 +22772,7 @@ "2.3.0", "app" ], - "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", + "content_sha256": "c53a54daf9307b32c0aa69a72792c852be660acff331a69288bf9425b6a194a6", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22280,9 +22789,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "javascript", @@ -22611,63 +23120,13 @@ "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: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22689,9 +23148,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "type", "example", @@ -22701,7 +23160,7 @@ "cache", "outbound-http" ], - "content_sha256": "33e95d6ca4043fc5f8bb4df2c22b92866c6b96101908f3a79c3d245bdde5b4c0", + "content_sha256": "7c58be673c4adce9e4ac6ad2928c95a42a645cf231c1f09d901ed10534e327cc", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -22718,9 +23177,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "type", "example", @@ -23069,201 +23528,13 @@ "workflow", "skill" ] - }, - { - "id": "http-examples-cache-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 192, - "line_end": 359, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "cache", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "response", - "caching", - "purging", - "via", - "interface.", - "reads", - "origin_host", - "environment", - "forwards", - "incoming", - "request", - "origin", - "caches", - "body", - "keyed", - "path.", - "subsequent", - "requests", - "same", - "path", - "cached", - "returned" - ] - }, - { - "id": "http-examples-cache-wasi-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 360, - "line_end": 366, - "keywords": [ - "configuration", - "env", - "var", - "required", - "description", - "---", - "origin_host", - "yes", - "base", - "url", - "upstream", - "origin", - "e.g.", - "https", - "api.example.com", - "returns", - "500", - "unset.", - "cache_ttl_ms", - "long", - "cache", - "responses", - "milliseconds.", - "default", - "60000" - ] - }, - { - "id": "http-examples-cache-wasi-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 367, - "line_end": 373, - "keywords": [ - "works", - "get", - "data", - "cache", - "miss", - "forward", - "origin_host", - "2xx", - "body", - "200", - "x-cache", - "hit", - "return", - "cached", - "key", - "path", - "query", - "only", - "responses", - "origin", - "error", - "pass", - "without", - "stored.", - "response", - "headers", - "replayed", - "cache-hit", - "use", - "content-type", - "application", - "octet-stream", - "sinc" - ] - }, - { - "id": "http-examples-cache-wasi-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 374, - "line_end": 378, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip2", - "release", - "cache_wasi.wasm" - ] - }, - { - "id": "http-examples-cache-wasi-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 379, - "line_end": 385, - "keywords": [ - "apis", - "used", - "fastedge", - "cache", - "get", - "key", - "retrieve", - "cached", - "bytes", - "returns", - "option", - "vec", - "set", - "ttl_ms", - "store", - "optional", - "ttl", - "milliseconds", - "none", - "means", - "expiry", - "wstd", - "http", - "client", - "new", - ".send", - "req", - ".await", - "async", - "outbound", - "request", - "origin" - ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-17 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -23285,9 +23556,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23297,7 +23568,7 @@ "languages", "diagnostic" ], - "content_sha256": "02dee0dd08a94591960bdd8f9c75f9702f9a6993c982331e93831f946db54abb", + "content_sha256": "e0b1cfb99fd9375902a7ab410213218de42cf836f17460e19e3cef595ebc872b", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -23314,9 +23585,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-17", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23817,342 +24088,125 @@ "store", "usage" ] - }, + } + ] + }, + { + "id": "http-examples-fetch-js", + "title": "http-examples-fetch-js", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-fetch-js.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "http" + ], + "languages": [ + "javascript" + ], + "tags": [ + "examples", + "fastedge-docs", + "http", + "javascript", + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23", + "fetch", + "outbound", + "requests", + "overview", + "fastedge", + "supports", + "via", + "standard", + "url" + ], + "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", + "sections": [ { - "id": "http-examples-diagnostic-logging-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 194, - "line_end": 307, + "id": "http-examples-fetch-js#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "diagnostic_logging", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "diagnostic", - "logging", - "example.", - "tiny", - "pass-through", - "proxy", - "writes", - "single", - "set_user_diag", - "tag", - "per", - "request", - "summarising", - "outcome", - "config_error", - "origin_unreachable", - "proxied", - "appears", - "fastedge", - "platform", - "per-request", - "log", - "viewer", - "distinct", - "stdout" + "auto-updated", + "true", + "sources", + "fastedge-sdk-js", + "ref", + "main", + "commit", + "81145a9a43ec499240c687bd49376ab20c72b11c", + "updated", + "2026-07-23" ] }, { - "id": "http-examples-diagnostic-logging-wasi-rust#configuration", - "heading": "Configuration", + "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", + "heading": "fetch — Outbound HTTP Requests", "level": 2, - "anchor": "configuration", - "line_start": 308, - "line_end": 311, + "anchor": "fetch-—-outbound-http-requests", + "line_start": 10, + "line_end": 93, "keywords": [ - "configuration", - "origin_url", - "environment", - "variable", - "origin", + "fetch", + "outbound", + "http", "requests", - "proxied", - "to." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", - "heading": "Outcomes", - "level": 2, - "anchor": "outcomes", - "line_start": 312, - "line_end": 321, - "keywords": [ - "outcomes", - "request", - "writes", - "exactly", - "one", - "condition", - "tag", - "---", - "origin_url", - "missing", - "outcome", - "config_error", - "reason", - "origin_missing", - "origin", - "unreachable", - "origin_unreachable", - "method", - "path", - "err", - "proxied", - "status" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", - "heading": "`set_user_diag` vs `println!`", - "level": 2, - "anchor": "set_user_diag-vs-println", - "line_start": 322, - "line_end": 330, - "keywords": [ - "set_user_diag", - "println", - "---", - "channel", - "stdout", - "general", - "application", - "logs", - "per-request", - "structured", - "tag", - "platform", - "log", - "viewer", - "cardinality", - "many", - "per", - "request", - "one", - "multiple", - "calls", - "leave", - "only", - "last", - "concatenated", - "undefined", - "best", - "verbose", - "traces", - "debug", - "details", - "single", - "filterable", - "outcome", - "label", - "forbidden", - "secrets", - "pii", - "tags", - "appear" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#convention", - "heading": "Convention", - "level": 2, - "anchor": "convention", - "line_start": 331, - "line_end": 336, - "keywords": [ - "convention", - "call", - "set_user_diag", - "once", - "branch", - "late", - "enough", - "handler", - "know", - "outcome.", - "logfmt", - "-ish", - "format", - "outcome", - "verb", - "key", - "value", - "easy", - "slice", - "log", - "search", - "tooling", - "keep", - "keys", - "short", - "stable", - "they", - "make", - "good", - "filter", - "terms." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#related", - "heading": "Related", - "level": 2, - "anchor": "related", - "line_start": 337, - "line_end": 342, - "keywords": [ - "related", - "cdn", - "proxy-wasm", - "variant", + "overview", "fastedge", - "proxywasm", - "utils", - "set_user_diag", - "same", - "semantics", - "different", - "module", - "path.", - "see", - "docs", - "cdn_apps.md" + "supports", + "via", + "standard", + "url", + "options", + "api.", + "runtime", + "exposes", + "subset", + "browser", + "api", + "sufficient", + "downstream", + "service", + "calls.", + "---", + "signature", + "string", + "requestinit", + "promise", + "response", + "parameters", + "parameter", + "type", + "required", + "description", + "-----------", + "------", + "----------", + "-------------", + "yes", + "absolute", + "resou" ] } ] }, { - "id": "http-examples-fetch-js", - "title": "http-examples-fetch-js", + "id": "http-examples-geo-redirect-js", + "title": "http-examples-geo-redirect-js", "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-fetch-js.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "http" - ], - "languages": [ - "javascript" - ], - "tags": [ - "examples", - "fastedge-docs", - "http", - "javascript", - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23", - "fetch", - "outbound", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url" - ], - "content_sha256": "5961ad4a2a280ee1220e96b8a1cbd47606052b9f07d61c565da785d71a81abfe", - "sections": [ - { - "id": "http-examples-fetch-js#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, - "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-js", - "ref", - "main", - "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", - "updated", - "2026-07-23" - ] - }, - { - "id": "http-examples-fetch-js#fetch-—-outbound-http-requests", - "heading": "fetch — Outbound HTTP Requests", - "level": 2, - "anchor": "fetch-—-outbound-http-requests", - "line_start": 10, - "line_end": 93, - "keywords": [ - "fetch", - "outbound", - "http", - "requests", - "overview", - "fastedge", - "supports", - "via", - "standard", - "url", - "options", - "api.", - "runtime", - "exposes", - "subset", - "browser", - "api", - "sufficient", - "downstream", - "service", - "calls.", - "---", - "signature", - "string", - "requestinit", - "promise", - "response", - "parameters", - "parameter", - "type", - "required", - "description", - "-----------", - "------", - "----------", - "-------------", - "yes", - "absolute", - "resou" - ] - } - ] - }, - { - "id": "http-examples-geo-redirect-js", - "title": "http-examples-geo-redirect-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-07-23 -->", - "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -29647,7 +29701,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-07-23 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29669,9 +29723,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -29681,7 +29735,7 @@ "request-inspection", "header-iteration" ], - "content_sha256": "2301b014adb12b2ba4442aef724edf0a892cda4df9240c2959f515a28095883b", + "content_sha256": "b33f1a09f2dca08a64f23ac6419b228e8de755a66bc4de6a66fffadee231998c", "sections": [ { "id": "http-examples-print-basic-rust#introduction", @@ -29698,9 +29752,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-07-23", + "2026-09-22", "---", "type", "example", @@ -30097,209 +30151,13 @@ "building", "patterns" ] - }, - { - "id": "http-examples-print-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 151, - "line_end": 214, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "print", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "result", - "fastedge", - "body", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "mut", - "string", - "method", - ".to_string", - "body.push_str", - "req.method", - ".as_str", - "nurl", - "req.uri", - "nheaders" - ] - }, - { - "id": "http-examples-print-basic-rust#what-it-demonstrates", - "heading": "What it demonstrates", - "level": 2, - "anchor": "what-it-demonstrates", - "line_start": 215, - "line_end": 222, - "keywords": [ - "demonstrates", - "reading", - "request", - "method", - "via", - "req.method", - ".as_str", - "uri", - "req.uri", - ".to_string", - "iterating", - "headers", - "req.headers", - "handling", - "non-utf-8", - "header", - "values", - "gracefully", - "match", - "v.to_str", - "building", - "plain-text", - "response", - "builder", - "body", - "..." - ] - }, - { - "id": "http-examples-print-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 223, - "line_end": 233, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "-----", - "---------", - "req.method", - ".as_str", - "http", - "method", - "string", - "slice", - "req.uri", - ".to_string", - "full", - "request", - "uri", - "req.headers", - "iterator", - "over", - "headername", - "headervalue", - "pairs", - "v.to_str", - "decode", - "header", - "value", - "str", - "returns", - "err", - "non-utf-8", - "response", - "builder", - ".status", - "...", - ".body", - "build", - "body", - "create" - ] - }, - { - "id": "http-examples-print-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 234, - "line_end": 240, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "print.wasm" - ] - }, - { - "id": "http-examples-print-basic-rust#expected-behaviour", - "heading": "Expected behaviour", - "level": 2, - "anchor": "expected-behaviour", - "line_start": 241, - "line_end": 259, - "keywords": [ - "expected", - "behaviour", - "request", - "response", - "body", - "plain-text", - "dump", - "details", - "method", - "get", - "url", - "path", - "query", - "value", - "headers", - "host", - "example.com", - "accept", - "...", - "status", - "200", - "content", - "plain", - "text", - "content-type", - "header", - "set", - "explicitly", - "platform", - "add", - "one", - "appears", - "own", - "line", - "indented", - "four", - "spaces", - "non-utf-8", - "values", - "replaced" - ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30323,9 +30181,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30334,7 +30192,7 @@ "fastedge", "apps" ], - "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", + "content_sha256": "08a57d5d0098acb645f51c29d3f5850d1dad67ec7d808d89c78453d0323ec904", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30351,9 +30209,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30787,356 +30645,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31158,9 +30673,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31170,7 +30685,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", + "content_sha256": "72cff95ae062275b261acaf37d03d72affe87facf10960cd16531237ff6bd4cf", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31187,9 +30702,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31628,247 +31143,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31890,9 +31171,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "example", @@ -31902,7 +31183,7 @@ "fastedge", "platform" ], - "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", + "content_sha256": "1d1ae1d9f636c28df70129574cad04baf9b39ec87e640e902ed814f97a07d13b", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31919,9 +31200,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "rust", @@ -32311,13 +31592,284 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 368, + "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": 369, + "line_end": 378, + "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": 379, + "line_end": 389, + "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": 390, + "line_end": 399, + "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": 400, + "line_end": 406, + "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": 407, + "line_end": 416, + "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": 417, + "line_end": 434, + "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32339,9 +31891,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32352,7 +31904,7 @@ "rotation", "using" ], - "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", + "content_sha256": "2cd00d58e3f6047c79cb57ea352f8e2684f86dec50a7357dadfe62d4bf0d8151", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32369,9 +31921,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32627,7 +32179,7 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 178, + "line_end": 177, "keywords": [ "implementation", "pattern", @@ -32653,20 +32205,19 @@ "result", "read", "slot", + "x-slot", "header", - "default", + "defaulting", "current", - "unix", "timestamp", "let", "u32", ".headers", ".get", - "x-slot", ".and_then", "v.to_str", ".ok", - "v.p" + ".and" ] }, { @@ -32674,8 +32225,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 179, - "line_end": 186, + "line_start": 178, + "line_end": 185, "keywords": [ "constraints", "gotchas", @@ -32724,8 +32275,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 187, - "line_end": 193, + "line_start": 186, + "line_end": 192, "keywords": [ "build", "cargo", @@ -32742,8 +32293,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 194, - "line_end": 200, + "line_start": 193, + "line_end": 199, "keywords": [ "see", "fastedge", @@ -32774,7 +32325,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32796,9 +32347,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32808,7 +32359,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", + "content_sha256": "ff728af48b322c2e67a2a06dd8e555818382d4535f559f6cf83b2f14e6732db5", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -32825,9 +32376,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32901,7 +32452,7 @@ "level": 2, "anchor": "request-headers", "line_start": 30, - "line_end": 35, + "line_end": 36, "keywords": [ "request", "headers", @@ -32923,7 +32474,14 @@ "httpbin.org", "get", "fetch", - "outbound" + "outbound", + "x-fetch-header", + "key", + "value", + "additional", + "forward", + "parsed", + "separator" ] }, { @@ -32931,8 +32489,8 @@ "heading": "Behavior", "level": 2, "anchor": "behavior", - "line_start": 36, - "line_end": 42, + "line_start": 37, + "line_end": 45, "keywords": [ "behavior", "read", @@ -32947,27 +32505,33 @@ "get", "absent", "unparseable.", - "build", + "begin", + "building", "outbound", "url", "accept", "application", "json", "header.", + "x-fetch-header", + "present", + "parseable", + "key", + "value", + "split", + "first", + "add", + "builder.", + "finalize", + "builder", + ".body", + "body", + "empty", "send", "via", "client", "new", - ".send", - "req", - ".await", - "return", - "upstream", - "response", - "body", - "directly", - "caller", - "decomposition." + ".send" ] }, { @@ -32975,8 +32539,8 @@ "heading": "Key API Patterns", "level": 2, "anchor": "key-api-patterns", - "line_start": 43, - "line_end": 99, + "line_start": 46, + "line_end": 122, "keywords": [ "key", "api", @@ -33025,8 +32589,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 100, - "line_end": 109, + "line_start": 123, + "line_end": 132, "keywords": [ "logging", "rust", @@ -33049,8 +32613,8 @@ "heading": "Cargo.toml Requirements", "level": 2, "anchor": "cargotoml-requirements", - "line_start": 110, - "line_end": 126, + "line_start": 133, + "line_end": 149, "keywords": [ "cargo.toml", "requirements", @@ -33083,8 +32647,8 @@ "heading": "WASI vs Basic HTTP Comparison", "level": 2, "anchor": "wasi-vs-basic-http-comparison", - "line_start": 127, - "line_end": 135, + "line_start": 150, + "line_end": 158, "keywords": [ "wasi", "basic", @@ -33121,8 +32685,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 136, - "line_end": 143, + "line_start": 159, + "line_end": 168, "keywords": [ "gotchas", "url", @@ -33151,19 +32715,17 @@ "values", "silently", "discarded", - "anyhow", - "declared", - "dependency", - "use", - "macro.", - "examples", - "http", - "wasi", - "same", - "async", - "main", - "wstd", - "http_server" + "x-fetch-header", + "parsed", + ".split_once", + "only", + "first", + "used", + "separator", + "containing", + "preserved", + "correctly.", + "build" ] }, { @@ -33171,8 +32733,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 144, - "line_end": 150, + "line_start": 169, + "line_end": 175, "keywords": [ "see", "fastedge-docs", @@ -33192,6 +32754,186 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 176, + "line_end": 271, + "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": 272, + "line_end": 277, + "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": 278, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 306, + "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" + ] } ] }, @@ -34267,7 +34009,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34289,9 +34031,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34301,7 +34043,7 @@ "streaming", "async" ], - "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", + "content_sha256": "fcd87ce4100ea3343d0f2b4543cce45409c480b856f3ded1838b4d752a43e3ea", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34318,9 +34060,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34649,6 +34391,144 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, @@ -37079,6 +36959,348 @@ } ] }, + { + "id": "platform-cdn-filter-runtime", + "title": "CDN Filter Runtime — Capabilities and Traps", + "description": "What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a dep", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items" + ], + "content_sha256": "6279754b693e44807306ef152f9b733bb0a575ece2390a0bccb5755b2be9799e", + "sections": [ + { + "id": "platform-cdn-filter-runtime#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items", + "here", + "invisible", + "local", + "tests", + "only", + "surface", + "deployed", + "app.", + "claim", + "carries", + "provenance", + "tag", + "meaning", + "---", + "live", + "measured", + "against", + "real", + "resource", + "source" + ] + }, + { + "id": "platform-cdn-filter-runtime#capability-boundary-—-filter-vs-http-app", + "heading": "Capability boundary — filter vs HTTP app", + "level": 2, + "anchor": "capability-boundary-—-filter-vs-http-app", + "line_start": 16, + "line_end": 41, + "keywords": [ + "capability", + "boundary", + "filter", + "http", + "app", + "core", + "wasm", + "module", + "linked", + "against", + "proxy-wasm", + "env", + "abi.", + "different", + "execution", + "world", + "subset", + "it.", + "designs", + "assume", + "symmetry", + "http-app", + "sdk", + "fail", + "deploy.", + "cdn", + "---", + "read", + "same", + "six", + "operations", + "open", + "get", + "scan", + "zrange_by_score", + "zscan", + "bf_exists", + "source", + "write", + "api" + ] + }, + { + "id": "platform-cdn-filter-runtime#reading-the-request-—-use-properties-not-headers", + "heading": "Reading the request — use properties, not headers", + "level": 2, + "anchor": "reading-the-request-—-use-properties-not-headers", + "line_start": 42, + "line_end": 69, + "keywords": [ + "reading", + "request", + "use", + "properties", + "headers", + "several", + "proxy-wasm", + "developer", + "reach", + "mangled", + "absent.", + "failures", + "silent", + "live", + "read", + "via", + "result", + "---", + "property", + "request.host", + "clean", + "client-requested", + "host", + "origin", + "rewrite", + "edge", + "tier", + "shield", + "nodes", + "carry", + "shield_", + "prefix", + "request.x_real_ip", + "true", + "client", + "ipv4", + "ipv6", + "request.path", + "includes", + "query" + ] + }, + { + "id": "platform-cdn-filter-runtime#security-—-gating-filters", + "heading": "Security — gating filters", + "level": 2, + "anchor": "security-—-gating-filters", + "line_start": 70, + "line_end": 105, + "keywords": [ + "security", + "gating", + "filters", + "cdn", + "normalise", + "path", + "filter", + "runs", + "live", + "traversal", + "separator", + "sequences", + "arrive", + "verbatim", + "bash", + "curl", + "--path-as-is", + "...", + "app", + "test", + "request.path", + "bypasses", + "auth", + "prefix", + "public", + "wave", + "admin", + "origin", + "back", + "authentication", + "bypass" + ] + }, + { + "id": "platform-cdn-filter-runtime#synthetic-responses", + "heading": "Synthetic responses", + "level": 2, + "anchor": "synthetic-responses", + "line_start": 106, + "line_end": 111, + "keywords": [ + "synthetic", + "responses", + "live", + "filter", + "serve", + "complete", + "response", + "via", + "send_http_response", + "status", + "arbitrary", + "headers", + "multiple", + "set-cookie", + "one", + "bodies", + "least", + "without", + "truncation.", + "single-app", + "designs", + "challenge", + "pages", + "interstitials", + "error", + "second", + "http", + "app.", + "---" + ] + }, + { + "id": "platform-cdn-filter-runtime#what-local-testing-cannot-prove", + "heading": "What local testing cannot prove", + "level": 2, + "anchor": "what-local-testing-cannot-prove", + "line_start": 112, + "line_end": 117, + "keywords": [ + "local", + "testing", + "cannot", + "prove", + "gcoredev", + "fastedge-test", + "runs", + "proxy-wasm", + "filters", + "locally", + "proves", + "flow", + "logic", + "routing", + "header", + "manipulation", + "gate", + "decisions.", + "host", + "behaviour", + "property", + "semantics", + "mangling", + "path", + "non-normalisation", + "instantiation", + "failures", + "cache", + "interaction.", + "trap", + "page", + "passes", + "suite", + "green.", + "confirm", + "host-dependent", + "deployed", + "app", + "attached", + "real" + ] + }, + { + "id": "platform-cdn-filter-runtime#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 118, + "line_end": 123, + "keywords": [ + "see", + "storage.md", + "cache", + "semantics", + "limits", + "measured", + "performance", + "cdn-integration.md", + "attaching", + "filters", + "cdn", + "resources", + "error-codes.md", + "530", + "diagnosis" + ] + } + ] + }, { "id": "platform-cdn-integration", "title": "CDN Integration — Attaching FastEdge Apps to CDN Resources", @@ -37117,7 +37339,7 @@ "effect", "app" ], - "content_sha256": "6cb942fb6413ec2d1faf71699c0e79ea10e04ccc089ed9e49aee100e7a6623fe", + "content_sha256": "e36fbf654e7eb8d595bd084a7fa6577ef61114287351099e7ccaa99cdf0f9eb6", "sections": [ { "id": "platform-cdn-integration#introduction", @@ -37272,7 +37494,7 @@ "level": 2, "anchor": "reading-and-updating", "line_start": 157, - "line_end": 185, + "line_end": 202, "keywords": [ "reading", "updating", @@ -37310,8 +37532,8 @@ "heading": "Operational Notes", "level": 2, "anchor": "operational-notes", - "line_start": 186, - "line_end": 195, + "line_start": 203, + "line_end": 212, "keywords": [ "operational", "notes", @@ -37358,8 +37580,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 196, - "line_end": 203, + "line_start": 213, + "line_end": 220, "keywords": [ "see", "rust", @@ -37436,7 +37658,7 @@ "distinguish", "edge" ], - "content_sha256": "d47de15e41e07d9b842b6690558f41b840a4121f7ec91a85c4d86d1dc966a436", + "content_sha256": "e0561d0b112db5f5f509a1ce16da82dedfa2cea834283fcf1b6102a118136876", "sections": [ { "id": "platform-error-codes#introduction", @@ -37489,7 +37711,7 @@ "level": 2, "anchor": "530-—-app-initialization-failed", "line_start": 9, - "line_end": 29, + "line_end": 38, "keywords": [ "530", "app", @@ -37501,35 +37723,36 @@ "start", "processing", "request.", + "signal", + "cdn", + "fails", + "instantiate", + "returns", + "x-cdn-internal-status", + "3100", + "header", + "distinguishes", + "ran", + "errored", + "531", "common", "causes", - "missing", - "required", - "environment", - "variables", - "reads", - "corrupted", - "invalid", + "unresolved", + "imports", + "apps", "binary", - "compiled", - "wrong", - "target", - "wasm32-wasip1", - "large", - "platform", - "rejects", - "uploads", - "beyond", - "50mb", - "investigate", - "bundled", - "debugging", - "steps", - "verify", - "built", - "correctly", - "fastedge-build", - "src" + "host", + "function", + "proxy-wasm", + "provide", + "commonly", + "http-app", + "component-model", + "api", + "used", + "filter", + "e.g.", + "top-leve" ] }, { @@ -37537,8 +37760,8 @@ "heading": "531 — Runtime Error", "level": 2, "anchor": "531-—-runtime-error", - "line_start": 30, - "line_end": 61, + "line_start": 39, + "line_end": 71, "keywords": [ "531", "runtime", @@ -37554,6 +37777,19 @@ "processing.", "common", "causes", + "calling", + "getenv", + "getsecret", + "module", + "top", + "level", + "they", + "request-time", + "only", + "call", + "them", + "inside", + "handler", "uncaught", "javascript", "typeerror", @@ -37566,20 +37802,7 @@ "err", "failed", "fetch", - "call", - "without", - "handling", - "json", - "parse", - "malformed", - "body", - "accessing", - "undefined", - "properties", - "debugging", - "steps", - "test", - "locally" + "without" ] }, { @@ -37587,8 +37810,8 @@ "heading": "532 — Timeout Exceeded", "level": 2, "anchor": "532-—-timeout-exceeded", - "line_start": 62, - "line_end": 93, + "line_start": 72, + "line_end": 103, "keywords": [ "532", "timeout", @@ -37637,8 +37860,8 @@ "heading": "533 — Memory Limit Exceeded", "level": 2, "anchor": "533-—-memory-limit-exceeded", - "line_start": 94, - "line_end": 122, + "line_start": 104, + "line_end": 132, "keywords": [ "533", "memory", @@ -37687,8 +37910,8 @@ "heading": "General Debugging Strategy", "level": 2, "anchor": "general-debugging-strategy", - "line_start": 123, - "line_end": 131, + "line_start": 133, + "line_end": 141, "keywords": [ "general", "debugging", @@ -38283,7 +38506,7 @@ "instantiates", "app" ], - "content_sha256": "98c4a8456ad2cedcb63fe13b4f4556e3b0a8b09ab2a621f0111a59b942d65cb8", + "content_sha256": "350d18b69668668c510056232b304d8bb3d9d3354bfc1493438b059d93d586b6", "sections": [ { "id": "platform-overview#introduction", @@ -38485,7 +38708,7 @@ "level": 2, "anchor": "request-lifecycle", "line_start": 93, - "line_end": 101, + "line_end": 109, "keywords": [ "request", "lifecycle", @@ -38518,7 +38741,15 @@ "instance", "destroyed", "memory", - "freed" + "freed", + "scheduled", + "deferred", + "work", + "there", + "scheduler", + "durable", + "background", + "execution." ] }, { @@ -38526,8 +38757,8 @@ "heading": "Resource Limits", "level": 2, "anchor": "resource-limits", - "line_start": 102, - "line_end": 115, + "line_start": 110, + "line_end": 123, "keywords": [ "resource", "limits", @@ -38576,8 +38807,8 @@ "heading": "Plans", "level": 2, "anchor": "plans", - "line_start": 116, - "line_end": 120, + "line_start": 124, + "line_end": 128, "keywords": [ "plans", "basic", @@ -38599,8 +38830,8 @@ "heading": "App Statuses", "level": 2, "anchor": "app-statuses", - "line_start": 121, - "line_end": 128, + "line_start": 129, + "line_end": 136, "keywords": [ "app", "statuses", @@ -38620,8 +38851,8 @@ "heading": "Networking", "level": 2, "anchor": "networking", - "line_start": 129, - "line_end": 135, + "line_start": 137, + "line_end": 143, "keywords": [ "networking", "apps", @@ -38652,8 +38883,8 @@ "heading": "Logging & Monitoring", "level": 2, "anchor": "logging-&-monitoring", - "line_start": 136, - "line_end": 141, + "line_start": 144, + "line_end": 149, "keywords": [ "logging", "monitoring", @@ -38679,6 +38910,261 @@ } ] }, + { + "id": "platform-storage", + "title": "Storage Semantics — KV Store and Cache", + "description": "Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your lan", + "path": "plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/storage.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance." + ], + "content_sha256": "65116f9b1f97e5e14b18ff2a6af86b58e926d192ad3101290e37fd23e8db5d3e", + "sections": [ + { + "id": "platform-storage#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance.", + "applies", + "http", + "apps", + "cdn", + "filters", + "unless", + "noted.", + "per-language", + "api", + "see", + "reference", + "your", + "language.", + "---", + "scope", + "global", + "account-level", + "assigned", + "per-pop", + "shared" + ] + }, + { + "id": "platform-storage#kv-store", + "heading": "KV Store", + "level": 2, + "anchor": "kv-store", + "line_start": 16, + "line_end": 58, + "keywords": [ + "store", + "get", + "cached", + "read", + "per", + "node", + "live", + "production", + "key", + "populates", + "per-node", + "cache", + "entry", + "lives", + "whether", + "existed", + "until", + "expires", + "keeps", + "returning", + "old", + "result", + "value", + "absent", + "now", + "exists.", + "sorted-set", + "reads", + "zrangebyscore", + "reflect", + "writes", + "write", + "visible", + "---", + "none", + "create", + "heavy", + "pol" + ] + }, + { + "id": "platform-storage#cache", + "heading": "Cache", + "level": 2, + "anchor": "cache", + "line_start": 59, + "line_end": 82, + "keywords": [ + "cache", + "interface", + "atomicity", + "source", + "get", + "set", + "delete", + "exists", + "incr", + "expire", + "purge", + "purge_prefix", + "only", + "atomic", + "primitive", + "compare-and-set", + "setnx", + "scripting", + "multi-key", + "operations", + "scan", + "coordination", + "expressible", + "increment", + "compare", + "returned", + "value", + "ttl", + "rules", + "live", + "ttls", + "silently", + "clamped", + "deployment", + "ceiling", + "default", + "days", + "rust", + "sdk", + "references" + ] + }, + { + "id": "platform-storage#measured-performance", + "heading": "Measured performance", + "level": 2, + "anchor": "measured-performance", + "line_start": 83, + "line_end": 103, + "keywords": [ + "measured", + "performance", + "live", + "production", + "point-in-time", + "one", + "account", + "mostly", + "pop", + "evidence", + "guarantees.", + "metric", + "value", + "---", + "cache.incr", + "p50", + "p95", + "max", + "102", + "cache.expire", + "write", + "visible", + "prior", + "get", + "polling", + "107", + "min", + "rate", + "client", + "sustained", + "writes", + "sorted-set", + "ingest", + "450", + "members", + "knee", + "concurrent", + "writers", + "largest", + "single" + ] + }, + { + "id": "platform-storage#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 104, + "line_end": 108, + "keywords": [ + "see", + "cdn-filter-runtime.md", + "cdn", + "filter", + "access", + "overview.md", + "account-level", + "stores", + "resource", + "limits" + ] + } + ] + }, { "id": "quickstart", "title": "FastEdge Quickstart", diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md index 8d0191c..0cf28dc 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/SKILL.md @@ -69,7 +69,9 @@ The reference directory is organised in two layers: - `./reference/platform/overview.md` — Architecture, PoPs, app types, request lifecycle, resource limits - `./reference/platform/error-codes.md` — 530–533 debugging strategies -- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), public-route disable pattern +- `./reference/platform/cdn-filter-runtime.md` — **MANDATORY before designing, writing, or reviewing any CDN app (proxy-wasm filter).** Filter-vs-HTTP-app capability boundary, which request properties/headers are reliable, path non-normalisation (auth-bypass risk), filters-before-cache, what local tests cannot prove. +- `./reference/platform/storage.md` — **Read before designing anything that uses KV or Cache.** KV per-node read caching, write API shape, missing operations, Cache TTL/`incr` rules, failure rate, measured performance, preprod-vs-production. +- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), API merge semantics, purge after rule changes, public-route disable pattern - `./reference/platform/operations.md` — Operational knobs with time-bounded behaviour (the 30-min `debug` logging toggle) - `./reference/platform/best-practices.md` — Agent-quality guidance: confirmation discipline, scaffold-first, TDD loop, resource preconditions, observation vs. request, ask-don't-guess - `./reference/platform/as-constraints.md` — **MANDATORY before writing or reviewing any AssemblyScript CDN app code.** Hard compile-time and runtime constraints where AssemblyScript diverges from TypeScript. Violating these produces wasm that traps at runtime or silently returns wrong values — not a compiler error. 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 bdad5fe..42303a5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) @@ -46,7 +46,7 @@ proxy-wasm = "0.2" log = "0.4" ``` -**Tier 2 — CDN app with FastEdge host services** (KV, secrets, dictionary): +**Tier 2 — CDN app with FastEdge host services** (KV, cache, secrets, dictionary): ```toml [package] @@ -195,12 +195,12 @@ impl HttpContext for HelloWorld { ## Lifecycle Callbacks -| Callback | Phase | Description | -| ----------------------------------------------------------------- | ---------------- | --------------------------------------------------- | -| `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 | +| Callback | Phase | Description | +| ----------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------- | +| `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. @@ -508,6 +508,91 @@ impl HttpContext for RateLimitFilter { } ``` +### Cache (`fastedge::proxywasm::cache`) + +Provides ephemeral cache storage, implemented by the host. Unlike `key_value::Store`, cache operations are not scoped to a named store or handle — every function is a free function keyed directly, and every entry is scoped to the calling application. + +```rust,ignore +pub fn get(key: &str) -> Result>, Error> +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> +pub fn delete(key: &str) -> Result<(), Error> +pub fn exists(key: &str) -> Result +pub fn incr(key: &str, delta: i64) -> Result +pub fn expire(key: &str, ttl_ms: u64) -> Result +pub fn purge() -> Result +pub fn purge_prefix(prefix: &str) -> Result +``` + +| Function | Return Type | Description | +| ---------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------- | +| `get(key: &str)` | `Result>, Error>` | Get the value for a key; `None` if the key does not exist | +| `set(key: &str, value: &[u8], ttl_ms: Option)` | `Result<(), Error>` | Set a value with an optional expiry; `None` means no expiry | +| `delete(key: &str)` | `Result<(), Error>` | Delete a key; a no-op if the key does not exist | +| `exists(key: &str)` | `Result` | Test whether a key exists | +| `incr(key: &str, delta: i64)` | `Result` | Atomically increment (or decrement) an integer value; returns the new value | +| `expire(key: &str, ttl_ms: u64)` | `Result` | Set or update a key's expiry; `false` if the key does not exist | +| `purge()` | `Result` | Delete all cache entries owned by the calling application | +| `purge_prefix(prefix: &str)` | `Result` | Delete all cache entries whose key begins with `prefix` | + +`purge()` and `purge_prefix()` both return the number of keys that were deleted. + +#### `Error` + +```rust,ignore +pub enum Error { + AccessDenied, + InternalError, + Other(String), +} +``` + +| Variant | Description | +| --------------- | ----------------------------------------------------------- | +| `AccessDenied` | The application does not have access to the specified cache | +| `InternalError` | An unexpected internal error occurred | +| `Other(String)` | An implementation-specific error (e.g., I/O failure) | + +#### Example — cache a computed value in the response headers phase + +```rust,no_run +use fastedge::proxywasm::cache; +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(CacheRoot) }); +}} + +struct CacheRoot; +impl Context for CacheRoot {} +impl RootContext for CacheRoot { + fn get_type(&self) -> Option { Some(ContextType::HttpContext) } + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CacheFilter)) + } +} + +struct CacheFilter; +impl Context for CacheFilter {} + +impl HttpContext for CacheFilter { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + match cache::get("key-3338664") { + Ok(Some(_cached)) => { + // reuse the cached value + } + Ok(None) => { + // store the value for 5 minutes + let _ = cache::set("key-3338664", b"value", Some(300_000)); + } + Err(_) => {} + } + Action::Continue + } +} +``` + ### Secret Management (`fastedge::proxywasm::secret`) Provides access to encrypted secrets stored in the FastEdge platform. @@ -733,16 +818,17 @@ The `log` crate macros (`info!`, `warn!`, `error!`, etc.) work when `proxy_wasm: ## API Comparison: HTTP vs CDN -| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | -| ------------- | ------------------------------------------------------------------- | -------------------------------------------------------- | -| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | -| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | -| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | -| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | -| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value) | -| Cargo feature | None required | `features = ["proxywasm"]` | -| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | -| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | +| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | +| ------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | +| Cache | `fastedge::cache` | `fastedge::proxywasm::cache` | +| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | +| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | +| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | +| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value, cache) | +| Cargo feature | None required | `features = ["proxywasm"]` | +| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | +| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | ## See Also 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 5aeb1a4..a001d60 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # API Key Validation — CDN (Rust) @@ -159,120 +159,3 @@ 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-cache-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-rust.md new file mode 100644 index 0000000..8a65925 --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-cache-rust.md @@ -0,0 +1,237 @@ + + +# CDN Cache — Rust Example + +## Overview + +CDN app demonstrating cache operations via the proxy-wasm ABI. All operations are scoped to the calling application and addressed by key alone — there are no named stores or handles (unlike the KV store). + +Operations are dispatched via query parameters on the incoming request. Responses are always JSON. Errors return HTTP 500 with `{"error": ""}`. + +## App Type + +- **Interface**: proxy-wasm (`fastedge::proxywasm::cache`) +- **App type**: CDN +- **Language**: Rust + +## Request Interface + +All operations are specified via query parameters. The `action` parameter selects the operation; `action=get` is the default when omitted. + +| Query string | Operation | +|---|---| +| `?action=get&key=` | Read a cached value. `response` is `null` when the key is absent. | +| `?action=set&key=&value=[&ttl=]` | Store a value. Omitting `ttl` stores with no expiry. | +| `?action=delete&key=` | Delete a key. No-op when the key is absent. | +| `?action=exists&key=` | Test key membership. | +| `?action=incr&key=&delta=` | Atomic increment. `delta` may be negative. A missing key starts at `0`. | +| `?action=expire&key=&ttl=` | Set or update a key's TTL. `response` is `false` when the key is absent. | +| `?action=purge` | Delete every key owned by this app. Returns count of deleted keys. | +| `?action=purgePrefix&prefix=` | Delete app-owned keys starting with `prefix`. Returns count deleted. | + +## API Reference + +All functions are in `fastedge::proxywasm::cache`. + +### `cache::get` + +```rust +pub fn get(key: &str) -> Result>, _> +``` + +Retrieve cached bytes by key. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(Some(Vec))` if the key exists, `Ok(None)` if absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "get", "key": "", "response": "" } +{ "action": "get", "key": "", "response": null } +``` + +--- + +### `cache::set` + +```rust +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), _> +``` + +Store bytes under a key with an optional TTL. + +- **Parameters**: + - `key` — cache key string + - `value` — byte slice to store + - `ttl_ms` — TTL in milliseconds; `None` means no expiry +- **Returns**: `Ok(())` on success, `Err` on failure +- **Constraint**: `ttl_ms` must be a positive integer when provided; invalid values produce an error response + +**JSON response shape:** +```json +{ "action": "set", "key": "", "value": "", "ttlMs": |null, "response": true } +``` + +--- + +### `cache::delete` + +```rust +pub fn delete(key: &str) -> Result<(), _> +``` + +Remove a key from the cache. No-op if the key does not exist. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(())` on success, `Err` on failure + +**JSON response shape:** +```json +{ "action": "delete", "key": "", "response": true } +``` + +--- + +### `cache::exists` + +```rust +pub fn exists(key: &str) -> Result +``` + +Test whether a key is present in the cache. + +- **Parameters**: `key` — cache key string +- **Returns**: `Ok(true)` if present, `Ok(false)` if absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "exists", "key": "", "response": true|false } +``` + +--- + +### `cache::incr` + +```rust +pub fn incr(key: &str, delta: i64) -> Result +``` + +Atomically increment (or decrement) a cached counter. If the key does not exist, it is treated as `0` before applying the delta. + +- **Parameters**: + - `key` — cache key string + - `delta` — signed 64-bit integer; may be negative for decrements +- **Returns**: `Ok(new_value)` — the value after increment, `Err` on failure + +**JSON response shape:** +```json +{ "action": "incr", "key": "", "delta": , "response": } +``` + +--- + +### `cache::expire` + +```rust +pub fn expire(key: &str, ttl_ms: u64) -> Result +``` + +Set or update the TTL on an existing key. + +- **Parameters**: + - `key` — cache key string + - `ttl_ms` — TTL in milliseconds; must be a positive integer +- **Returns**: `Ok(true)` if the key existed and was updated, `Ok(false)` if the key was absent, `Err` on failure + +**JSON response shape:** +```json +{ "action": "expire", "key": "", "ttlMs": , "response": true|false } +``` + +--- + +### `cache::purge` + +```rust +pub fn purge() -> Result +``` + +Delete all keys owned by the calling application. + +- **Parameters**: none +- **Returns**: `Ok(count)` — number of keys deleted, `Err` on failure + +**JSON response shape:** +```json +{ "action": "purge", "response": } +``` + +--- + +### `cache::purge_prefix` + +```rust +pub fn purge_prefix(prefix: &str) -> Result +``` + +Delete all app-owned keys whose names start with the given prefix. + +- **Parameters**: `prefix` — key prefix string +- **Returns**: `Ok(count)` — number of keys deleted, `Err` on failure + +**JSON response shape:** +```json +{ "action": "purgePrefix", "prefix": "", "response": } +``` + +--- + +## Error Handling + +- All errors return HTTP 500 with body `{"error": ""}`. +- Missing required query parameters produce descriptive error messages, e.g. `"Missing required param 'key' for 'get' action"`. +- Invalid parameter values (e.g. non-integer `ttl`, non-integer `delta`) produce descriptive parse error messages. +- Unknown `action` values produce an error listing all supported actions. + +## Dependencies + +```toml +proxy-wasm = "0.2" +fastedge = { path = "...", features = ["proxywasm"] } +querystring = "1.1" +serde_json = "1" +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache.wasm +``` + +## Lifecycle Notes + +- The app uses the proxy-wasm `RootContext` + `HttpContext` pattern. +- Logic executes in `on_http_response_body` (waits for `end_of_stream`). +- `on_http_response_headers` strips `content-length`, sets `content-type: application/json`, and sets `transfer-encoding: chunked` before body processing. +- Query string is read from `request.query` via `get_property`. + +## Key Constraints + +- Cache operations are scoped to the calling application — keys from one app are not accessible to another. +- There are no named stores or handles; the key is the sole address. +- `ttl` values are always in **milliseconds**. +- `delta` for `incr` is a signed 64-bit integer (`i64`); a missing key is treated as `0`. + +## See Also + +- fastedge-sdk-rust CDN key_value example (named KV store with handles, contrast to this cache API) +- fastedge-sdk-rust CDN examples overview +- proxy-wasm `HttpContext` and `RootContext` trait documentation 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 cf31db4..9f4fdd9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # CDN Example: Convert Image (Rust) @@ -202,300 +202,3 @@ Transformation is skipped (returning `Action::Continue` without modifying the re - FastEdge CDN app platform overview - FastEdge SDK Rust reference - FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/convert_image/src/lib.rs - -```rust -use image::*; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::{env, env::VarError, io::Cursor, str::from_utf8}; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); -}} - -struct ConvertImageRoot; - -impl Context for ConvertImageRoot {} - -impl RootContext for ConvertImageRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(ConvertImageContext)) - } -} - -struct ConvertImageContext; - -impl Context for ConvertImageContext {} - -impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // this header is used to select correct image version from cache - self.add_http_request_header("Image-Format", "original"); - - // get extension - let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); - println!("request.path={path:?}"); - let raw_ext = self.get_property(vec!["request.extension"]); - println!("request.extension={raw_ext:?}"); - let Some(ext) = raw_ext else { - println!("No extension in request path, not transforming"); - return Action::Continue; - }; - let Ok(ext) = from_utf8(&ext) else { - println!("Invalid UTF-8 in request extension, not transforming"); - return Action::Continue; - }; - if ext.is_empty() { - println!("No extension in request path, not transforming"); - return Action::Continue; - } - - // FORMATS_TO_TRANSFORM contains list of file extensions to transfor - // note that jpg and jpeg are different extensions - let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { - println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); - return Action::Continue; - }; - if !image_list.split(',').any(|entry| entry == ext) { - println!( - "extension {} is not in the list of formats to transform: {}, not transforming", - ext, image_list - ); - return Action::Continue; - } - - // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed - let Some(ua) = self.get_http_request_header("User-Agent") else { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - }; - if ua.is_empty() { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - } - if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { - if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { - println!("User-Agent is in ignore list, not transforming"); - return Action::Continue; - } - } - - // indicator for on_response_headers and for cache key - self.set_http_request_header("Image-Format", Some("image/avif")); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // only process 200 responses - if let Some(status) = self.rsp_status() { - if status != 200 { - println!( - "Response status is {} instead of expected 200, not transforming", - status - ); - return Action::Continue; - } - } else { - println!("Response status is not set, not transforming"); - return Action::Continue; - } - - // if "Image-Format" request header is not set, don't convert the image - let Some(content_type) = self.get_http_request_header("Image-Format") else { - return Action::Continue; - }; - // instruct cache to vary by this header so "original" and "image/avif" are cached separately - self.add_http_response_header("Vary", "Image-Format"); - - if content_type == "original" { - return Action::Continue; - }; - - // image to be transformed, set headers accordingly - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some(content_type.as_str())); - - // indicate to on_http_response_body that transformation is needed - self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // wait till we get complete body - return Action::Pause; - } - - let Some(content_type) = self.get_property(vec!["response.content-type"]) else { - return Action::Continue; - }; - - let Ok(content_type) = from_utf8(&content_type) else { - // should never happen - println!("Invalid UTF-8 in Content-Type"); - self.send_http_response(500, vec![], None); - return Action::Pause; - }; - - if content_type != "image/avif" { - // should never happen - println!( - "Content-Type {} is not supported, not transforming", - content_type - ); - return Action::Continue; - } - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let buf = body_bytes.as_bytes(); - let img = match load_from_memory(buf) { - Ok(i) => i, - Err(e) => { - println!("cannot load image to memory {}, not converting", e); - return Action::Continue; - } - }; - - let mut out = Vec::new(); - let mut c = Cursor::new(&mut out); - let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( - &mut c, - u8_param("AVIF_SPEED", 1, 10, 5), - u8_param("AVIF_QUALITY", 1, 100, 70), - )); - - match res { - Ok(_) => { - println!( - "{} bytes -> {} bytes {}", - body_size, - out.len(), - content_type - ); - self.set_http_response_body(0, body_size, &out) - } - Err(e) => println!("cannot store transformed image {}", e), - } - } else { - println!("No response body to transform"); - } - - Action::Continue - } -} - -impl ConvertImageContext { - fn rsp_status(&mut self) -> Option { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() != 2 { - println!("HTTP status property is not 2 bytes"); - return None; - } - return Some(u16::from_be_bytes([status[0], status[1]])); - } - None - } -} - -fn str_param(name: &str) -> Result { - let val = env::var(name)?; - if val.is_empty() { - return Err(VarError::NotPresent); - } - - Ok(val) -} - -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { - let Ok(val) = env::var(name) else { - println!("Param {} is not set, using default value {}", name, default); - return default; - }; - if val.is_empty() { - println!("Param {} is not set, using default value {}", name, default); - return default; - } - - let val = match val.parse() { - Err(_) => { - println!( - "Param {} is not a valid number, using default value {}", - name, default - ); - return default; - } - Ok(v) => v, - }; - if val < min { - println!( - "Param {} is below minimum {}, using default value {}", - name, min, default - ); - return default; - } - if val > max { - println!( - "Param {} is above maximum {}, using default value {}", - name, max, default - ); - return default; - } - - val -} -``` - - -### FILE: examples/cdn/convert_image/Cargo.toml - -```toml -[workspace] - -[package] -name = "convert_image" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -image = "0.25" -``` - - -### FILE: examples/cdn/convert_image/README.md - -``` -[← Back to examples](../../README.md) - -# Convert Image (CDN) - -Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. - -## Configuration - -- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) -- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip -- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) -- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) - -## How it works - -1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation -2. **on_response_headers** — sets response headers for AVIF content type on 200 responses -3. **on_response_body** — decodes the original image and re-encodes it as AVIF -``` diff --git a/plugins/gcore-fastedge-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 bf58305..ce71e32 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -292,241 +292,3 @@ export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; - CDN app platform overview - host-services-rust reference (for Rust equivalent patterns) - best-practices reference (response body buffering, hook sequencing) - -## Source Material - -### FILE: examples/customErrorPages/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - get_property, - log, - LogLevelValues, - registerRootContext, - RootContext, - set_buffer_bytes, - BufferTypeValues, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class ErrorPagesRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new ErrorPagesContext(context_id, this); - } -} - -class ErrorPagesContext extends Context { - constructor(context_id: u32, root_context: ErrorPagesRoot) { - super(context_id, root_context); - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterHeadersStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code >= 400 && code < 600) { - stream_context.headers.response.replace("Content-Type", "text/html"); - stream_context.headers.response.remove("Content-Length"); - stream_context.headers.response.replace("Transfer-Encoding", "Chunked"); - - log(LogLevelValues.info, "Error response detected: " + code.toString()); - } - - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - // Read response status from property (no instance state between hooks) - const statusBuf = get_property("response.status"); - if (statusBuf.byteLength < 2) { - return FilterDataStatusValues.Continue; - } - - const bytes = Uint8Array.wrap(statusBuf); - const code: u32 = (u32(bytes[0]) << 8) | u32(bytes[1]); - - if (code < 400 || code >= 600) { - return FilterDataStatusValues.Continue; - } - - if (!end_of_stream) { - return FilterDataStatusValues.StopIterationAndBuffer; - } - const title = this.getErrorTitle(code); - const description = this.getErrorDescription(code); - const category = code >= 500 ? "Server Error" : "Client Error"; - - const html = - '' + - '' + - "" + - code.toString() + - " — " + - title + - "" + - "

" + - "

" + - code.toString() + - "

" + - "

" + - title + - "

" + - "

" + - description + - "

" + - "

" + - category + - "

" + - "
"; - - const body = String.UTF8.encode(html); - // Replace the entire original body — pass body_buffer_length as the length - // to replace, not body.byteLength. Otherwise any original bytes beyond the - // new body's length survive at the tail of the response. - set_buffer_bytes( - BufferTypeValues.HttpResponseBody, - 0, - body_buffer_length as u32, - body, - ); - - return FilterDataStatusValues.Continue; - } - - private getErrorTitle(code: u32): string { - if (code == 400) return "Bad Request"; - if (code == 401) return "Unauthorized"; - if (code == 403) return "Forbidden"; - if (code == 404) return "Not Found"; - if (code == 405) return "Method Not Allowed"; - if (code == 408) return "Request Timeout"; - if (code == 429) return "Too Many Requests"; - if (code == 500) return "Internal Server Error"; - if (code == 502) return "Bad Gateway"; - if (code == 503) return "Service Unavailable"; - if (code == 504) return "Gateway Timeout"; - if (code >= 500) return "Server Error"; - return "Error"; - } - - private getErrorDescription(code: u32): string { - if (code == 400) - return "The server could not understand the request due to invalid syntax."; - if (code == 401) - return "You need to authenticate to access this resource."; - if (code == 403) - return "You do not have permission to access this resource."; - if (code == 404) - return "The requested page could not be found. It may have been moved or deleted."; - if (code == 405) - return "The request method is not supported for this resource."; - if (code == 408) - return "The server timed out waiting for the request."; - if (code == 429) - return "You have sent too many requests. Please try again later."; - if (code == 500) - return "The server encountered an unexpected condition that prevented it from fulfilling the request."; - if (code == 502) - return "The server received an invalid response from the upstream server."; - if (code == 503) - return "The server is temporarily unavailable. Please try again later."; - if (code == 504) - return "The server did not receive a timely response from the upstream server."; - if (code >= 500) - return "The server encountered an error processing your request."; - return "An error occurred processing your request."; - } -} - -registerRootContext((context_id: u32) => { - return new ErrorPagesRoot(context_id); -}, "customErrorPages"); -``` - - -### FILE: examples/customErrorPages/package.json - -```json -{ - "name": "fastedge-as-example-custom-error-pages", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Custom Error Pages — replace 4xx/5xx responses with branded HTML", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/customErrorPages/README.md - -``` -[← Back to examples](../README.md) - -# Custom Error Pages - -This application intercepts 4xx and 5xx error responses and replaces them with clean, branded HTML error pages. - -## What it does - -In `onResponseHeaders`, the app reads the `response.status` property. If it is in the 400-599 range, it sets the `Content-Type` to `text/html` and prepares for body replacement. - -In `onResponseBody`, the app buffers the full response, then replaces the body with a styled HTML page containing: - -- The numeric status code -- A human-readable error title (e.g. "Not Found", "Bad Gateway") -- A description explaining what went wrong -- An error category label ("Client Error" or "Server Error") - -Covers all common HTTP error codes (400, 401, 403, 404, 405, 408, 429, 500, 502, 503, 504) with specific messages and falls back to generic messages for other codes. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/customErrorPages.wasm` | Optimised release binary — upload this to FastEdge | -| `build/customErrorPages-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/customErrorPages.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` diff --git a/plugins/gcore-fastedge-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 981cd2f..7a52ea5 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-headers-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -66,14 +66,14 @@ registerRootContext((context_id: u32) => { All header operations are accessed via `stream_context.headers.request` or `stream_context.headers.response`. -| Method | Signature | Description | -| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | -| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | -| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | -| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | -| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | -| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | +| Method | Signature | Description | +| -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `get(name)` | `(name: string) => string` | Returns the value of the named header, or empty string if not present | +| `add(name, value)` | `(name: string, value: string) => void` | Adds a header; multiple calls with the same name produce multiple values | +| `replace(name, value)` | `(name: string, value: string) => void` | Upserts the header value — creates the header if it does not exist (see Known Issues) | +| `remove(name)` | `(name: string) => void` | Removes the header (see Known Issues) | +| `get_headers()` | `() => Headers` (alias: `HeaderPair[]`) | Returns all headers as an array of `{ key: ArrayBuffer, value: ArrayBuffer }` | +| `set_headers(headers)` | `(headers: Headers) => void` | Replaces the full header collection | ### `Headers` / `HeaderPair` type diff --git a/plugins/gcore-fastedge-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 bc289c7..95ba36c 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-http-call-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> ## Overview @@ -321,202 +321,3 @@ Dependencies (from `package.json`): - SDK API reference — full lifecycle hook signatures, `RootContext` and `Context` base class APIs, all enum values (`WasmResultValues`, `FilterHeadersStatusValues`, `BufferTypeValues`, `LogLevelValues`) - Platform overview — FastEdge CDN execution model, context lifecycle, how the runtime re-enters hooks after async callbacks - Best practices — timeout tuning, upstream cluster naming, error response patterns - -## Source Material - -### FILE: examples/httpCall/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - BaseContext, - BufferTypeValues, - Context, - FilterHeadersStatusValues, - get_buffer_bytes, - HeaderPair, - log, - LogLevelValues, - makeHeaderPair, - registerRootContext, - RootContext, - send_http_response, - stream_context, - WasmResultValues, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -const INTERNAL_SERVER_ERROR: u32 = 500; - -function handleHttpCallResponse( - ctx: BaseContext, - hdrs: u32, - bodySize: usize, - trls: u32, -): void { - if (hdrs == 0) { - log(LogLevelValues.error, "HTTP call failed — no response received"); - return; - } - - const userAgent = stream_context.headers.http_callback.get("user-agent"); - if (userAgent !== "") { - log(LogLevelValues.info, "User-Agent: " + userAgent); - } - - if (bodySize > 0) { - const bodyBytes = get_buffer_bytes( - BufferTypeValues.HttpCallResponseBody, - 0, - bodySize as u32, - ); - const bodyStr = String.UTF8.decode(bodyBytes); - log( - LogLevelValues.info, - "Response body (" + bodySize.toString() + " bytes): " + bodyStr, - ); - } else { - log(LogLevelValues.info, "Response body: empty"); - } -} - -class HttpCallRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new HttpCallContext(context_id, this); - } -} - -class HttpCallContext extends Context { - httpCallDispatched: bool = false; - - constructor(context_id: u32, root_context: HttpCallRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // FastEdge re-invokes this hook after the httpCall response is processed. - // The latch gates re-dispatch so the second invocation returns Continue - // instead of firing another HTTP call. See SDK docs → Outbound HTTP. - if (this.httpCallDispatched) { - log( - LogLevelValues.info, - "HTTP call response received, resuming request.", - ); - return FilterHeadersStatusValues.Continue; - } - - log(LogLevelValues.info, "onRequestHeaders >> dispatching HTTP call"); - - const headers = new Array(); - headers.push(makeHeaderPair(":scheme", "https")); - headers.push(makeHeaderPair(":authority", "httpbin.org")); - headers.push(makeHeaderPair(":path", "/ip")); - headers.push(makeHeaderPair(":method", "GET")); - headers.push(makeHeaderPair("User-Agent", "fastedge")); - - // 3000ms accommodates cold DNS + variable network conditions; tune per upstream in production. - const result = (this.root_context as HttpCallRoot).httpCall( - "httpbin.org", - headers, - new ArrayBuffer(0), - new Array(), - 3000, - this, - handleHttpCallResponse, - ); - - if (result != WasmResultValues.Ok) { - log( - LogLevelValues.error, - "Failed to dispatch HTTP call: " + result.toString(), - ); - send_http_response( - INTERNAL_SERVER_ERROR, - "internal server error", - String.UTF8.encode("Failed to dispatch HTTP call"), - [], - ); - return FilterHeadersStatusValues.StopIteration; - } - - this.httpCallDispatched = true; - log(LogLevelValues.info, "HTTP call dispatched, pausing request"); - - return FilterHeadersStatusValues.StopIteration; - } -} - -registerRootContext((context_id: u32) => { - return new HttpCallRoot(context_id); -}, "httpCall"); -``` - -### FILE: examples/httpCall/package.json - -```json -{ - "name": "fastedge-as-example-http-call", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: HTTP Call — async HTTP dispatch with callback", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - -### FILE: examples/httpCall/README.md - -``` -[← Back to examples](../README.md) - -# HTTP Call - -This application makes an asynchronous HTTP call to an external service using the proxy-wasm HTTP dispatch API. - -## What it does - -In `onRequestHeaders`, the app dispatches an outbound HTTP GET request to `httpbin.org/ip` and pauses the hook (`StopIteration`) until the response arrives. Dispatch is latched on an instance field so the second invocation of the hook (after the response is processed) returns `Continue` instead of dispatching again. - -Inside the response callback passed to `httpCall`: - -1. Checks whether the call succeeded (a `headers` value of `0` indicates failure — timeout, DNS error, etc.). -2. Reads and logs the `User-Agent` response header via `stream_context.headers.http_callback`. -3. Reads and logs the response body via `get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`. - -If the dispatch itself fails (e.g. invalid arguments), the app returns a `500` error to the client. - -## Key concepts - -- **`httpCall()`** on `RootContext` dispatches an async HTTP request. It accepts a cluster (host), headers, body, trailers, a timeout in milliseconds, the originating context, and a callback. -- **FastEdge resume model.** Returning `FilterHeadersStatusValues.StopIteration` pauses the hook. The runtime processes the HTTP response, invokes the callback, then **re-invokes `onRequestHeaders` on the same `Context` instance**. The `httpCallDispatched` latch gates re-dispatch so the hook returns `Continue` the second time. This differs from canonical proxy-wasm — calling `continueRequest()` is not required (and has no effect on FastEdge). -- **`stream_context.headers.http_callback`** provides access to the HTTP call response headers inside the callback. The SDK sets the effective context before the callback fires, so these lookups resolve against the originating request. -- **`get_buffer_bytes(BufferTypeValues.HttpCallResponseBody, ...)`** reads the response body inside the callback. - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/httpCall.wasm` | Optimised release binary — upload this to FastEdge | -| `build/httpCall-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/httpCall.wasm` to the FastEdge portal and attach it to your CDN application. No environment variables or secrets are required. -``` diff --git a/plugins/gcore-fastedge-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 35d0930..2cefbe5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # HTTP Call — CDN (Rust) @@ -285,6 +285,7 @@ impl Context for HttpHeaders { println!( "Received http call response with token id: {token_id}, num_headers: {num_headers}" ); + // If num_headers is 0, then the HTTP call failed. if num_headers != 0 { let headers = self.get_http_call_response_headers(); let headers_str = headers @@ -294,7 +295,7 @@ impl Context for HttpHeaders { .join(","); println!("Response headers: [{}]", headers_str); - self.state = 1; + self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. self.resume_http_request(); } else { 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 56aad05..620ee62 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -64,7 +64,7 @@ serde_json = "1" ### Proxy-wasm lifecycle hooks - **`on_http_response_headers`** — set response `content-type` and remove `content-length` before replacing the body -- **`on_http_response_body`** — read request query parameters via `get_property(vec!["request", "query"])`, open the store, dispatch to operations, and replace the body via `set_http_response_body` +- **`on_http_response_body`** — read request query parameters via `get_property(vec!["request.query"])`, open the store, dispatch to operations, and replace the body via `set_http_response_body` Query parameters are only available in `on_http_response_body` via `get_property` because request data is accessible throughout the response phase. @@ -151,7 +151,7 @@ match store.zrange_by_score("leaderboard", 0.0, 100.0) { ```rust let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); @@ -189,7 +189,7 @@ The complete example supports these query parameter combinations: fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); self.set_property( - vec!["response", "status"], + vec!["response.status"], Some(b"500"), ); let error_body = serde_json::json!({"error": msg}).to_string(); @@ -228,7 +228,9 @@ Sorted set entries shape: `[{"value": "", "score": }, ...]` - **`Store::open` takes a platform-configured name** — the store name must match a KV store resource attached to the app in the FastEdge platform. Passing an unknown name returns `Err`. - **`content-length` must be removed before body replacement** — when replacing the response body, always remove `content-length` in `on_http_response_headers` (set to `None`). Note: on the FastEdge CDN platform, passing `None` sets the header to an empty string rather than truly removing it; set `transfer-encoding: chunked` as a workaround. - **`on_http_response_body` must wait for end of stream** — return `Action::Pause` until `end_of_stream` is `true` before reading query parameters or replacing the body. -- **`query` property is available in the response phase** — `get_property(vec!["request", "query"])` works in `on_http_response_body` because request metadata is accessible throughout the response lifecycle. +- **`get_property` uses dot-notation path** — the correct call is `get_property(vec!["request.query"])` (a single dot-separated string), not `get_property(vec!["request", "query"])` (two separate path segments). The source uses `vec!["request.query"]`. +- **`query` property is available in the response phase** — `get_property(vec!["request.query"])` works in `on_http_response_body` because request metadata is accessible throughout the response lifecycle. +- **`set_property` uses dot-notation path** — the correct call is `set_property(vec!["response.status"], Some(b"500"))` (a single dot-separated string). - **Error type is a typed `Error` enum** (not a raw `u32` as with secrets). Match on the `Err(e)` variant and format with `format!("{}", e)` for human-readable messages. - **`store` query parameter is always required** — the example reads `store` from query params and passes it to `Store::open`. An absent `store` param returns a 500 error before any store operations are attempted. - **Empty query string is rejected** — if no query parameters are provided at all, the app returns a 500 error with the message `"App must be called with query parameters"`. @@ -240,3 +242,363 @@ 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) + +This example shows how to read and write data from a FastEdge KV store from a CDN app. +It intercepts the HTTP response, reads the request query string, and executes a KV operation against a named store. + +## What it does + +The app supports the following actions: + +- `get` — fetch one key +- `scan` — list keys matching a pattern +- `zrange` — read sorted-set entries by score range +- `zscan` — list sorted-set entries matching a pattern +- `bfExists` — check whether a Bloom filter contains an item + +The request must include at least: + +- `store=` — KV store name +- `action=` — optional, defaults to `get` + +For each action, the app validates required parameters and returns a JSON response body. + +## Supported query examples + +### Get a key + +```text +?store=my_store&action=get&key=user:42 +``` + +### List keys by pattern + +```text +?store=my_store&action=scan&match=user:* +``` + +### Read a sorted set by score range + +```text +?store=my_store&action=zrange&key=leaderboard&min=0&max=100 +``` + +### Search sorted-set members by pattern + +```text +?store=my_store&action=zscan&key=leaderboard&match=user:* +``` + +### Check a Bloom filter item + +```text +?store=my_store&action=bfExists&key=visitors&item=alice@example.com +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/key_value.wasm +``` + +This example is a CDN app, so it targets `wasm32-wasip1`. + +## Response format + +The app replaces the response body with JSON and sets `content-type: application/json`. +A successful response looks like this: + +```json +{ + "store": "my_store", + "action": "get", + "key": "user:42", + "response": "Alice" +} +``` + +If a required parameter is missing or the KV operation fails, the app responds with an error payload: + +```json +{ + "error": "Missing required param 'key' for 'get' action" +} +``` + +## Notes + +- The app requires query parameters to run. +- If `action` is omitted, it defaults to `get`. +- The code uses `fastedge::proxywasm::key_value::Store` and `proxy_wasm` lifecycle hooks to operate on the KV store. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md index 0961c7e..fe756b2 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 60f25c7bd35564e5bafb421be7f37aa4acf1bf81 - updated: 2026-05-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> # Large Dictionary — AssemblyScript (CDN) @@ -136,3 +136,129 @@ Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your - `getEnv` — standard WASI environment variable access (under 64 KB) - proxy-wasm-sdk-as SDK reference — full list of imports from `@gcoredev/proxy-wasm-sdk-as/assembly` and `@gcoredev/proxy-wasm-sdk-as/assembly/fastedge` - FastEdge CDN app examples (other concepts) — additional CDN app patterns in the fastedge-docs reference + +## Source Material + +### FILE: examples/largeDictionary/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getDictionary, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class LargeDictionaryRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new LargeDictionaryContext(context_id, this); + } +} + +class LargeDictionaryContext extends Context { + constructor(context_id: u32, root_context: LargeDictionaryRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Use getDictionary for environment variables that may exceed 64KB. + // For normal-sized env vars (< 64KB), use getEnv instead. + const config = getDictionary("LARGE_CONFIG"); + + const size = config.length; + log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); + + stream_context.headers.request.add("x-config-size", size.toString()); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new LargeDictionaryRoot(context_id); +}, "largeDictionary"); +``` + + +### FILE: examples/largeDictionary/package.json + +```json +{ + "name": "fastedge-as-example-large-dictionary", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/largeDictionary/README.md + +``` +[← Back to examples](../README.md) + +# Large Dictionary + +This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. + +## When to use `getDictionary` vs `getEnv` + +| Function | Use when | +|----------|----------| +| `getEnv(name)` | Variable value is under 64 KB (most cases) | +| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | + +The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. + +For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. + +## What it does + +In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. + +## Configuration + +Set the following on your FastEdge application: + +| Name | Type | Description | +|------|------|-------------| +| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | +| `build/largeDictionary-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/host-services-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/host-services-rust.md index ac21c92..3214224 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/host-services-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/host-services-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Rust Host Services Reference @@ -260,7 +260,7 @@ async fn main(_request: Request) -> anyhow::Result> { Module: `fastedge::dictionary` -Provides fast, read-only lookups for configuration values that do not change during the lifetime of a deployment. +Provides fast, read-only lookups for configuration values that do not change during the lifetime of a deployment. Dictionary = read-only config, key_value = persistent data, secret = encrypted credentials. ### API 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 6de33d5..eeb5488 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -153,114 +153,3 @@ cargo build --release --target wasm32-wasip1 - platform-overview (FastEdge request lifecycle, outbound request capabilities) - sdk-reference-rust (`fastedge::send_request`, `Body`, HTTP types) - best-practices (body buffering considerations, error handling patterns) - -## Source Material - -### FILE: examples/http/basic/backend/src/lib.rs - -```rust -use anyhow::{anyhow, Error, Result}; -use fastedge::body::Body; -use fastedge::http::{Method, Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request) -> Result> { - let (parts, body) = req.into_parts(); - let query = parts - .uri - .query() - .ok_or(anyhow!("missing uri query parameter"))?; - let params = querystring::querify(query); - let url = params - .iter() - .find(|(k, _)| k == &"url") - .ok_or(anyhow!("missing url parameter"))?; - let url = urlencoding::decode(url.1)?.to_string(); - println!("url = {:?}", url); - let request = Request::builder().uri(url).method(Method::GET).body(body)?; - - let response = fastedge::send_request(request).map_err(Error::msg)?; - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "len = {}, content-type = {:?}", - response.body().len(), - response.headers().get("Content-Type") - ))) - .map_err(Error::msg) -} -``` - - -### FILE: examples/http/basic/backend/Cargo.toml - -```toml -[workspace] - -[package] -name = "backend" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -querystring = "1.1" -urlencoding = "2.1" -``` - - -### FILE: examples/http/basic/backend/README.md - -``` -[← Back to examples](../../../README.md) - -# Backend (URL Proxy) - -A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. - -> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). - -## What it does - -1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). -2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. -3. Returns HTTP 200 with a plain-text body: - ``` - len = , content-type = - ``` -4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::send_request(request)` | Blocking outbound HTTP request | -| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | -| `fastedge::body::Body` | Request and response bodies | -| `querystring::querify` | Parse query string into key-value pairs | -| `urlencoding::decode` | Percent-decode the `?url=` value | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/backend.wasm -``` - -## Expected behavior - -| Request | Response status | Response body | -|---|---|---| -| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = , content-type = Some("")` | -| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | -| `GET /` (no query string) | 500 | `missing uri query parameter` | - -The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option` debug string (e.g. `Some("application/json")`). -``` diff --git a/plugins/gcore-fastedge-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 d9377ed..7acebb4 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -150,3 +150,164 @@ 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-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md index ab82a99..a1e64c6 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -133,3 +133,149 @@ use fastedge::http::{Request, Response, StatusCode}; - examples-cache-basic-rust (this file) pairs with the HTTP app deploy reference for upload and wiring steps - platform-overview for cache subsystem lifecycle and eviction behavior - best-practices for cache key namespacing and TTL selection guidance + +## Source Material + +### FILE: examples/http/basic/cache/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Example app demonstrating cache-aside pattern via the cache interface. + +On each request the app: + 1. Builds a cache key from the request path. + 2. Returns the cached body immediately on a hit (x-cache: hit). + 3. On a miss, generates a response body, stores it in the cache, and + returns it (x-cache: miss). + +Environment variables: + CACHE_TTL_MS How long to cache the generated body in milliseconds + (default: 30000) + +Build: + cargo build --release +*/ + +use std::env; + +use fastedge::body::Body; +use fastedge::cache; +use fastedge::http::{Request, Response, StatusCode}; + +#[fastedge::http] +fn main(req: Request) -> anyhow::Result> { + let ttl_ms: u64 = env::var("CACHE_TTL_MS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(30_000); + + let path = req.uri().path().to_string(); + let cache_key = format!("page:{path}"); + + // Cache hit — return stored body + if let Some(cached) = cache::get(&cache_key)? { + println!("cache hit: {cache_key}"); + return Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html") + .header("x-cache", "hit") + .body(Body::from(cached))?); + } + + // Cache miss — generate the response body + println!("cache miss: {cache_key}"); + let body = generate_body(&path); + + // Store in cache with TTL + cache::set(&cache_key, body.as_bytes(), Some(ttl_ms))?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/html") + .header("x-cache", "miss") + .body(Body::from(body))?) +} + +/// Simulates an expensive computation or template render. +fn generate_body(path: &str) -> String { + format!( + "\ + FastEdge Cache Demo\ + \ +

Hello from FastEdge

\ +

Path: {path}

\ +

This response was generated and is now cached.

\ + " + ) +} +``` + + +### FILE: examples/http/basic/cache/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_basic" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/basic/cache/README.md + +``` +[← Back to examples](../../../README.md) + +# Cache (Basic) + +Demonstrates the cache-aside pattern using `fastedge::cache` — store a generated response body with a TTL and serve it directly on subsequent requests without re-computing it. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `CACHE_TTL_MS` | No | How long to cache each response in milliseconds. Default: `30000` (30 s). | + +## How it works + +``` +GET /api/data → cache miss → generate body → store in cache → 200 (x-cache: miss) +GET /api/data → cache hit → return cached body → 200 (x-cache: hit) +``` + +The cache key is `page:`. Each unique path gets its own cache entry. The response body is a simple HTML page that includes the request path — stand in for any expensive computation or template render. + +## What it returns + +``` +HTTP/1.1 200 OK +content-type: text/html +x-cache: hit | miss + +... +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache_basic.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 +``` 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 c3bc2f1..46d3c9c 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # FastEdge Cache — JavaScript Examples @@ -331,279 +331,3 @@ 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-cache-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md index 4193334..b8d5f3c 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -188,197 +188,3 @@ cargo build --release - HTTP outbound requests via wstd (sdk-reference-rust) - Environment variable configuration (platform-overview) - HTTP app deploy workflow (deploy skill) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/ — purge keys whose cache key starts with cache:/ - -Environment variables: - ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com - CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) - -Build: - cargo build --release -*/ - -use std::env; - -use anyhow::anyhow; -use fastedge::cache; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let origin = env::var("ORIGIN_HOST") - .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; - - let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse().ok()) - .unwrap_or_else(|| { - env::var("CACHE_TTL_MS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(60_000) - }); - - // Build cache key from the request path (and query string if present) - let path_and_query = req - .uri() - .path_and_query() - .map(|pq| pq.as_str()) - .unwrap_or("/"); - - - // Handle purge requests before any cache/origin logic - if path_and_query == "/purge" { - let deleted = cache::purge()?; - println!("purge all: {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - if let Some(prefix) = path_and_query.strip_prefix("/purge/") { - let prefix = format!("cache:/{prefix}"); - let deleted = cache::purge_prefix(&prefix)?; - println!("purge prefix '{prefix}': {deleted} keys removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - if let Some(prefix) = path_and_query.strip_prefix("/delete/") { - let prefix = format!("cache:/{prefix}"); - cache::delete(&prefix)?; - println!("prefix '{prefix}': removed"); - return Ok(Response::builder() - .status(204) - .body(Body::empty())?); - } - - let cache_key = format!("cache:{path_and_query}"); - - // Return cached response if available - if let Some(cached) = cache::get(&cache_key)? { - println!("cache hit: {cache_key}"); - return Ok(Response::builder() - .status(200) - .header("content-type", "application/octet-stream") - .header("x-cache", "hit") - .body(Body::from(cached))?); - } - - // Cache miss — forward request to origin - let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); - println!("cache miss: {cache_key} → {upstream_url}"); - - let upstream_req = Request::get(&upstream_url) - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; - - let upstream_resp = Client::new() - .send(upstream_req) - .await - .map_err(|e| anyhow!("upstream request failed: {e}"))?; - - let status = upstream_resp.status(); - let headers: Vec<(String, String)> = upstream_resp - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - // Read body bytes - let mut body = upstream_resp.into_body(); - let body_bytes = body.contents().await?.to_vec(); - - // Only cache successful responses - if status.is_success() { - cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; - } - - // Replay original response - let mut builder = Response::builder() - .status(status) - .header("x-cache", "miss"); - for (k, v) in &headers { - builder = builder.header(k, v); - } - Ok(builder.body(Body::from(body_bytes))?) -} -``` - - -### FILE: examples/http/wasi/cache/Cargo.toml - -```toml -[workspace] - -[package] -name = "cache_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/cache/README.md - -``` -[← Back to examples](../../../README.md) - -# Cache (WASI) - -Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. - -## Configuration - -| Env var | Required | Description | -|---|---|---| -| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | -| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | - -## How it works - -GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) -GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) - -Cache key is `cache:?`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. - -## Build - -cargo build --release -# Output: target/wasm32-wasip2/release/cache_wasi.wasm - -## APIs used - -- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` -- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry -- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md index fe37566..da2a970 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -190,152 +190,3 @@ Use `key=value` pairs separated by spaces (`logfmt`-ish format). Benefits: - host-services-rust (other host service integrations available to Rust WASI apps) - CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, different module path (see CDN apps reference) - examples-kv-store-wasi-rust (another Rust WASI HTTP example showing KV Store usage) - -## Source Material - -### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Diagnostic logging example. - -Tiny pass-through proxy that writes a single `set_user_diag` tag per request -summarising the outcome (config_error, origin_unreachable, or proxied). The -tag appears in the FastEdge platform's per-request log viewer and is distinct -from stdout — it's intended for filterable outcome labels, not verbose -traces. - -Required configuration: - - Environment variable: ORIGIN_URL (origin to proxy to) - -Uses `logfmt`-ish formatting (`outcome= key=value ...`) so the tag is -easy to slice in log search tooling. -*/ - -use std::env; - -use fastedge::utils::set_user_diag; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let method = req.method().as_str().to_string(); - let path = req.uri().path().to_string(); - - let origin = match env::var("ORIGIN_URL") { - Ok(u) if !u.trim().is_empty() => u, - _ => { - set_user_diag("outcome=config_error reason=origin_missing"); - return Ok(Response::builder() - .status(500) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("ORIGIN_URL is not configured"))?); - } - }; - - let outbound = Request::get(&origin).body(Body::empty())?; - let resp = match Client::new().send(outbound).await { - Ok(r) => r, - Err(e) => { - set_user_diag(&format!( - "outcome=origin_unreachable method={method} path={path} err={e}" - )); - return Ok(Response::builder() - .status(502) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("origin unreachable"))?); - } - }; - - let status = resp.status().as_u16(); - set_user_diag(&format!( - "outcome=proxied method={method} path={path} status={status}" - )); - - let (parts, mut body) = resp.into_parts(); - let bytes = body.contents().await?; - let content_type = parts - .headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream") - .to_string(); - - Ok(Response::builder() - .status(parts.status) - .header("content-type", content_type) - .body(Body::from(bytes))?) -} -``` - - -### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml - -```toml -[workspace] - -[package] -name = "diagnostic_logging_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/wasi/diagnostic_logging/README.md - -``` -[← Back to examples](../../../README.md) - -# Diagnostic Logging (WASI) - -Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request -summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, -distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at -per-request outcomes. - -## Configuration - -- `ORIGIN_URL` environment variable — the origin that requests are proxied to. - -## Outcomes - -Each request writes exactly one of: - -| Condition | Tag | -| --- | --- | -| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | -| Origin unreachable | `outcome=origin_unreachable method= path=

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

status=` | - -## `set_user_diag` vs `println!` - -| | `println!` | `set_user_diag` | -| --- | --- | --- | -| Channel | stdout — general application logs | per-request structured tag in platform log viewer | -| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | -| Best for | verbose traces, debug details | a single filterable outcome label | -| Forbidden | — | secrets and PII (tags appear in platform logs) | - -## Convention - -Call `set_user_diag` **once**, on every branch, late enough in the handler to know the -outcome. The `logfmt`-ish format (`outcome= key=value key=value …`) is easy to slice in -log search tooling — keep keys short and stable so they make good filter terms. - -## Related - -CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, -different module path. See `docs/CDN_APPS.md`. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-print-basic-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-print-basic-rust.md index 0b6ff0c..0c80045 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-print-basic-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-print-basic-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -147,112 +147,3 @@ cargo build --release - sdk-reference-rust (full API reference for `fastedge` crate) - platform-overview (request lifecycle, header forwarding behaviour) - best-practices (response building patterns) - -## Source Material - -### FILE: examples/http/basic/print/src/lib.rs - -```rust -use anyhow::Result; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; - -#[allow(dead_code)] -#[fastedge::http] -fn main(req: Request) -> Result> { - let mut body: String = "Method: ".to_string(); - body.push_str(req.method().as_str()); - - body.push_str("\nURL: "); - body.push_str(req.uri().to_string().as_str()); - - body.push_str("\nHeaders:"); - for (h, v) in req.headers() { - body.push_str("\n "); - body.push_str(h.as_str()); - body.push_str(": "); - match v.to_str() { - Err(_) => body.push_str("not a valid text"), - Ok(a) => body.push_str(a), - } - } - let res = Response::builder() - .status(StatusCode::OK) - .body(Body::from(body))?; - Ok(res) -} -``` - -### FILE: examples/http/basic/print/Cargo.toml - -```toml -[workspace] - -[package] -name = "print" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -``` - -### FILE: examples/http/basic/print/README.md - -``` -[← Back to examples](../../../README.md) - -# Print - -Echoes the incoming request's method, URL, and all headers back in the response body as plain text. Useful for debugging and inspecting what a FastEdge app receives from clients and the platform. - -> **Note:** This example uses the legacy `#[fastedge::http]` sync handler (`wasm32-wasip1`). For new apps, prefer `#[wstd::http_server]` (async, `wasm32-wasip2`) — see [`examples/http/wasi/`](../../wasi/). - -## What it demonstrates - -- Reading request method via `req.method().as_str()` -- Reading the request URI via `req.uri().to_string()` -- Iterating all request headers via `req.headers()` -- Handling non-UTF-8 header values gracefully with a `match` on `v.to_str()` -- Building a plain-text response with `Response::builder()` and `Body::from(...)` - -## APIs used - -| API | Purpose | -|-----|---------| -| `req.method().as_str()` | HTTP method as a string slice | -| `req.uri().to_string()` | Full request URI as a `String` | -| `req.headers()` | Iterator over `(HeaderName, HeaderValue)` pairs | -| `v.to_str()` | Decode a header value to `&str` (returns `Err` for non-UTF-8) | -| `Response::builder().status(...).body(...)` | Build the HTTP response | -| `Body::from(string)` | Create a response body from a `String` | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/print.wasm -``` - -## Expected behaviour - -For any request, the response body is a plain-text dump of the request details: - -``` -Method: GET -URL: /some/path?query=value -Headers: - host: example.com - accept: */* - ... -``` - -- Status: `200 OK` -- Content: plain text (no `content-type` header is set explicitly; the platform may add one) -- Each header appears on its own line, indented with four spaces -- Non-UTF-8 header values are replaced with `not a valid text` -``` 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 e9a2822..b61ea98 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,178 +250,3 @@ 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 f789dad..f523acd 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -182,236 +182,3 @@ 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 d65296b..44ff104 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Secret Access — Rust HTTP Example @@ -251,3 +251,183 @@ 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 df75287..70416dc 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> # Secret Rollover (WASI, Rust) @@ -134,7 +134,7 @@ use wstd::http::{Request, Response}; #[wstd::http_server] async fn main(request: Request) -> anyhow::Result> { - // Read slot from header, default to current unix timestamp + // Read the slot from the x-slot header, defaulting to current timestamp let slot: u32 = request .headers() .get("x-slot") @@ -153,11 +153,10 @@ async fn main(request: Request) -> anyhow::Result> { .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 the current secret value (latest slot) + let current = secret::get(secret_name).map_err(|e| anyhow!("secret::get failed: {e}"))?; - // Get value effective at the given slot + // Get the secret effective at the requested slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; 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 5d19fe4..56c7e10 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -16,7 +16,7 @@ capabilities: [outbound-fetch, header-read, async] # Example: Simple Fetch (WASI HTTP, Rust) -Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` crate. Reads a target URL from an incoming request header and proxies the response back to the caller. +Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` crate. Reads a target URL from an incoming request header and proxies the response back to the caller. Optionally forwards a custom header to the outbound request. ## Crate and Handler @@ -32,13 +32,16 @@ Demonstrates outbound HTTP requests using the WASI-HTTP interface via the `wstd` | Header | Required | Type | Default | Description | |--------|----------|------|---------|-------------| | `x-fetch-url` | No | String (fully-qualified URL) | `https://httpbin.org/get` | URL to fetch outbound | +| `x-fetch-header` | No | String (`key: value`) | — | Additional header to forward to the outbound request; parsed on `:` separator | ## Behavior 1. Read `x-fetch-url` header from the incoming request; fall back to `https://httpbin.org/get` if absent or unparseable. -2. Build an outbound `GET` request to that URL with `accept: application/json` header. -3. Send via `Client::new().send(req).await`. -4. Return the upstream `Response` directly to the caller — no decomposition. +2. Begin building an outbound `GET` request to that URL with `accept: application/json` header. +3. If `x-fetch-header` is present and parseable as `key: value` (split on first `:`), add that header to the outbound request builder. +4. Finalize the builder with `.body(Body::empty())`. +5. Send via `Client::new().send(req).await`. +6. Return the upstream `Response` directly to the caller — no decomposition. ## Key API Patterns @@ -58,14 +61,34 @@ let target_url = request - `.unwrap_or(default)` provides a safe fallback - `.to_string()` required — `Request::get` takes a `&str` or `String`; ensure the URL is owned before use +### Conditionally Adding an Outbound Header + +```rust +let mut builder = Request::get(&target_url).header("accept", "application/json"); + +if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) +{ + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } +} +``` + +- The builder is `mut` to allow conditional chaining. +- `.split_once(':')` splits on the first `:`, returning `Option<(&str, &str)>`. +- `.trim()` removes surrounding whitespace from key and value before passing to `.header()`. +- If `x-fetch-header` is absent or unparseable, the builder proceeds without the extra header. + ### Building an Outbound Request ```rust use wstd::http::{Client, Request}; use wstd::http::body::Body; -let upstream_req = Request::get(&target_url) - .header("accept", "application/json") +let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; ``` @@ -138,6 +161,8 @@ package = "component:simple_fetch" - The URL extracted from the header must be converted to an owned `String` with `.to_string()` before passing to `Request::get`. - The header parsing chain (`.get` → `.to_str()` → `.ok()`) returns `Option` — always provide a fallback via `.unwrap_or`. - Non-UTF-8 header values are silently discarded by `.to_str().ok()`. +- `x-fetch-header` is parsed with `.split_once(':')` — only the first `:` is used as the separator; values containing `:` are preserved correctly. +- The request builder must be declared `mut` when conditionally adding headers after initial construction. - `anyhow` must be declared as a dependency to use the `anyhow!()` macro. - All examples in `examples/http/wasi/` use the same `async fn main` + `#[wstd::http_server]` pattern. @@ -147,3 +172,134 @@ 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 mut builder = Request::get(&target_url).header("accept", "application/json"); + + if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) + { + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } + } + + let upstream_req = builder + .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.1" +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 interface via the `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-streaming-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md index 8a6e7c9..f972648 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -143,3 +143,105 @@ chunk 4 - examples-streaming-js reference (JavaScript mirror of this example) - wstd HTTP body reference - FastEdge SDK Rust reference + +## Source Material + +### FILE: examples/http/wasi/streaming/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https:///` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} +``` + + +### FILE: examples/http/wasi/streaming/Cargo.toml + +```toml +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" +``` + + +### FILE: examples/http/wasi/streaming/README.md + +``` +[← Back to examples](../../../README.md) + +# Streaming Response (WASI) + +Generates a response body on the fly — five text chunks, one every 200 ms — using +`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it +is produced, not all at once at the end. + +Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async +stream generation, and `wstd::time::Timer` for per-chunk delays. + +## Testing the streaming behaviour + +```sh +curl -N https://.fastedge.cdn.gc.onl/ +``` + +`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a +time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. + +## Other streaming patterns + +- **Pass-through streaming** — return an upstream response's body directly. See + [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. +- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, + then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. +- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where + `chunks` is any iterable of `Into`. + +## Related + +Mirror of `FastEdge-sdk-js/examples/streaming/`. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md new file mode 100644 index 0000000..91b5ad4 --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md @@ -0,0 +1,122 @@ +# CDN Filter Runtime — Capabilities and Traps + +What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a deployed app. + +Every claim carries its provenance: + +| Tag | Meaning | +|---|---| +| `[live]` | Measured against a deployed app on a real CDN resource | +| `[source]` | Read from the FastEdge runtime or SDK source | + +Last verified: 2026-08-25 (SDK surface re-checked 2026-09-23). + +--- + +## Capability boundary — filter vs HTTP app + +A filter is a core wasm module linked against the proxy-wasm `env` ABI. It is a different execution world from an HTTP app, not a subset of it. Designs that assume symmetry with the HTTP-app SDK fail at deploy. + +| Capability | HTTP app | CDN filter | +|---|---|---| +| KV read | ✅ | ✅ same six operations: `open · get · scan · zrange_by_score · zscan · bf_exists` `[source]` | +| KV write | ❌ (API only — see [storage.md](./storage.md)) | ❌ (API only) | +| Cache (`get/set/delete/exists/incr/expire/purge/purgePrefix`) | ✅ | ✅ host-supported; **SDK support varies — check the SDK reference for your language** | +| Secrets / dictionary | ✅ | ✅ via the SDK's proxy-wasm module | +| Outbound `fetch` | ✅ | proxy-wasm `http_call` only | +| Deferred / background work | JS `waitUntil` only (not a timer) | ❌ none | +| State between requests | ❌ | ❌ — consecutive requests hit different nodes `[live]` | + +### Use only the SDK's proxy-wasm modules + +HTTP-app host APIs are component-model (WIT) imports. A proxy-wasm host cannot resolve them, so **one reference anywhere in a filter makes the app fail to instantiate — HTTP 530 on every path**, including paths that never reach the call `[live]`. It compiles and links with no warning. + +- **Rust:** in a filter import only from `fastedge::proxywasm::*`. Never use a top-level `fastedge::` module (`fastedge::cache`, `fastedge::secret`, …) — each has a `fastedge::proxywasm::` counterpart. +- **Check before deploying** — any `gcore:fastedge/...` import in a filter build means a 530 at runtime: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` + +--- + +## Reading the request — use properties, not headers + +Several headers a proxy-wasm developer would reach for are mangled or absent. **All failures are silent** `[live]`. + +| Read via | Result | +|---|---| +| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) on the edge tier; on shield nodes it may carry a `shield_` prefix | +| property `request.x_real_ip` | ✅ true client IP, IPv4 and IPv6 | +| property `request.path` | ⚠️ **includes the query string** — see below | +| property `request.query` | ✅ query without `?`; not set by the host when there is no query (how "not set" surfaces — `None`, empty buffer — depends on the SDK) | +| property `request.asn` / `.country` / `.city` / `.region` / `.continent` | ✅ | +| property `source.address` | ❌ does not exist (Envoy's name) — returns nothing | +| header `host` | ❌ mangled to `_cache_sharded` | +| headers `:path`, `:authority` | ❌ absent | +| headers `x-real-ip`, `x-forwarded-for`, `cookie`, `user-agent` | ✅ intact (`cookie` is visible even with `ignore_cookie: true`) | + +### `request.path` includes the query string + +``` +GET /x?a=1&b=2 +request.path = "/x?a=1&b=2" +request.query = "a=1&b=2" +``` + +Prefix checks are unaffected; **equality checks silently fail on any request with a query** — auth callbacks are exactly where equality checks live. Split on `?` first. Corollary: because it is path+query, `request.path` works directly as a relative `Location` for a same-origin redirect, which avoids the mangled `host` header. + +--- + +## Security — gating filters + +### The CDN does not normalise the path before the filter runs + +`[live]` Traversal and separator sequences arrive verbatim: + +```bash +curl --path-as-is ".../app/x/../test" # request.path = "/app/x/../test" +curl --path-as-is ".../app//test" # request.path = "/app//test" +``` + +A filter that bypasses auth on a prefix (`/auth/`, `/public/`) will wave through `/auth/../admin`, which the origin may normalise back to `/admin`. **That is an authentication bypass**, and it is the natural way to write the code. + +Reject suspicious sequences before any prefix bypass, and **fail closed** — fall through to the auth check. Never normalise the path yourself and proceed; you will not match the origin's rules. + +```rust +fn has_suspicious_sequence(path: &str) -> bool { + if path.contains("..") || path.contains("//") || path.contains('\\') { return true; } + let lower = path.to_ascii_lowercase(); + lower.contains("%2e") || lower.contains("%2f") + || lower.contains("%5c") || lower.contains("%25") // %25 catches double-encoding +} +``` + +### Filters run before the CDN cache lookup + +`[live, edge tier only]` A request-phase filter runs on **every** request, including cache hits. + +- **Security:** a cached response cannot bypass a request-phase gate. +- **Cost:** every request pays for whatever the filter does — KV reads, cache ops — even on cache hits. +- A `no-store` response from a filter is not cached. Varying the cache key on a session cookie fragments the cache per visitor. + +Only `execute_on_edge` was measured; ordering on the shield tier is unverified. + +--- + +## Synthetic responses + +`[live]` A filter can serve a complete response via `send_http_response`: any status, arbitrary headers, multiple `Set-Cookie` headers on one response, and bodies up to at least 1 MB without truncation. Single-app designs (challenge pages, interstitials, error pages) do not need a second HTTP app. + +--- + +## What local testing cannot prove + +`@gcoredev/fastedge-test` runs proxy-wasm filters locally and proves **flow logic** — routing, header manipulation, gate decisions. It cannot prove **host behaviour**: property semantics, header mangling, path non-normalisation, instantiation failures, cache interaction. Every trap on this page passes a local suite green. Confirm host-dependent behaviour on a deployed app attached to a real CDN resource. + +--- + +## See Also + +- [storage.md](./storage.md) — KV and Cache semantics, limits, measured performance +- [cdn-integration.md](./cdn-integration.md) — attaching filters to CDN resources +- [error-codes.md](./error-codes.md) — 530 diagnosis diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-integration.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-integration.md index 2024764..49aa783 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-integration.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/cdn-integration.md @@ -181,8 +181,25 @@ curl -s -X PATCH "https://api.gcore.com/cdn/resources/" \ }' ``` +**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. Do not confuse this with the rule-vs-resource replace semantics above, which govern request-time override, not the API. + Rules are managed through their own endpoints under `/cdn/resources/{resource_id}/rules`. The full rule API is in the Gcore CDN documentation. +### Cached content after a routing change + +Request-phase filters run before the cache lookup (see [cdn-filter-runtime.md](./cdn-filter-runtime.md)), so attaching or changing an `on_request_headers` filter takes effect on cached paths without a purge. What does **not** change on its own is content already cached from the previous configuration — e.g. after a rule that points a path at a different origin, or a change to response-phase output. That content keeps being served until it expires (observed ~15 min). + +In that case, purge only the affected paths, then verify: + +```bash +curl -s -X POST "https://api.gcore.com/cdn/resources//purge" \ + -H "Authorization: APIKey $GCORE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"paths": ["/affected/path/*"]}' +``` + +`{"paths": []}` purges **the entire resource** and sends all traffic back to origin — use it only with explicit user confirmation. The FastEdge MCP server's default access policy blocks this endpoint; when it is blocked, give the user the command to run themselves. + ## Operational Notes - **`app_id` is a string.** The FastEdge apps API (`GET /fastedge/v1/apps`) returns `id` as an integer, but in the `options.fastedge..app_id` field it must be quoted as a string. Mixing types is a common source of validation errors. @@ -191,7 +208,7 @@ Rules are managed through their own endpoints under `/cdn/resources/{resource_id - **Hook absence vs. `enabled: false`.** Both result in the hook not running. Removing the key entirely is cleaner; toggling `enabled: false` preserves the hook's other config for easy re-enable. - **`execute_on_shield` only matters when origin shielding is on.** If `shielded: false` on the resource, the shield-layer setting has no effect — the hook only ever runs at the edge. - **Rule weight ordering.** When multiple rules match a request path, only the rule with the lowest weight applies. Rules don't compose. -- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. +- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Content cached under the old configuration may also need a path-scoped purge (above). ## See Also diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/error-codes.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/error-codes.md index 23b24f5..fb81210 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/error-codes.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/error-codes.md @@ -10,7 +10,10 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The Wasm module failed to start up before processing the request. +**Signal:** a CDN app that fails to instantiate also returns `x-cdn-internal-status: 3100`. That header distinguishes "failed to start" from "the app ran and errored" (531). + **Common Causes:** +- **Unresolved imports (CDN apps)** — the binary imports a host function the proxy-wasm host does not provide, most commonly an HTTP-app (component-model) API used in a filter, e.g. top-level `fastedge::cache` in Rust instead of `fastedge::proxywasm::cache`. **Every path returns 530**, including paths that never reach the call, and the build gives no warning. See [cdn-filter-runtime.md](./cdn-filter-runtime.md). - Missing required environment variables that the app reads during initialization - Corrupted or invalid Wasm binary - Binary was compiled for the wrong target (not `wasm32-wasip1`) @@ -21,9 +24,15 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco - JS: `fastedge-build ./src/index.js ./.wasm` completed without errors - Rust: `cargo build --release --target wasm32-wasip1` succeeded 2. Check binary size: `ls -la *.wasm` or `ls -la target/wasm32-wasip1/release/*.wasm` -3. Test locally: `fastedge-run http -w ./app.wasm --port 8080` -4. Verify all required env vars are set via API or portal -5. Re-upload the binary and update the app +3. **CDN apps:** inspect the import table — any `gcore:fastedge/...` import in a proxy-wasm build is unresolvable: + ```bash + wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u + ``` +4. Run locally: + - HTTP apps: `fastedge-run http -w ./app.wasm --port 8080` + - CDN apps: `fastedge-run` does not run proxy-wasm — use `@gcoredev/fastedge-test`. Note a local run does not reproduce host instantiation, so a clean local run does not rule out step 3. +5. Verify all required env vars are set via API or portal +6. Re-upload the binary and update the app --- @@ -32,6 +41,7 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco **Meaning:** The app started successfully but threw an unhandled exception during request processing. **Common Causes:** +- Calling `getEnv()` / `getSecret()` at module top level (JS) — they are request-time only; call them inside the handler - Uncaught JavaScript exception (TypeError, ReferenceError, etc.) - Rust panic (`unwrap()` on `None` or `Err`) - Failed `fetch()` call without error handling diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/overview.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/overview.md index 415dc14..78300ee 100644 --- a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/overview.md +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/overview.md @@ -99,6 +99,14 @@ What is appropriate: per-end-user or per-session key prefixes within your app's 5. **Response Delivery** — Response sent back to client 6. **Cleanup** — Wasm instance destroyed, memory freed +### No scheduled or deferred work + +There is no scheduler or durable background execution. Timers exist within a request (e.g. JS `setTimeout`) but live only as long as that instance. JS `event.waitUntil()` extends the instance until a promise settles, starting immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no post-response work API. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron. + +### PoP identity + +`getEnv("dc")` returns the PoP's lowercase short code (e.g. `ls1`, `am3`) — the only PoP identifier available to an app. **It can be empty**; an empty-string fallback silently merges every PoP's keys into one namespace. Fail loudly instead. Node count per PoP varies, and the number of active PoPs is not discoverable from inside an app. + ## Resource Limits | Resource | Basic Plan | Pro Plan | diff --git a/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/storage.md b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/storage.md new file mode 100644 index 0000000..e55ac0c --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/fastedge-docs/reference/platform/storage.md @@ -0,0 +1,107 @@ +# Storage Semantics — KV Store and Cache + +Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your language. + +| | KV Store | Cache | +|---|---|---| +| Scope | Global, account-level, assigned to apps | Per-PoP, shared by every node in the PoP `[source + told]` | +| Consistency | Eventually consistent; **reads cached per node** (below) | Strong within a PoP | +| Writes from app code | ❌ API only | ✅ | +| Atomic primitive | — | `incr` only | + +Provenance tags: `[live]` measured on a deployed app · `[source]` read from the runtime source · `[told]` stated by the FastEdge team · `[unverified]` not yet confirmed. Last verified: 2026-08-27. + +--- + +## KV Store + +### `get()` is a cached read — ~30 s per node + +`[live, production]` Each `get(key)` populates a per-node cache entry that lives ~30 s, **whether or not the key existed**. Until it expires, that node keeps returning the old result — the old value, or "absent" for a key that now exists. Sorted-set reads (`zrangeByScore`) are **not** cached and reflect writes in 0–1 s. + +| Reads before the write | Write | Visible after | +|---|---|---| +| none | create `kv` key | 1 s | +| heavy `get()` polling of the absent key | create `kv` key | **31 s** | +| heavy `get()` polling of the existing key | update its value | **30 s, then flapped** | +| heavy `zrangeByScore` polling | add `sorted_set` member | 0 s | + +- **Values flap.** Warm and expired nodes answer differently at the same instant, so a polled `kv` flag can turn on, off, and on again during the window. For access gating this is worse than a delay. +- **Polling makes it worse** — every poll refreshes the staleness hiding the change. +- **Absence is not a safe signal.** "Key missing ⇒ not started" fails in the direction of "my change hasn't happened". + +**Rule:** use `kv` + `get()` for values written once and read as configuration. For anything polled that must see changes promptly (a "has this happened yet?" flag), use a **sorted set with one member whose score is the timestamp** — uncached and consistent across nodes. + +### Writes go through the API only + +`[live]` `PUT /fastedge/v1/kv/{store_id}/data` — never from inside an app. Many entries can be batched in one call. + +```jsonc +[{ "op": "add", // add | del_key | del_entries — REQUIRED + "key": "…", + "datatype": "kv", // kv | sorted_set | bloom_filter + "payload": { "value": "…", "encoding": "plain" } }] // encoding: plain | base64 | sha256 +``` + +- **`sorted_set` nests differently:** its `payload` *is* the member array. The 400 only says `doesn't match any schema from "anyOf"`. To learn a payload shape, write one entry by hand and read it back with `GET /fastedge/v1/kv/{store}/data/{key}` — the read shape mirrors the write shape. +- **`datatype` scopes `del_key`.** Deleting a `kv` key with `datatype: "sorted_set"` returns **200 with `del_count: 0`** and removes nothing; a later `zrangeByScore` on that name then throws a type error. **Check `del_count`** on cleanup writes, especially when migrating a key between datatypes. +- The response includes `revision`, a store-wide monotonic counter — a freshness signal independent of clocks. +- Attaching a store to an app: `stores: {"": {"id": }}`; omit the inner `name` (read-only). + +### Missing operations + +`[source]` The read surface is exactly `open · get · scan · zrange_by_score · zscan · bf_exists`. There is **no** rank-based `ZRANGE`, `ZCOUNT`, `ZCARD` or `ZSCORE`. Do not emulate them in app code: `zrange_by_score(-inf, +inf)` transfers every member with its score into the wasm instance on every call. + +`zrange_by_score` on a missing key returns an empty list, not an error. + +--- + +## Cache + +### Interface and atomicity + +`[source]` `get · set · delete · exists · incr · expire · purge · purge_prefix`. **`incr` is the only atomic primitive** — no compare-and-set, `SETNX`, scripting, multi-key operations, or `SCAN`. Coordination must be expressible as "increment and compare the returned value". + +### TTL rules + +`[source + live]` + +- **TTLs are silently clamped** to a deployment ceiling (default 4 days). The JS and Rust SDK references describe `set` without a TTL as "no expiry"; the host actually applies the default ceiling (runtime source, and a no-TTL `set` key measured gone within 22 days). **Treat the host behaviour as authoritative** — do not rely on any entry outliving ~4 days. There is no TTL-read primitive, so the clamp is invisible. +- **`incr` sets no TTL and does not index the key.** `incr`-created keys escape both the default-TTL cleanup and `purge_prefix` — measured still present after 22 days. Every `incr` key needs an `expire`, but *when* depends on the key shape — `expire` sets a deadline relative to now, so calling it again pushes the deadline out: + - **Fixed key** (e.g. `rl:`): call `expire` only when `incr` returns `1`, and retry that `expire` within the request budget. Expiring on every call resets the window and keeps the key alive under continuous traffic. If the one `expire` fails for good, the key never expires — there is no TTL-read to detect it. + - **Time-bucketed key** (window id in the name, e.g. `rl::`): calling `expire` on every call is safe and self-healing — writes stop when the window passes, so the key expires one TTL after its last write even if earlier `expire` calls failed. Prefer this shape when a permanently leaked key is costly. +- `expire` returns `false` for an absent key and `Err` for a failure. Do not collapse the two. + +### Failures are real — retry + +`[live, production]` Cache operations fail outright with explicit errors (not timeouts) at ~0.44% overall, bursty and PoP-specific (one PoP showed 1.5%). Plan for retries rather than assuming success, but bound them by the remaining execution budget — a single op can take ~100 ms at the tail, beyond the Basic plan's 50 ms limit. Decide deliberately whether an exhausted retry fails open or closed. + +> **CDN filters:** the figures and TTL behaviour on this page were measured through HTTP apps. Proxy-wasm cache support is newer and is `[unverified]` to share the same backend behaviour. + +--- + +## Measured performance + +`[live, production]` Point-in-time, one account, mostly one PoP — evidence, not guarantees. + +| Metric | Value | +|---|---| +| `Cache.incr` | p50 1 ms · p95 27 ms · max 102 ms | +| `Cache.expire` | p50 2 ms · p95 6 ms · max 21 ms | +| KV write → visible (no prior `get()` polling) | 61 / 80 / 107 ms (min / p50 / max) | +| KV write rate, one client | sustained ~94 writes/s | +| KV sorted-set ingest | ~2,450 members/s; knee at 32–64 concurrent writers | +| Largest single batched `PUT` | ~1,560 entries — bounded by a 30 s gateway timeout, not payload size | +| Sorted-set storage | ~31 bytes/member | +| Intra-PoP clock skew | ≤5 ms, probably ≤1 ms | + +### Preprod is not production + +Preprod KV propagation was bimodal — typically ~210 ms, but ~1 in 8–15 writes invisible for over 45 s. Production did not do this. Do not benchmark or size designs on preprod. Before attributing a ~30 s delay to either, check the cached-read behaviour above: if something was polling the key with `get()`, or the value flaps once it appears, it is the per-node read cache. + +--- + +## See Also + +- [cdn-filter-runtime.md](./cdn-filter-runtime.md) — what a CDN filter can access +- [overview.md](./overview.md) — account-level KV stores, resource limits 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 695920d..2393c2e 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/ab-testing-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -222,3 +222,233 @@ pnpm run asbuild - proxy-wasm-sdk-as assembly API reference - FastEdge environment variable configuration - FastEdge CDN application deployment guide + +## Source Material + +### FILE: examples/abTesting/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; +import { getCurrentTime } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge/utils/runtime"; + +class AbTestingRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new AbTestingContext(context_id, this); + } +} + +class AbTestingContext extends Context { + constructor(context_id: u32, root_context: AbTestingRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const experimentName = getEnv("EXPERIMENT_NAME"); + if (experimentName === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode("App misconfigured - EXPERIMENT_NAME must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const variantAPath = getEnv("VARIANT_A_PATH"); + const variantBPath = getEnv("VARIANT_B_PATH"); + if (variantAPath === "" || variantBPath === "") { + send_http_response( + 500, + "internal server error", + String.UTF8.encode( + "App misconfigured - VARIANT_A_PATH and VARIANT_B_PATH must be set", + ), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Check for existing experiment cookie + const cookieName = "fe_exp_" + experimentName; + const cookieHeader = stream_context.headers.request.get("Cookie"); + let assignedVariant = this.getCookieValue(cookieHeader, cookieName); + + // Assign variant if not already set + if (assignedVariant !== "A" && assignedVariant !== "B") { + // Use current time as a simple entropy source for 50/50 split + const now = getCurrentTime(); + assignedVariant = now % 2 == 0 ? "A" : "B"; + } + + // Rewrite the request path to the variant path + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + return FilterHeadersStatusValues.Continue; + } + const originalPath = String.UTF8.decode(pathArrBuf); + const variantPath = assignedVariant === "A" ? variantAPath : variantBPath; + const newPath = variantPath + originalPath; + + // Reconstruct request.url from its decomposed parts rather than splicing + // the path out of the full URL — splicing breaks when the path happens to + // appear inside the host, and it can silently lose the query string. + const schemeBuf = get_property("request.scheme"); + const hostBuf = get_property("request.host"); + if (schemeBuf.byteLength > 0 && hostBuf.byteLength > 0) { + const scheme = String.UTF8.decode(schemeBuf); + const host = String.UTF8.decode(hostBuf); + const queryBuf = get_property("request.query"); + const query = queryBuf.byteLength > 0 ? String.UTF8.decode(queryBuf) : ""; + const newUrl = + scheme + "://" + host + newPath + (query.length > 0 ? "?" + query : ""); + log(LogLevelValues.info, `A/B routing: ${newUrl}`); + set_property("request.url", String.UTF8.encode(newUrl)); + } + + // Add variant header for upstream visibility + stream_context.headers.request.add("X-Experiment", experimentName); + stream_context.headers.request.add("X-Variant", assignedVariant); + + log( + LogLevelValues.info, + `A/B test "${experimentName}": variant ${assignedVariant}, path ${newPath}`, + ); + + return FilterHeadersStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Recover the assigned variant from the request header set in onRequestHeaders. + // Instance state (this.variant) does not survive the nginx -> core-proxy hop. + const variant = stream_context.headers.request.get("X-Variant"); + if (variant === "") { + return FilterHeadersStatusValues.Continue; + } + + const experimentName = getEnv("EXPERIMENT_NAME"); + const cookieName = "fe_exp_" + experimentName; + + // Set the experiment cookie so subsequent requests stick to the same variant + stream_context.headers.response.add( + "Set-Cookie", + cookieName + "=" + variant + "; Path=/; Max-Age=86400; SameSite=Lax", + ); + + // Add variant as response header for observability + stream_context.headers.response.add("X-Variant", variant); + + return FilterHeadersStatusValues.Continue; + } + + private getCookieValue(cookieHeader: string, name: string): string { + if (cookieHeader === "") return ""; + const pairs = cookieHeader.split(";"); + const prefix = name + "="; + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].trim(); + if (pair.startsWith(prefix)) { + return pair.substring(prefix.length); + } + } + return ""; + } +} + +registerRootContext((context_id: u32) => { + return new AbTestingRoot(context_id); +}, "abTesting"); +``` + + +### FILE: examples/abTesting/package.json + +```json +{ + "name": "fastedge-as-example-ab-testing", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: A/B Testing — cookie-based traffic splitting at the CDN layer", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/abTesting/README.md + +``` +[← Back to examples](../README.md) + +# A/B Testing + +This application performs cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. + +## What it does + +In `onRequestHeaders`, the app: + +1. Checks for an existing experiment cookie (`fe_exp_`). +2. If no cookie is found, assigns the user to variant **A** or **B** (50/50 split). +3. Rewrites the request path by prepending the variant-specific path prefix (e.g. `/variant-a/original/path`). +4. Adds `X-Experiment` and `X-Variant` request headers for upstream visibility. + +In `onResponseHeaders`, the app sets a `Set-Cookie` header to persist the variant assignment for subsequent requests (24-hour TTL). + +> **Note on variant assignment entropy:** New-visitor assignment uses `getCurrentTime() % 2` as a simple 50/50 source. This is illustrative — it is not sticky across two requests that arrive in the same millisecond and is not reproducible in tests. Production A/B implementations typically hash a stable visitor identifier (e.g. client IP or session token) for deterministic, sticky pre-cookie assignment. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `EXPERIMENT_NAME` | `homepage-redesign` | Name of the experiment (required) | +| `VARIANT_A_PATH` | `/variant-a` | Path prefix for variant A (required) | +| `VARIANT_B_PATH` | `/variant-b` | Path prefix for variant B (required) | + +Your origin server should serve different content at each variant path prefix. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/abTesting.wasm` | Optimised release binary — upload this to FastEdge | +| `build/abTesting-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/abTesting.wasm` to the FastEdge portal and attach it to your CDN application. Configure the experiment environment variables in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/api-key-rust.md index 4295b4b..f97e840 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -212,3 +212,122 @@ target = "wasm32-wasip1" - FastEdge secrets configuration (dashboard secret variable setup) - auth-jwt-rust reference (JWT-based authentication — use when token expiry and claims are needed) - platform-overview reference (CDN vs HTTP app type distinction) + +## Source Material + +### FILE: examples/cdn/api_key/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating API key validation. + +Validates requests using an X-API-Key header checked against a stored +secret. Simpler alternative to JWT when token expiry and claims are +not needed. + +Required configuration: + - Secret: API_KEY +*/ + +use fastedge::proxywasm::secret; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { 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 07cdb77..9da636d 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -267,3 +267,204 @@ pnpm run asbuild - `@gcoredev/as-jwt` npm package - proxy-wasm-sdk-as host services reference (getSecret, send_http_response, stream_context) - cdn-base skeleton blueprint + +## Source Material + +### FILE: examples/jwt/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getSecret, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +import { jwtVerify, JwtValidation } from "@gcoredev/as-jwt/assembly"; + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class AuthRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set log level to info is the default setting. This is purely here for demonstration purposes + return new Auth(context_id, this); + } +} + +class Auth extends Context { + constructor(context_id: u32, root_context: AuthRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const secret = getSecret("SECRET"); + if (!secret) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const authHeader = stream_context.headers.request.get("Authorization"); + if (!authHeader) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("No Authorization header"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + if (!authHeader.startsWith("Bearer ")) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Authorization header must use Bearer scheme"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const token = authHeader.slice(7); // strip "Bearer " prefix + if (!token) { + send_http_response( + UNAUTHORIZED, + "unauthorized", + String.UTF8.encode("Token not found"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + // Decode the JWT token + const jwtResult = jwtVerify(token, secret); + if (jwtResult !== JwtValidation.Ok) { + if (jwtResult === JwtValidation.Expired) { + log(LogLevelValues.info, "Token Expired"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Expired token"), + [], + ); + } else { + log(LogLevelValues.info, "Bad Token"); + send_http_response( + FORBIDDEN, + "forbidden", + String.UTF8.encode("Invalid token"), + [], + ); + } + return FilterHeadersStatusValues.StopIteration; + } + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new AuthRoot(context_id); +}, "auth"); +``` + +### FILE: examples/jwt/package.json + +```json +{ + "name": "fastedge-as-example-jwt", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: JWT validation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/as-jwt": "^1.0.3", + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3", + "assemblyscript-json": "^1.1.0" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + +### FILE: examples/jwt/README.md + +``` +[← Back to examples](../README.md) + +# JWT Validation + +This application validates a JWT Bearer token on every incoming request using the [`@gcoredev/as-jwt`](https://www.npmjs.com/package/@gcoredev/as-jwt) library. + +## What it does + +In `onRequestHeaders`, the app: + +1. Reads the HMAC secret from a FastEdge secret variable named `SECRET`. +2. Checks that the `Authorization` header uses the `Bearer` scheme; rejects other schemes with `401`. +3. Verifies the token signature and expiry using `jwtVerify()`. +4. Allows the request through on a valid token, or returns `401`/`403` on missing, invalid scheme, expired, or invalid tokens. + +## Configuration + +Set the following secret variable on your FastEdge application: + +| Secret | Description | +| -------- | ------------------------------------------------------------------ | +| `SECRET` | The HMAC-SHA256 signing secret (at least 256 bits / 32 characters) | + +## Testing tokens + +**Expired token** (will return `403 Forbidden`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk3ODMxMDg2MX0.egSSDoDdAHz8Kqee7be9N168CDEwOiOej96Idm2c1yQ +``` + +**Valid token** (expiry: 2035-01-01, will return `200 OK`): + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjIwNTEyMjYwNjF9.zn_pSdcBo8T3SvNgMVYzWc5CU_MKqOlms7TpZXhPtJU +``` + +Both tokens use the secret `a-string-secret-at-least-256-bits-long-thats-hard-to-break`. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ---------------------- | -------------------------------------------------- | +| `build/jwt.wasm` | Optimised release binary — upload this to FastEdge | +| `build/jwt-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/jwt.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `SECRET` secret variable in the application settings. + +For more on secrets and secret rotation slots, see the FastEdge secrets documentation. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/auth-jwt-rust.md index c3da37d..a1badbb 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -259,142 +259,3 @@ target = "wasm32-wasip1" - FastEdge SDK Rust reference (proxywasm module, secret API) - FastEdge secrets configuration (dashboard secret variable setup) - platform-overview reference (CDN vs HTTP app type distinction) - -## Source Material - -### FILE: examples/cdn/jwt/src/lib.rs - -```rust -use std::time::{SystemTime, UNIX_EPOCH}; -use headers::HeaderValue; -use headers::authorization::{Bearer, Credentials}; - -use fastedge::proxywasm::secret; -use jsonwebtoken::{decode, DecodingKey, Validation}; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde::Deserialize; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { 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 3f82231..6a43237 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -14,8 +14,8 @@ languages: [assemblyscript] template_origin: cdn-base source_example: proxy-wasm-sdk-as/examples/helloWorld source_repo: proxy-wasm-sdk-as -source_ref: 8e3bb621bc013a0aed7e52122066b417ad62a207 -updated: 2026-08-20 +source_ref: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 +updated: 2026-09-22 --- # Base Skeleton: CDN AssemblyScript @@ -263,118 +263,3 @@ registerRootContext((context_id: u32) => { - platform-overview (CDN filter pipeline, hook execution order) - examples-headers-cdn-assemblyscript (header manipulation feature blueprint) - examples-body-cdn-assemblyscript (body transformation feature blueprint) - -## Source Material - -### FILE: examples/helloWorld/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. -import { - Context, - FilterDataStatusValues, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; - -class HelloWorldRoot extends RootContext { - createContext(context_id: u32): Context { - return new HelloWorld(context_id, this); - } -} - -class HelloWorld extends Context { - constructor(context_id: u32, root_context: HelloWorldRoot) { - super(context_id, root_context); - } - - onRequestHeaders( - headers: u32, - end_of_stream: bool, - ): FilterHeadersStatusValues { - log(LogLevelValues.info, "onRequestHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onRequestBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onRequestBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } - - onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - log(LogLevelValues.info, "onResponseHeaders >> Hello World!"); - return FilterHeadersStatusValues.Continue; - } - - onResponseBody( - body_buffer_length: usize, - end_of_stream: bool, - ): FilterDataStatusValues { - log(LogLevelValues.info, "onResponseBody >> Hello World!"); - return FilterDataStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new HelloWorldRoot(context_id); -}, "helloWorld"); -``` - - -### FILE: examples/helloWorld/package.json - -```json -{ - "name": "fastedge-as-example-hello-world", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Hello World — minimal CDN app skeleton", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/helloWorld/asconfig.json - -```json -{ - "extends": "./node_modules/@assemblyscript/wasi-shim/asconfig.json", - "targets": { - "debug": { - "outFile": "build/helloWorld-debug.wasm", - "textFile": "build/helloWorld-debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/helloWorld.wasm", - "textFile": "build/helloWorld.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "use": "abort=abort_proc_exit" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/base-rust.md index d3ad5df..c74dfa9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -13,8 +13,8 @@ app_type: cdn languages: [rust] template_origin: cdn-base source_repo: https://github.com/G-Core/FastEdge-sdk-rust -source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-20 +source_ref: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 +updated: 2026-09-22 --- # Base Skeleton: CDN Rust @@ -208,77 +208,3 @@ 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 837648f..89784fd 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -360,3 +360,218 @@ Upload `build/body.wasm` via the FastEdge portal and attach it to your CDN appli - cdn-base skeleton - FastEdge portal deployment guide - fastedge-test reference (for testing body manipulation logic) + +## Source Material + +### FILE: examples/body/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + BufferTypeValues, + Context, + FilterDataStatusValues, + FilterHeadersStatusValues, + get_buffer_bytes, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + set_buffer_bytes, + set_property, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { setLogLevel } from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class HttpBodyRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); // Set the log level to info - for more logging reduce this to LogLevelValues.debug + return new HttpBody(context_id, this); + } +} + +class HttpBody extends Context { + constructor(context_id: u32, root_context: HttpBodyRoot) { + super(context_id, root_context); + } + + onRequestHeaders( + headers: u32, + end_of_stream: bool + ): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onRequestHeaders >>"); + // Remove the "content-length" header + stream_context.headers.request.remove("content-length"); + return FilterHeadersStatusValues.Continue; + } + + onRequestBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onRequestBody >>"); + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.debug, "onRequestBody >> bodyStr: " + bodyStr); + if (bodyStr.includes("Client")) { + const newBody = `Original message body (${body_buffer_length.toString()} bytes) redacted.\n`; + set_buffer_bytes( + BufferTypeValues.HttpRequestBody, + 0, + body_buffer_length, + String.UTF8.encode(newBody) + ); + } + } + return FilterDataStatusValues.Continue; + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.debug, "onResponseHeaders >>"); + + // Remove "content-length" header as the body size will change + stream_context.headers.response.remove("content-length"); + + // Set "transfer-encoding" to "chunked" + stream_context.headers.response.replace("transfer-encoding", "Chunked"); + + const contentType = stream_context.headers.response.get("content-type"); + if (contentType.length > 0) { + set_property("response.content_type", String.UTF8.encode(contentType)); + } + + return FilterHeadersStatusValues.Continue; + } + + onResponseBody( + body_buffer_length: usize, + end_of_stream: bool + ): FilterDataStatusValues { + log(LogLevelValues.debug, "onResponseBody >>" + end_of_stream.toString()); + + if (!end_of_stream) { + // Wait until the complete body is buffered + return FilterDataStatusValues.StopIterationAndBuffer; + } + + log( + LogLevelValues.debug, + "onResponseBody >> body_buffer_length: " + body_buffer_length.toString() + ); + + // Retrieve the request URL + const urlBytes = get_property("request.url"); + const url = urlBytes.byteLength === 0 ? "" : String.UTF8.decode(urlBytes); + if (url !== "") { + log(LogLevelValues.info, `url=${url}`); + } + + // Retrieve the response content type stored in onResponseHeaders + const contentTypeBytes = get_property("response.content_type"); + const contentType = + contentTypeBytes.byteLength === 0 + ? "" + : String.UTF8.decode(contentTypeBytes); + if (contentType !== "") { + log(LogLevelValues.info, `contentType=${contentType}`); + } + + // Retrieve the body from the HttpRequestBody buffer + const bodyBytes = get_buffer_bytes( + BufferTypeValues.HttpResponseBody, + 0, + body_buffer_length + ); + + if (bodyBytes.byteLength > 0) { + const bodyStr = String.UTF8.decode(bodyBytes); + log(LogLevelValues.info, "onResponseBody >> bodyStr: " + bodyStr); + } + return FilterDataStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new HttpBodyRoot(context_id); +}, "httpbody"); +``` + + +### FILE: examples/body/package.json + +```json +{ + "name": "fastedge-as-example-body", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Body manipulation", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/body/README.md + +``` +[← Back to examples](../README.md) + +# Body + +This application modifies the request and response body using the `onRequestBody` and `onResponseBody` lifecycle hooks. + +## What it does + +1. **`onRequestHeaders`** — removes the `content-length` header, required because the body content will be altered. + +2. **`onRequestBody`** — buffers the full request body then checks whether it contains the word `Client`. If found, the body is replaced with a redaction notice. + +3. **`onResponseHeaders`** — removes `content-length`, sets `transfer-encoding: Chunked`, and captures the `content-type` into a runtime property. + +4. **`onResponseBody`** — logs the request URL, content type, and full response body once the stream is complete. + +This demonstrates the basic flow for body manipulation across all lifecycle hooks. Key points: + +- Headers must be adjusted _before_ modifying the body (`content-length`, `transfer-encoding`). +- The `end_of_stream` flag is checked before processing, allowing the body to be buffered across multiple invocations before acting on it. + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +| ----------------------- | -------------------------------------------------- | +| `build/body.wasm` | Optimised release binary — upload this to FastEdge | +| `build/body-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/body.wasm` to the FastEdge portal and attach it to your CDN application. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/body-rust.md index 4b49934..47fbc4a 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -243,3 +243,125 @@ Requires `log = "0.4"` in `Cargo.toml`. Log level is set to `Trace` at startup v - proxy-wasm HttpContext trait reference - host-services-rust reference (property API, logging) - platform-overview reference (CDN app lifecycle) + +## Source Material + +### FILE: examples/cdn/body/src/lib.rs + +```rust +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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 f49e70c..63a5c49 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-control-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -187,3 +187,191 @@ Scripts defined in `package.json`: - cdn-base skeleton (base class structure and proxy-wasm lifecycle) - sdk-reference assemblyscript (full API surface for stream_context, get_property, getEnv) - platform-overview (CDN app deployment and environment variable configuration) + +## Source Material + +### FILE: examples/cacheControl/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + stream_context, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +class CacheControlRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new CacheControlContext(context_id, this); + } +} + +class CacheControlContext extends Context { + constructor(context_id: u32, root_context: CacheControlRoot) { + super(context_id, root_context); + } + + onResponseHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + const statusBuf = get_property("response.status"); + let statusCode: u32 = 200; + if (statusBuf.byteLength >= 2) { + const bytes = Uint8Array.wrap(statusBuf); + statusCode = (u32(bytes[0]) << 8) | u32(bytes[1]); + } + + // Only cache successful responses + if (statusCode < 200 || statusCode >= 400) { + stream_context.headers.response.replace( + "Cache-Control", + "no-store", + ); + return FilterHeadersStatusValues.Continue; + } + + // Determine cache policy based on content type + const contentType = stream_context.headers.response.get("Content-Type"); + + const rawStaticMaxAge = getEnv("STATIC_MAX_AGE"); + const staticMaxAge = rawStaticMaxAge === "" ? "31536000" : rawStaticMaxAge; + const rawHtmlMaxAge = getEnv("HTML_MAX_AGE"); + const htmlMaxAge = rawHtmlMaxAge === "" ? "3600" : rawHtmlMaxAge; + const rawApiMaxAge = getEnv("API_MAX_AGE"); + const apiMaxAge = rawApiMaxAge === "" ? "0" : rawApiMaxAge; + + let cacheControl: string; + + if (this.isStaticAsset(contentType)) { + // Static assets: long cache, immutable + cacheControl = "public, max-age=" + staticMaxAge + ", immutable"; + } else if (contentType.includes("text/html")) { + // HTML: short cache, must revalidate + cacheControl = "public, max-age=" + htmlMaxAge + ", must-revalidate"; + stream_context.headers.response.add("Vary", "Accept-Encoding"); + } else if ( + contentType.includes("application/json") || + contentType.includes("application/xml") + ) { + // API responses: configurable, private by default + if (apiMaxAge === "0") { + cacheControl = "no-cache, no-store, must-revalidate"; + } else { + cacheControl = "private, max-age=" + apiMaxAge + ", must-revalidate"; + } + stream_context.headers.response.add("Vary", "Accept, Authorization"); + } else { + // Default: moderate cache + cacheControl = "public, max-age=600"; + } + + stream_context.headers.response.replace("Cache-Control", cacheControl); + + log( + LogLevelValues.info, + "Cache-Control: " + cacheControl + " (content-type: " + contentType + ")", + ); + + return FilterHeadersStatusValues.Continue; + } + + private isStaticAsset(contentType: string): bool { + return ( + contentType.includes("image/") || + contentType.includes("font/") || + contentType.includes("application/javascript") || + contentType.includes("text/css") || + contentType.includes("text/javascript") || + contentType.includes("application/wasm") + ); + } +} + +registerRootContext((context_id: u32) => { + return new CacheControlRoot(context_id); +}, "cacheControl"); +``` + + +### FILE: examples/cacheControl/package.json + +```json +{ + "name": "fastedge-as-example-cache-control", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Cache Control — content-type-aware cache headers", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/cacheControl/README.md + +``` +[← Back to examples](../README.md) + +# Cache Control + +This application sets `Cache-Control` response headers based on the content type and response status, giving you fine-grained control over CDN caching behaviour. + +## What it does + +In `onResponseHeaders`, the app inspects the `Content-Type` and `response.status` to apply an appropriate caching policy: + +| Content Type | Cache Policy | Default Max-Age | +|---|---|---| +| Images, fonts, JS, CSS, WASM | `public, max-age=, immutable` | 1 year (31536000s) | +| `text/html` | `public, max-age=, must-revalidate` | 1 hour (3600s) | +| `application/json`, `application/xml` | `private, max-age=, must-revalidate` or `no-cache, no-store` | 0 (no cache) | +| Other | `public, max-age=600` | 10 minutes | +| Error responses (4xx/5xx) | `no-store` | — | + +Also adds `Vary` headers where appropriate (`Accept-Encoding` for HTML, `Accept, Authorization` for API responses). + +## Configuration + +All environment variables are optional — sensible defaults are applied when unset. + +| Variable | Default | Description | +|----------|---------|-------------| +| `STATIC_MAX_AGE` | `31536000` | Max-age for static assets (seconds) | +| `HTML_MAX_AGE` | `3600` | Max-age for HTML responses (seconds) | +| `API_MAX_AGE` | `0` | Max-age for API responses (0 = no-cache) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/cacheControl.wasm` | Optimised release binary — upload this to FastEdge | +| `build/cacheControl-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/cacheControl.wasm` to the FastEdge portal and attach it to your CDN application. Optionally configure the max-age environment variables. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-rust.md new file mode 100644 index 0000000..7b6e966 --- /dev/null +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cache-rust.md @@ -0,0 +1,228 @@ + + +# CDN Cache — Rust Example + +## Overview + +Demonstrates cache operations in a CDN app using the proxy-wasm ABI via `fastedge::proxywasm::cache`. The cache has no named stores or handles — every operation is scoped to the calling application and addressed by key alone. This distinguishes it from the KV store, which uses named stores. + +Operations are selected via the `action` query parameter. All responses are JSON. Errors return HTTP 500 with `{"error": "..."}`. + +## App Type + +- **App type**: CDN (proxy-wasm) +- **Language**: Rust +- **Crate type**: `cdylib` +- **Entry point**: `on_http_response_body` — operations run during the response phase + +## Dependencies + +```toml +proxy-wasm = "0.2" +fastedge = { path = "...", features = ["proxywasm"] } +querystring = "1.1" +serde_json = "1" +``` + +Feature flag `proxywasm` must be enabled on the `fastedge` crate. + +## API Reference + +All functions are in the `fastedge::proxywasm::cache` module. + +### `cache::get` + +```rust +pub fn get(key: &str) -> Result>, Error> +``` + +Retrieves cached bytes by key. + +- Returns `Ok(Some(Vec))` when the key exists. +- Returns `Ok(None)` when the key is absent. +- Returns `Err` on host error. + +### `cache::set` + +```rust +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> +``` + +Stores bytes under a key with an optional TTL. + +- `ttl_ms`: milliseconds until expiry. Pass `None` for no expiry. +- Overwrites any existing value for the key. + +### `cache::delete` + +```rust +pub fn delete(key: &str) -> Result<(), Error> +``` + +Removes a key. No-op when the key is absent. + +### `cache::exists` + +```rust +pub fn exists(key: &str) -> Result +``` + +Tests whether a key is present. Returns `true` if the key exists, `false` otherwise. + +### `cache::incr` + +```rust +pub fn incr(key: &str, delta: i64) -> Result +``` + +Atomically increments (or decrements) a numeric counter stored under `key`. + +- `delta` may be negative. +- A missing key is treated as starting at `0`. +- Returns the new value after applying `delta`. + +### `cache::expire` + +```rust +pub fn expire(key: &str, ttl_ms: u64) -> Result +``` + +Sets or updates the expiry of an existing key. + +- `ttl_ms`: milliseconds from now until the key expires. +- Returns `true` if the key existed and was updated, `false` if the key was absent. + +### `cache::purge` + +```rust +pub fn purge() -> Result +``` + +Deletes every key owned by the calling application. + +- Returns the number of keys deleted. + +### `cache::purge_prefix` + +```rust +pub fn purge_prefix(prefix: &str) -> Result +``` + +Deletes all keys owned by the calling application whose names start with `prefix`. + +- Returns the number of keys deleted. + +## Query Parameter Interface + +The example app exposes all cache operations via HTTP query parameters. + +| Query string | Operation | +|---|---| +| `?action=get&key=` | Read a value. `response` is `null` when absent. | +| `?action=set&key=&value=[&ttl=]` | Store a value. Omit `ttl` for no expiry. | +| `?action=delete&key=` | Delete a key. No-op when absent. | +| `?action=exists&key=` | Key membership check. | +| `?action=incr&key=&delta=` | Atomic increment/decrement. Missing key starts at `0`. | +| `?action=expire&key=&ttl=` | Set or update expiry. `response` is `false` when key absent. | +| `?action=purge` | Delete all keys for this app; returns count deleted. | +| `?action=purgePrefix&prefix=` | Delete keys with prefix; returns count deleted. | + +Default action when `action` is omitted: `get`. + +## Response Format + +Successful responses are JSON objects. Shape varies by action: + +**get** +```json +{ "action": "get", "key": "", "response": "" | null } +``` + +**set** +```json +{ "action": "set", "key": "", "value": "", "ttlMs": | null, "response": true } +``` + +**delete** +```json +{ "action": "delete", "key": "", "response": true } +``` + +**exists** +```json +{ "action": "exists", "key": "", "response": true | false } +``` + +**incr** +```json +{ "action": "incr", "key": "", "delta": , "response": } +``` + +**expire** +```json +{ "action": "expire", "key": "", "ttlMs": , "response": true | false } +``` + +**purge** +```json +{ "action": "purge", "response": } +``` + +**purgePrefix** +```json +{ "action": "purgePrefix", "prefix": "", "response": } +``` + +**Error (HTTP 500)** +```json +{ "error": "" } +``` + +## Proxy-wasm Integration + +The app uses the proxy-wasm `HttpContext` trait. Key implementation details: + +- Operations execute in `on_http_response_body`, triggered when `end_of_stream` is `true`. +- The request query string is read via `self.get_property(vec!["request.query"])`. +- Response headers are set in `on_http_response_headers`: removes `content-length`, sets `content-type: application/json`, sets `transfer-encoding: chunked`. +- The upstream response body is replaced entirely using `set_http_response_body`. +- Error status is set via `self.set_property(vec!["response.status"], Some(b"500"))`. +- The app pauses body accumulation (`Action::Pause`) until `end_of_stream` is received. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache.wasm +``` + +## Usage Examples + +```sh +curl 'https:///?action=set&key=hits&value=0&ttl=60000' +curl 'https:///?action=incr&key=hits&delta=1' +curl 'https:///?action=get&key=hits' +``` + +## Constraints and Behavior Notes + +- Cache scope is per-application. Two apps cannot share cache entries. +- `set` with no `ttl` parameter stores the value with no expiry. +- `incr` operates atomically. The key must store a numeric value; if the key is absent it is initialized to `0` before applying `delta`. +- `expire` returns `false` (not an error) when the target key does not exist. +- `purge` and `purge_prefix` return the count of deleted keys, not an error, when no matching keys exist. +- All values are stored and retrieved as bytes (`&[u8]` / `Vec`). String conversion is the caller's responsibility. + +## See Also + +- CDN KV store example (key_value) — named stores, per-store handles, similar operation set +- `fastedge::proxywasm` module — proxy-wasm host service bindings +- proxy-wasm `HttpContext` trait — lifecycle hooks used by CDN apps +- FastEdge CDN app platform overview — app type selection, deployment model 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 e156022..1fa5481 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -38,12 +38,12 @@ image = "0.25" ## Environment Variables (Configuration) -| Variable | Required | Type | Constraints | Default | Description | -|-----------------------|----------|-----------------------------|-----------------|---------|------------------------------------------------------------------| -| `FORMATS_TO_TRANSFORM`| Yes | Comma-separated strings | Non-empty | — | File extensions to convert (e.g., `jpg,jpeg,png`). Note: `jpg` and `jpeg` are distinct entries. | -| `IGNORED_UA_LIST` | No | Comma-separated strings | — | — | User-Agent substrings; requests matching any entry are skipped. | -| `AVIF_SPEED` | No | u8 | 1–10 inclusive | 5 | AVIF encoding speed. Values outside range fall back to default. | -| `AVIF_QUALITY` | No | u8 | 1–100 inclusive | 70 | AVIF encoding quality. Values outside range fall back to default.| +| Variable | Required | Type | Constraints | Default | Description | +|------------------------|----------|-------------------------|------------------|---------|---------------------------------------------------------------------------------------------------| +| `FORMATS_TO_TRANSFORM` | Yes | Comma-separated strings | Non-empty | — | File extensions to convert (e.g., `jpg,jpeg,png`). Note: `jpg` and `jpeg` are distinct entries. | +| `IGNORED_UA_LIST` | No | Comma-separated strings | — | — | User-Agent substrings; requests matching any entry are skipped. | +| `AVIF_SPEED` | No | u8 | 1–10 inclusive | 5 | AVIF encoding speed. Values outside range fall back to default. | +| `AVIF_QUALITY` | No | u8 | 1–100 inclusive | 70 | AVIF encoding quality. Values outside range fall back to default. | Parameter helper behavior: if a variable is absent, empty, non-numeric, or out of range, the default is used and a trace log is emitted. @@ -95,7 +95,7 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action let Ok(ext) = from_utf8(&ext) else { return Action::Continue; }; if ext.is_empty() { return Action::Continue; } ``` - Safe decoding chain: `Option>` → check `None` → decode UTF-8 → check empty. + Safe decoding chain: `Option>` → check `None` → decode UTF-8 → check empty. This extracts the file extension without manual path splitting. 3. Check `FORMATS_TO_TRANSFORM` env var — if the extension is not in the comma-separated list, pass through unchanged. 4. Read `User-Agent` request header — if absent or empty, pass through. 5. Check `IGNORED_UA_LIST` env var — if the UA contains any listed substring, pass through. @@ -243,12 +243,12 @@ Returns `Err(VarError::NotPresent)` if the variable is absent or empty. ## Cross-Hook Signalling Summary -| Signal | Set by | Read by | Mechanism | -|-------------------------|----------------------|---------------------------|------------------------------------------------| -| `Image-Format: original`| request hook (always)| response-headers hook | request header | -| `Image-Format: image/avif` | request hook (on match) | response-headers hook | request header | -| `response.content-type` | response-headers hook| response-body hook | `set_property` / `get_property` | -| `Vary: Image-Format` | response-headers hook| CDN cache | response header | +| Signal | Set by | Read by | Mechanism | +|----------------------------|------------------------------|---------------------------|------------------------------------------| +| `Image-Format: original` | request hook (always) | response-headers hook | request header | +| `Image-Format: image/avif` | request hook (on match) | response-headers hook | request header | +| `response.content-type` | response-headers hook | response-body hook | `set_property` / `get_property` | +| `Vary: Image-Format` | response-headers hook | CDN cache | response header | --- @@ -265,19 +265,19 @@ proxy_wasm::main! {{ ## Error Conditions -| Condition | Behavior | -|----------------------------------------|---------------------------------------------------| -| No file extension in request path | Pass through without transformation | -| Extension not in `FORMATS_TO_TRANSFORM`| Pass through without transformation | -| `FORMATS_TO_TRANSFORM` not set | Pass through without transformation | -| No or empty `User-Agent` header | Pass through without transformation | -| UA matches `IGNORED_UA_LIST` entry | Pass through without transformation | -| Response status not 200 | Pass through without transformation | -| Response status property not exactly 2 bytes | Treat as missing status, pass through | -| No response body (`get_http_response_body` returns None) | Log and continue (original served) | -| Image decode failure (`load_from_memory`) | Log error, serve original body | -| AVIF encode failure | Log error, serve original body | -| `response.content-type` invalid UTF-8 | Send HTTP 500, return `Action::Pause` | +| Condition | Behavior | +|-------------------------------------------------------------------|-----------------------------------------------------| +| No file extension in request path | Pass through without transformation | +| Extension not in `FORMATS_TO_TRANSFORM` | Pass through without transformation | +| `FORMATS_TO_TRANSFORM` not set | Pass through without transformation | +| No or empty `User-Agent` header | Pass through without transformation | +| UA matches `IGNORED_UA_LIST` entry | Pass through without transformation | +| Response status not 200 | Pass through without transformation | +| Response status property not exactly 2 bytes | Treat as missing status, pass through | +| No response body (`get_http_response_body` returns None) | Log and continue (original served) | +| Image decode failure (`load_from_memory`) | Log error, serve original body | +| AVIF encode failure | Log error, serve original body | +| `response.content-type` invalid UTF-8 | Send HTTP 500, return `Action::Pause` | --- @@ -287,3 +287,300 @@ proxy_wasm::main! {{ - scaffold reference for CDN app type - FastEdge SDK Rust host services reference (proxy-wasm ABI, `get_property`, `set_property`) - platform overview (CDN app lifecycle, cache variation with `Vary` headers) + +## Source Material + +### FILE: examples/cdn/convert_image/src/lib.rs + +```rust +use image::*; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::{env, env::VarError, io::Cursor, str::from_utf8}; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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/cors-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-rust.md index 2ae30f6..172455f 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/cors-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -267,3 +267,178 @@ Output binary: `target/wasm32-wasip1/release/cors.wasm` - headers-rust reference (simpler single-hook CDN filter for header manipulation) - deploy skill reference (uploading and registering the compiled WASM binary) - Platform overview reference (environment variable configuration) + +## Source Material + +### FILE: examples/cdn/cors/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating CORS header management. + +Handles preflight OPTIONS requests and adds CORS response headers +for allowed origins. Supports configurable origin allow-lists, +methods, and exposed headers. + +Configuration: + - Environment variable: ALLOWED_ORIGINS (comma-separated origins or "*") + When unset or empty the filter is dormant — requests pass through + without CORS headers, so browsers will block cross-origin access. + - Environment variable: ALLOWED_METHODS (default: "GET, POST, PUT, DELETE, OPTIONS") + - Environment variable: MAX_AGE (default: "86400") + - Environment variable: EXPOSE_HEADERS (response headers to expose) +*/ + +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(CorsRoot) }); +}} + +struct CorsRoot; + +impl Context for CorsRoot {} + +impl RootContext for CorsRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CorsContext)) + } +} + +struct CorsContext; + +impl Context for CorsContext {} + +impl HttpContext for CorsContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let origin = match self.get_http_request_header("Origin") { + Some(o) if !o.is_empty() => o, + _ => return Action::Continue, + }; + + let allowed_origins = env::var("ALLOWED_ORIGINS").unwrap_or_default(); + if !is_origin_allowed(&origin, &allowed_origins) { + return Action::Continue; + } + + // Handle preflight OPTIONS request + let method = self + .get_http_request_header(":method") + .unwrap_or_default(); + + if method == "OPTIONS" { + let allow_methods = env::var("ALLOWED_METHODS") + .unwrap_or_else(|_| "GET, POST, PUT, DELETE, OPTIONS".to_string()); + let allow_headers = self + .get_http_request_header("Access-Control-Request-Headers") + .unwrap_or_else(|| "Content-Type, Authorization".to_string()); + let max_age = env::var("MAX_AGE").unwrap_or_else(|_| "86400".to_string()); + + let effective_origin = if allowed_origins == "*" { + "*".to_string() + } else { + origin + }; + + let mut headers = vec![ + ("Access-Control-Allow-Origin", effective_origin.as_str()), + ("Access-Control-Allow-Methods", allow_methods.as_str()), + ("Access-Control-Allow-Headers", allow_headers.as_str()), + ("Access-Control-Max-Age", max_age.as_str()), + ("Content-Length", "0"), + ]; + + // Vary: Origin is needed when the response varies by origin, + // so shared caches don't serve a cached response for a different origin. + // Not needed when Allow-Origin is "*" (response is the same for all origins). + if effective_origin != "*" { + headers.push(("Vary", "Origin")); + } + + self.send_http_response(204, headers, None); + + return Action::Pause; + } + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let origin = match self.get_http_request_header("Origin") { + Some(o) if !o.is_empty() => o, + _ => return Action::Continue, + }; + + let allowed_origins = env::var("ALLOWED_ORIGINS").unwrap_or_default(); + if !is_origin_allowed(&origin, &allowed_origins) { + return Action::Continue; + } + + let effective_origin = if allowed_origins == "*" { + "*".to_string() + } else { + origin + }; + + self.add_http_response_header("Access-Control-Allow-Origin", &effective_origin); + if effective_origin != "*" { + self.add_http_response_header("Vary", "Origin"); + } + + if let Ok(expose) = env::var("EXPOSE_HEADERS") { + if !expose.is_empty() { + self.add_http_response_header("Access-Control-Expose-Headers", &expose); + } + } + + Action::Continue + } +} + +fn is_origin_allowed(origin: &str, allowed: &str) -> bool { + if allowed.is_empty() { + return false; + } + if allowed == "*" { + return true; + } + allowed.split(',').any(|o| o.trim() == origin) +} +``` + +### FILE: examples/cdn/cors/Cargo.toml + +```toml +[workspace] + +[package] +name = "cors" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/cors/README.md + +``` +[← Back to examples](../../README.md) + +# CORS (CDN) + +Handles Cross-Origin Resource Sharing: preflight OPTIONS responses and CORS headers on all responses for allowed origins. +``` 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 eca26cd..a3463d6 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -310,3 +310,207 @@ Edit `public/styles.css` directly. No build tools required. Styles are embedded - host-services-rust reference (property API) - platform-overview reference (CDN app lifecycle) - body-rust blueprint (general body manipulation pattern) + +## Source Material + +### FILE: examples/cdn/custom_error_pages/src/lib.rs + +```rust +use handlebars::Handlebars; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde_json::json; +use std::collections::HashMap; +use std::env; + +// Include the generated image map +include!(concat!(env!("OUT_DIR"), "/image_map.rs")); +include!(concat!(env!("OUT_DIR"), "/message_map.rs")); + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { 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/env-secrets-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/env-secrets-rust.md index e8a660a..3da5935 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -175,3 +175,104 @@ println!("PASSWORD: {}", password); - FastEdge app secrets management (platform docs) - proxy-wasm HttpContext trait reference - fastedge crate proxywasm feature documentation + +## Source Material + +### FILE: examples/cdn/variables_and_secrets/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating environment variables and secrets access. + +Reads USERNAME from environment variables and PASSWORD from secrets, +then forwards both as request headers to the upstream origin. + +Required configuration: + - Environment variable: USERNAME + - Secret: PASSWORD +*/ + +use fastedge::proxywasm::secret; +use std::env; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { 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 c505afe..35f839a 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geo-redirect-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -266,3 +266,185 @@ Build outputs: - platform-overview (runtime properties, Geo-IP data) - deploy skill reference - manage skill reference (environment variable configuration) + +## Source Material + +### FILE: examples/geoRedirect/assembly/index.ts + +```ts +export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; // this exports the required functions for the proxy to interact with us. +import { + Context, + FilterHeadersStatusValues, + get_property, + log, + LogLevelValues, + registerRootContext, + RootContext, + send_http_response, + set_property, +} from "@gcoredev/proxy-wasm-sdk-as/assembly"; +import { + getEnv, + setLogLevel, +} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; + +const BAD_GATEWAY: u32 = 502; +const INTERNAL_SERVER_ERROR: u32 = 500; + +class GeoRedirectRoot extends RootContext { + createContext(context_id: u32): Context { + setLogLevel(LogLevelValues.info); + return new GeoRedirect(context_id, this); + } +} + +class GeoRedirect extends Context { + constructor(context_id: u32, root_context: GeoRedirectRoot) { + super(context_id, root_context); + } + + onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + log(LogLevelValues.info, "onRequestHeaders >> "); + + const defaultOrigin = getEnv("DEFAULT"); + + if (!defaultOrigin) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("App misconfigured - DEFAULT must be set"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const countryArrBuf = get_property("request.country"); + if (countryArrBuf.byteLength === 0) { + send_http_response( + BAD_GATEWAY, + "bad gateway", + String.UTF8.encode("Missing country information"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + const countryCode = String.UTF8.decode(countryArrBuf); + const countrySpecificOrigin = getEnv(countryCode); + + log( + LogLevelValues.info, + `Country code: ( ${countryCode} ): ${ + countrySpecificOrigin === "" ? "no matching origin" : countrySpecificOrigin + }`, + ); + + const hostArrBuf = get_property("request.host"); + if (hostArrBuf.byteLength > 0) { + const host = String.UTF8.decode(hostArrBuf); + log(LogLevelValues.info, `Provided Host: ${host}`); + } + + const pathArrBuf = get_property("request.path"); + if (pathArrBuf.byteLength === 0) { + send_http_response( + INTERNAL_SERVER_ERROR, + "internal server error", + String.UTF8.encode("Internal server error - no request path"), + [], + ); + return FilterHeadersStatusValues.StopIteration; + } + + const path = String.UTF8.decode(pathArrBuf); + const origin = countrySpecificOrigin === "" ? defaultOrigin : countrySpecificOrigin; + // remove trailing slashes from the origin + const cleanedOrigin = origin.endsWith("/") ? origin.slice(0, -1) : origin; + + const requestUrl = `${cleanedOrigin}${path}`; + + log(LogLevelValues.info, `request-url: ${requestUrl}`); + + set_property("request.url", String.UTF8.encode(requestUrl)); + + return FilterHeadersStatusValues.Continue; + } +} + +registerRootContext((context_id: u32) => { + return new GeoRedirectRoot(context_id); +}, "geoRedirect"); +``` + + +### FILE: examples/geoRedirect/package.json + +```json +{ + "name": "fastedge-as-example-georedirect", + "version": "1.0.0", + "description": "FastEdge AssemblyScript example: Geo Redirect", + "scripts": { + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release" + }, + "dependencies": { + "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" + }, + "devDependencies": { + "@assemblyscript/wasi-shim": "^0.1.0", + "assemblyscript": "^0.28.9" + } +} +``` + + +### FILE: examples/geoRedirect/README.md + +``` +[← Back to examples](../README.md) + +# Geo Redirect + +This application redirects requests to different origin URLs based on the client's country code. + +## What it does + +In `onRequestHeaders`, the app reads the client's country code from the `request.country` runtime property (populated by FastEdge's Geo-IP data) and looks up a matching environment variable by that country code. The `request.url` runtime property is then set to route the upstream fetch to the corresponding origin. + +- If a country-specific origin is configured (e.g. env var `DE=https://de.example.com`), the request is routed there. +- Otherwise it falls back to the `DEFAULT` origin. +- Uses [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes. + +> **Routing mechanism:** This is not an HTTP redirect (no `Location` header, no 302 response). Setting `request.url` rewrites the upstream fetch target transparently — the client sees a normal 200 response from the matched origin. + +> **Observability:** The app logs the country code, matched origin, and final request URL at INFO level, visible in the FastEdge application logs. + +## Configuration + +Set the following environment variables on your FastEdge application: + +| Variable | Example | Description | +|----------|---------|-------------| +| `DEFAULT` | `https://origin.example.com` | Fallback origin URL (required) | +| `` | `DE=https://de.example.com` | Per-country origin URL (optional, one per country) | + +## Build + +```sh +pnpm install +pnpm run asbuild +``` + +Build output: + +| File | Description | +|------|-------------| +| `build/geoRedirect.wasm` | Optimised release binary — upload this to FastEdge | +| `build/geoRedirect-debug.wasm` | Debug binary with source maps | + +## Deploy + +Upload `build/geoRedirect.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `DEFAULT` environment variable and any per-country overrides in the application settings. +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md index c0544ac..2b9a311 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/geoblock-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -158,10 +158,10 @@ registerRootContext((context_id: u32) => { Both blocked and allowed requests are logged at `INFO` level, providing an audit trail for all traffic decisions. -| Event | Log message | -| ---------------- | ------------------------------------------------ | -| Request blocked | `"geoBlock: blocked request from " + countryStr` | -| Request allowed | `"geoBlock: allowed request from " + countryStr` | +| Event | Log message | +| --------------- | ------------------------------------------------ | +| Request blocked | `"geoBlock: blocked request from " + countryStr` | +| Request allowed | `"geoBlock: allowed request from " + countryStr` | Log level is set via `setLogLevel(LogLevelValues.info)` inside `createContext`. diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/headers-rust.md index 0fb98ea..c275404 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -87,7 +87,6 @@ impl Context for HttpHeaders {} impl HttpContext for HttpHeaders { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { ... } fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { ... } - fn on_log(&mut self) { ... } } ``` @@ -296,18 +295,6 @@ Return `Action::Continue` when processing should proceed normally. --- -## Lifecycle Hook - -```rust -fn on_log(&mut self) { - println!("#{} completed.", self.context_id); -} -``` - -Called after the request/response cycle completes. Use for per-request logging or cleanup. - ---- - ## Constraints - `add_http_request_header` / `add_http_response_header` append new entries; existing entries with the same name are preserved (duplicates allowed). @@ -327,356 +314,3 @@ 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/kv-store-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-rust.md index 451f32a..e705d21 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/kv-store-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -111,11 +111,11 @@ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Ac ## Query Parameter Parsing -Query string is retrieved from the request property path `["request", "query"]`. +Query string is retrieved from the request property path `["request.query"]` (dot-notation single string). ```rust let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); @@ -251,12 +251,12 @@ Replaces the full upstream response body. First argument is offset (always `0`), ## Error Handling -Errors are logged via `println!` and returned as `{"error": ""}` JSON. Response status is set to `500` via the `response.status` property. +Errors are logged via `println!` and returned as `{"error": ""}` JSON. Response status is set to `500` via the `response.status` property (dot-notation single string). ```rust fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); - self.set_property(vec!["response", "status"], Some(b"500")); + 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()); } @@ -292,6 +292,7 @@ fn send_error(&self, msg: &str, body_size: usize) { - `min` and `max` for `zrange` must be parseable as `f64`; any non-numeric string returns an error. - The store name must correspond to a KV store provisioned and bound to the FastEdge app. Attempting to open an unbound store returns an error. - `send_error` uses `println!` for logging (not `proxy_wasm::hostcalls::log`). +- Property paths use dot-notation single strings: `"request.query"` and `"response.status"` (not separate vector elements). ## See Also @@ -299,3 +300,363 @@ fn send_error(&self, msg: &str, body_size: usize) { - proxy-wasm HttpContext trait (on_http_response_headers, on_http_response_body, set_http_response_body) - cdn-base skeleton (RootContext and HttpContext wiring) - platform-overview reference (KV store provisioning and binding) + +## 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) + +This example shows how to read and write data from a FastEdge KV store from a CDN app. +It intercepts the HTTP response, reads the request query string, and executes a KV operation against a named store. + +## What it does + +The app supports the following actions: + +- `get` — fetch one key +- `scan` — list keys matching a pattern +- `zrange` — read sorted-set entries by score range +- `zscan` — list sorted-set entries matching a pattern +- `bfExists` — check whether a Bloom filter contains an item + +The request must include at least: + +- `store=` — KV store name +- `action=` — optional, defaults to `get` + +For each action, the app validates required parameters and returns a JSON response body. + +## Supported query examples + +### Get a key + +```text +?store=my_store&action=get&key=user:42 +``` + +### List keys by pattern + +```text +?store=my_store&action=scan&match=user:* +``` + +### Read a sorted set by score range + +```text +?store=my_store&action=zrange&key=leaderboard&min=0&max=100 +``` + +### Search sorted-set members by pattern + +```text +?store=my_store&action=zscan&key=leaderboard&match=user:* +``` + +### Check a Bloom filter item + +```text +?store=my_store&action=bfExists&key=visitors&item=alice@example.com +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/key_value.wasm +``` + +This example is a CDN app, so it targets `wasm32-wasip1`. + +## Response format + +The app replaces the response body with JSON and sets `content-type: application/json`. +A successful response looks like this: + +```json +{ + "store": "my_store", + "action": "get", + "key": "user:42", + "response": "Alice" +} +``` + +If a required parameter is missing or the KV operation fails, the app responds with an error payload: + +```json +{ + "error": "Missing required param 'key' for 'get' action" +} +``` + +## Notes + +- The app requires query parameters to run. +- If `action` is omitted, it defaults to `get`. +- The code uses `fastedge::proxywasm::key_value::Store` and `proxy_wasm` lifecycle hooks to operate on the KV store. +``` 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 aaa3722..bb51254 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/large-dictionary-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-20 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -138,129 +138,3 @@ No additional dependencies beyond the cdn-base skeleton. - cdn-base skeleton reference - proxy-wasm-sdk-as SDK reference (AssemblyScript) - platform-overview reference for environment variable configuration - -## Source Material - -### FILE: examples/largeDictionary/assembly/index.ts - -```ts -export * from "@gcoredev/proxy-wasm-sdk-as/assembly/proxy"; -import { - Context, - FilterHeadersStatusValues, - log, - LogLevelValues, - registerRootContext, - RootContext, - stream_context, -} from "@gcoredev/proxy-wasm-sdk-as/assembly"; -import { - getDictionary, - setLogLevel, -} from "@gcoredev/proxy-wasm-sdk-as/assembly/fastedge"; - -class LargeDictionaryRoot extends RootContext { - createContext(context_id: u32): Context { - setLogLevel(LogLevelValues.info); - return new LargeDictionaryContext(context_id, this); - } -} - -class LargeDictionaryContext extends Context { - constructor(context_id: u32, root_context: LargeDictionaryRoot) { - super(context_id, root_context); - } - - onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { - // Use getDictionary for environment variables that may exceed 64KB. - // For normal-sized env vars (< 64KB), use getEnv instead. - const config = getDictionary("LARGE_CONFIG"); - - const size = config.length; - log(LogLevelValues.info, "LARGE_CONFIG size: " + size.toString() + " bytes"); - - stream_context.headers.request.add("x-config-size", size.toString()); - - return FilterHeadersStatusValues.Continue; - } -} - -registerRootContext((context_id: u32) => { - return new LargeDictionaryRoot(context_id); -}, "largeDictionary"); -``` - - -### FILE: examples/largeDictionary/package.json - -```json -{ - "name": "fastedge-as-example-large-dictionary", - "version": "1.0.0", - "description": "FastEdge AssemblyScript example: Large Dictionary — read env vars exceeding the 64KB WASI limit", - "scripts": { - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release", - "asbuild": "npm run asbuild:debug && npm run asbuild:release" - }, - "dependencies": { - "@gcoredev/proxy-wasm-sdk-as": "^1.2.3" - }, - "devDependencies": { - "@assemblyscript/wasi-shim": "^0.1.0", - "assemblyscript": "^0.28.9" - } -} -``` - - -### FILE: examples/largeDictionary/README.md - -``` -[← Back to examples](../README.md) - -# Large Dictionary - -This application demonstrates how to read large environment variables (> 64 KB) using the proxy-wasm dictionary API. - -## When to use `getDictionary` vs `getEnv` - -| Function | Use when | -|----------|----------| -| `getEnv(name)` | Variable value is under 64 KB (most cases) | -| `getDictionary(name)` | Variable value may exceed the 64 KB WASI env var size limit | - -The WASI environment variable interface has a **64 KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, PEM certificates, policy documents), use `getDictionary` which calls `proxy_dictionary_get` and bypasses this limit. - -For all other environment variable access, prefer `getEnv` as it uses the standard WASI environment interface. - -## What it does - -In `onRequestHeaders`, the app reads the `LARGE_CONFIG` environment variable using `getDictionary`, logs its size, and adds it as an `x-config-size` request header for the upstream to see. - -## Configuration - -Set the following on your FastEdge application: - -| Name | Type | Description | -|------|------|-------------| -| `LARGE_CONFIG` | Environment variable | A large configuration payload (e.g. JSON, PEM certificate) | - -## Build - -```sh -pnpm install -pnpm run asbuild -``` - -Build output: - -| File | Description | -|------|-------------| -| `build/largeDictionary.wasm` | Optimised release binary — upload this to FastEdge | -| `build/largeDictionary-debug.wasm` | Debug binary with source maps | - -## Deploy - -Upload `build/largeDictionary.wasm` to the FastEdge portal and attach it to your CDN application. Configure the `LARGE_CONFIG` environment variable in the application settings. -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md index f6472f8..416db62 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-as.md @@ -3,8 +3,8 @@ sources: - id: proxy-wasm-sdk-as ref: master - commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 - updated: 2026-08-17 + commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 + updated: 2026-09-22 --> --- @@ -302,9 +302,12 @@ class Properties extends Context { } onRequestHeaders(a: u32, end_of_stream: bool): FilterHeadersStatusValues { + // Error codes 551–559 identify the absent property via the HTTP response status: + // 551=uri 552=host 553=path 554=scheme 555=extension 556=query 557=x_real_ip 558=country 559=city if (!this.handleProperty(REQUEST_URI, 551, "uri", "request-uri")) { return FilterHeadersStatusValues.StopIteration; } + // host must be present for upstream routing; validated but not logged or exposed as a response header if (!this.handleProperty(REQUEST_HOST, 552)) { return FilterHeadersStatusValues.StopIteration; } diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-rust.md index a007b08..116d81c 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/cdn/properties-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -416,19 +416,31 @@ impl HttpContext for PropertiesContext { let params = querystring::querify(query_str); if let Some(url) = params.iter().find_map(|(k, v)| { - if "url".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "url".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_URI], Some(url.as_bytes())); } if let Some(host) = params.iter().find_map(|(k, v)| { - if "host".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "host".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_HOST], Some(host.as_bytes())); } if let Some(path) = params.iter().find_map(|(k, v)| { - if "path".eq_ignore_ascii_case(k) { Some(v) } else { None } + if "path".eq_ignore_ascii_case(k) { + Some(v) + } else { + None + } }) { self.set_property(vec![REQUEST_PATH], Some(path.as_bytes())); } 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 0edcedd..6fc190d 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -217,214 +217,3 @@ Response::builder() - fastedge-sdk-rust platform overview - host-services-rust reference (outbound HTTP, environment variables) - best-practices reference (cookie security, entropy sources) - -## Source Material - -### FILE: examples/http/wasi/ab_testing/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Cookie-based A/B testing example. - -Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically -assign the visitor to weighted variants of each configured test, then proxies the -request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-` -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/bloom-filter-denylist-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-ts.md index ba67275..eaa4402 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -176,3 +176,67 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide + +## Source Material + +### FILE: examples/bloom-filter-denylist/src/index.js + +```js +import { getEnv } from 'fastedge::env'; +import { KvStore } from 'fastedge::kv'; + +const BLOOM_KEY = 'blocked-ips'; + +function app(event) { + const storeName = getEnv('DENYLIST_STORE'); + if (!storeName) { + return Response.json( + { error: 'DENYLIST_STORE environment variable is not configured' }, + { status: 500 }, + ); + } + + const ip = event.client.address; + if (!ip) { + return Response.json({ error: 'client address unavailable' }, { status: 500 }); + } + + let blocked; + try { + const store = KvStore.open(storeName); + blocked = store.bfExists(BLOOM_KEY, ip); + } catch (error) { + return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); + } + + if (blocked) { + // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. + // Acceptable for a denylist (you over-block some legitimate users); not acceptable for + // allowlists or anything requiring exact membership — use KvStore.get() for that. + return Response.json({ allowed: false, ip }, { status: 403 }); + } + + return Response.json({ allowed: true, ip }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/bloom-filter-denylist/package.json + +```json +{ + "name": "fastedge-example-bloom-filter-denylist", + "version": "1.0.0", + "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md index c90f65f..0ca1b79 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -193,3 +193,117 @@ 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 914ed37..c10ff23 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -227,7 +227,121 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` ## See Also -- `fastedge::kv` reference — globally replicated key/value storage (use when cross-POP visibility is required) -- `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface +- fastedge::kv reference — globally replicated key/value storage (use when cross-POP visibility is required) +- 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 4a59ac4..82141f5 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -315,3 +315,281 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise { + // `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 78af466..93b6a95 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -288,201 +288,3 @@ cargo build --release - http-base skeleton - outbound-http feature blueprint - platform-overview (environment variable configuration) - -## Source Material - -### FILE: examples/http/wasi/cache/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Example app demonstrating response caching and cache purging via the cache interface. - -The app reads ORIGIN_HOST from the environment, forwards the incoming request -to that origin, and caches the response body keyed by the request path. -On subsequent requests for the same path the cached body is returned directly -without hitting the origin. - -Cache reads and writes use the synchronous `fastedge::cache` API; upstream -HTTP I/O still uses the async `wstd` client. - -Special purge routes (handled before any origin call): - GET /purge — purge all cached keys; returns 200 with deleted count - GET /purge/ — 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 10afb0e..643e5bd 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -260,3 +260,100 @@ 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 c4395fb..fb3c0c9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -190,3 +190,104 @@ 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-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-ts.md index b4ffbb2..1fba134 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/geo-redirect-ts.md @@ -3,14 +3,14 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- type: feature app_type: http languages: [typescript, javascript] -capabilities: [geo-routing, geo-redirect] +capabilities: [geo-routing] base_skeleton: http-base source_example: FastEdge-sdk-js/examples/geo-redirect --- @@ -113,7 +113,7 @@ addEventListener('fetch', (event) => { - Environment variables are set in the Gcore dashboard or via the API when creating or updating the FastEdge app. - Country-specific origins are configured as environment variables using ISO 3166-1 alpha-2 codes. If no matching variable exists, `BASE_ORIGIN` is used as the fallback. - If `BASE_ORIGIN` is not set, the handler returns HTTP 500 with a descriptive error message. -- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `81145a9a43ec499240c687bd49376ab20c72b11c`). +- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `9c8c7886f0d1ec5ac2296b4080805966a96ca817`). ## Source Material 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 4554a90..b36860f 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -154,119 +154,3 @@ cargo build --release - http-base skeleton reference - FastEdge platform overview (geoip-country-code header injection) - deploy skill reference (uploading WASM binary and setting environment variables) - -## Source Material - -### FILE: examples/http/wasi/geo_redirect/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example WASI-HTTP app demonstrating geo-based redirects. - -Reads the country code from the geoip-country-code request header -and redirects to a country-specific origin URL. Falls back to -BASE_ORIGIN when no country-specific mapping is configured. - -Required configuration: - - Environment variable: BASE_ORIGIN (fallback origin URL) - - Environment variable: (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 54b3f87..108a855 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -127,3 +127,47 @@ 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 a23be87..b6fde53 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -350,218 +350,3 @@ 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/large-env-variable-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/large-env-variable-wasi-rust.md index 0b2a974..11bc794 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/large-env-variable-wasi-rust.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/large-env-variable-wasi-rust.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-07-23 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -131,3 +131,92 @@ The crate type must be `cdylib` for WASM/WASI compilation. - http-base reference (base skeleton for WASI HTTP apps) - fastedge-sdk-rust SDK reference (full `fastedge` crate API) - platform-overview reference (environment variable limits and configuration) + +## Source Material + +### FILE: examples/http/wasi/large_env_variable/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example WASI-HTTP app demonstrating access to large environment variables. + +Uses `fastedge::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::dictionary; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + // 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(); + + Ok(Response::builder() + .status(200) + .body(Body::from(format!( + "LARGE_CONFIG loaded: {} bytes", + size + )))?) +} +``` + + +### FILE: examples/http/wasi/large_env_variable/Cargo.toml + +```toml +[workspace] + +[package] +name = "large_env_variable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/large_env_variable/README.md + +``` +[← Back to examples](../../../README.md) + +# Large Environment Variable (WASI) + +Demonstrates how to read **large environment variables** (> 64KB) using `fastedge::dictionary`. + +## When to use `dictionary` vs `std::env` + +| Method | Use when | +|--------|----------| +| `std::env::var("KEY")` | Variable value is under 64KB (most cases) | +| `fastedge::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/mcp-server-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/mcp-server-ts.md index 8a36751..055d200 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -291,7 +291,7 @@ Include verbatim: "@gcoredev/fastedge-sdk-js": "^2.3.0", "@hono/mcp": "^0.2.5", "@modelcontextprotocol/sdk": "^1.29.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "zod": "^4.3.6" ``` 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 aeaede1..6f84bac 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -127,90 +127,3 @@ Both errors propagate via `?` and result in a 500-class response from the FastEd - `outbound-modify-response` (WASI, Rust) — fetches upstream and reshapes the body into a new JSON response - `streaming` (WASI, Rust) — handler that generates its own streaming response without an upstream fetch - `outbound-fetch` (JavaScript) — mirror of this example in the FastEdge SDK JS - -## Source Material - -### FILE: examples/http/wasi/outbound_fetch/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Minimal outbound fetch example. - -Makes a GET request to an upstream HTTP origin and returns the upstream -response verbatim — status, headers, and body pass through unchanged. - -For a variant that reads and transforms the upstream body, see -`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. - -Mirror of the FastEdge-sdk-js `outbound-fetch` example. -*/ - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request) -> 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-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md index c33ea71..eb8fd8f 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -216,82 +216,3 @@ 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/react-with-hono-server-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/react-with-hono-server-ts.md index 3882cd6..50dc4c2 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/react-with-hono-server-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/react-with-hono-server-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -35,7 +35,7 @@ Over `http-base`, add the following to `package.json`. **Runtime dependencies:** ```json "@gcoredev/fastedge-sdk-js": "^2.3.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" ``` @@ -43,7 +43,7 @@ Over `http-base`, add the following to `package.json`. **Dev dependencies:** ```json "@eslint/js": "^9.36.0", -"@hono/node-server": "^1.19.14", +"@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -125,13 +125,13 @@ Script semantics: }, "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25", + "hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" }, "devDependencies": { "@eslint/js": "^9.36.0", - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", 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 5e3fdf9..4da7354 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -208,65 +208,3 @@ 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 cf34807..2ea2f92 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -18,11 +18,11 @@ source_example: FastEdge-sdk-rust/examples/http/wasi/simple_fetch # Simple Fetch (WASI HTTP, Rust) -Outbound HTTP request to a caller-supplied URL, returning the upstream response directly. Uses the WASI-HTTP interface via the `wstd` crate with an async handler. +Outbound HTTP request to a caller-supplied URL, returning the upstream response directly. Supports optional header forwarding from the incoming request to the upstream request. Uses the WASI-HTTP interface via the `wstd` crate with an async handler. ## When to Use -Use this pattern when the app must make an outbound HTTP request to a URL specified by the incoming request (via the `x-fetch-url` header) and return the upstream response directly to the caller without decomposing it. +Use this pattern when the app must make an outbound HTTP request to a URL specified by the incoming request (via the `x-fetch-url` header) and return the upstream response directly to the caller without decomposing it. Optionally forwards a caller-specified header to the upstream request via `x-fetch-header`. ## Crate Dependencies @@ -69,17 +69,30 @@ let target_url = request - `.and_then(|v| v.to_str().ok())` — converts to `Option<&str>`, discarding invalid UTF-8 - `.unwrap_or(default)` — fallback value when header is absent or invalid -### Build Outbound Request +### Build Outbound Request with Conditional Header Forwarding ```rust -let upstream_req = Request::get(&target_url) - .header("accept", "application/json") +let mut builder = Request::get(&target_url).header("accept", "application/json"); + +if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) +{ + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } +} + +let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; ``` - `Request::get(url)` — initiates a GET request builder -- `.header(key, value)` — appends a request header +- `.header(key, value)` — appends a request header; chainable +- `mut builder` — builder is held as a mutable variable to allow conditional header addition before finalizing +- `fetch_header.split_once(':')` — splits `"Key: Value"` into `("Key", " Value")`; `.trim()` strips whitespace - `.body(Body::empty())` — finalizes with an empty body; returns `Result` - `.map_err(...)` — converts builder error into `anyhow::Error` @@ -104,6 +117,7 @@ Ok(response) | Header | Required | Default | Description | |--------|----------|---------|-------------| | `x-fetch-url` | No | `https://httpbin.org/get` | URL to fetch outbound | +| `x-fetch-header` | No | — | A single header to forward to the upstream request, in `Key: Value` format | ## Imports @@ -130,6 +144,7 @@ use wstd::http::{Client, Request, Response}; | Outbound send failure | `map_err` converts to `anyhow::Error`; propagated via `?` | | 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 | +| `x-fetch-header` missing `:` separator | `split_once(':')` returns `None`; header is silently ignored | ## Build @@ -142,6 +157,7 @@ cargo component build --release ```bash curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +curl -H "x-fetch-url: https://httpbin.org/get" -H "x-fetch-header: x-custom-key: myvalue" https:/// ``` ## Source Material @@ -177,8 +193,19 @@ async fn main(request: Request) -> anyhow::Result> { println!("Fetching: {target_url}"); - let upstream_req = Request::get(&target_url) - .header("accept", "application/json") + let mut builder = Request::get(&target_url).header("accept", "application/json"); + + if let Some(fetch_header) = request + .headers() + .get("x-fetch-header") + .and_then(|v| v.to_str().ok()) + { + if let Some((key, value)) = fetch_header.split_once(':') { + builder = builder.header(key.trim(), value.trim()); + } + } + + let upstream_req = builder .body(Body::empty()) .map_err(|e| anyhow!("failed to build request: {e}"))?; @@ -201,7 +228,7 @@ async fn main(request: Request) -> anyhow::Result> { [package] name = "simple_fetch" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false @@ -266,6 +293,7 @@ cargo build --release - http-base skeleton (base_skeleton for this feature) - outbound-fetch capability reference +- header-driven-routing capability reference - wstd crate (crates.io/crates/wstd) - WASI-HTTP interface specification (WebAssembly/wasi-http) - FastEdge-sdk-rust examples/http/wasi/ for other WASI async patterns diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-ts.md index 9141628..dbd2a9b 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/static-assets-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -312,7 +312,7 @@ footer a { "license": "ISC", "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25" + "hono": "^4.13.5" }, "devDependencies": { "npm-run-all2": "^9.0.2" @@ -348,7 +348,7 @@ Additions over `http-base`: | Package | Type | Version | |---|---|---| | `@gcoredev/fastedge-sdk-js` | runtime | `^2.3.0` | -| `hono` | runtime | `^4.12.25` | +| `hono` | runtime | `^4.13.5` | | `npm-run-all2` | devDependency | `^9.0.2` | ## Key API Patterns 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 053da59..4d314c9 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -115,50 +115,3 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference - -## Source Material - -### FILE: examples/streaming/src/index.js - -```js -function app(event) { - const encoder = new TextEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { setTimeout(resolve, 200); }); - controller.enqueue(encoder.encode(`chunk ${i}\n`)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/streaming/package.json - -```json -{ - "name": "fastedge-example-streaming", - "version": "1.0.0", - "description": "FastEdge JS example: streaming response with ReadableStream", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/streaming.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/streaming-wasi-rust.md index 956f361..ca4dcdc 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -141,67 +141,3 @@ 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-ab-testing-ts.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md index ee50bab..eb66618 100644 --- a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md +++ b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-07-23 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,91 +202,3 @@ addEventListener('fetch', (event) => { - http-base skeleton (base event handler structure and build setup) - fastedge-build CLI reference (WASM compilation) - Handlebars documentation (template syntax and compilation API) - -## Source Material - -### FILE: examples/template-invoice-ab-testing/src/index.js - -```javascript -import Handlebars from 'handlebars'; - -import { getStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { getLogoBrand } 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({ request }) { - const isAbTestLogo = request.headers.get('ab-test-logo') === 'bottle'; - const logo = getLogoBrand(isAbTestLogo); - - const abTestFont = request.headers.get('ab-test-font'); - const cssStyles = getStyles(abTestFont); - - const rawHtmlTemplate = htmlTemplate(abTestFont); - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logo, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/template-invoice-ab-testing/package.json - -```json -{ - "name": "fastedge-example-template-invoice-ab-testing", - "version": "1.0.0", - "description": "FastEdge JS example: Handlebars invoice with A/B test header variants", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice-ab-testing.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` 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 36b8b19..8538296 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,86 +202,3 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference - -## Source Material - -### FILE: examples/template-invoice/src/index.js - -```js -import Handlebars from 'handlebars'; - -import { cssStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { logoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler() { - const rawHtmlTemplate = htmlTemplate(); - - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logoBrand, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler()); -}); -``` - -### FILE: examples/template-invoice/package.json - -```json -{ - "name": "fastedge-example-template-invoice", - "version": "1.0.0", - "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge-cursor/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md index 00d44d9..df8a8c7 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 @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-rust ref: main - commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-20 + commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 + updated: 2026-09-22 --> --- @@ -135,89 +135,3 @@ cargo build --release - deploy skill reference (uploading the compiled `.wasm` binary) - manage skill reference (setting environment variables and secrets on an app) - FastEdge platform overview (secret storage model) - -## Source Material - -### FILE: examples/http/wasi/variables_and_secrets/src/lib.rs - -```rust -use fastedge::secret; -use std::env; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(_request: Request) -> 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 c822ef7..67ce176 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.10", + "version": "0.2.11", "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 9b35495..338f770 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-09-09T13:28:16.173Z", + "generated_at": "2026-09-24T09:24:50.621Z", "source": { "repo": "fastedge-plugin", - "commit": "99dc55a", + "commit": "503a8fc", "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -29,9 +29,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "sdk", "cdn", @@ -41,7 +41,7 @@ "building", "filters" ], - "content_sha256": "55cf970e41b7f8c4bce161ef5272413f4f155ed90b01d90510d5f21dce23930e", + "content_sha256": "6a79f809ecedc76ed7617ab078604096a5933da07a44a2059849f2042a42910a", "sections": [ { "id": "cdn-apps-rust#introduction", @@ -58,9 +58,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "rust", "sdk", @@ -150,9 +150,9 @@ "0.2", "log", "0.4", + "cache", "secrets", - "dictionary", - "depend" + "dictionary" ] }, { @@ -245,7 +245,7 @@ "callback", "phase", "description", - "-----------------------------------------------------------------", + "-----------------------------------------------------------------------------------------------", "----------------", "---------------------------------------------------", "on_http_request_headers", @@ -256,11 +256,7 @@ "action", "request", "headers", - "inspect", - "modify", - "forwarding", - "on_http_request_body", - "body_size" + "inspect" ] }, { @@ -431,7 +427,7 @@ "level": 2, "anchor": "host-services-for-cdn-apps", "line_start": 423, - "line_end": 733, + "line_end": 818, "keywords": [ "host", "services", @@ -477,8 +473,8 @@ "heading": "API Comparison: HTTP vs CDN", "level": 2, "anchor": "api-comparison-http-vs-cdn", - "line_start": 734, - "line_end": 746, + "line_start": 819, + "line_end": 832, "keywords": [ "api", "comparison", @@ -491,12 +487,11 @@ "proxywasm", "-------------", "-------------------------------------------------------------------", - "--------------------------------------------------------", + "-----------------------------------------------------------------", "key-value", "fastedge", "key_value", - "store", - "secrets" + "store" ] }, { @@ -504,8 +499,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 747, - "line_end": 751, + "line_start": 833, + "line_end": 837, "keywords": [ "see", "sdk", @@ -533,7 +528,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-as.md", "skill": "fastedge-docs", "category": "examples", @@ -555,9 +550,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "testing", "cookie-based", "traffic", @@ -567,7 +562,7 @@ "requests", "different" ], - "content_sha256": "a8a65c5b6a1c0cfb93650966caa7539474e92037e1a305816ef0944e3e09a1fc", + "content_sha256": "271da479486db22cd96d1891a8b2e5755921af0d4fd4c733df1732ef35e67684", "sections": [ { "id": "cdn-examples-ab-testing-as#introduction", @@ -584,9 +579,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "testing", "assemblyscript", "cdn", @@ -1001,7 +996,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -1023,9 +1018,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -1035,7 +1030,7 @@ "ab-testing", "traffic-splitting" ], - "content_sha256": "02a0716c80b4d8908f4996e26cf4a36904a49faf5c7a78d99d5abacf7512e387", + "content_sha256": "8c9d6d203917852f4ca811313b5c30779ba0b4d523dab6f7597099f4675c2e4f", "sections": [ { "id": "cdn-examples-ab-testing-rust#introduction", @@ -1052,9 +1047,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -1657,7 +1652,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-as.md", "skill": "fastedge-docs", "category": "examples", @@ -1679,9 +1674,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -1691,7 +1686,7 @@ "api-key-auth", "header-validation" ], - "content_sha256": "d47decc23a11899c2cdd3360355fb8c5e9db6fb48d6b2ca5c90bee4cc5c8ae2b", + "content_sha256": "44acbfa72b450892ad45f4e360dd734b0a451a6df664428a2925e1419a6f8a6e", "sections": [ { "id": "cdn-examples-api-key-as#introduction", @@ -1708,9 +1703,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -2071,7 +2066,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2093,9 +2088,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2105,7 +2100,7 @@ "requests", "against" ], - "content_sha256": "55118b7b2a0e68b75f57892ee0ea64dae4e5bffc4354ecf2941ae41658040b8e", + "content_sha256": "6c28a1664d639ebeb12397dea7294bdd293848795b65c88c2f5aab81e2bee614", "sections": [ { "id": "cdn-examples-api-key-rust#introduction", @@ -2122,9 +2117,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "api", "key", "validation", @@ -2369,63 +2364,13 @@ "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-20 --> --- capabilities: - jwt-auth - request-header-inspection - secret-access - http-response type: example app_type: cdn languages: - assemblyscrip", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 --> --- 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", @@ -2449,9 +2394,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "jwt-auth", @@ -2462,7 +2407,7 @@ "example", "app_type" ], - "content_sha256": "b1fb0a577490fc4af605c215e8ece9ba673631e87fe3755435cda8df374760f4", + "content_sha256": "7defe19d2cd21c1429dbf522d5fe31e212ec1f4880b241375b44f179c2add564", "sections": [ { "id": "cdn-examples-auth-jwt-as#introduction", @@ -2479,9 +2424,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "jwt-auth", @@ -2927,7 +2872,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -2949,9 +2894,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -2961,7 +2906,7 @@ "auth", "jwt" ], - "content_sha256": "fab044cd88079e758ffa7433aa100275b9466d2b19457fe94f40539fca11ec06", + "content_sha256": "d154eba097a174823554d450040d60276c5d185a7c0875e5214ceb2ded34f624", "sections": [ { "id": "cdn-examples-auth-jwt-rust#introduction", @@ -2978,9 +2923,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -3483,7 +3428,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-as.md", "skill": "fastedge-docs", "category": "examples", @@ -3505,9 +3450,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "title", "body", @@ -3517,7 +3462,7 @@ "app_type", "languages" ], - "content_sha256": "94226f2f7783dd5978e697ebea97008427e93dbddfd4b033a8c05d74268e9a34", + "content_sha256": "cf0366213848652a381fe943b6a887700b4a939b885f9bb54dc3c6232f8b6ab0", "sections": [ { "id": "cdn-examples-body-as#introduction", @@ -3534,9 +3479,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "title", "body", @@ -3969,7 +3914,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -3991,9 +3936,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -4003,7 +3948,7 @@ "body-inspection", "body-modification" ], - "content_sha256": "8ae1b1e52a1c6d5700b08e8892ae6cda7aad01471796e76d73ec79dcf32e297b", + "content_sha256": "dd9c88154cf902f53948cceb8f2653719e2fed5373b2dd3ab3c80ba7efca05e8", "sections": [ { "id": "cdn-examples-body-rust#introduction", @@ -4020,9 +3965,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -4425,7 +4370,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-as.md", "skill": "fastedge-docs", "category": "examples", @@ -4449,9 +4394,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -4461,7 +4406,7 @@ "cache-control", "response-headers" ], - "content_sha256": "3b3e6161bccadfc2c7ed913237cf413f6147239e7386715b566aa6893587902d", + "content_sha256": "0a153a37be4f5636fac31f8469be52c9af84394259760e897788b7654ba00fb9", "sections": [ { "id": "cdn-examples-cache-control-as#introduction", @@ -4478,9 +4423,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -5055,7 +5000,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5077,9 +5022,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -5089,7 +5034,7 @@ "cache-control", "content-type-aware-caching" ], - "content_sha256": "b0fabdfa84d4803e073c0d36e6b51424537f4051ef1a9e6ba50b56a4e184e169", + "content_sha256": "4c8bfcc4519e20ce422726bb54b90f80816615eb4f5d26cc74cc14a067da4903", "sections": [ { "id": "cdn-examples-cache-control-rust#introduction", @@ -5106,9 +5051,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -5638,10 +5583,416 @@ } ] }, + { + "id": "cdn-examples-cache-rust", + "title": "CDN Cache — Rust Example", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cache", + "example", + "overview", + "app", + "demonstrating", + "operations" + ], + "content_sha256": "4c6cf165f6d058b5f6d401c04abf933162f423e82a02a2b81d3a9f1ada41c285", + "sections": [ + { + "id": "cdn-examples-cache-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 11, + "keywords": [ + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "cdn", + "cache", + "rust", + "example" + ] + }, + { + "id": "cdn-examples-cache-rust#overview", + "heading": "Overview", + "level": 2, + "anchor": "overview", + "line_start": 12, + "line_end": 17, + "keywords": [ + "overview", + "cdn", + "app", + "demonstrating", + "cache", + "operations", + "via", + "proxy-wasm", + "abi.", + "scoped", + "calling", + "application", + "addressed", + "key", + "alone", + "there", + "named", + "stores", + "handles", + "unlike", + "store", + "dispatched", + "query", + "parameters", + "incoming", + "request.", + "responses", + "always", + "json.", + "errors", + "return", + "http", + "500", + "error", + "message" + ] + }, + { + "id": "cdn-examples-cache-rust#app-type", + "heading": "App Type", + "level": 2, + "anchor": "app-type", + "line_start": 18, + "line_end": 23, + "keywords": [ + "app", + "type", + "interface", + "proxy-wasm", + "fastedge", + "proxywasm", + "cache", + "cdn", + "language", + "rust" + ] + }, + { + "id": "cdn-examples-cache-rust#request-interface", + "heading": "Request Interface", + "level": 2, + "anchor": "request-interface", + "line_start": 24, + "line_end": 38, + "keywords": [ + "request", + "interface", + "operations", + "specified", + "via", + "query", + "parameters.", + "action", + "parameter", + "selects", + "operation", + "get", + "default", + "omitted.", + "string", + "---", + "key", + "read", + "cached", + "value.", + "response", + "null", + "absent.", + "set", + "value", + "ttl", + "store", + "omitting", + "stores", + "expiry.", + "delete", + "key.", + "no-op", + "exists" + ] + }, + { + "id": "cdn-examples-cache-rust#api-reference", + "heading": "API Reference", + "level": 2, + "anchor": "api-reference", + "line_start": 39, + "line_end": 195, + "keywords": [ + "api", + "reference", + "functions", + "fastedge", + "proxywasm", + "cache", + "get", + "rust", + "pub", + "key", + "str", + "result", + "option", + "vec", + "retrieve", + "cached", + "bytes", + "key.", + "parameters", + "string", + "returns", + "exists", + "none", + "absent", + "err", + "failure", + "json", + "response", + "shape", + "action", + "value", + "null", + "---", + "set" + ] + }, + { + "id": "cdn-examples-cache-rust#error-handling", + "heading": "Error Handling", + "level": 2, + "anchor": "error-handling", + "line_start": 196, + "line_end": 202, + "keywords": [ + "error", + "handling", + "errors", + "return", + "http", + "500", + "body", + "message", + "missing", + "required", + "query", + "parameters", + "produce", + "descriptive", + "messages", + "e.g.", + "param", + "key", + "get", + "action", + "invalid", + "parameter", + "values", + "non-integer", + "ttl", + "delta", + "parse", + "messages.", + "unknown", + "listing", + "supported", + "actions." + ] + }, + { + "id": "cdn-examples-cache-rust#dependencies", + "heading": "Dependencies", + "level": 2, + "anchor": "dependencies", + "line_start": 203, + "line_end": 211, + "keywords": [ + "dependencies", + "toml", + "proxy-wasm", + "0.2", + "fastedge", + "path", + "...", + "features", + "proxywasm", + "querystring", + "1.1", + "serde_json" + ] + }, + { + "id": "cdn-examples-cache-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 212, + "line_end": 218, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache.wasm" + ] + }, + { + "id": "cdn-examples-cache-rust#lifecycle-notes", + "heading": "Lifecycle Notes", + "level": 2, + "anchor": "lifecycle-notes", + "line_start": 219, + "line_end": 225, + "keywords": [ + "lifecycle", + "notes", + "app", + "uses", + "proxy-wasm", + "rootcontext", + "httpcontext", + "pattern.", + "logic", + "executes", + "on_http_response_body", + "waits", + "end_of_stream", + "on_http_response_headers", + "strips", + "content-length", + "sets", + "content-type", + "application", + "json", + "transfer-encoding", + "chunked", + "body", + "processing.", + "query", + "string", + "read", + "request.query", + "via", + "get_property" + ] + }, + { + "id": "cdn-examples-cache-rust#key-constraints", + "heading": "Key Constraints", + "level": 2, + "anchor": "key-constraints", + "line_start": 226, + "line_end": 232, + "keywords": [ + "key", + "constraints", + "cache", + "operations", + "scoped", + "calling", + "application", + "keys", + "one", + "app", + "accessible", + "another.", + "there", + "named", + "stores", + "handles", + "sole", + "address.", + "ttl", + "values", + "always", + "milliseconds", + "delta", + "incr", + "signed", + "64-bit", + "integer", + "i64", + "missing", + "treated" + ] + }, + { + "id": "cdn-examples-cache-rust#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 233, + "line_end": 238, + "keywords": [ + "see", + "fastedge-sdk-rust", + "cdn", + "key_value", + "example", + "named", + "store", + "handles", + "contrast", + "cache", + "api", + "examples", + "overview", + "proxy-wasm", + "httpcontext", + "rootcontext", + "trait", + "documentation" + ] + } + ] + }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -5663,9 +6014,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "example", "convert", "image", @@ -5675,7 +6026,7 @@ "avif", "format" ], - "content_sha256": "a13f67a53c9fe7aea572cc4340f710693d326f6f3b9570a1e7428e88b43da733", + "content_sha256": "c9e876d444727919f19871887b51f11a25b8653ddc28e5f19f241a707cadd786", "sections": [ { "id": "cdn-examples-convert-image-rust#introduction", @@ -5692,9 +6043,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "cdn", "example", "convert", @@ -6159,129 +6510,13 @@ "error", "codes" ] - }, - { - "id": "cdn-examples-convert-image-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 206, - "line_end": 488, - "keywords": [ - "source", - "material", - "file", - "examples", - "cdn", - "convert_image", - "src", - "lib.rs", - "rust", - "use", - "image", - "proxy_wasm", - "traits", - "types", - "std", - "env", - "varerror", - "cursor", - "str", - "from_utf8", - "main", - "set_log_level", - "loglevel", - "trace", - "set_root_context", - "box", - "dyn", - "rootcontext", - "new", - "convertimageroot", - "struct", - "impl", - "context", - "get_type", - "self" - ] - }, - { - "id": "cdn-examples-convert-image-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 489, - "line_end": 495, - "keywords": [ - "configuration", - "environment", - "variable", - "formats_to_transform", - "comma-separated", - "list", - "file", - "extensions", - "convert", - "e.g.", - "jpg", - "jpeg", - "png", - "ignored_ua_list", - "optional", - "user-agent", - "substrings", - "skip", - "avif_speed", - "avif", - "encoding", - "speed", - "1-10", - "default", - "avif_quality", - "quality", - "1-100" - ] - }, - { - "id": "cdn-examples-convert-image-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 496, - "line_end": 502, - "keywords": [ - "works", - "on_request_headers", - "checks", - "file", - "extension", - "user-agent", - "sets", - "image-format", - "header", - "cache", - "variation", - "on_response_headers", - "response", - "headers", - "avif", - "content", - "type", - "200", - "responses", - "on_response_body", - "decodes", - "original", - "image", - "re-encodes" - ] } ] }, { "id": "cdn-examples-cors-as", "title": "CORS — AssemblyScript (CDN)", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-as.md", "skill": "fastedge-docs", "category": "examples", @@ -6303,9 +6538,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "cors", "response-header", "injection", @@ -6315,7 +6550,7 @@ "proxy-wasm", "sdk." ], - "content_sha256": "dcfb3c1c7467c4e745a43d3b8821e1ef8725789583876c7d2ffafd9955cc8efa", + "content_sha256": "204de98ec6e870b35f9cdd8f9b2a9d9b23c1e7152db0ee88fa8b62aaf3a5d02b", "sections": [ { "id": "cdn-examples-cors-as#introduction", @@ -6332,9 +6567,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "cors", "assemblyscript", "cdn", @@ -6903,7 +7138,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -6925,9 +7160,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -6937,7 +7172,7 @@ "cors", "preflight" ], - "content_sha256": "0fb3e333f7c0f3374c1253af8febc8847afb1ee90ffeabcc90ca5cdd75762c5b", + "content_sha256": "02601a51af83f28ad9b82be9607ee55878e53af965ee28d0033dc33ac25c8fac", "sections": [ { "id": "cdn-examples-cors-rust#introduction", @@ -6954,9 +7189,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7457,7 +7692,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-as.md", "skill": "fastedge-docs", "category": "examples", @@ -7479,9 +7714,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7491,7 +7726,7 @@ "response-headers", "response-body" ], - "content_sha256": "d27efa1750b8e49c981eadabe5896041d527217f351a1935fb6b5da2ba740ce0", + "content_sha256": "db4273297015e84837b705d18e071b4684410000b7bc9fcf363fe82c1c58ea15", "sections": [ { "id": "cdn-examples-custom-error-pages-as#introduction", @@ -7508,9 +7743,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -7940,155 +8175,125 @@ "hook", "sequencing" ] - }, + } + ] + }, + { + "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: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", + "skill": "fastedge-docs", + "category": "examples", + "app_types": [ + "cdn" + ], + "languages": [ + "rust" + ], + "tags": [ + "examples", + "fastedge-docs", + "cdn", + "rust", + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22", + "custom", + "error", + "pages", + "overview", + "intercepts", + "4xx", + "5xx", + "http" + ], + "content_sha256": "efad33874c535871854509dd840e49c337a0b3f8a7971d0743d6d1eebff0826d", + "sections": [ { - "id": "cdn-examples-custom-error-pages-as#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 296, - "line_end": 501, + "id": "cdn-examples-custom-error-pages-rust#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 9, "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" + "auto-updated", + "true", + "sources", + "fastedge-sdk-rust", + "ref", + "main", + "commit", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", + "updated", + "2026-09-22" ] }, { - "id": "cdn-examples-custom-error-pages-as#what-it-does", - "heading": "What it does", + "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", + "heading": "Custom Error Pages — CDN (Rust)", "level": 2, - "anchor": "what-it-does", - "line_start": 502, - "line_end": 514, + "anchor": "custom-error-pages-—-cdn-rust", + "line_start": 10, + "line_end": 297, "keywords": [ - "onresponseheaders", - "app", - "reads", - "response.status", - "property.", - "400-599", - "range", - "sets", - "content-type", - "text", - "html", - "prepares", - "body", - "replacement.", - "onresponsebody", - "buffers", - "full", - "response", + "custom", + "error", + "pages", + "cdn", + "rust", + "overview", + "intercepts", + "4xx", + "5xx", + "http", + "responses", + "edge", "replaces", + "their", + "bodies", + "branded", + "html", + "rendered", + "via", + "handlebars", + "templates.", + "use", + "pattern", + "you", + "consistent", "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", + "served", + "regardless", + "origin", + "returns.", "build", - "customerrorpages.wasm", - "fastedge", - "portal", - "attach", - "your", - "cdn", - "application.", - "environment", - "variables", - "secrets", - "required." + "script", + "build.rs", + "runs", + "compile", + "time", + "embed", + "images", + "messages", + "public" ] } ] }, { - "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-20 -->", - "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md", + "id": "cdn-examples-custom-rust", + "title": "CDN Example: Custom — Rust", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", "skill": "fastedge-docs", "category": "examples", "app_types": [ @@ -8109,120 +8314,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", - "custom", - "error", - "pages", - "overview", - "intercepts", - "4xx", - "5xx", - "http" - ], - "content_sha256": "6ca59f2c7881b6cfa873c1aa90ca38a3135eeee3546ba894a62801be63e0266e", - "sections": [ - { - "id": "cdn-examples-custom-error-pages-rust#introduction", - "heading": "Introduction", - "level": 1, - "anchor": "introduction", - "line_start": 1, - "line_end": 9, - "keywords": [ - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-20" - ] - }, - { - "id": "cdn-examples-custom-error-pages-rust#custom-error-pages-—-cdn-rust", - "heading": "Custom Error Pages — CDN (Rust)", - "level": 2, - "anchor": "custom-error-pages-—-cdn-rust", - "line_start": 10, - "line_end": 297, - "keywords": [ - "custom", - "error", - "pages", - "cdn", - "rust", - "overview", - "intercepts", - "4xx", - "5xx", - "http", - "responses", - "edge", - "replaces", - "their", - "bodies", - "branded", - "html", - "rendered", - "via", - "handlebars", - "templates.", - "use", - "pattern", - "you", - "consistent", - "styled", - "served", - "regardless", - "origin", - "returns.", - "build", - "script", - "build.rs", - "runs", - "compile", - "time", - "embed", - "images", - "messages", - "public" - ] - } - ] - }, - { - "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-20 -->", - "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", - "skill": "fastedge-docs", - "category": "examples", - "app_types": [ - "cdn" - ], - "languages": [ - "rust" - ], - "tags": [ - "examples", - "fastedge-docs", - "cdn", - "rust", - "auto-updated", - "true", - "sources", - "fastedge-sdk-rust", - "ref", - "main", - "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", - "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -8232,7 +8326,7 @@ "request-headers", "synthetic-response" ], - "content_sha256": "a73e35e2bec26a8caef88e750c20eec94b8a4e22bd45622922f8ab9f586d554a", + "content_sha256": "05c5008d51ac8a9fe2b0b76d6b1d52a5b3c964d3a4e07c1227591206ce8c70b0", "sections": [ { "id": "cdn-examples-custom-rust#introduction", @@ -8249,9 +8343,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -8639,7 +8733,7 @@ { "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-20 --> --- type: example app_type: cdn languages: [as] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 --> --- 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", @@ -8661,9 +8755,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -8674,7 +8768,7 @@ "secrets", "environment" ], - "content_sha256": "0c73ed18f10ce8d8e06a88f54b4d3f23ac0b67968d5c25e974066c47408cfacb", + "content_sha256": "3c64cf405e55a27101a1b957168091583141e9c42e286db01b35cc3770d77601", "sections": [ { "id": "cdn-examples-env-secrets-as#introduction", @@ -8691,9 +8785,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -9110,7 +9204,7 @@ { "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-20 --> --- type: example app_type: cdn languages: [rust] capabilities: [env-vars, secrets] ---", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 --> --- 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", @@ -9132,9 +9226,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -9144,7 +9238,7 @@ "env-vars", "secrets" ], - "content_sha256": "c649f355de98cfbff02eb0209ff30aec29948b20dda1e664b62b81a49e8a518a", + "content_sha256": "6a8766856d5c193566c9b8cdc05b6e635c040c503595795b5a2f9207b948cab2", "sections": [ { "id": "cdn-examples-env-secrets-rust#introduction", @@ -9161,9 +9255,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -9566,7 +9660,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-as.md", "skill": "fastedge-docs", "category": "examples", @@ -9588,9 +9682,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -9600,7 +9694,7 @@ "geo-routing", "header-manipulation" ], - "content_sha256": "77bc8d7bb75d5073d55729d167f95732aae1c14e0d2cc8f5dbd17a584fe83bdf", + "content_sha256": "bfd821f4dcef5d6f8a06638c0f353365ad97cccd76619f27e6b5fc8ad6e1d48a", "sections": [ { "id": "cdn-examples-geo-redirect-as#introduction", @@ -9617,9 +9711,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -10191,7 +10285,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -10213,9 +10307,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -10225,7 +10319,7 @@ "geo-routing", "origin-rewrite" ], - "content_sha256": "e261c562cb83f75dba70f28b364fbf919b2f5013ba58235119ad284f266ad0d9", + "content_sha256": "a4ba8ac9ed821f5617d6bda76cfbd9d6d0d37c5b5b30e9206a0b2e15b713e730", "sections": [ { "id": "cdn-examples-geo-redirect-rust#introduction", @@ -10242,9 +10336,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -10747,7 +10841,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-as.md", "skill": "fastedge-docs", "category": "examples", @@ -10769,9 +10863,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -10781,7 +10875,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "84af0ea22b79918d77cf3d4bfb495acc76a3246c5535c41b1555f7052fcc183d", + "content_sha256": "763b705d33e44d10af681bf94505be744a28c35e28877e80f0b077006f56f37d", "sections": [ { "id": "cdn-examples-geoblock-as#introduction", @@ -10798,9 +10892,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11276,7 +11370,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -11298,9 +11392,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11310,7 +11404,7 @@ "geoblock", "geo-filtering" ], - "content_sha256": "a7c062df70dea7d93851c4767e7e29ebd0ea6f820171bab1417c892702ee748e", + "content_sha256": "c88ed2e2cb497f3a3cac9866b08afcc3e2abd09c7f09fa25422cede185b2368c", "sections": [ { "id": "cdn-examples-geoblock-rust#introduction", @@ -11327,9 +11421,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11806,7 +11900,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-as.md", "skill": "fastedge-docs", "category": "examples", @@ -11830,9 +11924,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11842,7 +11936,7 @@ "headers", "request-headers" ], - "content_sha256": "caec0cba8cdcb98575ff941504f05fe0ce60d9be18d75841452201ad7f996683", + "content_sha256": "eb268cb0cde0417bb082dbadb140da4f26f837195f2c6d93a907bc1643fc243e", "sections": [ { "id": "cdn-examples-headers-as#introduction", @@ -11859,9 +11953,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -11999,9 +12093,11 @@ "method", "signature", "description", - "-------------------", - "-----------------------------------------------", - "--------------------------------------------------------------------------------------------------------" + "--------------------------", + "---------------------------------------", + "-----------------------------------------------------------------------------------------------------", + "get", + "nam" ] }, { @@ -12439,7 +12535,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -12461,9 +12557,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -12473,7 +12569,7 @@ "headers", "request-headers" ], - "content_sha256": "26db929462e18b733d0d8ff6392d1a509520adb603ee47545516c24043493a11", + "content_sha256": "bf7f55f6e1a3e672e73e99857adf01eae6f05e539dcd246c132da96a418bbe7e", "sections": [ { "id": "cdn-examples-headers-rust#introduction", @@ -12490,9 +12586,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -12837,8 +12933,8 @@ }, { "id": "cdn-examples-http-call-as", - "title": "HTTP Call", - "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: 8e3bb621bc013a0aed7e52122066b417ad62a207 updated: 2026-08-20 -->", + "title": "cdn-examples-http-call-as", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-as.md", "skill": "fastedge-docs", "category": "examples", @@ -12862,9 +12958,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "overview", "httpcall", "dispatches", @@ -12875,7 +12971,7 @@ "fastedge", "proxy-wasm" ], - "content_sha256": "86bdde87724666835a6a0c4dd7190437c0ea08fb8715e577c65cd2486a0b11c0", + "content_sha256": "5fd3877e09d4896bd6c69ed4c0cb9cc56dc6cd0d8fadb01c02391285f4b8bce6", "sections": [ { "id": "cdn-examples-http-call-as#introduction", @@ -12892,9 +12988,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -13166,209 +13262,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -13390,9 +13290,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "makes", @@ -13402,7 +13302,7 @@ "services", "timeout" ], - "content_sha256": "d1e86eef023d89dc75f57a3863c60f70ab78a1b7f854f40563265c4f403fcb2b", + "content_sha256": "5e665b5b4838fd593db0ee7e992ade82f31afd9c62cf39aa13075f9ffdf2b6ee", "sections": [ { "id": "cdn-examples-http-call-rust#introduction", @@ -13419,9 +13319,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "call", "cdn", @@ -13823,7 +13723,7 @@ "level": 2, "anchor": "complete-example", "line_start": 247, - "line_end": 359, + "line_end": 360, "keywords": [ "complete", "example", @@ -13863,8 +13763,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 360, - "line_end": 371, + "line_start": 361, + "line_end": 372, "keywords": [ "gotchas", "action", @@ -13913,8 +13813,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 372, - "line_end": 378, + "line_start": 373, + "line_end": 379, "keywords": [ "see", "proxy-wasm", @@ -13941,7 +13841,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-as.md", "skill": "fastedge-docs", "category": "examples", @@ -13965,9 +13865,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -13977,7 +13877,7 @@ "kv-store", "sorted-sets" ], - "content_sha256": "7c82be09762a859504b8de768a520016b4dd63da93c478964e74af2344eb76b3", + "content_sha256": "bfdb069b5c22829eb076f681740fba8b7d13263e17108b9905e3e513a3ae7350", "sections": [ { "id": "cdn-examples-kv-store-as#introduction", @@ -13994,9 +13894,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -14615,7 +14515,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -14637,9 +14537,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14650,7 +14550,7 @@ "example", "app_type" ], - "content_sha256": "d382cf1a71b92057d477f9447f2538d3008c8abd3f00d3c51323f300c086227a", + "content_sha256": "23c7f8c100db8a05f960e1382c3a9933d52661da3f5ad5a253cfd75e1d720c90", "sections": [ { "id": "cdn-examples-kv-store-rust#introduction", @@ -14667,9 +14567,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -14826,7 +14726,7 @@ "level": 2, "anchor": "gotchas", "line_start": 223, - "line_end": 237, + "line_end": 239, "keywords": [ "gotchas", "get", @@ -14875,8 +14775,8 @@ "heading": "Related", "level": 2, "anchor": "related", - "line_start": 238, - "line_end": 243, + "line_start": 240, + "line_end": 245, "keywords": [ "related", "host", @@ -14911,13 +14811,250 @@ "vs.", "differences" ] + }, + { + "id": "cdn-examples-kv-store-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 246, + "line_end": 518, + "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-kv-store-rust#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 519, + "line_end": 535, + "keywords": [ + "app", + "supports", + "following", + "actions", + "get", + "fetch", + "one", + "key", + "scan", + "list", + "keys", + "matching", + "pattern", + "zrange", + "read", + "sorted-set", + "entries", + "score", + "range", + "zscan", + "bfexists", + "check", + "whether", + "bloom", + "filter", + "contains", + "item", + "request", + "include", + "least", + "store", + "name", + "action", + "operation", + "optional", + "defaults", + "validates", + "required", + "parameters", + "returns" + ] + }, + { + "id": "cdn-examples-kv-store-rust#supported-query-examples", + "heading": "Supported query examples", + "level": 2, + "anchor": "supported-query-examples", + "line_start": 536, + "line_end": 567, + "keywords": [ + "supported", + "query", + "examples", + "get", + "key", + "text", + "store", + "my_store", + "action", + "user", + "list", + "keys", + "pattern", + "scan", + "match", + "read", + "sorted", + "set", + "score", + "range", + "zrange", + "leaderboard", + "min", + "max", + "100", + "search", + "sorted-set", + "members", + "zscan", + "check", + "bloom", + "filter", + "item", + "bfexists", + "visitors", + "alice", + "examp" + ] + }, + { + "id": "cdn-examples-kv-store-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 568, + "line_end": 576, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "key_value.wasm", + "example", + "cdn", + "app", + "targets" + ] + }, + { + "id": "cdn-examples-kv-store-rust#response-format", + "heading": "Response format", + "level": 2, + "anchor": "response-format", + "line_start": 577, + "line_end": 598, + "keywords": [ + "response", + "format", + "app", + "replaces", + "body", + "json", + "sets", + "content-type", + "application", + "successful", + "looks", + "like", + "store", + "my_store", + "action", + "get", + "key", + "user", + "alice", + "required", + "parameter", + "missing", + "operation", + "fails", + "responds", + "error", + "payload", + "param" + ] + }, + { + "id": "cdn-examples-kv-store-rust#notes", + "heading": "Notes", + "level": 2, + "anchor": "notes", + "line_start": 599, + "line_end": 605, + "keywords": [ + "notes", + "app", + "requires", + "query", + "parameters", + "run.", + "action", + "omitted", + "defaults", + "get", + "code", + "uses", + "fastedge", + "proxywasm", + "key_value", + "store", + "proxy_wasm", + "lifecycle", + "hooks", + "operate", + "store." + ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-as.md", "skill": "fastedge-docs", "category": "examples", @@ -14939,9 +15076,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "large", "dictionary", "overview", @@ -14950,7 +15087,7 @@ "environment", "variables" ], - "content_sha256": "5c0712f113a19fa2bcc1667657ca33586d5a538706d60bc80ad976fc408270c3", + "content_sha256": "06c3e8963a2738ac48fbbfbf6217cded6950009199039727a0ed53963ab8474d", "sections": [ { "id": "cdn-examples-large-dictionary-as#introduction", @@ -14967,9 +15104,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "large", "dictionary", "assemblyscript", @@ -15273,13 +15410,212 @@ "patterns", "fastedge-docs" ] + }, + { + "id": "cdn-examples-large-dictionary-as#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 140, + "line_end": 223, + "keywords": [ + "source", + "material", + "file", + "examples", + "largedictionary", + "assembly", + "index.ts", + "export", + "gcoredev", + "proxy-wasm-sdk-as", + "proxy", + "import", + "context", + "filterheadersstatusvalues", + "log", + "loglevelvalues", + "registerrootcontext", + "rootcontext", + "stream_context", + "getdictionary", + "setloglevel", + "fastedge", + "class", + "largedictionaryroot", + "extends", + "createcontext", + "context_id", + "u32", + "con" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#when-to-use-getdictionary-vs-getenv", + "heading": "When to use `getDictionary` vs `getEnv`", + "level": 2, + "anchor": "when-to-use-getdictionary-vs-getenv", + "line_start": 224, + "line_end": 234, + "keywords": [ + "use", + "getdictionary", + "getenv", + "function", + "----------", + "name", + "variable", + "value", + "under", + "cases", + "exceed", + "wasi", + "env", + "var", + "size", + "limit", + "environment", + "interface", + "per", + "variable.", + "your", + "app", + "needs", + "read", + "larger", + "values", + "e.g.", + "large", + "json", + "configs", + "pem", + "certificates", + "policy", + "documents", + "calls", + "proxy_dictionary_get", + "bypasses", + "thi" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#what-it-does", + "heading": "What it does", + "level": 2, + "anchor": "what-it-does", + "line_start": 235, + "line_end": 238, + "keywords": [ + "onrequestheaders", + "app", + "reads", + "large_config", + "environment", + "variable", + "using", + "getdictionary", + "logs", + "size", + "adds", + "x-config-size", + "request", + "header", + "upstream", + "see." + ] + }, + { + "id": "cdn-examples-large-dictionary-as#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 239, + "line_end": 246, + "keywords": [ + "configuration", + "set", + "following", + "your", + "fastedge", + "application", + "name", + "type", + "description", + "------", + "-------------", + "large_config", + "environment", + "variable", + "large", + "payload", + "e.g.", + "json", + "pem", + "certificate" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 247, + "line_end": 260, + "keywords": [ + "build", + "pnpm", + "install", + "run", + "asbuild", + "output", + "file", + "description", + "------", + "-------------", + "largedictionary.wasm", + "optimised", + "release", + "binary", + "upload", + "fastedge", + "largedictionary-debug.wasm", + "debug", + "source", + "maps" + ] + }, + { + "id": "cdn-examples-large-dictionary-as#deploy", + "heading": "Deploy", + "level": 2, + "anchor": "deploy", + "line_start": 261, + "line_end": 265, + "keywords": [ + "deploy", + "upload", + "build", + "largedictionary.wasm", + "fastedge", + "portal", + "attach", + "your", + "cdn", + "application.", + "configure", + "large_config", + "environment", + "variable", + "application", + "settings." + ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15301,9 +15637,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "large-env-var", @@ -15313,7 +15649,7 @@ "app_type", "languages" ], - "content_sha256": "1eb33add15b946d5567ac85d662c67d674c0322a60fd9aa28e0930be81001b54", + "content_sha256": "79abc10593f47efe0567a64ce1580bbb0c5be10f9cfca293be10963f46bb4c9c", "sections": [ { "id": "cdn-examples-large-dictionary-rust#introduction", @@ -15330,9 +15666,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "large-env-var", @@ -15399,7 +15735,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-as.md", "skill": "fastedge-docs", "category": "examples", @@ -15423,9 +15759,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -15435,7 +15771,7 @@ "logging", "time" ], - "content_sha256": "0cd462da5aca1fa0b6b8f81e6775b0dea2879447776f153e4cc8522280774585", + "content_sha256": "5266245f16c9a92efe159d422b1d6efbb80268cecbd4be0e9cbf62f79073c1d5", "sections": [ { "id": "cdn-examples-log-time-as#introduction", @@ -15452,9 +15788,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -15798,7 +16134,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -15820,9 +16156,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -15832,7 +16168,7 @@ "logging", "timing" ], - "content_sha256": "20055b088c670a075eae455b07e6b9a38488a69a6c3199d9566cab1f18d3beaf", + "content_sha256": "17de7b3f08a526cb98293c820d7bd7c1613cadab078a2b31d101893d8def383e", "sections": [ { "id": "cdn-examples-log-time-rust#introduction", @@ -15849,9 +16185,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -16189,7 +16525,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -16211,9 +16547,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -16223,7 +16559,7 @@ "response-body-transform", "request-path-rewrite" ], - "content_sha256": "1b26d02c4a91ae4a45b55116506d088b7e878c81183e345f40fb5d7a4e9c6f95", + "content_sha256": "f8fbf0e4875baadddcd666f15ec0b2921599d2698c6c647e115d812aa0130fce", "sections": [ { "id": "cdn-examples-md2html-rust#introduction", @@ -16240,9 +16576,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -16671,7 +17007,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-as.md", "skill": "fastedge-docs", "category": "examples", @@ -16693,9 +17029,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "runtime", "properties", "language", @@ -16705,7 +17041,7 @@ "type", "overview" ], - "content_sha256": "1a7cb1c6e4a4a24400ab5988ca4ec6145ff264706a8559555d2c0a9c37c4273c", + "content_sha256": "3bbe18f8ddfeb71ed284002040e77bd67ef56bb6a5281f9165ebb30794f1cf43", "sections": [ { "id": "cdn-examples-properties-as#introduction", @@ -16722,9 +17058,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "cdn", "runtime", "properties", @@ -17222,7 +17558,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -17244,9 +17580,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -17256,7 +17592,7 @@ "request-properties", "geo-data" ], - "content_sha256": "eb4c151ead93e097ac9ac3e38b0515c3c08db7a6c4eaf57ed6516291ea67ae49", + "content_sha256": "f5e9104e3624d8f2bb64dfb10d2389a9ef745159f38f2ce8dc7fdeff64000933", "sections": [ { "id": "cdn-examples-properties-rust#introduction", @@ -17273,9 +17609,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -18244,7 +18580,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -18263,9 +18599,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "host", "services", "reference", @@ -18276,7 +18612,7 @@ "storage", "secret" ], - "content_sha256": "815f0408c27342ac5a6ee1babbd38b3bf41d35228b85062807f8172c7ef86788", + "content_sha256": "3c1895152a7a969acdb7c1f5e670bd6441f6b3d44fadf5a8640adab15cc3185e", "sections": [ { "id": "host-services-rust#introduction", @@ -18293,9 +18629,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "rust", "host", "services", @@ -18443,6 +18779,13 @@ "change", "lifetime", "deployment.", + "config", + "key_value", + "persistent", + "data", + "secret", + "encrypted", + "credentials.", "api", "rust", "pub", @@ -18462,15 +18805,7 @@ "decoded", "utf-8.", "environment", - "variables", - "set", - "deployment", - "time", - "via", - "platform", - "same", - "management", - "mechanism" + "vari" ] }, { @@ -18565,7 +18900,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md", "skill": "fastedge-docs", "category": "examples", @@ -18587,9 +18922,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "testing", "fastedge", "example", @@ -18600,7 +18935,7 @@ "visitors", "test" ], - "content_sha256": "25feb56e225dc3c2de096f569836c7a02371f3ad66f47c95f09b1d43cae9d0af", + "content_sha256": "035d8a71d445ebe35d7be302675769576440de7ae354afe090ab0810e7efa2ae", "sections": [ { "id": "http-examples-ab-testing-js#introduction", @@ -18617,9 +18952,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "testing", "fastedge", "example" @@ -19126,7 +19461,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19148,9 +19483,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -19160,7 +19495,7 @@ "ab-testing", "cookie-parsing" ], - "content_sha256": "21cff74a5cb7a1216109a412f205162be95a99647d070c31cfb71ff8c06626fb", + "content_sha256": "cc40993ca04fabb9520799bde4b5cca46303197434948ca87de607ddb25b659f", "sections": [ { "id": "http-examples-ab-testing-wasi-rust#introduction", @@ -19177,9 +19512,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -19654,7 +19989,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -19676,9 +20011,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "examples-api-wrapper-basic-rust", "type", "example", @@ -19688,7 +20023,7 @@ "language", "app" ], - "content_sha256": "eb2a0df93df00172d7020dc806527d1f98e18809247273240ede8f6952fccbf6", + "content_sha256": "e441eccf269500eb32517fe80bc7b7dcc0840b906593b5298f97ea78452293fd", "sections": [ { "id": "http-examples-api-wrapper-basic-rust#introduction", @@ -19705,9 +20040,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "examples-api-wrapper-basic-rust", "type", "http", @@ -20237,7 +20572,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md", "skill": "fastedge-docs", "category": "examples", @@ -20259,9 +20594,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "authentication", "patterns", "fastedge", @@ -20272,7 +20607,7 @@ "shared", "secrets" ], - "content_sha256": "2e7f44f68db995e99a39e614304ec39247aa583cb6ccb04e1674dd9b579b49e7", + "content_sha256": "3b2f936aaf35dbf11bd593c13d4027863de2650ffc16c981b157e041ad29dbfd", "sections": [ { "id": "http-examples-auth-js#introduction", @@ -20289,9 +20624,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "authentication", "patterns", "javascript", @@ -20588,7 +20923,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -20610,9 +20945,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -20622,7 +20957,7 @@ "outbound-http", "query-params" ], - "content_sha256": "7ed671dc0d577b2f44a9be8be61ed5840b34e1ef7ff5b28f5169a14a97e79b0e", + "content_sha256": "653bed0caf43133e7cf62e0930264bec8ced4e99d9db75ad00188b1f618c266d", "sections": [ { "id": "http-examples-backend-basic-rust#introduction", @@ -20639,9 +20974,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21063,207 +21398,13 @@ "handling", "patterns" ] - }, - { - "id": "http-examples-backend-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 157, - "line_end": 228, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "backend", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "error", - "result", - "fastedge", - "body", - "method", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "parts", - "req.into_parts", - "query", - ".uri", - ".query", - ".ok_or", - "missing", - "uri", - "parameter", - "params", - "querystring", - "querify" - ] - }, - { - "id": "http-examples-backend-basic-rust#what-it-does", - "heading": "What it does", - "level": 2, - "anchor": "what-it-does", - "line_start": 229, - "line_end": 238, - "keywords": [ - "parses", - "url", - "query", - "parameter", - "request", - "uri", - "percent-decodes", - "via", - "urlencoding", - "decode", - "builds", - "outbound", - "get", - "using", - "fastedge", - "send_request", - "returns", - "http", - "200", - "plain-text", - "body", - "len", - "body-length", - "content-type", - "upstream-content-type", - "500", - "error", - "message", - "absent", - "string", - "missing." - ] - }, - { - "id": "http-examples-backend-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 239, - "line_end": 249, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "---", - "fastedge", - "http", - "sync", - "request-response", - "handler", - "macro", - "send_request", - "request", - "blocking", - "outbound", - "response", - "statuscode", - "method", - "types", - "body", - "bodies", - "querystring", - "querify", - "parse", - "query", - "string", - "key-value", - "pairs", - "urlencoding", - "decode", - "percent-decode", - "url", - "value" - ] - }, - { - "id": "http-examples-backend-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 250, - "line_end": 256, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "backend.wasm" - ] - }, - { - "id": "http-examples-backend-basic-rust#expected-behavior", - "heading": "Expected behavior", - "level": 2, - "anchor": "expected-behavior", - "line_start": 257, - "line_end": 267, - "keywords": [ - "expected", - "behavior", - "request", - "response", - "status", - "body", - "---", - "get", - "url", - "https", - "2fhttpbin.org", - "2fget", - "200", - "len", - "content-type", - "mime", - "hello", - "key", - "500", - "missing", - "parameter", - "query", - "string", - "uri", - "value", - "byte", - "length", - "upstream", - "body.", - "header", - "returned", - "server", - "formatted", - "rust", - "option" - ] } ] }, { "id": "http-examples-bloom-filter-denylist-wasi-rust", "title": "Example: Bloom Filter IP Denylist (WASI, Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21285,9 +21426,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21297,7 +21438,7 @@ "kv-store", "bloom-filter" ], - "content_sha256": "fa4e6b140a8db8f8879eb511aa72c60e9e495039ae9a1b80cfd2c9841d0bef19", + "content_sha256": "a447a165a864934a9567ab24a9258a36cc441050690b08e0afe96a7a9523a505", "sections": [ { "id": "http-examples-bloom-filter-denylist-wasi-rust#introduction", @@ -21314,9 +21455,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21769,13 +21910,195 @@ "platform-overview", "concepts" ] + }, + { + "id": "http-examples-bloom-filter-denylist-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 154, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 304, + "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": 305, + "line_end": 309, + "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": 310, + "line_end": 314, + "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -21797,9 +22120,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -21809,7 +22132,7 @@ "cache", "basic" ], - "content_sha256": "51fe5b585b86d0b39142ab2d5313ea57a260b1d8830ad7d38bce3b839f9f9961", + "content_sha256": "58aa39343f1d956e9ab8ab5ea82eba707a1033fac18ca3f195c40b96b7b3016b", "sections": [ { "id": "http-examples-cache-basic-rust#introduction", @@ -21826,9 +22149,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -22221,13 +22544,199 @@ "selection", "guidance" ] + }, + { + "id": "http-examples-cache-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 137, + "line_end": 244, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "basic", + "cache", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "example", + "app", + "demonstrating", + "cache-aside", + "pattern", + "via", + "interface.", + "request", + "builds", + "key", + "path.", + "returns", + "cached", + "body", + "immediately", + "hit", + "x-cache", + "miss", + "generates", + "response", + "stores", + "environment", + "variables", + "cache_ttl_ms", + "long" + ] + }, + { + "id": "http-examples-cache-basic-rust#configuration", + "heading": "Configuration", + "level": 2, + "anchor": "configuration", + "line_start": 245, + "line_end": 250, + "keywords": [ + "configuration", + "env", + "var", + "required", + "description", + "---", + "cache_ttl_ms", + "long", + "cache", + "response", + "milliseconds.", + "default", + "30000" + ] + }, + { + "id": "http-examples-cache-basic-rust#how-it-works", + "heading": "How it works", + "level": 2, + "anchor": "how-it-works", + "line_start": 251, + "line_end": 259, + "keywords": [ + "works", + "get", + "api", + "data", + "cache", + "miss", + "generate", + "body", + "store", + "200", + "x-cache", + "hit", + "return", + "cached", + "key", + "page", + "request-path", + "unique", + "path", + "gets", + "own", + "entry.", + "response", + "simple", + "html", + "includes", + "request", + "stand", + "expensive", + "computation", + "template", + "render." + ] + }, + { + "id": "http-examples-cache-basic-rust#what-it-returns", + "heading": "What it returns", + "level": 2, + "anchor": "what-it-returns", + "line_start": 260, + "line_end": 269, + "keywords": [ + "returns", + "http", + "1.1", + "200", + "content-type", + "text", + "html", + "x-cache", + "hit", + "miss", + "doctype", + "..." + ] + }, + { + "id": "http-examples-cache-basic-rust#build", + "heading": "Build", + "level": 2, + "anchor": "build", + "line_start": 270, + "line_end": 276, + "keywords": [ + "build", + "cargo", + "--release", + "output", + "target", + "wasm32-wasip1", + "release", + "cache_basic.wasm" + ] + }, + { + "id": "http-examples-cache-basic-rust#apis-used", + "heading": "APIs used", + "level": 2, + "anchor": "apis-used", + "line_start": 277, + "line_end": 282, + "keywords": [ + "apis", + "used", + "fastedge", + "cache", + "get", + "key", + "retrieve", + "cached", + "bytes", + "returns", + "option", + "vec", + "set", + "ttl_ms", + "store", + "optional", + "ttl", + "milliseconds", + "none", + "means", + "expiry" + ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md", "skill": "fastedge-docs", "category": "examples", @@ -22251,9 +22760,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "module", @@ -22263,7 +22772,7 @@ "2.3.0", "app" ], - "content_sha256": "1836a727bbc36819e95787b311b6d05ca2b8ac657190824b76fc727e7fa81ceb", + "content_sha256": "c53a54daf9307b32c0aa69a72792c852be660acff331a69288bf9425b6a194a6", "sections": [ { "id": "http-examples-cache-js#introduction", @@ -22280,9 +22789,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "cache", "javascript", @@ -22611,63 +23120,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -22689,9 +23148,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -22701,7 +23160,7 @@ "cache", "outbound-http" ], - "content_sha256": "31cfe4b83115feeb78ff407e5c37207e21a522d87f06aeb8d7607aa6ad31cd67", + "content_sha256": "7c58be673c4adce9e4ac6ad2928c95a42a645cf231c1f09d901ed10534e327cc", "sections": [ { "id": "http-examples-cache-wasi-rust#introduction", @@ -22718,9 +23177,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -23069,201 +23528,13 @@ "workflow", "skill" ] - }, - { - "id": "http-examples-cache-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 192, - "line_end": 359, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "cache", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "example", - "app", - "demonstrating", - "response", - "caching", - "purging", - "via", - "interface.", - "reads", - "origin_host", - "environment", - "forwards", - "incoming", - "request", - "origin", - "caches", - "body", - "keyed", - "path.", - "subsequent", - "requests", - "same", - "path", - "cached", - "returned" - ] - }, - { - "id": "http-examples-cache-wasi-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 360, - "line_end": 366, - "keywords": [ - "configuration", - "env", - "var", - "required", - "description", - "---", - "origin_host", - "yes", - "base", - "url", - "upstream", - "origin", - "e.g.", - "https", - "api.example.com", - "returns", - "500", - "unset.", - "cache_ttl_ms", - "long", - "cache", - "responses", - "milliseconds.", - "default", - "60000" - ] - }, - { - "id": "http-examples-cache-wasi-rust#how-it-works", - "heading": "How it works", - "level": 2, - "anchor": "how-it-works", - "line_start": 367, - "line_end": 373, - "keywords": [ - "works", - "get", - "data", - "cache", - "miss", - "forward", - "origin_host", - "2xx", - "body", - "200", - "x-cache", - "hit", - "return", - "cached", - "key", - "path", - "query", - "only", - "responses", - "origin", - "error", - "pass", - "without", - "stored.", - "response", - "headers", - "replayed", - "cache-hit", - "use", - "content-type", - "application", - "octet-stream", - "sinc" - ] - }, - { - "id": "http-examples-cache-wasi-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 374, - "line_end": 378, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip2", - "release", - "cache_wasi.wasm" - ] - }, - { - "id": "http-examples-cache-wasi-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 379, - "line_end": 385, - "keywords": [ - "apis", - "used", - "fastedge", - "cache", - "get", - "key", - "retrieve", - "cached", - "bytes", - "returns", - "option", - "vec", - "set", - "ttl_ms", - "store", - "optional", - "ttl", - "milliseconds", - "none", - "means", - "expiry", - "wstd", - "http", - "client", - "new", - ".send", - "req", - ".await", - "async", - "outbound", - "request", - "origin" - ] } ] }, { "id": "http-examples-diagnostic-logging-wasi-rust", "title": "Diagnostic Logging — Rust (WASI HTTP)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -23285,9 +23556,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23297,7 +23568,7 @@ "languages", "diagnostic" ], - "content_sha256": "b55cd1e2b402fa75e701e2336c62aa6314ec4be802409333a130cb1ffcbfe91c", + "content_sha256": "e0b1cfb99fd9375902a7ab410213218de42cf836f17460e19e3cef595ebc872b", "sections": [ { "id": "http-examples-diagnostic-logging-wasi-rust#introduction", @@ -23314,9 +23585,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "diagnostic-logging", @@ -23817,230 +24088,13 @@ "store", "usage" ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 194, - "line_end": 307, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "wasi", - "diagnostic_logging", - "src", - "lib.rs", - "rust", - "copyright", - "2025", - "g-core", - "innovations", - "sarl", - "diagnostic", - "logging", - "example.", - "tiny", - "pass-through", - "proxy", - "writes", - "single", - "set_user_diag", - "tag", - "per", - "request", - "summarising", - "outcome", - "config_error", - "origin_unreachable", - "proxied", - "appears", - "fastedge", - "platform", - "per-request", - "log", - "viewer", - "distinct", - "stdout" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#configuration", - "heading": "Configuration", - "level": 2, - "anchor": "configuration", - "line_start": 308, - "line_end": 311, - "keywords": [ - "configuration", - "origin_url", - "environment", - "variable", - "origin", - "requests", - "proxied", - "to." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#outcomes", - "heading": "Outcomes", - "level": 2, - "anchor": "outcomes", - "line_start": 312, - "line_end": 321, - "keywords": [ - "outcomes", - "request", - "writes", - "exactly", - "one", - "condition", - "tag", - "---", - "origin_url", - "missing", - "outcome", - "config_error", - "reason", - "origin_missing", - "origin", - "unreachable", - "origin_unreachable", - "method", - "path", - "err", - "proxied", - "status" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#set_user_diag-vs-println", - "heading": "`set_user_diag` vs `println!`", - "level": 2, - "anchor": "set_user_diag-vs-println", - "line_start": 322, - "line_end": 330, - "keywords": [ - "set_user_diag", - "println", - "---", - "channel", - "stdout", - "general", - "application", - "logs", - "per-request", - "structured", - "tag", - "platform", - "log", - "viewer", - "cardinality", - "many", - "per", - "request", - "one", - "multiple", - "calls", - "leave", - "only", - "last", - "concatenated", - "undefined", - "best", - "verbose", - "traces", - "debug", - "details", - "single", - "filterable", - "outcome", - "label", - "forbidden", - "secrets", - "pii", - "tags", - "appear" - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#convention", - "heading": "Convention", - "level": 2, - "anchor": "convention", - "line_start": 331, - "line_end": 336, - "keywords": [ - "convention", - "call", - "set_user_diag", - "once", - "branch", - "late", - "enough", - "handler", - "know", - "outcome.", - "logfmt", - "-ish", - "format", - "outcome", - "verb", - "key", - "value", - "easy", - "slice", - "log", - "search", - "tooling", - "keep", - "keys", - "short", - "stable", - "they", - "make", - "good", - "filter", - "terms." - ] - }, - { - "id": "http-examples-diagnostic-logging-wasi-rust#related", - "heading": "Related", - "level": 2, - "anchor": "related", - "line_start": 337, - "line_end": 342, - "keywords": [ - "related", - "cdn", - "proxy-wasm", - "variant", - "fastedge", - "proxywasm", - "utils", - "set_user_diag", - "same", - "semantics", - "different", - "module", - "path.", - "see", - "docs", - "cdn_apps.md" - ] } ] }, { "id": "http-examples-fetch-js", "title": "http-examples-fetch-js", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md", "skill": "fastedge-docs", "category": "examples", @@ -24062,9 +24116,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fetch", "outbound", "requests", @@ -24075,7 +24129,7 @@ "standard", "url" ], - "content_sha256": "032adc2beba6b9b9dade30fcf609e1febf5a9b7ddc47a88c37889d1eec835870", + "content_sha256": "9d09d5b86b672a1ceca9e554cda3515013cbfff850d242803d0014706b1e022f", "sections": [ { "id": "http-examples-fetch-js#introduction", @@ -24092,9 +24146,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -24151,7 +24205,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md", "skill": "fastedge-docs", "category": "examples", @@ -24173,9 +24227,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "example", "geo-redirect", "redirects", @@ -24187,7 +24241,7 @@ "visitor", "country" ], - "content_sha256": "07208f0d1266ed5eecd1ac212b47f0a27cb432220881b2f7fac6a6d4aee0bbd4", + "content_sha256": "f1a1a79fa9a815fb6ded9e34dbccf9e56e95413f718fa7a5203871f4814e706c", "sections": [ { "id": "http-examples-geo-redirect-js#introduction", @@ -24204,9 +24258,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -24264,7 +24318,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -24286,9 +24340,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -24298,7 +24352,7 @@ "geo-redirect", "headers" ], - "content_sha256": "92d1d6dcd29aa84e8c9a383b7f31e2a4c3550686a7586e2c164187596b5df709", + "content_sha256": "ebf328ce37861f9d7fe2fd1a9d745988abae0e3f0d80397151c464f8ed4182cc", "sections": [ { "id": "http-examples-geo-redirect-wasi-rust#introduction", @@ -24315,9 +24369,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -24890,7 +24944,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md", "skill": "fastedge-docs", "category": "examples", @@ -24912,9 +24966,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "headers", "example", "fastedge", @@ -24926,7 +24980,7 @@ "environment", "variable" ], - "content_sha256": "d4a5d1b949bfe5116aee65279e796b023d55d59544854876eaf493c110eef90c", + "content_sha256": "a375b46028720ee5a9a69d73ca41c1884f6ca1e34db5f2e2ef475340fef83f6c", "sections": [ { "id": "http-examples-headers-js#introduction", @@ -24943,9 +24997,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -24997,7 +25051,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25019,9 +25073,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -25031,7 +25085,7 @@ "headers", "env-vars" ], - "content_sha256": "b6142b5b461c8cc47c3422fa749ff3d7ca808ac8618aa481eaecd952f6b80483", + "content_sha256": "faa792bdc1043deed4c61b7b4e581f50616924995335ac4293a7722745dddd4b", "sections": [ { "id": "http-examples-headers-wasi-rust#introduction", @@ -25048,9 +25102,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -25646,7 +25700,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -25668,9 +25722,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -25680,7 +25734,7 @@ "hello-world", "basic-handler" ], - "content_sha256": "c3aac88e44de0d555bc462d6d70424ab34d4f0dec25606a2625ceb5e39b67400", + "content_sha256": "37e082cfacf1c690062e7464ba6b456feb35ca7e3c27e75c4182965562a648da", "sections": [ { "id": "http-examples-hello-world-basic-rust#introduction", @@ -25697,9 +25751,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -26078,7 +26132,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26100,9 +26154,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -26112,7 +26166,7 @@ "hello-world", "wasi" ], - "content_sha256": "b15ce34c19d4cf004bf6f602e6bf370c34f33abd962e77ce72f4bc13969016fe", + "content_sha256": "d6dc36ec9e0a07fbf165cb7912f5025529a82884768c0b4f9af1305288940b2b", "sections": [ { "id": "http-examples-hello-world-wasi-rust#introduction", @@ -26129,9 +26183,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -26530,7 +26584,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md", "skill": "fastedge-docs", "category": "examples", @@ -26554,9 +26608,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "hono", "patterns", "fastedge", @@ -26565,7 +26619,7 @@ "apps.", "small" ], - "content_sha256": "4c75f73bbc6d7fbc5686f1d701a49ba1af7af0c650a910e3d47ae72272afdf1e", + "content_sha256": "4332733a5c2543a348bf8e164983d9f7e108418959d01ef10691440e45081d31", "sections": [ { "id": "http-examples-hono-js#introduction", @@ -26582,9 +26636,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "hono", "patterns", "fastedge", @@ -26811,7 +26865,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md", "skill": "fastedge-docs", "category": "examples", @@ -26833,9 +26887,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "store", "example", "reference", @@ -26847,7 +26901,7 @@ "fastedge", "edge" ], - "content_sha256": "bbee208b19ac9c128c58c2b3a483f133bf01d56becf4d62a328e664fb466c646", + "content_sha256": "bba21afe2826aed5c7c9e8f316219400c9ae56a59e1d5c0230c64196b8d85b88", "sections": [ { "id": "http-examples-kv-store-js#introduction", @@ -26864,9 +26918,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20" + "2026-09-22" ] }, { @@ -26922,7 +26976,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -26944,9 +26998,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -26956,7 +27010,7 @@ "languages", "store" ], - "content_sha256": "315969c42f8aa2c0325e094018cc3d59e5f68550acd5744a480b2302a15d0860", + "content_sha256": "828de5dab25ce52b865c81265bfcd8f41ea2c236d44bafb9045985ef9759cb95", "sections": [ { "id": "http-examples-kv-store-wasi-rust#introduction", @@ -26973,9 +27027,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "capabilities", "kv-store", @@ -27408,7 +27462,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27430,9 +27484,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "large", "environment", "variable", @@ -27442,7 +27496,7 @@ "demonstrates", "read" ], - "content_sha256": "ab40bc5e1a189d77f212f789961be555b81ceead639067c4ccfbdc9f2e4e1d6c", + "content_sha256": "094c1dcce620b2e59578200c905580766b4a3800979c0e057d373e855651f1d1", "sections": [ { "id": "http-examples-large-env-variable-wasi-rust#introduction", @@ -27459,9 +27513,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "large", "environment", "variable", @@ -27797,7 +27851,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -27819,9 +27873,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -27831,7 +27885,7 @@ "outbound-fetch", "env-vars" ], - "content_sha256": "e170431974501dcb609073b8be6b0f0c93a73841a33255b961077dec00c66f68", + "content_sha256": "bf7a3959832f6f6813babb0f96e471a0c68d3cbc420f6baa775c2e2229ce1329", "sections": [ { "id": "http-examples-markdown-render-basic-rust#introduction", @@ -27848,9 +27902,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -28323,7 +28377,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28345,9 +28399,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -28357,7 +28411,7 @@ "outbound-fetch", "json-parsing" ], - "content_sha256": "7d9b8470977ba554efeff699feeee8f078a5b682b5d396d070f0589f49f8044f", + "content_sha256": "9d41b008474919364325a73334175ac51ca10b8fbaabee8bee40b6415b2b8c9e", "sections": [ { "id": "http-examples-outbound-fetch-basic-rust#introduction", @@ -28374,9 +28428,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -28750,7 +28804,7 @@ { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -28772,9 +28826,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "outbound", "fetch", "wasi", @@ -28784,7 +28838,7 @@ "upstream", "origin" ], - "content_sha256": "c5a6c3a406e12b4a82281f715d7f268d84b732cc76ec9376b34314302cea12dc", + "content_sha256": "57f3381591f07f1ac6daab49fe28d96aa6a757531829f89c225680550a56abfb", "sections": [ { "id": "http-examples-outbound-fetch-wasi-rust#introduction", @@ -28801,9 +28855,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "outbound", "fetch", "wasi", @@ -29200,7 +29254,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29222,9 +29276,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -29234,7 +29288,7 @@ "outbound-fetch", "json-transform" ], - "content_sha256": "f6509335e7b9957b364049295ea3a84967dce13ae1e03c5ebbd8412f6e0dfb05", + "content_sha256": "f8cc2f72f5324a9da973c0e8eac43544bd59637ba3cbcd84ea858acd9de6596f", "sections": [ { "id": "http-examples-outbound-modify-response-wasi-rust#introduction", @@ -29251,9 +29305,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -29647,7 +29701,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -29669,9 +29723,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -29681,7 +29735,7 @@ "request-inspection", "header-iteration" ], - "content_sha256": "7c8720cc88a63e99432da519839a4f5b8438520df5ac92432b3c524afbaa1f85", + "content_sha256": "b33f1a09f2dca08a64f23ac6419b228e8de755a66bc4de6a66fffadee231998c", "sections": [ { "id": "http-examples-print-basic-rust#introduction", @@ -29698,9 +29752,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -30097,209 +30151,13 @@ "building", "patterns" ] - }, - { - "id": "http-examples-print-basic-rust#source-material", - "heading": "Source Material", - "level": 2, - "anchor": "source-material", - "line_start": 151, - "line_end": 214, - "keywords": [ - "source", - "material", - "file", - "examples", - "http", - "basic", - "print", - "src", - "lib.rs", - "rust", - "use", - "anyhow", - "result", - "fastedge", - "body", - "request", - "response", - "statuscode", - "allow", - "dead_code", - "main", - "req", - "let", - "mut", - "string", - "method", - ".to_string", - "body.push_str", - "req.method", - ".as_str", - "nurl", - "req.uri", - "nheaders" - ] - }, - { - "id": "http-examples-print-basic-rust#what-it-demonstrates", - "heading": "What it demonstrates", - "level": 2, - "anchor": "what-it-demonstrates", - "line_start": 215, - "line_end": 222, - "keywords": [ - "demonstrates", - "reading", - "request", - "method", - "via", - "req.method", - ".as_str", - "uri", - "req.uri", - ".to_string", - "iterating", - "headers", - "req.headers", - "handling", - "non-utf-8", - "header", - "values", - "gracefully", - "match", - "v.to_str", - "building", - "plain-text", - "response", - "builder", - "body", - "..." - ] - }, - { - "id": "http-examples-print-basic-rust#apis-used", - "heading": "APIs used", - "level": 2, - "anchor": "apis-used", - "line_start": 223, - "line_end": 233, - "keywords": [ - "apis", - "used", - "api", - "purpose", - "-----", - "---------", - "req.method", - ".as_str", - "http", - "method", - "string", - "slice", - "req.uri", - ".to_string", - "full", - "request", - "uri", - "req.headers", - "iterator", - "over", - "headername", - "headervalue", - "pairs", - "v.to_str", - "decode", - "header", - "value", - "str", - "returns", - "err", - "non-utf-8", - "response", - "builder", - ".status", - "...", - ".body", - "build", - "body", - "create" - ] - }, - { - "id": "http-examples-print-basic-rust#build", - "heading": "Build", - "level": 2, - "anchor": "build", - "line_start": 234, - "line_end": 240, - "keywords": [ - "build", - "cargo", - "--release", - "output", - "target", - "wasm32-wasip1", - "release", - "print.wasm" - ] - }, - { - "id": "http-examples-print-basic-rust#expected-behaviour", - "heading": "Expected behaviour", - "level": 2, - "anchor": "expected-behaviour", - "line_start": 241, - "line_end": 259, - "keywords": [ - "expected", - "behaviour", - "request", - "response", - "body", - "plain-text", - "dump", - "details", - "method", - "get", - "url", - "path", - "query", - "value", - "headers", - "host", - "example.com", - "accept", - "...", - "status", - "200", - "content", - "plain", - "text", - "content-type", - "header", - "set", - "explicitly", - "platform", - "add", - "one", - "appears", - "own", - "line", - "indented", - "four", - "spaces", - "non-utf-8", - "values", - "replaced" - ] } ] }, { "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md", "skill": "fastedge-docs", "category": "examples", @@ -30323,9 +30181,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30334,7 +30192,7 @@ "fastedge", "apps" ], - "content_sha256": "cf79e7be2bb9093568bcf8495bf36926610c71e4cb36df51b122a605fdfb3725", + "content_sha256": "08a57d5d0098acb645f51c29d3f5850d1dad67ec7d808d89c78453d0323ec904", "sections": [ { "id": "http-examples-proxy-js#introduction", @@ -30351,9 +30209,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "proxy", "response", "transform", @@ -30787,356 +30645,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31158,9 +30673,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31170,7 +30685,7 @@ "s3-upload", "presigned-url" ], - "content_sha256": "a8c2d5d395a46515ff38614cfb280eb3ac460c00da9f34cd6c710cf4df130f29", + "content_sha256": "72cff95ae062275b261acaf37d03d72affe87facf10960cd16531237ff6bd4cf", "sections": [ { "id": "http-examples-s3upload-basic-rust#introduction", @@ -31187,9 +30702,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -31628,247 +31143,13 @@ "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -31890,9 +31171,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "example", @@ -31902,7 +31183,7 @@ "fastedge", "platform" ], - "content_sha256": "99a13e72a317ae2f8118520b3f21a52ecaaacabefc6ae0000004391353e8c1fa", + "content_sha256": "1d1ae1d9f636c28df70129574cad04baf9b39ec87e640e902ed814f97a07d13b", "sections": [ { "id": "http-examples-secret-basic-rust#introduction", @@ -31919,9 +31200,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "access", "rust", @@ -32311,13 +31592,284 @@ "list", "delete" ] + }, + { + "id": "http-examples-secret-basic-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 255, + "line_end": 368, + "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": 369, + "line_end": 378, + "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": 379, + "line_end": 389, + "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": 390, + "line_end": 399, + "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": 400, + "line_end": 406, + "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": 407, + "line_end": 416, + "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": 417, + "line_end": 434, + "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32339,9 +31891,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32352,7 +31904,7 @@ "rotation", "using" ], - "content_sha256": "535ab5dbaa52d9d716c1762d05b466c6182ff507db6ec46650bf241318a0fb65", + "content_sha256": "2cd00d58e3f6047c79cb57ea352f8e2684f86dec50a7357dadfe62d4bf0d8151", "sections": [ { "id": "http-examples-secret-rollover-wasi-rust#introduction", @@ -32369,9 +31921,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "secret", "rollover", "wasi", @@ -32627,7 +32179,7 @@ "level": 2, "anchor": "implementation-pattern", "line_start": 124, - "line_end": 178, + "line_end": 177, "keywords": [ "implementation", "pattern", @@ -32653,20 +32205,19 @@ "result", "read", "slot", + "x-slot", "header", - "default", + "defaulting", "current", - "unix", "timestamp", "let", "u32", ".headers", ".get", - "x-slot", ".and_then", "v.to_str", ".ok", - "v.p" + ".and" ] }, { @@ -32674,8 +32225,8 @@ "heading": "Constraints and Gotchas", "level": 2, "anchor": "constraints-and-gotchas", - "line_start": 179, - "line_end": 186, + "line_start": 178, + "line_end": 185, "keywords": [ "constraints", "gotchas", @@ -32724,8 +32275,8 @@ "heading": "Build", "level": 2, "anchor": "build", - "line_start": 187, - "line_end": 193, + "line_start": 186, + "line_end": 192, "keywords": [ "build", "cargo", @@ -32742,8 +32293,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 194, - "line_end": 200, + "line_start": 193, + "line_end": 199, "keywords": [ "see", "fastedge", @@ -32774,7 +32325,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -32796,9 +32347,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32808,7 +32359,7 @@ "outbound-fetch", "header-read" ], - "content_sha256": "d8d6a23390c7c0c319f1e3e0db70657d19a195129c9633d73217efa35f1408b3", + "content_sha256": "ff728af48b322c2e67a2a06dd8e555818382d4535f559f6cf83b2f14e6732db5", "sections": [ { "id": "http-examples-simple-fetch-wasi-rust#introduction", @@ -32825,9 +32376,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -32901,7 +32452,7 @@ "level": 2, "anchor": "request-headers", "line_start": 30, - "line_end": 35, + "line_end": 36, "keywords": [ "request", "headers", @@ -32923,7 +32474,14 @@ "httpbin.org", "get", "fetch", - "outbound" + "outbound", + "x-fetch-header", + "key", + "value", + "additional", + "forward", + "parsed", + "separator" ] }, { @@ -32931,8 +32489,8 @@ "heading": "Behavior", "level": 2, "anchor": "behavior", - "line_start": 36, - "line_end": 42, + "line_start": 37, + "line_end": 45, "keywords": [ "behavior", "read", @@ -32947,27 +32505,33 @@ "get", "absent", "unparseable.", - "build", + "begin", + "building", "outbound", "url", "accept", "application", "json", "header.", + "x-fetch-header", + "present", + "parseable", + "key", + "value", + "split", + "first", + "add", + "builder.", + "finalize", + "builder", + ".body", + "body", + "empty", "send", "via", "client", "new", - ".send", - "req", - ".await", - "return", - "upstream", - "response", - "body", - "directly", - "caller", - "decomposition." + ".send" ] }, { @@ -32975,8 +32539,8 @@ "heading": "Key API Patterns", "level": 2, "anchor": "key-api-patterns", - "line_start": 43, - "line_end": 99, + "line_start": 46, + "line_end": 122, "keywords": [ "key", "api", @@ -33025,8 +32589,8 @@ "heading": "Logging", "level": 2, "anchor": "logging", - "line_start": 100, - "line_end": 109, + "line_start": 123, + "line_end": 132, "keywords": [ "logging", "rust", @@ -33049,8 +32613,8 @@ "heading": "Cargo.toml Requirements", "level": 2, "anchor": "cargotoml-requirements", - "line_start": 110, - "line_end": 126, + "line_start": 133, + "line_end": 149, "keywords": [ "cargo.toml", "requirements", @@ -33083,8 +32647,8 @@ "heading": "WASI vs Basic HTTP Comparison", "level": 2, "anchor": "wasi-vs-basic-http-comparison", - "line_start": 127, - "line_end": 135, + "line_start": 150, + "line_end": 158, "keywords": [ "wasi", "basic", @@ -33121,8 +32685,8 @@ "heading": "Gotchas", "level": 2, "anchor": "gotchas", - "line_start": 136, - "line_end": 143, + "line_start": 159, + "line_end": 168, "keywords": [ "gotchas", "url", @@ -33151,19 +32715,17 @@ "values", "silently", "discarded", - "anyhow", - "declared", - "dependency", - "use", - "macro.", - "examples", - "http", - "wasi", - "same", - "async", - "main", - "wstd", - "http_server" + "x-fetch-header", + "parsed", + ".split_once", + "only", + "first", + "used", + "separator", + "containing", + "preserved", + "correctly.", + "build" ] }, { @@ -33171,8 +32733,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 144, - "line_end": 150, + "line_start": 169, + "line_end": 175, "keywords": [ "see", "fastedge-docs", @@ -33192,13 +32754,193 @@ "host", "services" ] + }, + { + "id": "http-examples-simple-fetch-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 176, + "line_end": 271, + "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": 272, + "line_end": 277, + "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": 278, + "line_end": 283, + "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": 284, + "line_end": 289, + "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": 290, + "line_end": 296, + "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": 297, + "line_end": 306, + "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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33220,9 +32962,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -33232,7 +32974,7 @@ "outbound-http", "env-vars" ], - "content_sha256": "3195fb4f9d583f55e2834ef95604d838f310817302a02705bec7c99b51685485", + "content_sha256": "c6819909d1d3bae6f1ad77d1911a32135ca0630d1e21028c5ff44435274e40ee", "sections": [ { "id": "http-examples-smart-switch-basic-rust#introduction", @@ -33249,9 +32991,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -33665,7 +33407,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -33689,9 +33431,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -33701,7 +33443,7 @@ "static-assets", "routing" ], - "content_sha256": "dcefd9ae20154d629c0a4b74b11bb152c2e0ddd0712407cb4ead1a03002ede52", + "content_sha256": "39d87feeba8b9d276b0f9d5d32b54faf0196b3dfd31a1aec8b88b3328960cbe2", "sections": [ { "id": "http-examples-static-assets-wasi-rust#introduction", @@ -33718,9 +33460,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34267,7 +34009,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34289,9 +34031,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34301,7 +34043,7 @@ "streaming", "async" ], - "content_sha256": "37832c99ab707686c986f78e014d061a2cd98753ae825dfc69b21c6217efd5fd", + "content_sha256": "fcd87ce4100ea3343d0f2b4543cce45409c480b856f3ded1838b4d752a43e3ea", "sections": [ { "id": "http-examples-streaming-wasi-rust#introduction", @@ -34318,9 +34060,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34649,13 +34391,151 @@ "fastedge", "sdk" ] + }, + { + "id": "http-examples-streaming-wasi-rust#source-material", + "heading": "Source Material", + "level": 2, + "anchor": "source-material", + "line_start": 147, + "line_end": 225, + "keywords": [ + "source", + "material", + "file", + "examples", + "http", + "wasi", + "streaming", + "src", + "lib.rs", + "rust", + "copyright", + "2025", + "g-core", + "innovations", + "sarl", + "response", + "example.", + "generates", + "body", + "fly", + "five", + "text", + "chunks", + "one", + "200ms", + "using", + "from_stream", + "backed", + "futures_lite", + "stream", + "runtime", + "polls", + "sent", + "flow", + "client", + "they", + "produced", + "instead", + "once", + "end." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#testing-the-streaming-behaviour", + "heading": "Testing the streaming behaviour", + "level": 2, + "anchor": "testing-the-streaming-behaviour", + "line_start": 226, + "line_end": 234, + "keywords": [ + "testing", + "streaming", + "behaviour", + "curl", + "https", + "your-app", + ".fastedge.cdn.gc.onl", + "disables", + "client-side", + "buffering", + "without", + "you", + "won", + "see", + "chunks", + "appear", + "one", + "time.", + "chunk", + "print", + "200ms", + "intervals." + ] + }, + { + "id": "http-examples-streaming-wasi-rust#other-streaming-patterns", + "heading": "Other streaming patterns", + "level": 2, + "anchor": "other-streaming-patterns", + "line_start": 235, + "line_end": 243, + "keywords": [ + "streaming", + "patterns", + "pass-through", + "return", + "upstream", + "response", + "body", + "directly.", + "see", + "outbound_fetch", + "no-buffer", + "variant.", + "transform", + "use", + "http_body_util", + "bodyext", + "map_frame", + "incoming", + "from_http_body", + "wrap", + "back.", + "useful", + "chunk-level", + "rewrites.", + "stream", + "bytes", + "from_stream", + "futures_lite", + "iter", + "chunks", + "iterable" + ] + }, + { + "id": "http-examples-streaming-wasi-rust#related", + "heading": "Related", + "level": 2, + "anchor": "related", + "line_start": 244, + "line_end": 248, + "keywords": [ + "related", + "mirror", + "fastedge-sdk-js", + "examples", + "streaming" + ] } ] }, { "id": "http-examples-variables-and-secrets-wasi-rust", "title": "Variables and Secrets — WASI (Rust)", - "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -34677,9 +34557,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -34689,7 +34569,7 @@ "env-vars", "secrets" ], - "content_sha256": "9d8807dc93609399f430a94d714047bb9d9458bc07f16c18e6a0814c194ffb94", + "content_sha256": "6437dc0501dd6889d30bfd370a0d370c4211dbad78063f0d8e1981a360070c6d", "sections": [ { "id": "http-examples-variables-and-secrets-wasi-rust#introduction", @@ -34706,9 +34586,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "---", "type", "example", @@ -35165,7 +35045,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md", "skill": "fastedge-docs", "category": "examples", @@ -35187,9 +35067,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "example", "watermark", "basic", @@ -35199,7 +35079,7 @@ "handler", "fastedge" ], - "content_sha256": "1fee467e8245bcae3adf9bcb0e06e0dda564403fccf9374939f5a72c90358ab3", + "content_sha256": "3b885f5aaf85aabb9c3cc0ee1f3c2133bd266f799afb7defc58d391825e6adaf", "sections": [ { "id": "http-examples-watermark-basic-rust#introduction", @@ -35216,9 +35096,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "http", "example", "watermark", @@ -35740,7 +35620,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md", "skill": "fastedge-docs", "category": "platform", @@ -35756,9 +35636,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "runtime", "constraints", @@ -35770,7 +35650,7 @@ "particularly", "affecting" ], - "content_sha256": "cac0e481a35b2a82d139b5878410a53c91f3f39c30c0190dd1a4ae6a1b229904", + "content_sha256": "f6077c39210df74e17ef03a34f4e5ba18b9d0e1a8857e120c393a98b85ee0b48", "sections": [ { "id": "js-runtime#introduction", @@ -35787,9 +35667,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "runtime", "constraints", @@ -37079,6 +36959,348 @@ } ] }, + { + "id": "platform-cdn-filter-runtime", + "title": "CDN Filter Runtime — Capabilities and Traps", + "description": "What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a dep", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/cdn-filter-runtime.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items" + ], + "content_sha256": "6279754b693e44807306ef152f9b733bb0a575ece2390a0bccb5755b2be9799e", + "sections": [ + { + "id": "platform-cdn-filter-runtime#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "cdn", + "filter", + "runtime", + "capabilities", + "traps", + "proxy-wasm", + "app", + "cannot", + "fastedge", + "host", + "behaviours", + "silently", + "differ", + "generic", + "http", + "apps.", + "read", + "designing", + "reviewing", + "items", + "here", + "invisible", + "local", + "tests", + "only", + "surface", + "deployed", + "app.", + "claim", + "carries", + "provenance", + "tag", + "meaning", + "---", + "live", + "measured", + "against", + "real", + "resource", + "source" + ] + }, + { + "id": "platform-cdn-filter-runtime#capability-boundary-—-filter-vs-http-app", + "heading": "Capability boundary — filter vs HTTP app", + "level": 2, + "anchor": "capability-boundary-—-filter-vs-http-app", + "line_start": 16, + "line_end": 41, + "keywords": [ + "capability", + "boundary", + "filter", + "http", + "app", + "core", + "wasm", + "module", + "linked", + "against", + "proxy-wasm", + "env", + "abi.", + "different", + "execution", + "world", + "subset", + "it.", + "designs", + "assume", + "symmetry", + "http-app", + "sdk", + "fail", + "deploy.", + "cdn", + "---", + "read", + "same", + "six", + "operations", + "open", + "get", + "scan", + "zrange_by_score", + "zscan", + "bf_exists", + "source", + "write", + "api" + ] + }, + { + "id": "platform-cdn-filter-runtime#reading-the-request-—-use-properties-not-headers", + "heading": "Reading the request — use properties, not headers", + "level": 2, + "anchor": "reading-the-request-—-use-properties-not-headers", + "line_start": 42, + "line_end": 69, + "keywords": [ + "reading", + "request", + "use", + "properties", + "headers", + "several", + "proxy-wasm", + "developer", + "reach", + "mangled", + "absent.", + "failures", + "silent", + "live", + "read", + "via", + "result", + "---", + "property", + "request.host", + "clean", + "client-requested", + "host", + "origin", + "rewrite", + "edge", + "tier", + "shield", + "nodes", + "carry", + "shield_", + "prefix", + "request.x_real_ip", + "true", + "client", + "ipv4", + "ipv6", + "request.path", + "includes", + "query" + ] + }, + { + "id": "platform-cdn-filter-runtime#security-—-gating-filters", + "heading": "Security — gating filters", + "level": 2, + "anchor": "security-—-gating-filters", + "line_start": 70, + "line_end": 105, + "keywords": [ + "security", + "gating", + "filters", + "cdn", + "normalise", + "path", + "filter", + "runs", + "live", + "traversal", + "separator", + "sequences", + "arrive", + "verbatim", + "bash", + "curl", + "--path-as-is", + "...", + "app", + "test", + "request.path", + "bypasses", + "auth", + "prefix", + "public", + "wave", + "admin", + "origin", + "back", + "authentication", + "bypass" + ] + }, + { + "id": "platform-cdn-filter-runtime#synthetic-responses", + "heading": "Synthetic responses", + "level": 2, + "anchor": "synthetic-responses", + "line_start": 106, + "line_end": 111, + "keywords": [ + "synthetic", + "responses", + "live", + "filter", + "serve", + "complete", + "response", + "via", + "send_http_response", + "status", + "arbitrary", + "headers", + "multiple", + "set-cookie", + "one", + "bodies", + "least", + "without", + "truncation.", + "single-app", + "designs", + "challenge", + "pages", + "interstitials", + "error", + "second", + "http", + "app.", + "---" + ] + }, + { + "id": "platform-cdn-filter-runtime#what-local-testing-cannot-prove", + "heading": "What local testing cannot prove", + "level": 2, + "anchor": "what-local-testing-cannot-prove", + "line_start": 112, + "line_end": 117, + "keywords": [ + "local", + "testing", + "cannot", + "prove", + "gcoredev", + "fastedge-test", + "runs", + "proxy-wasm", + "filters", + "locally", + "proves", + "flow", + "logic", + "routing", + "header", + "manipulation", + "gate", + "decisions.", + "host", + "behaviour", + "property", + "semantics", + "mangling", + "path", + "non-normalisation", + "instantiation", + "failures", + "cache", + "interaction.", + "trap", + "page", + "passes", + "suite", + "green.", + "confirm", + "host-dependent", + "deployed", + "app", + "attached", + "real" + ] + }, + { + "id": "platform-cdn-filter-runtime#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 118, + "line_end": 123, + "keywords": [ + "see", + "storage.md", + "cache", + "semantics", + "limits", + "measured", + "performance", + "cdn-integration.md", + "attaching", + "filters", + "cdn", + "resources", + "error-codes.md", + "530", + "diagnosis" + ] + } + ] + }, { "id": "platform-cdn-integration", "title": "CDN Integration — Attaching FastEdge Apps to CDN Resources", @@ -37117,7 +37339,7 @@ "effect", "app" ], - "content_sha256": "6cb942fb6413ec2d1faf71699c0e79ea10e04ccc089ed9e49aee100e7a6623fe", + "content_sha256": "e36fbf654e7eb8d595bd084a7fa6577ef61114287351099e7ccaa99cdf0f9eb6", "sections": [ { "id": "platform-cdn-integration#introduction", @@ -37272,7 +37494,7 @@ "level": 2, "anchor": "reading-and-updating", "line_start": 157, - "line_end": 185, + "line_end": 202, "keywords": [ "reading", "updating", @@ -37310,8 +37532,8 @@ "heading": "Operational Notes", "level": 2, "anchor": "operational-notes", - "line_start": 186, - "line_end": 195, + "line_start": 203, + "line_end": 212, "keywords": [ "operational", "notes", @@ -37358,8 +37580,8 @@ "heading": "See Also", "level": 2, "anchor": "see-also", - "line_start": 196, - "line_end": 203, + "line_start": 213, + "line_end": 220, "keywords": [ "see", "rust", @@ -37436,7 +37658,7 @@ "distinguish", "edge" ], - "content_sha256": "d47de15e41e07d9b842b6690558f41b840a4121f7ec91a85c4d86d1dc966a436", + "content_sha256": "e0561d0b112db5f5f509a1ce16da82dedfa2cea834283fcf1b6102a118136876", "sections": [ { "id": "platform-error-codes#introduction", @@ -37489,7 +37711,7 @@ "level": 2, "anchor": "530-—-app-initialization-failed", "line_start": 9, - "line_end": 29, + "line_end": 38, "keywords": [ "530", "app", @@ -37501,35 +37723,36 @@ "start", "processing", "request.", + "signal", + "cdn", + "fails", + "instantiate", + "returns", + "x-cdn-internal-status", + "3100", + "header", + "distinguishes", + "ran", + "errored", + "531", "common", "causes", - "missing", - "required", - "environment", - "variables", - "reads", - "corrupted", - "invalid", + "unresolved", + "imports", + "apps", "binary", - "compiled", - "wrong", - "target", - "wasm32-wasip1", - "large", - "platform", - "rejects", - "uploads", - "beyond", - "50mb", - "investigate", - "bundled", - "debugging", - "steps", - "verify", - "built", - "correctly", - "fastedge-build", - "src" + "host", + "function", + "proxy-wasm", + "provide", + "commonly", + "http-app", + "component-model", + "api", + "used", + "filter", + "e.g.", + "top-leve" ] }, { @@ -37537,8 +37760,8 @@ "heading": "531 — Runtime Error", "level": 2, "anchor": "531-—-runtime-error", - "line_start": 30, - "line_end": 61, + "line_start": 39, + "line_end": 71, "keywords": [ "531", "runtime", @@ -37554,6 +37777,19 @@ "processing.", "common", "causes", + "calling", + "getenv", + "getsecret", + "module", + "top", + "level", + "they", + "request-time", + "only", + "call", + "them", + "inside", + "handler", "uncaught", "javascript", "typeerror", @@ -37566,20 +37802,7 @@ "err", "failed", "fetch", - "call", - "without", - "handling", - "json", - "parse", - "malformed", - "body", - "accessing", - "undefined", - "properties", - "debugging", - "steps", - "test", - "locally" + "without" ] }, { @@ -37587,8 +37810,8 @@ "heading": "532 — Timeout Exceeded", "level": 2, "anchor": "532-—-timeout-exceeded", - "line_start": 62, - "line_end": 93, + "line_start": 72, + "line_end": 103, "keywords": [ "532", "timeout", @@ -37637,8 +37860,8 @@ "heading": "533 — Memory Limit Exceeded", "level": 2, "anchor": "533-—-memory-limit-exceeded", - "line_start": 94, - "line_end": 122, + "line_start": 104, + "line_end": 132, "keywords": [ "533", "memory", @@ -37687,8 +37910,8 @@ "heading": "General Debugging Strategy", "level": 2, "anchor": "general-debugging-strategy", - "line_start": 123, - "line_end": 131, + "line_start": 133, + "line_end": 141, "keywords": [ "general", "debugging", @@ -38283,7 +38506,7 @@ "instantiates", "app" ], - "content_sha256": "98c4a8456ad2cedcb63fe13b4f4556e3b0a8b09ab2a621f0111a59b942d65cb8", + "content_sha256": "350d18b69668668c510056232b304d8bb3d9d3354bfc1493438b059d93d586b6", "sections": [ { "id": "platform-overview#introduction", @@ -38485,7 +38708,7 @@ "level": 2, "anchor": "request-lifecycle", "line_start": 93, - "line_end": 101, + "line_end": 109, "keywords": [ "request", "lifecycle", @@ -38518,7 +38741,15 @@ "instance", "destroyed", "memory", - "freed" + "freed", + "scheduled", + "deferred", + "work", + "there", + "scheduler", + "durable", + "background", + "execution." ] }, { @@ -38526,8 +38757,8 @@ "heading": "Resource Limits", "level": 2, "anchor": "resource-limits", - "line_start": 102, - "line_end": 115, + "line_start": 110, + "line_end": 123, "keywords": [ "resource", "limits", @@ -38576,8 +38807,8 @@ "heading": "Plans", "level": 2, "anchor": "plans", - "line_start": 116, - "line_end": 120, + "line_start": 124, + "line_end": 128, "keywords": [ "plans", "basic", @@ -38599,8 +38830,8 @@ "heading": "App Statuses", "level": 2, "anchor": "app-statuses", - "line_start": 121, - "line_end": 128, + "line_start": 129, + "line_end": 136, "keywords": [ "app", "statuses", @@ -38620,8 +38851,8 @@ "heading": "Networking", "level": 2, "anchor": "networking", - "line_start": 129, - "line_end": 135, + "line_start": 137, + "line_end": 143, "keywords": [ "networking", "apps", @@ -38652,8 +38883,8 @@ "heading": "Logging & Monitoring", "level": 2, "anchor": "logging-&-monitoring", - "line_start": 136, - "line_end": 141, + "line_start": 144, + "line_end": 149, "keywords": [ "logging", "monitoring", @@ -38679,6 +38910,261 @@ } ] }, + { + "id": "platform-storage", + "title": "Storage Semantics — KV Store and Cache", + "description": "Behaviour of FastEdge's two storage primitives that the SDK references do not state: consistency, write shapes, TTL rules, missing operations, and measured performance. Applies to HTTP apps and CDN filters unless noted. For the per-language API, see the SDK reference for your lan", + "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/platform/storage.md", + "skill": "fastedge-docs", + "category": "platform", + "app_types": [], + "languages": [], + "tags": [ + "platform", + "fastedge-docs", + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance." + ], + "content_sha256": "65116f9b1f97e5e14b18ff2a6af86b58e926d192ad3101290e37fd23e8db5d3e", + "sections": [ + { + "id": "platform-storage#introduction", + "heading": "Introduction", + "level": 1, + "anchor": "introduction", + "line_start": 1, + "line_end": 15, + "keywords": [ + "storage", + "semantics", + "store", + "cache", + "behaviour", + "fastedge", + "two", + "primitives", + "sdk", + "references", + "state", + "consistency", + "write", + "shapes", + "ttl", + "rules", + "missing", + "operations", + "measured", + "performance.", + "applies", + "http", + "apps", + "cdn", + "filters", + "unless", + "noted.", + "per-language", + "api", + "see", + "reference", + "your", + "language.", + "---", + "scope", + "global", + "account-level", + "assigned", + "per-pop", + "shared" + ] + }, + { + "id": "platform-storage#kv-store", + "heading": "KV Store", + "level": 2, + "anchor": "kv-store", + "line_start": 16, + "line_end": 58, + "keywords": [ + "store", + "get", + "cached", + "read", + "per", + "node", + "live", + "production", + "key", + "populates", + "per-node", + "cache", + "entry", + "lives", + "whether", + "existed", + "until", + "expires", + "keeps", + "returning", + "old", + "result", + "value", + "absent", + "now", + "exists.", + "sorted-set", + "reads", + "zrangebyscore", + "reflect", + "writes", + "write", + "visible", + "---", + "none", + "create", + "heavy", + "pol" + ] + }, + { + "id": "platform-storage#cache", + "heading": "Cache", + "level": 2, + "anchor": "cache", + "line_start": 59, + "line_end": 82, + "keywords": [ + "cache", + "interface", + "atomicity", + "source", + "get", + "set", + "delete", + "exists", + "incr", + "expire", + "purge", + "purge_prefix", + "only", + "atomic", + "primitive", + "compare-and-set", + "setnx", + "scripting", + "multi-key", + "operations", + "scan", + "coordination", + "expressible", + "increment", + "compare", + "returned", + "value", + "ttl", + "rules", + "live", + "ttls", + "silently", + "clamped", + "deployment", + "ceiling", + "default", + "days", + "rust", + "sdk", + "references" + ] + }, + { + "id": "platform-storage#measured-performance", + "heading": "Measured performance", + "level": 2, + "anchor": "measured-performance", + "line_start": 83, + "line_end": 103, + "keywords": [ + "measured", + "performance", + "live", + "production", + "point-in-time", + "one", + "account", + "mostly", + "pop", + "evidence", + "guarantees.", + "metric", + "value", + "---", + "cache.incr", + "p50", + "p95", + "max", + "102", + "cache.expire", + "write", + "visible", + "prior", + "get", + "polling", + "107", + "min", + "rate", + "client", + "sustained", + "writes", + "sorted-set", + "ingest", + "450", + "members", + "knee", + "concurrent", + "writers", + "largest", + "single" + ] + }, + { + "id": "platform-storage#see-also", + "heading": "See Also", + "level": 2, + "anchor": "see-also", + "line_start": 104, + "line_end": 108, + "keywords": [ + "see", + "cdn-filter-runtime.md", + "cdn", + "filter", + "access", + "overview.md", + "account-level", + "stores", + "resource", + "limits" + ] + } + ] + }, { "id": "quickstart", "title": "FastEdge Quickstart", @@ -38933,7 +39419,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-as.md", "skill": "fastedge-docs", "category": "reference", @@ -38954,9 +39440,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "quickstart", "cdn", "apps", @@ -38967,7 +39453,7 @@ "compile", "webassembly" ], - "content_sha256": "fff52212e2f6238e7ed758a19a80e19a19974e5089f08928df7abac1819c1ada", + "content_sha256": "44a67860b9459a777487ccc86b1f45572d7be137091e04b009c1cda5acfd6676", "sections": [ { "id": "quickstart-as#introduction", @@ -38984,9 +39470,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "quickstart", "assemblyscript", "cdn", @@ -39367,7 +39853,7 @@ { "id": "quickstart-js", "title": "FastEdge JavaScript Quickstart", - "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c updated: 2026-08-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md", "skill": "fastedge-docs", "category": "reference", @@ -39388,9 +39874,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "quickstart", "prerequisites", @@ -39401,7 +39887,7 @@ "install", "bash" ], - "content_sha256": "51cef432b6305a0da282efc422dcd4b0a9f385cb40f047aee77022cb9258e771", + "content_sha256": "471dc8495fa13ecd040aad10ee3cba07813ec793a027cec8c4c507d6e75b64fa", "sections": [ { "id": "quickstart-js#introduction", @@ -39418,9 +39904,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "javascript", "quickstart" @@ -39708,7 +40194,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md", "skill": "fastedge-docs", "category": "reference", @@ -39727,9 +40213,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "sdk", "quickstart", @@ -39739,7 +40225,7 @@ "wasm32-wasip1", "target" ], - "content_sha256": "4e1d3d5a01a3e8aaf9f864c197899fbbc06cc30f86aba6a34d8207f67a629c25", + "content_sha256": "e345fdfaec0a005a9969b4bced75bacadcf838209b350e6fc06de4fc330536ca", "sections": [ { "id": "quickstart-rust#introduction", @@ -39756,9 +40242,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "fastedge", "rust", "sdk", @@ -40453,7 +40939,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-20 -->", + "description": "auto-updated: true sources: - id: proxy-wasm-sdk-as ref: master commit: ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-as.md", "skill": "fastedge-docs", "category": "sdk", @@ -40474,9 +40960,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "proxy-wasm", "reference", "gcoredev", @@ -40486,7 +40972,7 @@ "writing", "cdn" ], - "content_sha256": "76f2b7665b04f19b479f02f492977afaf6fffe062287c53fcfb4f387c5012e89", + "content_sha256": "a14b716b83a08310d5fea5c9097ebefc7fece6329111d0f21c21b97751bd7093", "sections": [ { "id": "sdk-reference-as#introduction", @@ -40503,9 +40989,9 @@ "ref", "master", "commit", - "8e3bb621bc013a0aed7e52122066b417ad62a207", + "ddb8d2e76e8f4fab30de6bce8dd0150231cd7d31", "updated", - "2026-08-20", + "2026-09-22", "assemblyscript", "proxy-wasm", "sdk", @@ -41000,7 +41486,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-js ref: main commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md", "skill": "fastedge-docs", "category": "sdk", @@ -41019,9 +41505,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "reference", "gcoredev", "source", @@ -41031,7 +41517,7 @@ "authoritative", "api" ], - "content_sha256": "b24418a0122025cc0863b8fa10c1295844a91aeb97a941495530e3cd5bcbcf1b", + "content_sha256": "cad65330e951f705a83aad21f8106ba15e3fd8cc2d51901140d09c58bc1577ee", "sections": [ { "id": "sdk-reference-js#introduction", @@ -41048,9 +41534,9 @@ "ref", "main", "commit", - "81145a9a43ec499240c687bd49376ab20c72b11c", + "9c8c7886f0d1ec5ac2296b4080805966a96ca817", "updated", - "2026-08-20", + "2026-09-22", "javascript", "sdk", "reference", @@ -41595,7 +42081,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-20 -->", + "description": "auto-updated: true sources: - id: fastedge-sdk-rust ref: main commit: 6eedcca9d5c0ddd4ff79ca475965393891da2d75 updated: 2026-09-22 -->", "path": "plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md", "skill": "fastedge-docs", "category": "sdk", @@ -41614,9 +42100,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "reference", "fastedge", "crate", @@ -41626,7 +42112,7 @@ "start", "cargo.toml" ], - "content_sha256": "e3598e9cb99f2f28beb8527ea3284480bee8c63132151da4c8874089bf9bf01b", + "content_sha256": "6ac7d972a17430f2d0d829470c262db798ec8ff3e81ca8dfbf8bed93067b7496", "sections": [ { "id": "sdk-reference-rust#introduction", @@ -41643,9 +42129,9 @@ "ref", "main", "commit", - "6347a7c2fda0d03e66f1214db5eec041c16801b7", + "6eedcca9d5c0ddd4ff79ca475965393891da2d75", "updated", - "2026-08-20", + "2026-09-22", "rust", "sdk", "reference",