From c1347b78d085eb94ad86ca1686cba75849b63913 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Wed, 29 Apr 2026 16:04:49 +0100 Subject: [PATCH 01/12] latest --- fastedge-plugin-source/manifest.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fastedge-plugin-source/manifest.json b/fastedge-plugin-source/manifest.json index b31495d..ceb6dd2 100644 --- a/fastedge-plugin-source/manifest.json +++ b/fastedge-plugin-source/manifest.json @@ -19,6 +19,12 @@ "description": "CDN app guide — proxy-wasm lifecycle, fastedge::proxywasm::* API surface, request/response manipulation" }, + "quickstart": { + "files": ["docs/quickstart.md"], + "required": true, + "description": "Installation, project setup, async vs sync handler, basic build for HTTP and CDN apps" + }, + "http-hello-world-blueprint": { "files": [ "examples/http/wasi/hello_world/src/lib.rs", @@ -347,6 +353,11 @@ "section": null }, + "quickstart": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md", + "section": null + }, + "http-hello-world-blueprint": { "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md", "section": null From 3f47bb848bc13b81e9896497cffa13e01d072abf Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Mon, 11 May 2026 16:26:38 +0100 Subject: [PATCH 02/12] fix: remove on_log from CDN examples + docs (finding #13) FastEdge does not currently dispatch proxy_on_log -- neither the edge runtime nor the local debugger (@gcoredev/fastedge-test) invokes it. Examples that implement on_log mislead developers into thinking end-of-request logging works when it doesn't. Removed the on_log trait method from two CDN examples (headers, log_time). The log_time example still demonstrates per-phase timing via on_http_request_headers and on_http_response_headers. Documented the platform gap in two architecture docs so future contributors don't reintroduce on_log: - context/architecture/HOST_SDK_CONTRACT.md (FFI table annotation) - context/architecture/REQUEST_LIFECYCLE.md (lifecycle-diagram note) SDK still leaves the trait method definition in place for forward- compat -- when/if the platform starts dispatching it, no source change is needed. Note: cdn/properties/src/lib.rs was intentionally left untouched per in-session direction; on_log can be removed there in a follow-up. --- context/architecture/HOST_SDK_CONTRACT.md | 4 +++- context/architecture/REQUEST_LIFECYCLE.md | 4 +++- examples/cdn/headers/src/lib.rs | 4 ---- examples/cdn/log_time/src/lib.rs | 4 ---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/context/architecture/HOST_SDK_CONTRACT.md b/context/architecture/HOST_SDK_CONTRACT.md index f6f9107..de1c061 100644 --- a/context/architecture/HOST_SDK_CONTRACT.md +++ b/context/architecture/HOST_SDK_CONTRACT.md @@ -120,11 +120,13 @@ The `#[fastedge::http]` macro generates the `Guest` trait implementation that br | `proxy_on_request_body(context_id, body_size, end_of_stream)` | Handle request body phase | | `proxy_on_response_headers(context_id, num_headers)` | Handle response headers phase | | `proxy_on_response_body(context_id, body_size, end_of_stream)` | Handle response body phase | -| `proxy_on_log(context_id)` | Final logging callback | +| `proxy_on_log(context_id)` | Final logging callback — **NOT dispatched on FastEdge today** (see note below) | | `proxy_on_http_call_response(ctx, call_id, h_size, b_size, t_size)` | HTTP callout response delivered | See `architecture/REQUEST_LIFECYCLE.md` for the order these are called. +**`proxy_on_log` is part of the proxy-wasm spec but the FastEdge platform does not currently invoke it.** Neither the edge runtime nor the local debugger (`@gcoredev/fastedge-test`) dispatches the symbol. The SDK still exports it for forward-compat. Examples and production code must not rely on `on_log` firing — use the four phase hooks (`on_request_*`, `on_response_*`) for end-of-request observability instead. + --- ## Execution Constraints diff --git a/context/architecture/REQUEST_LIFECYCLE.md b/context/architecture/REQUEST_LIFECYCLE.md index cabfa22..7c13afa 100644 --- a/context/architecture/REQUEST_LIFECYCLE.md +++ b/context/architecture/REQUEST_LIFECYCLE.md @@ -62,11 +62,13 @@ CDN-mode apps using the proxy-wasm interface have a multi-phase lifecycle. The h | 7. Response Body proxy_on_response_body(ctx, size, end_of_stream) | -8. Log proxy_on_log(ctx) +8. Log proxy_on_log(ctx) // NOT dispatched on FastEdge today (see note below) | 9. Instance discarded ``` +> **NOTE on `proxy_on_log`:** The hook is part of the proxy-wasm spec but the FastEdge platform does not currently invoke it (verified in both the edge runtime and `@gcoredev/fastedge-test`). The SDK still exports the symbol for forward-compat. Place end-of-request observability into the four phase hooks (`on_request_headers`/`on_request_body`/`on_response_headers`/`on_response_body`) — typically `on_response_body` with `end_of_stream = true` is the closest functional substitute. + ### Actions Each phase handler returns an action code that controls flow: diff --git a/examples/cdn/headers/src/lib.rs b/examples/cdn/headers/src/lib.rs index f28c47f..921969c 100644 --- a/examples/cdn/headers/src/lib.rs +++ b/examples/cdn/headers/src/lib.rs @@ -347,8 +347,4 @@ impl HttpContext for HttpHeaders { Action::Continue } - - fn on_log(&mut self) { - println!("#{} completed.", self.context_id); - } } diff --git a/examples/cdn/log_time/src/lib.rs b/examples/cdn/log_time/src/lib.rs index c36568a..57cb311 100644 --- a/examples/cdn/log_time/src/lib.rs +++ b/examples/cdn/log_time/src/lib.rs @@ -52,8 +52,4 @@ impl HttpContext for HttpHeaders { ); Action::Continue } - - fn on_log(&mut self) { - info!("#{} completed.", self.context_id); - } } From 2ee120fa87a1750076323a63a88af2b849f8a4a7 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Mon, 11 May 2026 16:32:29 +0100 Subject: [PATCH 03/12] fix: headers example response-phase validation (finding #14) After #11 fixed the response-phase host check, the Rust headers example still returned HTTP 552 because onResponseHeaders used a strict-equality check (`expected != diff`) that assumed `remove()` left the header present with empty value. On FastEdge, `set_http_response_header(name, None)` actually deletes the header -- the actual response has no `new-header-01` line at all -- so `diff` is a strict subset of `expected` and the != comparison fired on every request. Switch onResponseHeaders to the subset-check pattern already used by onRequestHeaders at line 150 (`!diff.difference(&expected).is_empty()`). Passes when actual response headers are a subset of expected. The two validation patterns are now symmetric within the file. Also fixed a small typo at line 158: the bytes-variant println printed the string-variant `diff` instead of `diff_bytes`. Only visible when the bytes-diff branch fires. Builds clean against wasm32-wasip1. Note: fastedge-test/test-applications/cdn-apps/rust/cdn-headers/src/lib.rs mirrors this file and has the same response-phase bug at line 325/332. Mechanical port pending confirmation. --- examples/cdn/headers/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/cdn/headers/src/lib.rs b/examples/cdn/headers/src/lib.rs index 921969c..ae3cc75 100644 --- a/examples/cdn/headers/src/lib.rs +++ b/examples/cdn/headers/src/lib.rs @@ -155,7 +155,7 @@ impl HttpContext for HttpHeaders { let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); if !diff_bytes.is_empty() { - println!("different headers bytes: {:?}", diff); + println!("different headers bytes: {:?}", diff_bytes); self.send_http_response(552, vec![], None); return Action::Pause; } @@ -307,16 +307,17 @@ impl HttpContext for HttpHeaders { .difference(&original_headers_bytes) .collect::>(); - if expected != diff { - let diff = diff.difference(&expected).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; } - if expected_bytes != diff_bytes { - let diff = diff_bytes.difference(&expected_bytes).collect::>(); - println!("different headers bytes: {:?}", diff); + 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; } From 155821f13738bd5e22e1ce89ef2f8bb7e7050233 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 12 May 2026 13:50:04 +0100 Subject: [PATCH 04/12] chore(docgen): preserve failed outputs + salvage preamble leaks Apply parity with the fastedge-plugin generate-docs-template.sh. Rejected `claude -p` outputs now persist under docs/.failures/ for prompt-debugging, and the salvage path strips one-line conversational preambles (a known Sonnet update-mode failure) rather than retrying. Tightens OUTPUT CONSTRAINT and existing-content prompt blocks to ban the "outputting verbatim" acknowledgement class explicitly. --- .gitignore | 4 +++ fastedge-plugin-source/generate-docs.sh | 47 ++++++++++++++++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 79085ea..fadca07 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ target/ # FastEdge debugger artifacts **/.fastedge-debug/ + +# Doc-generator failure artifacts — rejected/preamble-leaked claude -p +# outputs preserved for prompt-debugging. Prune manually. +docs/.failures/ diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh index 6381563..79cc944 100755 --- a/fastedge-plugin-source/generate-docs.sh +++ b/fastedge-plugin-source/generate-docs.sh @@ -210,6 +210,8 @@ $(cat "$full_path") # Existing Content for docs/$target Use this as the baseline. Preserve all accurate content and manual additions. Only change what is incorrect, incomplete, or missing per the source code. Keep sections not covered by the instructions above. Apply table formatting rules to all tables. +If the existing content is already accurate against the source code, output it verbatim with zero preamble or acknowledgement — your output starts at the # of the level-1 heading regardless of whether you made changes. + $existing_doc @@ -223,7 +225,7 @@ $existing_doc # sometimes produces conversational preamble or asks for permission. local prompt prompt="$(cat < "$tmpfile" <<<"$prompt" - # Validate: first non-empty line must start with # - local first_line - first_line=$(grep -m1 '.' "$tmpfile" || true) - if [[ "$first_line" == \#* ]]; then - mv "$tmpfile" "$DOCS_DIR/$target" + # Validate + salvage. The model intermittently leaks a conversational + # preamble like "I'll write the markdown now." before the real document, + # despite the OUTPUT CONSTRAINT in the prompt. Rather than discard those + # outputs and retry (wasting API quota on otherwise-good content), find + # the first level-1 heading and treat everything from there forward as + # the doc. The original is still saved to .failures/ so the prompt can + # be tuned later. + local stripped + stripped=$(awk '/^# / { found=1 } found' "$tmpfile") + + if [ -n "$stripped" ]; then + # Detect preamble: anything before the first '# ' line is preamble. + local first_heading_line + first_heading_line=$(grep -n -m1 '^# ' "$tmpfile" | cut -d: -f1) + if [ "${first_heading_line:-1}" -gt 1 ]; then + local preamble_copy="$failure_dir/${target}.preamble.attempt-${attempt}.$(date +%s).md" + cp "$tmpfile" "$preamble_copy" + echo " Stripped $((first_heading_line - 1)) preamble line(s) from $target (attempt $attempt) — original saved to $preamble_copy" + fi + printf '%s\n' "$stripped" > "$DOCS_DIR/$target" + rm -f "$tmpfile" echo " Done: docs/$target" return 0 fi - echo " Attempt $attempt/$max_attempts failed for $target (got conversational output), retrying..." + # No level-1 heading anywhere — genuine failure. Save and retry. + local failed_copy="$failure_dir/${target}.attempt-${attempt}.$(date +%s).md" + cp "$tmpfile" "$failed_copy" + echo " Attempt $attempt/$max_attempts failed for $target (no '# ' heading found in output) — saved to $failed_copy, retrying..." attempt=$((attempt + 1)) done From b55a3e6d8c9459f297c0acb9fcb274296648d724 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Fri, 22 May 2026 15:55:04 +0100 Subject: [PATCH 05/12] cdn examples all have fixtures and pass locally --- .../fixtures/existing-cookie-a.live.json | 10 + .../fixtures/existing-cookie-b.live.json | 10 + .../fixtures/missing-config.live.json | 6 + .../ab_testing/fixtures/new-visitor.live.json | 9 + .../cdn/api_key/fixtures/happy-path.live.json | 6 + .../api_key/fixtures/invalid-key.live.json | 7 + .../api_key/fixtures/missing-header.live.json | 9 + .../api_key/fixtures/missing-secret.live.json | 6 + examples/cdn/body/fixtures/client.live.json | 14 + examples/cdn/body/fixtures/skip.live.json | 14 + .../fixtures/error-status.live.json | 11 + .../fixtures/happy-path.live.json | 12 + .../fixtures/happy-path.test.json | 10 +- .../fixtures/javascript-asset.live.json | 11 + .../fixtures/javascript-asset.test.json | 10 +- .../cache_control/fixtures/json-api.live.json | 12 + .../fixtures/static-asset.live.json | 11 + .../fixtures/static-asset.test.json | 10 +- .../fixtures/unknown-content-type.live.json | 11 + .../fixtures/unknown-content-type.test.json | 10 +- .../cache_control/fixtures/xml-api.live.json | 12 + examples/cdn/convert_image/Cargo.lock | 1024 +++++++++++++++++ examples/cdn/convert_image/fixtures/.env | 2 + .../fixtures/conversion-scheduled.live.json | 10 + .../fixtures/conversion-scheduled.test.json | 31 + .../fixtures/extension-not-in-list.live.json | 9 + .../fixtures/extension-not-in-list.test.json | 31 + .../fixtures/formats-not-set.live.json | 9 + .../fixtures/formats-not-set.test.json | 27 + .../fixtures/no-extension.live.json | 9 + .../fixtures/no-extension.test.json | 27 + .../fixtures/ua-in-ignore-list.live.json | 9 + .../fixtures/ua-in-ignore-list.test.json | 31 + examples/cdn/convert_image/src/lib.rs | 22 +- .../cors/fixtures/disallowed-origin.live.json | 5 + .../cdn/cors/fixtures/happy-path.live.json | 10 + .../cdn/cors/fixtures/no-origin.live.json | 5 + .../cdn/cors/fixtures/preflight.live.json | 12 + .../cors/fixtures/wildcard-origins.live.json | 8 + .../cors/fixtures/wildcard-origins.test.json | 2 +- examples/cdn/cors/fixtures/wildcard/.env | 3 + .../custom/fixtures/invalid-status.live.json | 6 + .../cdn/custom/fixtures/status-200.live.json | 5 + .../cdn/custom/fixtures/status-403.live.json | 5 + .../fixtures/200-passthrough.live.json | 5 + .../fixtures/400-bad-request.live.json | 10 + .../fixtures/404-not-found.live.json | 10 + .../fixtures/418-fallback.live.json | 10 + .../500-internal-server-error.live.json | 10 + .../fixtures/502-bad-gateway.live.json | 10 + .../geo_redirect/fixtures/default.live.json | 8 + .../geo_redirect/fixtures/germany.live.json | 8 + .../fixtures/allowed-country.live.json | 5 + .../fixtures/blocked-country.live.json | 6 + .../fixtures/missing-config.live.json | 6 + examples/cdn/geoblock/src/lib.rs | 18 +- .../cdn/headers/fixtures/happy-path.live.json | 12 + examples/cdn/hello_world/README.md | 30 + .../hello_world/fixtures/happy-path.live.json | 14 + examples/cdn/hello_world/src/lib.rs | 5 + .../http_call/fixtures/happy-path.live.json | 9 + examples/cdn/jwt/Cargo.lock | 983 ++++++++++++++++ examples/cdn/jwt/fixtures/.env | 1 + .../cdn/jwt/fixtures/expired-token.live.json | 7 + .../cdn/jwt/fixtures/expired-token.test.json | 22 + .../cdn/jwt/fixtures/invalid-token.live.json | 7 + .../cdn/jwt/fixtures/invalid-token.test.json | 22 + .../fixtures/missing-auth-header.live.json | 7 + .../fixtures/missing-auth-header.test.json | 21 + .../cdn/jwt/fixtures/missing-secret.live.json | 7 + .../cdn/jwt/fixtures/missing-secret.test.json | 18 + .../cdn/jwt/fixtures/valid-token.live.json | 6 + .../cdn/jwt/fixtures/valid-token.test.json | 28 + examples/cdn/key_value/Cargo.lock | 542 +++++++++ .../cdn/key_value/fixtures/kv-get.live.json | 8 + .../cdn/key_value/fixtures/kv-get.test.json | 26 + .../fixtures/missing-store-param.live.json | 8 + .../fixtures/missing-store-param.test.json | 26 + .../fixtures/no-query-params.live.json | 8 + .../fixtures/no-query-params.test.json | 23 + .../fixtures/happy-path.live.json | 8 + .../fixtures/missing-config.live.json | 8 + .../log_time/fixtures/happy-path.live.json | 9 + .../fixtures/convert-to-html.live.json | 14 + .../fixtures/html-passthrough.live.json | 16 + examples/cdn/properties/README.md | 51 +- .../properties/fixtures/happy-path.live.json | 27 + examples/cdn/properties/src/lib.rs | 36 +- .../fixtures/happy-path.live.json | 10 + examples/http/basic/cache/Cargo.lock | 497 ++++++++ 90 files changed, 4060 insertions(+), 75 deletions(-) create mode 100644 examples/cdn/ab_testing/fixtures/existing-cookie-a.live.json create mode 100644 examples/cdn/ab_testing/fixtures/existing-cookie-b.live.json create mode 100644 examples/cdn/ab_testing/fixtures/missing-config.live.json create mode 100644 examples/cdn/ab_testing/fixtures/new-visitor.live.json create mode 100644 examples/cdn/api_key/fixtures/happy-path.live.json create mode 100644 examples/cdn/api_key/fixtures/invalid-key.live.json create mode 100644 examples/cdn/api_key/fixtures/missing-header.live.json create mode 100644 examples/cdn/api_key/fixtures/missing-secret.live.json create mode 100644 examples/cdn/body/fixtures/client.live.json create mode 100644 examples/cdn/body/fixtures/skip.live.json create mode 100644 examples/cdn/cache_control/fixtures/error-status.live.json create mode 100644 examples/cdn/cache_control/fixtures/happy-path.live.json create mode 100644 examples/cdn/cache_control/fixtures/javascript-asset.live.json create mode 100644 examples/cdn/cache_control/fixtures/json-api.live.json create mode 100644 examples/cdn/cache_control/fixtures/static-asset.live.json create mode 100644 examples/cdn/cache_control/fixtures/unknown-content-type.live.json create mode 100644 examples/cdn/cache_control/fixtures/xml-api.live.json create mode 100644 examples/cdn/convert_image/Cargo.lock create mode 100644 examples/cdn/convert_image/fixtures/.env create mode 100644 examples/cdn/convert_image/fixtures/conversion-scheduled.live.json create mode 100644 examples/cdn/convert_image/fixtures/conversion-scheduled.test.json create mode 100644 examples/cdn/convert_image/fixtures/extension-not-in-list.live.json create mode 100644 examples/cdn/convert_image/fixtures/extension-not-in-list.test.json create mode 100644 examples/cdn/convert_image/fixtures/formats-not-set.live.json create mode 100644 examples/cdn/convert_image/fixtures/formats-not-set.test.json create mode 100644 examples/cdn/convert_image/fixtures/no-extension.live.json create mode 100644 examples/cdn/convert_image/fixtures/no-extension.test.json create mode 100644 examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json create mode 100644 examples/cdn/convert_image/fixtures/ua-in-ignore-list.test.json create mode 100644 examples/cdn/cors/fixtures/disallowed-origin.live.json create mode 100644 examples/cdn/cors/fixtures/happy-path.live.json create mode 100644 examples/cdn/cors/fixtures/no-origin.live.json create mode 100644 examples/cdn/cors/fixtures/preflight.live.json create mode 100644 examples/cdn/cors/fixtures/wildcard-origins.live.json create mode 100644 examples/cdn/cors/fixtures/wildcard/.env create mode 100644 examples/cdn/custom/fixtures/invalid-status.live.json create mode 100644 examples/cdn/custom/fixtures/status-200.live.json create mode 100644 examples/cdn/custom/fixtures/status-403.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/200-passthrough.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/400-bad-request.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/404-not-found.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/418-fallback.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/500-internal-server-error.live.json create mode 100644 examples/cdn/custom_error_pages/fixtures/502-bad-gateway.live.json create mode 100644 examples/cdn/geo_redirect/fixtures/default.live.json create mode 100644 examples/cdn/geo_redirect/fixtures/germany.live.json create mode 100644 examples/cdn/geoblock/fixtures/allowed-country.live.json create mode 100644 examples/cdn/geoblock/fixtures/blocked-country.live.json create mode 100644 examples/cdn/geoblock/fixtures/missing-config.live.json create mode 100644 examples/cdn/headers/fixtures/happy-path.live.json create mode 100644 examples/cdn/hello_world/fixtures/happy-path.live.json create mode 100644 examples/cdn/http_call/fixtures/happy-path.live.json create mode 100644 examples/cdn/jwt/Cargo.lock create mode 100644 examples/cdn/jwt/fixtures/.env create mode 100644 examples/cdn/jwt/fixtures/expired-token.live.json create mode 100644 examples/cdn/jwt/fixtures/expired-token.test.json create mode 100644 examples/cdn/jwt/fixtures/invalid-token.live.json create mode 100644 examples/cdn/jwt/fixtures/invalid-token.test.json create mode 100644 examples/cdn/jwt/fixtures/missing-auth-header.live.json create mode 100644 examples/cdn/jwt/fixtures/missing-auth-header.test.json create mode 100644 examples/cdn/jwt/fixtures/missing-secret.live.json create mode 100644 examples/cdn/jwt/fixtures/missing-secret.test.json create mode 100644 examples/cdn/jwt/fixtures/valid-token.live.json create mode 100644 examples/cdn/jwt/fixtures/valid-token.test.json create mode 100644 examples/cdn/key_value/Cargo.lock create mode 100644 examples/cdn/key_value/fixtures/kv-get.live.json create mode 100644 examples/cdn/key_value/fixtures/kv-get.test.json create mode 100644 examples/cdn/key_value/fixtures/missing-store-param.live.json create mode 100644 examples/cdn/key_value/fixtures/missing-store-param.test.json create mode 100644 examples/cdn/key_value/fixtures/no-query-params.live.json create mode 100644 examples/cdn/key_value/fixtures/no-query-params.test.json create mode 100644 examples/cdn/large_env_variable/fixtures/happy-path.live.json create mode 100644 examples/cdn/large_env_variable/fixtures/missing-config.live.json create mode 100644 examples/cdn/log_time/fixtures/happy-path.live.json create mode 100644 examples/cdn/md2html/fixtures/convert-to-html.live.json create mode 100644 examples/cdn/md2html/fixtures/html-passthrough.live.json create mode 100644 examples/cdn/properties/fixtures/happy-path.live.json create mode 100644 examples/cdn/variables_and_secrets/fixtures/happy-path.live.json create mode 100644 examples/http/basic/cache/Cargo.lock diff --git a/examples/cdn/ab_testing/fixtures/existing-cookie-a.live.json b/examples/cdn/ab_testing/fixtures/existing-cookie-a.live.json new file mode 100644 index 0000000..02547df --- /dev/null +++ b/examples/cdn/ab_testing/fixtures/existing-cookie-a.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "logs": ["A/B test \"homepage-hero\": variant A, path /a/landing"], + "headers": { + "set-cookie": "fe_exp_homepage-hero=A; Path=/; Max-Age=86400; SameSite=Lax", + "x-variant": "A" + }, + "status": 200 + } +} diff --git a/examples/cdn/ab_testing/fixtures/existing-cookie-b.live.json b/examples/cdn/ab_testing/fixtures/existing-cookie-b.live.json new file mode 100644 index 0000000..8cc50a1 --- /dev/null +++ b/examples/cdn/ab_testing/fixtures/existing-cookie-b.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "logs": ["A/B test \"homepage-hero\": variant B, path /b/landing"], + "headers": { + "set-cookie": "fe_exp_homepage-hero=B; Path=/; Max-Age=86400; SameSite=Lax", + "x-variant": "B" + }, + "status": 200 + } +} diff --git a/examples/cdn/ab_testing/fixtures/missing-config.live.json b/examples/cdn/ab_testing/fixtures/missing-config.live.json new file mode 100644 index 0000000..71a4bc5 --- /dev/null +++ b/examples/cdn/ab_testing/fixtures/missing-config.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "App misconfigured - EXPERIMENT_NAME must be set" + } +} diff --git a/examples/cdn/ab_testing/fixtures/new-visitor.live.json b/examples/cdn/ab_testing/fixtures/new-visitor.live.json new file mode 100644 index 0000000..ab930a2 --- /dev/null +++ b/examples/cdn/ab_testing/fixtures/new-visitor.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "logs": ["A/B test \"homepage-hero\": variant "], + "headers": { + "set-cookie": { "contains": "fe_exp_homepage-hero=" } + }, + "status": 200 + } +} diff --git a/examples/cdn/api_key/fixtures/happy-path.live.json b/examples/cdn/api_key/fixtures/happy-path.live.json new file mode 100644 index 0000000..cf47e4a --- /dev/null +++ b/examples/cdn/api_key/fixtures/happy-path.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "logs": ["API key validated successfully"], + "status": 200 + } +} diff --git a/examples/cdn/api_key/fixtures/invalid-key.live.json b/examples/cdn/api_key/fixtures/invalid-key.live.json new file mode 100644 index 0000000..c5a370e --- /dev/null +++ b/examples/cdn/api_key/fixtures/invalid-key.live.json @@ -0,0 +1,7 @@ +{ + "expected": { + "logs": ["API key validation failed"], + "status": 403, + "body": "Invalid API key" + } +} diff --git a/examples/cdn/api_key/fixtures/missing-header.live.json b/examples/cdn/api_key/fixtures/missing-header.live.json new file mode 100644 index 0000000..ec0e97a --- /dev/null +++ b/examples/cdn/api_key/fixtures/missing-header.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 401, + "headers": { + "www-authenticate": "API-Key" + }, + "body": "Missing X-API-Key header" + } +} diff --git a/examples/cdn/api_key/fixtures/missing-secret.live.json b/examples/cdn/api_key/fixtures/missing-secret.live.json new file mode 100644 index 0000000..c501089 --- /dev/null +++ b/examples/cdn/api_key/fixtures/missing-secret.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 500, + "body": "App misconfigured" + } +} diff --git a/examples/cdn/body/fixtures/client.live.json b/examples/cdn/body/fixtures/client.live.json new file mode 100644 index 0000000..16790f3 --- /dev/null +++ b/examples/cdn/body/fixtures/client.live.json @@ -0,0 +1,14 @@ +{ + "expected": { + "status": 200, + "headers": { + "transfer-encoding": "Chunked" + }, + "logs": [ + "url=http://fastedge-builtin.debug", + "content_type=text/plain" + ], + "bodyContains": "Original message body (52 bytes) redacted.", + "noLogs": ["error"] + } +} diff --git a/examples/cdn/body/fixtures/skip.live.json b/examples/cdn/body/fixtures/skip.live.json new file mode 100644 index 0000000..2e3cd54 --- /dev/null +++ b/examples/cdn/body/fixtures/skip.live.json @@ -0,0 +1,14 @@ +{ + "expected": { + "status": 200, + "headers": { + "transfer-encoding": "Chunked" + }, + "logs": [ + "url=http://fastedge-builtin.debug", + "content_type=text/plain" + ], + "body": "Hello World, this is a test message", + "noLogs": ["redacted"] + } +} diff --git a/examples/cdn/cache_control/fixtures/error-status.live.json b/examples/cdn/cache_control/fixtures/error-status.live.json new file mode 100644 index 0000000..a5243cf --- /dev/null +++ b/examples/cdn/cache_control/fixtures/error-status.live.json @@ -0,0 +1,11 @@ +{ + "expected": { + "status": 500, + "headers": { + "cache-control": "no-store" + }, + "noLogs": [ + "Cache-Control:" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/happy-path.live.json b/examples/cdn/cache_control/fixtures/happy-path.live.json new file mode 100644 index 0000000..055fdf7 --- /dev/null +++ b/examples/cdn/cache_control/fixtures/happy-path.live.json @@ -0,0 +1,12 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "public, max-age=3600, must-revalidate", + "vary": "Accept-Encoding" + }, + "logs": [ + "Cache-Control: public, max-age=3600, must-revalidate (content-type: text/html; charset=utf-8)" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/happy-path.test.json b/examples/cdn/cache_control/fixtures/happy-path.test.json index a8c949c..e7cd02a 100644 --- a/examples/cdn/cache_control/fixtures/happy-path.test.json +++ b/examples/cdn/cache_control/fixtures/happy-path.test.json @@ -5,13 +5,9 @@ "method": "GET", "url": "http://fastedge-builtin.debug", "headers": { - "host": "example.com" - }, - "body": "" - }, - "response": { - "headers": { - "content-type": "text/html; charset=utf-8" + "host": "example.com", + "content-type": "text/html; charset=utf-8", + "x-debugger-content": "body-only" }, "body": "Hello" }, diff --git a/examples/cdn/cache_control/fixtures/javascript-asset.live.json b/examples/cdn/cache_control/fixtures/javascript-asset.live.json new file mode 100644 index 0000000..2803e55 --- /dev/null +++ b/examples/cdn/cache_control/fixtures/javascript-asset.live.json @@ -0,0 +1,11 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "public, max-age=31536000, immutable" + }, + "logs": [ + "Cache-Control: public, max-age=31536000, immutable (content-type: application/javascript)" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/javascript-asset.test.json b/examples/cdn/cache_control/fixtures/javascript-asset.test.json index 7d069df..b1e156e 100644 --- a/examples/cdn/cache_control/fixtures/javascript-asset.test.json +++ b/examples/cdn/cache_control/fixtures/javascript-asset.test.json @@ -5,13 +5,9 @@ "method": "GET", "url": "http://fastedge-builtin.debug/app.js", "headers": { - "host": "example.com" - }, - "body": "" - }, - "response": { - "headers": { - "content-type": "application/javascript" + "host": "example.com", + "content-type": "application/javascript", + "x-debugger-content": "body-only" }, "body": "console.log('hello');" }, diff --git a/examples/cdn/cache_control/fixtures/json-api.live.json b/examples/cdn/cache_control/fixtures/json-api.live.json new file mode 100644 index 0000000..7ea65e0 --- /dev/null +++ b/examples/cdn/cache_control/fixtures/json-api.live.json @@ -0,0 +1,12 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "no-cache, no-store, must-revalidate", + "vary": "Accept, Authorization" + }, + "logs": [ + "Cache-Control: no-cache, no-store, must-revalidate (content-type: application/json)" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/static-asset.live.json b/examples/cdn/cache_control/fixtures/static-asset.live.json new file mode 100644 index 0000000..d10a340 --- /dev/null +++ b/examples/cdn/cache_control/fixtures/static-asset.live.json @@ -0,0 +1,11 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "public, max-age=31536000, immutable" + }, + "logs": [ + "Cache-Control: public, max-age=31536000, immutable (content-type: image/png)" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/static-asset.test.json b/examples/cdn/cache_control/fixtures/static-asset.test.json index 80648ca..882b177 100644 --- a/examples/cdn/cache_control/fixtures/static-asset.test.json +++ b/examples/cdn/cache_control/fixtures/static-asset.test.json @@ -5,13 +5,9 @@ "method": "GET", "url": "http://fastedge-builtin.debug/logo.png", "headers": { - "host": "example.com" - }, - "body": "" - }, - "response": { - "headers": { - "content-type": "image/png" + "host": "example.com", + "content-type": "image/png", + "x-debugger-content": "body-only" }, "body": "" }, diff --git a/examples/cdn/cache_control/fixtures/unknown-content-type.live.json b/examples/cdn/cache_control/fixtures/unknown-content-type.live.json new file mode 100644 index 0000000..4eeb87e --- /dev/null +++ b/examples/cdn/cache_control/fixtures/unknown-content-type.live.json @@ -0,0 +1,11 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "public, max-age=600" + }, + "logs": [ + "Cache-Control: public, max-age=600 (content-type: text/csv)" + ] + } +} diff --git a/examples/cdn/cache_control/fixtures/unknown-content-type.test.json b/examples/cdn/cache_control/fixtures/unknown-content-type.test.json index efc021b..f75b8d5 100644 --- a/examples/cdn/cache_control/fixtures/unknown-content-type.test.json +++ b/examples/cdn/cache_control/fixtures/unknown-content-type.test.json @@ -5,13 +5,9 @@ "method": "GET", "url": "http://fastedge-builtin.debug/data.csv", "headers": { - "host": "example.com" - }, - "body": "" - }, - "response": { - "headers": { - "content-type": "text/csv" + "host": "example.com", + "content-type": "text/csv", + "x-debugger-content": "body-only" }, "body": "col1,col2\nval1,val2" }, diff --git a/examples/cdn/cache_control/fixtures/xml-api.live.json b/examples/cdn/cache_control/fixtures/xml-api.live.json new file mode 100644 index 0000000..7ea65e0 --- /dev/null +++ b/examples/cdn/cache_control/fixtures/xml-api.live.json @@ -0,0 +1,12 @@ +{ + "expected": { + "status": 200, + "headers": { + "cache-control": "no-cache, no-store, must-revalidate", + "vary": "Accept, Authorization" + }, + "logs": [ + "Cache-Control: no-cache, no-store, must-revalidate (content-type: application/json)" + ] + } +} diff --git a/examples/cdn/convert_image/Cargo.lock b/examples/cdn/convert_image/Cargo.lock new file mode 100644 index 0000000..ca6665b --- /dev/null +++ b/examples/cdn/convert_image/Cargo.lock @@ -0,0 +1,1024 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "convert_image" +version = "0.1.0" +dependencies = [ + "image", + "proxy-wasm", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685" +dependencies = [ + "hashbrown", + "log", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/examples/cdn/convert_image/fixtures/.env b/examples/cdn/convert_image/fixtures/.env new file mode 100644 index 0000000..3d9c151 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/.env @@ -0,0 +1,2 @@ +FASTEDGE_VAR_ENV_FORMATS_TO_TRANSFORM=jpg,png +FASTEDGE_VAR_ENV_IGNORED_UA_LIST=Googlebot,bingbot diff --git a/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json b/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json new file mode 100644 index 0000000..b9d8082 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "status": 200, + "logs": ["cannot load image"], + "headers": { + "Vary": "Image-Format" + }, + "noLogs": ["not transforming"] + } +} diff --git a/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json b/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json new file mode 100644 index 0000000..710126e --- /dev/null +++ b/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json @@ -0,0 +1,31 @@ +{ + "appType": "proxy-wasm", + "description": "All conditions met — extension jpg in list, valid User-Agent — Image-Format: image/avif set; empty body causes graceful decode failure (logs 'cannot load image')", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug/photo.jpg", + "headers": { + "host": "example.com", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "image/jpeg" + }, + "body": "" + }, + "properties": { + "request.extension": "jpg" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json b/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json new file mode 100644 index 0000000..c2aae34 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "logs": ["is not in the list of formats to transform"], + "headers": { + "Vary": "Image-Format" + } + } +} diff --git a/examples/cdn/convert_image/fixtures/extension-not-in-list.test.json b/examples/cdn/convert_image/fixtures/extension-not-in-list.test.json new file mode 100644 index 0000000..5302e65 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/extension-not-in-list.test.json @@ -0,0 +1,31 @@ +{ + "appType": "proxy-wasm", + "description": "Extension gif is not in FORMATS_TO_TRANSFORM list (jpg,png) — app does not schedule conversion", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug/animation.gif", + "headers": { + "host": "example.com", + "User-Agent": "Mozilla/5.0 TestBrowser" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "image/gif" + }, + "body": "" + }, + "properties": { + "request.extension": "gif" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/convert_image/fixtures/formats-not-set.live.json b/examples/cdn/convert_image/fixtures/formats-not-set.live.json new file mode 100644 index 0000000..721ce45 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/formats-not-set.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "logs": ["FORMATS_TO_TRANSFORM param is not set, not transforming"], + "headers": { + "Vary": "Image-Format" + } + } +} diff --git a/examples/cdn/convert_image/fixtures/formats-not-set.test.json b/examples/cdn/convert_image/fixtures/formats-not-set.test.json new file mode 100644 index 0000000..122e6aa --- /dev/null +++ b/examples/cdn/convert_image/fixtures/formats-not-set.test.json @@ -0,0 +1,27 @@ +{ + "appType": "proxy-wasm", + "description": "Extension jpg present but FORMATS_TO_TRANSFORM env var not set — app does not schedule conversion", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug/photo.jpg", + "headers": { + "host": "example.com", + "User-Agent": "Mozilla/5.0 TestBrowser" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "image/jpeg" + }, + "body": "" + }, + "properties": { + "request.extension": "jpg" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/convert_image/fixtures/no-extension.live.json b/examples/cdn/convert_image/fixtures/no-extension.live.json new file mode 100644 index 0000000..32375c6 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/no-extension.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "logs": ["No extension in request path, not transforming"], + "headers": { + "Vary": "Image-Format" + } + } +} diff --git a/examples/cdn/convert_image/fixtures/no-extension.test.json b/examples/cdn/convert_image/fixtures/no-extension.test.json new file mode 100644 index 0000000..846ab89 --- /dev/null +++ b/examples/cdn/convert_image/fixtures/no-extension.test.json @@ -0,0 +1,27 @@ +{ + "appType": "proxy-wasm", + "description": "Request path has no file extension — app sets Image-Format: original and does not schedule conversion", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug/api/data", + "headers": { + "host": "example.com", + "User-Agent": "Mozilla/5.0 TestBrowser" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "application/json" + }, + "body": "{\"ok\":true}" + }, + "properties": { + "request.extension": "" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json new file mode 100644 index 0000000..58cd17c --- /dev/null +++ b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "logs": ["User-Agent is in ignore list, not transforming"], + "headers": { + "Vary": "Image-Format" + } + } +} diff --git a/examples/cdn/convert_image/fixtures/ua-in-ignore-list.test.json b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.test.json new file mode 100644 index 0000000..d1ee1fb --- /dev/null +++ b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.test.json @@ -0,0 +1,31 @@ +{ + "appType": "proxy-wasm", + "description": "User-Agent matches IGNORED_UA_LIST substring — app does not schedule conversion", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug/photo.jpg", + "headers": { + "host": "example.com", + "User-Agent": "Googlebot/2.1 (+http://www.google.com/bot.html)" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "image/jpeg" + }, + "body": "" + }, + "properties": { + "request.extension": "jpg" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/convert_image/src/lib.rs b/examples/cdn/convert_image/src/lib.rs index e79bafa..0693b07 100644 --- a/examples/cdn/convert_image/src/lib.rs +++ b/examples/cdn/convert_image/src/lib.rs @@ -5,28 +5,28 @@ 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(HttpBodyRoot) }); + proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); }} -struct HttpBodyRoot; +struct ConvertImageRoot; -impl Context for HttpBodyRoot {} +impl Context for ConvertImageRoot {} -impl RootContext for HttpBodyRoot { +impl RootContext for ConvertImageRoot { fn get_type(&self) -> Option { Some(ContextType::HttpContext) } fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(HttpBody)) + Some(Box::new(ConvertImageContext)) } } -struct HttpBody; +struct ConvertImageContext; -impl Context for HttpBody {} +impl Context for ConvertImageContext {} -impl HttpContext for HttpBody { +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 @@ -62,6 +62,10 @@ impl HttpContext for HttpBody { 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"); @@ -167,7 +171,7 @@ impl HttpContext for HttpBody { } } -impl HttpBody { +impl ConvertImageContext { fn rsp_status(&mut self) -> Option { if let Some(status)= self.get_property(vec!["response.status"]) { if status.len() != 2 { diff --git a/examples/cdn/cors/fixtures/disallowed-origin.live.json b/examples/cdn/cors/fixtures/disallowed-origin.live.json new file mode 100644 index 0000000..60dc3f7 --- /dev/null +++ b/examples/cdn/cors/fixtures/disallowed-origin.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 200 + } +} diff --git a/examples/cdn/cors/fixtures/happy-path.live.json b/examples/cdn/cors/fixtures/happy-path.live.json new file mode 100644 index 0000000..8019980 --- /dev/null +++ b/examples/cdn/cors/fixtures/happy-path.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "status": 200, + "headers": { + "access-control-allow-origin": "https://app.example.com", + "vary": "Origin", + "access-control-expose-headers": "X-Request-Id, X-RateLimit-Remaining" + } + } +} diff --git a/examples/cdn/cors/fixtures/no-origin.live.json b/examples/cdn/cors/fixtures/no-origin.live.json new file mode 100644 index 0000000..60dc3f7 --- /dev/null +++ b/examples/cdn/cors/fixtures/no-origin.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 200 + } +} diff --git a/examples/cdn/cors/fixtures/preflight.live.json b/examples/cdn/cors/fixtures/preflight.live.json new file mode 100644 index 0000000..d287ea0 --- /dev/null +++ b/examples/cdn/cors/fixtures/preflight.live.json @@ -0,0 +1,12 @@ +{ + "expected": { + "status": 204, + "headers": { + "access-control-allow-origin": "https://app.example.com", + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "Content-Type, Authorization, X-Custom-Header", + "access-control-max-age": "86400", + "vary": "Origin" + } + } +} diff --git a/examples/cdn/cors/fixtures/wildcard-origins.live.json b/examples/cdn/cors/fixtures/wildcard-origins.live.json new file mode 100644 index 0000000..63f56eb --- /dev/null +++ b/examples/cdn/cors/fixtures/wildcard-origins.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "status": 200, + "headers": { + "access-control-allow-origin": "*" + } + } +} diff --git a/examples/cdn/cors/fixtures/wildcard-origins.test.json b/examples/cdn/cors/fixtures/wildcard-origins.test.json index e0044e8..6eb7caf 100644 --- a/examples/cdn/cors/fixtures/wildcard-origins.test.json +++ b/examples/cdn/cors/fixtures/wildcard-origins.test.json @@ -18,7 +18,7 @@ }, "dotenv": { "enabled": true, - "path": "fixtures/wildcard.env" + "path": "wildcard" }, "logLevel": 2, "wasm": { diff --git a/examples/cdn/cors/fixtures/wildcard/.env b/examples/cdn/cors/fixtures/wildcard/.env new file mode 100644 index 0000000..650ee7e --- /dev/null +++ b/examples/cdn/cors/fixtures/wildcard/.env @@ -0,0 +1,3 @@ +FASTEDGE_VAR_ENV_ALLOWED_ORIGINS=* +FASTEDGE_VAR_ENV_ALLOWED_METHODS=GET, POST, OPTIONS +FASTEDGE_VAR_ENV_MAX_AGE=3600 diff --git a/examples/cdn/custom/fixtures/invalid-status.live.json b/examples/cdn/custom/fixtures/invalid-status.live.json new file mode 100644 index 0000000..824b613 --- /dev/null +++ b/examples/cdn/custom/fixtures/invalid-status.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "status": 400, + "body": "Malformed request - invalid status code" + } +} diff --git a/examples/cdn/custom/fixtures/status-200.live.json b/examples/cdn/custom/fixtures/status-200.live.json new file mode 100644 index 0000000..60dc3f7 --- /dev/null +++ b/examples/cdn/custom/fixtures/status-200.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 200 + } +} diff --git a/examples/cdn/custom/fixtures/status-403.live.json b/examples/cdn/custom/fixtures/status-403.live.json new file mode 100644 index 0000000..4b41c29 --- /dev/null +++ b/examples/cdn/custom/fixtures/status-403.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 403 + } +} diff --git a/examples/cdn/custom_error_pages/fixtures/200-passthrough.live.json b/examples/cdn/custom_error_pages/fixtures/200-passthrough.live.json new file mode 100644 index 0000000..60dc3f7 --- /dev/null +++ b/examples/cdn/custom_error_pages/fixtures/200-passthrough.live.json @@ -0,0 +1,5 @@ +{ + "expected": { + "status": 200 + } +} diff --git a/examples/cdn/custom_error_pages/fixtures/400-bad-request.live.json b/examples/cdn/custom_error_pages/fixtures/400-bad-request.live.json new file mode 100644 index 0000000..0fa2f62 --- /dev/null +++ b/examples/cdn/custom_error_pages/fixtures/400-bad-request.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "status": 400, + "headers": { + "content-type": "text/html", + "transfer-encoding": "Chunked" + }, + "bodyContains": [" Box { Box::new(HttpHeadersRoot) }); + proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoblockRoot) }); }} -struct HttpHeadersRoot; +struct GeoblockRoot; -impl Context for HttpHeadersRoot {} +impl Context for GeoblockRoot {} -impl RootContext for HttpHeadersRoot { +impl RootContext for GeoblockRoot { fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(HttpHeaders {})) + Some(Box::new(GeoblockContext {})) } fn get_type(&self) -> Option { @@ -23,15 +23,15 @@ impl RootContext for HttpHeadersRoot { } } -struct HttpHeaders {} +struct GeoblockContext {} -impl Context for HttpHeaders {} +impl Context for GeoblockContext {} const BAD_GATEWAY: u32 = 502; const FORBIDDEN: u32 = 403; const INTERNAL_SERVER_ERROR: u32 = 500; -impl HttpContext for HttpHeaders { +impl HttpContext for GeoblockContext { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { let Ok(blacklist) = env::var("BLACKLIST") else { self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); @@ -69,7 +69,7 @@ impl HttpContext for HttpHeaders { .unwrap() .as_secs(); - if now > tw_start || now <= tw_end { + if now >= tw_start && now <= tw_end { self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); return Action::Pause; } diff --git a/examples/cdn/headers/fixtures/happy-path.live.json b/examples/cdn/headers/fixtures/happy-path.live.json new file mode 100644 index 0000000..e5ef7d6 --- /dev/null +++ b/examples/cdn/headers/fixtures/happy-path.live.json @@ -0,0 +1,12 @@ +{ + "expected": { + "status": 200, + "headers": { + "new-response-header": "value-02" + }, + "logs": [ + "-> host: example.com", + "-> new-response-header: value-02" + ] + } +} diff --git a/examples/cdn/hello_world/README.md b/examples/cdn/hello_world/README.md index 12d09d2..553d143 100644 --- a/examples/cdn/hello_world/README.md +++ b/examples/cdn/hello_world/README.md @@ -3,3 +3,33 @@ # Hello World (CDN) Minimal CDN app demonstrating the proxy-wasm lifecycle. Logs a message at each request/response phase and adds an `x-powered-by: FastEdge` response header. Use as a starting point for new CDN apps. + +## What it does + +Implements all four proxy-wasm HTTP lifecycle hooks. Each hook logs a message and returns `Continue` so every request passes through unchanged: + +| Hook | Phase | +|---|---| +| `on_http_request_headers` | Incoming request headers | +| `on_http_request_body` | Incoming request body | +| `on_http_response_headers` | Origin response headers — also adds `x-powered-by: FastEdge` | +| `on_http_response_body` | Origin response body | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/hello_world.wasm +``` + +## Expected output + +Logs (in order): +``` +Hello from on_http_request_headers +Hello from on_http_request_body +Hello from on_http_response_headers +Hello from on_http_response_body +``` + +Response header added: `x-powered-by: FastEdge` diff --git a/examples/cdn/hello_world/fixtures/happy-path.live.json b/examples/cdn/hello_world/fixtures/happy-path.live.json new file mode 100644 index 0000000..5db37d4 --- /dev/null +++ b/examples/cdn/hello_world/fixtures/happy-path.live.json @@ -0,0 +1,14 @@ +{ + "expected": { + "logs": [ + "Hello from on_http_request_headers", + "Hello from on_http_request_body", + "Hello from on_http_response_headers", + "Hello from on_http_response_body" + ], + "headers": { + "x-powered-by": "FastEdge" + }, + "status": 200 + } +} diff --git a/examples/cdn/hello_world/src/lib.rs b/examples/cdn/hello_world/src/lib.rs index e6d6460..11e2bae 100644 --- a/examples/cdn/hello_world/src/lib.rs +++ b/examples/cdn/hello_world/src/lib.rs @@ -1,3 +1,8 @@ + + + + + use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; diff --git a/examples/cdn/http_call/fixtures/happy-path.live.json b/examples/cdn/http_call/fixtures/happy-path.live.json new file mode 100644 index 0000000..38ad1e0 --- /dev/null +++ b/examples/cdn/http_call/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "logs": [ + "state: 0", + "Dispatched http call with token id:", + "Received http call response with token id:" + ] + } +} diff --git a/examples/cdn/jwt/Cargo.lock b/examples/cdn/jwt/Cargo.lock new file mode 100644 index 0000000..dc0777c --- /dev/null +++ b/examples/cdn/jwt/Cargo.lock @@ -0,0 +1,983 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "jwt" +version = "0.1.0" +dependencies = [ + "fastedge", + "headers", + "jsonwebtoken", + "proxy-wasm", + "serde", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685" +dependencies = [ + "hashbrown 0.16.1", + "log", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/cdn/jwt/fixtures/.env b/examples/cdn/jwt/fixtures/.env new file mode 100644 index 0000000..7f82b5f --- /dev/null +++ b/examples/cdn/jwt/fixtures/.env @@ -0,0 +1 @@ +FASTEDGE_VAR_SECRET_secret=test-secret-key diff --git a/examples/cdn/jwt/fixtures/expired-token.live.json b/examples/cdn/jwt/fixtures/expired-token.live.json new file mode 100644 index 0000000..6693e69 --- /dev/null +++ b/examples/cdn/jwt/fixtures/expired-token.live.json @@ -0,0 +1,7 @@ +{ + "expected": { + "logs": ["Token expired"], + "status": 403, + "body": "Token expired" + } +} diff --git a/examples/cdn/jwt/fixtures/expired-token.test.json b/examples/cdn/jwt/fixtures/expired-token.test.json new file mode 100644 index 0000000..3ca7f56 --- /dev/null +++ b/examples/cdn/jwt/fixtures/expired-token.test.json @@ -0,0 +1,22 @@ +{ + "appType": "proxy-wasm", + "description": "Expired JWT (exp in 2020) — returns 403", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjE1Nzc4MzY4MDB9.o2sMzlEidVwpvt0_96jdm6Zs68Kxi6MHDRo9EU4bGV0" + }, + "body": "" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/jwt/fixtures/invalid-token.live.json b/examples/cdn/jwt/fixtures/invalid-token.live.json new file mode 100644 index 0000000..5dda304 --- /dev/null +++ b/examples/cdn/jwt/fixtures/invalid-token.live.json @@ -0,0 +1,7 @@ +{ + "expected": { + "logs": ["Token is invalid"], + "status": 403, + "bodyContains": "Could not decode token" + } +} diff --git a/examples/cdn/jwt/fixtures/invalid-token.test.json b/examples/cdn/jwt/fixtures/invalid-token.test.json new file mode 100644 index 0000000..8aea886 --- /dev/null +++ b/examples/cdn/jwt/fixtures/invalid-token.test.json @@ -0,0 +1,22 @@ +{ + "appType": "proxy-wasm", + "description": "JWT with wrong signature — returns 403 token invalid", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjQxMDI0NDQ4MDB9.INVALIDSIGNATUREXXXXXXXXXXXXXXXXXXXXXXX" + }, + "body": "" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/jwt/fixtures/missing-auth-header.live.json b/examples/cdn/jwt/fixtures/missing-auth-header.live.json new file mode 100644 index 0000000..b6bd9c7 --- /dev/null +++ b/examples/cdn/jwt/fixtures/missing-auth-header.live.json @@ -0,0 +1,7 @@ +{ + "expected": { + "logs": ["Auth header is empty"], + "status": 401, + "body": "No Authorization header" + } +} diff --git a/examples/cdn/jwt/fixtures/missing-auth-header.test.json b/examples/cdn/jwt/fixtures/missing-auth-header.test.json new file mode 100644 index 0000000..e89eea5 --- /dev/null +++ b/examples/cdn/jwt/fixtures/missing-auth-header.test.json @@ -0,0 +1,21 @@ +{ + "appType": "proxy-wasm", + "description": "No Authorization header — returns 401", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/jwt/fixtures/missing-secret.live.json b/examples/cdn/jwt/fixtures/missing-secret.live.json new file mode 100644 index 0000000..8c75fbc --- /dev/null +++ b/examples/cdn/jwt/fixtures/missing-secret.live.json @@ -0,0 +1,7 @@ +{ + "expected": { + "logs": ["'secret' param not set"], + "status": 500, + "body": "App misconfigured" + } +} diff --git a/examples/cdn/jwt/fixtures/missing-secret.test.json b/examples/cdn/jwt/fixtures/missing-secret.test.json new file mode 100644 index 0000000..bb63e39 --- /dev/null +++ b/examples/cdn/jwt/fixtures/missing-secret.test.json @@ -0,0 +1,18 @@ +{ + "appType": "proxy-wasm", + "description": "Secret not configured — returns 500 app misconfigured", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjQxMDI0NDQ4MDB9.hS9YgaBPC0uo7LQi1_ai0FdI_vu20wU9wG1-PG6-CG0" + }, + "body": "" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/jwt/fixtures/valid-token.live.json b/examples/cdn/jwt/fixtures/valid-token.live.json new file mode 100644 index 0000000..9a71dcb --- /dev/null +++ b/examples/cdn/jwt/fixtures/valid-token.live.json @@ -0,0 +1,6 @@ +{ + "expected": { + "logs": ["Token ok"], + "status": 200 + } +} diff --git a/examples/cdn/jwt/fixtures/valid-token.test.json b/examples/cdn/jwt/fixtures/valid-token.test.json new file mode 100644 index 0000000..8cb7a0f --- /dev/null +++ b/examples/cdn/jwt/fixtures/valid-token.test.json @@ -0,0 +1,28 @@ +{ + "appType": "proxy-wasm", + "description": "Valid HS256 JWT with far-future exp — request passes through", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjQxMDI0NDQ4MDB9.hS9YgaBPC0uo7LQi1_ai0FdI_vu20wU9wG1-PG6-CG0" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "OK" + }, + "dotenv": { + "enabled": true, + "path": "." + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/key_value/Cargo.lock b/examples/cdn/key_value/Cargo.lock new file mode 100644 index 0000000..2cb4935 --- /dev/null +++ b/examples/cdn/key_value/Cargo.lock @@ -0,0 +1,542 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9042fccaffdd2171e8ff481e5c21d22f15b8a4454b37273c2f3f902fb3d375e" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acacf180f92cbdf6f2fe1a772b7d92301e430ba207fb15b2fc87ccab4e418ff9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "key_value" +version = "0.1.0" +dependencies = [ + "fastedge", + "proxy-wasm", + "querystring", + "serde_json", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685" +dependencies = [ + "hashbrown 0.16.1", + "log", +] + +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/cdn/key_value/fixtures/kv-get.live.json b/examples/cdn/key_value/fixtures/kv-get.live.json new file mode 100644 index 0000000..7f3f552 --- /dev/null +++ b/examples/cdn/key_value/fixtures/kv-get.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "mystore", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/key_value/fixtures/kv-get.test.json b/examples/cdn/key_value/fixtures/kv-get.test.json new file mode 100644 index 0000000..c485403 --- /dev/null +++ b/examples/cdn/key_value/fixtures/kv-get.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "KV get operation — calls Store::open host function (runner gap expected)", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "store=mystore&action=get&key=mykey" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/key_value/fixtures/missing-store-param.live.json b/examples/cdn/key_value/fixtures/missing-store-param.live.json new file mode 100644 index 0000000..3b27b7c --- /dev/null +++ b/examples/cdn/key_value/fixtures/missing-store-param.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "Missing required param 'store'", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/key_value/fixtures/missing-store-param.test.json b/examples/cdn/key_value/fixtures/missing-store-param.test.json new file mode 100644 index 0000000..c3708a9 --- /dev/null +++ b/examples/cdn/key_value/fixtures/missing-store-param.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "Query params present but 'store' param missing — returns error", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "action=get&key=mykey" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/key_value/fixtures/no-query-params.live.json b/examples/cdn/key_value/fixtures/no-query-params.live.json new file mode 100644 index 0000000..b47bbb8 --- /dev/null +++ b/examples/cdn/key_value/fixtures/no-query-params.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "App must be called with query parameters", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/key_value/fixtures/no-query-params.test.json b/examples/cdn/key_value/fixtures/no-query-params.test.json new file mode 100644 index 0000000..0741519 --- /dev/null +++ b/examples/cdn/key_value/fixtures/no-query-params.test.json @@ -0,0 +1,23 @@ +{ + "appType": "proxy-wasm", + "description": "No query parameters — returns error about missing query params", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/large_env_variable/fixtures/happy-path.live.json b/examples/cdn/large_env_variable/fixtures/happy-path.live.json new file mode 100644 index 0000000..9458385 --- /dev/null +++ b/examples/cdn/large_env_variable/fixtures/happy-path.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "logs": [ + "LARGE_CONFIG size: 109 bytes" + ], + "status": 200 + } +} diff --git a/examples/cdn/large_env_variable/fixtures/missing-config.live.json b/examples/cdn/large_env_variable/fixtures/missing-config.live.json new file mode 100644 index 0000000..f3fb662 --- /dev/null +++ b/examples/cdn/large_env_variable/fixtures/missing-config.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "logs": [ + "LARGE_CONFIG size: 0 bytes" + ], + "status": 200 + } +} diff --git a/examples/cdn/log_time/fixtures/happy-path.live.json b/examples/cdn/log_time/fixtures/happy-path.live.json new file mode 100644 index 0000000..f5166a5 --- /dev/null +++ b/examples/cdn/log_time/fixtures/happy-path.live.json @@ -0,0 +1,9 @@ +{ + "expected": { + "status": 200, + "logs": [ + "on_http_request_headers:", + "on_http_response_headers:" + ] + } +} diff --git a/examples/cdn/md2html/fixtures/convert-to-html.live.json b/examples/cdn/md2html/fixtures/convert-to-html.live.json new file mode 100644 index 0000000..3f651df --- /dev/null +++ b/examples/cdn/md2html/fixtures/convert-to-html.live.json @@ -0,0 +1,14 @@ +{ + "expected": { + "logs": [ + "On response headers", + "Response is markdown, converting to HTML", + "Converted" + ], + "headers": { + "content-type": "text/html" + }, + "status": 200, + "bodyContains": "" + } +} diff --git a/examples/cdn/md2html/fixtures/html-passthrough.live.json b/examples/cdn/md2html/fixtures/html-passthrough.live.json new file mode 100644 index 0000000..5e446ae --- /dev/null +++ b/examples/cdn/md2html/fixtures/html-passthrough.live.json @@ -0,0 +1,16 @@ +{ + "expected": { + "logs": [ + "On response headers" + ], + "noLogs": [ + "converting to HTML", + "Converted" + ], + "headers": { + "content-type": "text/html" + }, + "status": 200, + "body": "Already HTML" + } +} diff --git a/examples/cdn/properties/README.md b/examples/cdn/properties/README.md index 2f2bdbb..e86747e 100644 --- a/examples/cdn/properties/README.md +++ b/examples/cdn/properties/README.md @@ -2,4 +2,53 @@ # Properties (CDN) -Extracts and manipulates request properties — URL, path, host, and geo data — using the proxy-wasm ABI. +Extracts and manipulates request properties — URL, path, host, and geo data — using the proxy-wasm ABI. Forwards each extracted value as a response header so downstream clients or logging pipelines can inspect them. + +## What it does + +On every request, reads the following properties and adds each as a response header: + +| Property key | Response header | +|---|---| +| `request.url` | `request-uri` | +| `request.host` | `request-host` | +| `request.path` | `request-path` | +| `request.scheme` | `request-scheme` | +| `request.extension` | `request-extension` | +| `request.query` | `request-query` | +| `request.x_real_ip` | `request-x-real-ip` | +| `request.country` | `request-country` | +| `request.city` | `request-city` | +| `request.asn` | `request-asn` | +| `request.geo.long` | `request-long` | +| `request.geo.lat` | `request-lat` | +| `request.country.name` | `request-country-names` | +| `request.region` | `request-country-region` | +| `request.continent` | `request-continent` | + +If any property is missing the handler sends a 55x error response and stops — each property has a unique status code to identify exactly which lookup failed. + +## Query-param overrides + +After extracting all properties, the handler checks for override query parameters and rewrites the corresponding upstream request property if present: + +| Query param | Overwrites | +|---|---| +| `?url=` | `request.url` | +| `?host=` | `request.host` | +| `?path=` | `request.path` | + +## nginx log field + +Sets `nginx.log_field1` to `"from_wasm nginx.log_field1"` on every request, demonstrating how to write custom values into the CDN access log. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/properties.wasm +``` + +## Notes + +`fixtures/force-server-properties.json` is a visual-debugger configuration file (no `.test.json` extension), not a test fixture. It is used to seed server-side properties when running the app in the FastEdge visual debugger. diff --git a/examples/cdn/properties/fixtures/happy-path.live.json b/examples/cdn/properties/fixtures/happy-path.live.json new file mode 100644 index 0000000..1124291 --- /dev/null +++ b/examples/cdn/properties/fixtures/happy-path.live.json @@ -0,0 +1,27 @@ +{ + "expected": { + "logs": [ + "uri = http://fastedge-builtin.debug?test=value", + "host = fastedge-builtin.debug", + "path = /?test=value", + "scheme = http", + "query=test=value", + "client_ip = 203.0.113.1", + "country = LU", + "city = Luxembourg", + "asn = 12345" + ], + "headers": { + "request-uri": "http://fastedge-builtin.debug?test=value", + "request-host": "fastedge-builtin.debug", + "request-path": "/?test=value", + "request-scheme": "http", + "request-query": "test=value", + "request-x-real-ip": "203.0.113.1", + "request-country": "LU", + "request-city": "Luxembourg", + "request-asn": "12345" + }, + "status": 200 + } +} diff --git a/examples/cdn/properties/src/lib.rs b/examples/cdn/properties/src/lib.rs index 405e199..5cf32ee 100644 --- a/examples/cdn/properties/src/lib.rs +++ b/examples/cdn/properties/src/lib.rs @@ -1,20 +1,18 @@ use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; -use std::borrow::Cow; - proxy_wasm::main! {{ proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); + proxy_wasm::set_root_context(|_| -> Box { Box::new(PropertiesRoot) }); }} -struct HttpHeadersRoot; +struct PropertiesRoot; -impl Context for HttpHeadersRoot {} +impl Context for PropertiesRoot {} -impl RootContext for HttpHeadersRoot { +impl RootContext for PropertiesRoot { fn create_http_context(&self, context_id: u32) -> Option> { - Some(Box::new(HttpHeaders { context_id })) + Some(Box::new(PropertiesContext { context_id })) } fn get_type(&self) -> Option { @@ -22,11 +20,11 @@ impl RootContext for HttpHeadersRoot { } } -struct HttpHeaders { +struct PropertiesContext { context_id: u32, } -impl Context for HttpHeaders {} +impl Context for PropertiesContext {} pub const REQUEST_URI: &str = "request.url"; pub const REQUEST_HOST: &str = "request.host"; @@ -44,7 +42,7 @@ pub const REQUEST_REGION: &str = "request.region"; pub const REQUEST_CONTINENT: &str = "request.continent"; pub const REQUEST_COUNTRY_NAME: &str = "request.country.name"; -impl HttpContext for HttpHeaders { +impl HttpContext for PropertiesContext { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { let Some(uri) = self.get_property(vec![REQUEST_URI]) else { self.send_http_response(551, vec![], None); @@ -110,7 +108,7 @@ impl HttpContext for HttpHeaders { self.add_http_response_header_bytes("request-city", &city); let Some(value) = self.get_property(vec![REQUEST_ASN]) else { - self.send_http_response(561, vec![], None); + self.send_http_response(560, vec![], None); return Action::Pause; }; println!(" asn = {} ", String::from_utf8_lossy(&value)); @@ -201,19 +199,3 @@ impl HttpContext for HttpHeaders { } } -pub fn deserialize_country_names(bytes: &[u8]) -> Vec> { - let mut path = Vec::new(); - if bytes.is_empty() { - return path; - } - let mut p = 0; - while p < bytes.len() { - let s = p; - while p < bytes.len() && bytes[p] != 0 { - p += 1; - } - path.push(String::from_utf8_lossy(&bytes[s..p])); - p += 1; - } - path -} diff --git a/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json b/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json new file mode 100644 index 0000000..d9979d2 --- /dev/null +++ b/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json @@ -0,0 +1,10 @@ +{ + "expected": { + "status": 200, + "logs": [ + "USERNAME: cdn-test-user", + "PASSWORD: cdn-test-secret" + ], + "bodyContains": "x-env-username" + } +} diff --git a/examples/http/basic/cache/Cargo.lock b/examples/http/basic/cache/Cargo.lock new file mode 100644 index 0000000..73701c0 --- /dev/null +++ b/examples/http/basic/cache/Cargo.lock @@ -0,0 +1,497 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cache_basic" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.3.5" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.3.5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 30697131c2024e6c767d94916b7e271125147534 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Fri, 22 May 2026 17:10:50 +0100 Subject: [PATCH 06/12] fix logging in cdn examples --- .gitignore | 6 +++ examples/cdn/ab_testing/src/lib.rs | 12 ++---- examples/cdn/api_key/src/lib.rs | 4 +- examples/cdn/cache_control/src/lib.rs | 12 ++---- examples/cdn/geo_redirect/src/lib.rs | 2 +- examples/cdn/key_value/src/lib.rs | 2 +- examples/cdn/large_env_variable/src/lib.rs | 6 +-- .../properties/fixtures/happy-path.live.json | 39 +++++++++---------- examples/cdn/properties/src/lib.rs | 5 +-- .../fixtures/happy-path.live.json | 4 +- examples/cdn/variables_and_secrets/src/lib.rs | 4 +- 11 files changed, 42 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index fadca07..e98231d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,12 @@ target/ # FastEdge debugger artifacts **/.fastedge-debug/ +# example project lock files +examples/**/pnpm-lock.yaml +examples/**/package-lock.json +examples/**/livetest.config.json + + # Doc-generator failure artifacts — rejected/preamble-leaked claude -p # outputs preserved for prompt-debugging. Prune manually. docs/.failures/ diff --git a/examples/cdn/ab_testing/src/lib.rs b/examples/cdn/ab_testing/src/lib.rs index 4d1c09d..27adb8a 100644 --- a/examples/cdn/ab_testing/src/lib.rs +++ b/examples/cdn/ab_testing/src/lib.rs @@ -108,14 +108,10 @@ impl HttpContext for AbTestingContext { self.add_http_request_header("X-Experiment", &experiment_name); self.add_http_request_header("X-Variant", &assigned); - proxy_wasm::hostcalls::log( - LogLevel::Info, - &format!( - "A/B test \"{}\": variant {}, path {}", - experiment_name, assigned, new_path - ), - ) - .ok(); + println!( + "A/B test \"{}\": variant {}, path {}", + experiment_name, assigned, new_path + ); Action::Continue } diff --git a/examples/cdn/api_key/src/lib.rs b/examples/cdn/api_key/src/lib.rs index 509c202..6484e61 100644 --- a/examples/cdn/api_key/src/lib.rs +++ b/examples/cdn/api_key/src/lib.rs @@ -68,7 +68,7 @@ impl HttpContext for ApiKeyContext { }; if provided_key != expected_key { - proxy_wasm::hostcalls::log(LogLevel::Info, "API key validation failed").ok(); + println!("API key validation failed"); self.send_http_response(403, vec![], Some(b"Invalid API key")); return Action::Pause; } @@ -76,7 +76,7 @@ impl HttpContext for ApiKeyContext { // Strip the API key header before forwarding to upstream self.set_http_request_header("X-API-Key", None); - proxy_wasm::hostcalls::log(LogLevel::Info, "API key validated successfully").ok(); + println!("API key validated successfully"); Action::Continue } } diff --git a/examples/cdn/cache_control/src/lib.rs b/examples/cdn/cache_control/src/lib.rs index 90c720a..a10b708 100644 --- a/examples/cdn/cache_control/src/lib.rs +++ b/examples/cdn/cache_control/src/lib.rs @@ -88,14 +88,10 @@ impl HttpContext for CacheControlContext { self.set_http_response_header("Cache-Control", Some(&cache_control)); - proxy_wasm::hostcalls::log( - LogLevel::Info, - &format!( - "Cache-Control: {} (content-type: {})", - cache_control, content_type - ), - ) - .ok(); + println!( + "Cache-Control: {} (content-type: {})", + cache_control, content_type + ); Action::Continue } diff --git a/examples/cdn/geo_redirect/src/lib.rs b/examples/cdn/geo_redirect/src/lib.rs index 0cf6c10..cdd3be0 100644 --- a/examples/cdn/geo_redirect/src/lib.rs +++ b/examples/cdn/geo_redirect/src/lib.rs @@ -75,7 +75,7 @@ impl HttpContext for GeoRedirectContext { let request_url = format!("{}{}", origin, path); - proxy_wasm::hostcalls::log(LogLevel::Info, &format!("Redirecting to: {}", request_url)).ok(); + println!("Redirecting to: {}", request_url); self.set_property(vec!["request.url"], Some(request_url.as_bytes())); diff --git a/examples/cdn/key_value/src/lib.rs b/examples/cdn/key_value/src/lib.rs index 5d2203a..520b8d6 100644 --- a/examples/cdn/key_value/src/lib.rs +++ b/examples/cdn/key_value/src/lib.rs @@ -223,7 +223,7 @@ impl KvStoreContext { } fn send_error(&self, msg: &str, body_size: usize) { - proxy_wasm::hostcalls::log(LogLevel::Error, msg).ok(); + println!("{}", msg); self.set_property( vec!["response", "status"], Some(b"500"), diff --git a/examples/cdn/large_env_variable/src/lib.rs b/examples/cdn/large_env_variable/src/lib.rs index e91d459..03bea9f 100644 --- a/examples/cdn/large_env_variable/src/lib.rs +++ b/examples/cdn/large_env_variable/src/lib.rs @@ -49,11 +49,7 @@ impl HttpContext for LargeEnvContext { let config = dictionary::get("LARGE_CONFIG").unwrap_or_default(); let size = config.len(); - proxy_wasm::hostcalls::log( - LogLevel::Info, - &format!("LARGE_CONFIG size: {} bytes", size), - ) - .ok(); + println!("LARGE_CONFIG size: {} bytes", size); self.add_http_request_header("x-config-size", &size.to_string()); diff --git a/examples/cdn/properties/fixtures/happy-path.live.json b/examples/cdn/properties/fixtures/happy-path.live.json index 1124291..4116184 100644 --- a/examples/cdn/properties/fixtures/happy-path.live.json +++ b/examples/cdn/properties/fixtures/happy-path.live.json @@ -1,27 +1,26 @@ { "expected": { "logs": [ - "uri = http://fastedge-builtin.debug?test=value", - "host = fastedge-builtin.debug", - "path = /?test=value", - "scheme = http", - "query=test=value", - "client_ip = 203.0.113.1", - "country = LU", - "city = Luxembourg", - "asn = 12345" + "uri = ", + "host = ", + "path = ", + "scheme = https", + "extension = ", + "query = ", + "client_ip = ", + "country = ", + "city = ", + "asn = ", + "long = ", + "lat = ", + "country names = ", + "region = ", + "continent = ", + "query=test=value" ], "headers": { - "request-uri": "http://fastedge-builtin.debug?test=value", - "request-host": "fastedge-builtin.debug", - "request-path": "/?test=value", - "request-scheme": "http", - "request-query": "test=value", - "request-x-real-ip": "203.0.113.1", - "request-country": "LU", - "request-city": "Luxembourg", - "request-asn": "12345" - }, - "status": 200 + "request-scheme": "https", + "request-query": "test=value" + } } } diff --git a/examples/cdn/properties/src/lib.rs b/examples/cdn/properties/src/lib.rs index 5cf32ee..6953c38 100644 --- a/examples/cdn/properties/src/lib.rs +++ b/examples/cdn/properties/src/lib.rs @@ -72,10 +72,7 @@ impl HttpContext for PropertiesContext { println!(" scheme = {} ", String::from_utf8_lossy(&scheme)); self.add_http_response_header_bytes("request-scheme", &scheme); - let Some(extension) = self.get_property(vec![REQUEST_EXTENSION]) else { - self.send_http_response(555, vec![], None); - return Action::Pause; - }; + let extension = self.get_property(vec![REQUEST_EXTENSION]).unwrap_or_default(); println!(" extension = {} ", String::from_utf8_lossy(&extension)); self.add_http_response_header_bytes("request-extension", &extension); diff --git a/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json b/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json index d9979d2..2cf4613 100644 --- a/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json +++ b/examples/cdn/variables_and_secrets/fixtures/happy-path.live.json @@ -1,10 +1,8 @@ { "expected": { - "status": 200, "logs": [ "USERNAME: cdn-test-user", "PASSWORD: cdn-test-secret" - ], - "bodyContains": "x-env-username" + ] } } diff --git a/examples/cdn/variables_and_secrets/src/lib.rs b/examples/cdn/variables_and_secrets/src/lib.rs index 9886e3f..39a518f 100644 --- a/examples/cdn/variables_and_secrets/src/lib.rs +++ b/examples/cdn/variables_and_secrets/src/lib.rs @@ -49,8 +49,8 @@ impl HttpContext for VariablesContext { .and_then(|v| String::from_utf8(v).ok()) .unwrap_or_default(); - proxy_wasm::hostcalls::log(LogLevel::Info, &format!("USERNAME: {}", username)).ok(); - proxy_wasm::hostcalls::log(LogLevel::Info, &format!("PASSWORD: {}", password)).ok(); + println!("USERNAME: {}", username); + println!("PASSWORD: {}", password); self.add_http_request_header("x-env-username", &username); self.add_http_request_header("x-env-password", &password); From 96bdcf02f013bf427a38be9d2bf82b8661a34c97 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Fri, 22 May 2026 19:09:16 +0100 Subject: [PATCH 07/12] tidied cdn examples --- .gitignore | 3 + .../fixtures/conversion-scheduled.live.json | 7 +- .../fixtures/conversion-scheduled.test.json | 8 +- .../fixtures/extension-not-in-list.live.json | 6 +- .../fixtures/formats-not-set.live.json | 8 +- .../fixtures/no-extension.live.json | 6 +- .../fixtures/ua-in-ignore-list.live.json | 6 +- examples/cdn/convert_image/src/lib.rs | 78 ++++++++++++------- examples/cdn/http_call/src/lib.rs | 23 ++---- examples/cdn/jwt/fixtures/.env | 3 +- .../cdn/jwt/fixtures/expired-token.test.json | 2 +- .../fixtures/missing-auth-header.live.json | 2 +- .../cdn/jwt/fixtures/valid-token.live.json | 3 +- .../cdn/jwt/fixtures/valid-token.test.json | 2 +- 14 files changed, 75 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index e98231d..614a059 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ target/ # FastEdge debugger artifacts **/.fastedge-debug/ +# build artifacts +**/*.wasm + # example project lock files examples/**/pnpm-lock.yaml examples/**/package-lock.json diff --git a/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json b/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json index b9d8082..fb88971 100644 --- a/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json +++ b/examples/cdn/convert_image/fixtures/conversion-scheduled.live.json @@ -1,10 +1,9 @@ { "expected": { - "status": 200, - "logs": ["cannot load image"], + "logs": [" bytes -> ", "image/avif"], "headers": { - "Vary": "Image-Format" + "Content-Type": "image/avif" }, - "noLogs": ["not transforming"] + "noLogs": ["not transforming", "cannot load image"] } } diff --git a/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json b/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json index 710126e..b980e14 100644 --- a/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json +++ b/examples/cdn/convert_image/fixtures/conversion-scheduled.test.json @@ -1,9 +1,9 @@ { "appType": "proxy-wasm", - "description": "All conditions met — extension jpg in list, valid User-Agent — Image-Format: image/avif set; empty body causes graceful decode failure (logs 'cannot load image')", + "description": "All conditions met — extension png in list, valid User-Agent — Image-Format: image/avif set; empty body causes graceful decode failure (logs 'cannot load image')", "request": { "method": "GET", - "url": "http://fastedge-builtin.debug/photo.jpg", + "url": "http://fastedge-builtin.debug/418.png", "headers": { "host": "example.com", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" @@ -12,12 +12,12 @@ }, "response": { "headers": { - "content-type": "image/jpeg" + "content-type": "image/png" }, "body": "" }, "properties": { - "request.extension": "jpg" + "request.extension": "png" }, "dotenv": { "enabled": true, diff --git a/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json b/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json index c2aae34..ed0bcc7 100644 --- a/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json +++ b/examples/cdn/convert_image/fixtures/extension-not-in-list.live.json @@ -1,9 +1,5 @@ { "expected": { - "status": 200, - "logs": ["is not in the list of formats to transform"], - "headers": { - "Vary": "Image-Format" - } + "logs": ["is not in the list of formats to transform"] } } diff --git a/examples/cdn/convert_image/fixtures/formats-not-set.live.json b/examples/cdn/convert_image/fixtures/formats-not-set.live.json index 721ce45..cc62a54 100644 --- a/examples/cdn/convert_image/fixtures/formats-not-set.live.json +++ b/examples/cdn/convert_image/fixtures/formats-not-set.live.json @@ -1,9 +1,3 @@ { - "expected": { - "status": 200, - "logs": ["FORMATS_TO_TRANSFORM param is not set, not transforming"], - "headers": { - "Vary": "Image-Format" - } - } + "_skip_live": "FORMATS_TO_TRANSFORM is set on the deployed app — this scenario requires a separate deployment with no env vars" } diff --git a/examples/cdn/convert_image/fixtures/no-extension.live.json b/examples/cdn/convert_image/fixtures/no-extension.live.json index 32375c6..a654196 100644 --- a/examples/cdn/convert_image/fixtures/no-extension.live.json +++ b/examples/cdn/convert_image/fixtures/no-extension.live.json @@ -1,9 +1,5 @@ { "expected": { - "status": 200, - "logs": ["No extension in request path, not transforming"], - "headers": { - "Vary": "Image-Format" - } + "logs": ["No extension in request path, not transforming"] } } diff --git a/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json index 58cd17c..3ae1e73 100644 --- a/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json +++ b/examples/cdn/convert_image/fixtures/ua-in-ignore-list.live.json @@ -1,9 +1,5 @@ { "expected": { - "status": 200, - "logs": ["User-Agent is in ignore list, not transforming"], - "headers": { - "Vary": "Image-Format" - } + "logs": ["User-Agent is in ignore list, not transforming"] } } diff --git a/examples/cdn/convert_image/src/lib.rs b/examples/cdn/convert_image/src/lib.rs index 0693b07..d1b74df 100644 --- a/examples/cdn/convert_image/src/lib.rs +++ b/examples/cdn/convert_image/src/lib.rs @@ -1,6 +1,6 @@ +use image::*; use proxy_wasm::traits::*; use proxy_wasm::types::*; -use image::*; use std::{env, env::VarError, io::Cursor, str::from_utf8}; proxy_wasm::main! {{ @@ -27,13 +27,12 @@ struct ConvertImageContext; impl Context for ConvertImageContext {} impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action - { + 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 Some(ext)= self.get_property(vec!["request.extension"]) else { + let Some(ext) = self.get_property(vec!["request.extension"]) else { println!("No extension in request path, not transforming"); return Action::Continue; }; @@ -53,7 +52,10 @@ impl HttpContext for ConvertImageContext { 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); + println!( + "extension {} is not in the list of formats to transform: {}, not transforming", + ext, image_list + ); return Action::Continue; } @@ -79,12 +81,14 @@ impl HttpContext for ConvertImageContext { Action::Continue } - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action - { + 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); + println!( + "Response status is {} instead of expected 200, not transforming", + status + ); return Action::Continue; } } else { @@ -114,13 +118,13 @@ impl HttpContext for ConvertImageContext { 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 + 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 { + let Some(content_type) = self.get_property(vec!["response.content-type"]) else { return Action::Continue; }; @@ -133,7 +137,10 @@ impl HttpContext for ConvertImageContext { if content_type != "image/avif" { // should never happen - println!("Content-Type {} is not supported, not transforming", content_type); + println!( + "Content-Type {} is not supported, not transforming", + content_type + ); return Action::Continue; } @@ -143,25 +150,29 @@ impl HttpContext for ConvertImageContext { Ok(i) => i, Err(e) => { println!("cannot load image to memory {}, not converting", e); - return Action::Continue + 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)) - ); + 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); + 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) + Err(e) => println!("cannot store transformed image {}", e), } } else { println!("No response body to transform"); @@ -173,7 +184,7 @@ impl HttpContext for ConvertImageContext { impl ConvertImageContext { fn rsp_status(&mut self) -> Option { - if let Some(status)= self.get_property(vec!["response.status"]) { + if let Some(status) = self.get_property(vec!["response.status"]) { if status.len() != 2 { println!("HTTP status property is not 2 bytes"); return None; @@ -184,8 +195,7 @@ impl ConvertImageContext { } } -fn str_param(name: &str) -> Result -{ +fn str_param(name: &str) -> Result { let val = env::var(name)?; if val.is_empty() { return Err(VarError::NotPresent); @@ -194,8 +204,7 @@ fn str_param(name: &str) -> Result Ok(val) } -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 -{ +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; @@ -207,17 +216,26 @@ fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 let val = match val.parse() { Err(_) => { - println!("Param {} is not a valid number, using default value {}", name, default); + 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); + 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); + println!( + "Param {} is above maximum {}, using default value {}", + name, max, default + ); return default; } diff --git a/examples/cdn/http_call/src/lib.rs b/examples/cdn/http_call/src/lib.rs index bf7ef6c..bed56c5 100644 --- a/examples/cdn/http_call/src/lib.rs +++ b/examples/cdn/http_call/src/lib.rs @@ -30,7 +30,7 @@ impl Context for HttpHeaders { &mut self, token_id: u32, num_headers: usize, - body_size: usize, + _body_size: usize, _num_trailers: usize, ) { println!( @@ -38,26 +38,17 @@ impl Context for HttpHeaders { ); //If num_headers is 0, then the HTTP call failed. if num_headers != 0 { - let user_agent = self.get_http_call_response_header("user-agent"); - println!("User-Agent: {:?}", user_agent); - let headers = self.get_http_call_response_headers(); - println!("Response headers:"); - for (name, value) in &headers { - println!(" {}: {}", name, value); - } - let headers_value = self.get_http_call_response_headers_bytes(); - for (name, value) in &headers_value { - println!(" {}: {:?}", name, value); - } - - let body = self.get_http_call_response_body(0, body_size); - println!("Response body: {:?}", body); + let headers_str = headers + .iter() + .map(|(name, value)| format!("\"{}: {}\"", name, value)) + .collect::>() + .join(","); + println!("Response headers: [{}]", headers_str); self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. self.resume_http_request(); - // or self.resume_http_response() } else { self.reset_http_request(); } diff --git a/examples/cdn/jwt/fixtures/.env b/examples/cdn/jwt/fixtures/.env index 7f82b5f..ae14290 100644 --- a/examples/cdn/jwt/fixtures/.env +++ b/examples/cdn/jwt/fixtures/.env @@ -1 +1,2 @@ -FASTEDGE_VAR_SECRET_secret=test-secret-key +FASTEDGE_VAR_SECRET_SECRET=a-string-secret-at-least-256-bits-long-thats-hard-to-break + diff --git a/examples/cdn/jwt/fixtures/expired-token.test.json b/examples/cdn/jwt/fixtures/expired-token.test.json index 3ca7f56..fb5260c 100644 --- a/examples/cdn/jwt/fixtures/expired-token.test.json +++ b/examples/cdn/jwt/fixtures/expired-token.test.json @@ -6,7 +6,7 @@ "url": "http://fastedge-builtin.debug", "headers": { "host": "example.com", - "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjE1Nzc4MzY4MDB9.o2sMzlEidVwpvt0_96jdm6Zs68Kxi6MHDRo9EU4bGV0" + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjE1Nzc4MzY4MDB9.4jxrYjcCcbcMrifZ4qzxtMzrHiNw9sHzvfW0dWkgj2w" }, "body": "" }, diff --git a/examples/cdn/jwt/fixtures/missing-auth-header.live.json b/examples/cdn/jwt/fixtures/missing-auth-header.live.json index b6bd9c7..eae3f6e 100644 --- a/examples/cdn/jwt/fixtures/missing-auth-header.live.json +++ b/examples/cdn/jwt/fixtures/missing-auth-header.live.json @@ -1,6 +1,6 @@ { "expected": { - "logs": ["Auth header is empty"], + "logs": ["No auth header"], "status": 401, "body": "No Authorization header" } diff --git a/examples/cdn/jwt/fixtures/valid-token.live.json b/examples/cdn/jwt/fixtures/valid-token.live.json index 9a71dcb..3ddba8a 100644 --- a/examples/cdn/jwt/fixtures/valid-token.live.json +++ b/examples/cdn/jwt/fixtures/valid-token.live.json @@ -1,6 +1,5 @@ { "expected": { - "logs": ["Token ok"], - "status": 200 + "logs": ["Token ok"] } } diff --git a/examples/cdn/jwt/fixtures/valid-token.test.json b/examples/cdn/jwt/fixtures/valid-token.test.json index 8cb7a0f..fdc7b82 100644 --- a/examples/cdn/jwt/fixtures/valid-token.test.json +++ b/examples/cdn/jwt/fixtures/valid-token.test.json @@ -6,7 +6,7 @@ "url": "http://fastedge-builtin.debug", "headers": { "host": "example.com", - "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjQxMDI0NDQ4MDB9.hS9YgaBPC0uo7LQi1_ai0FdI_vu20wU9wG1-PG6-CG0" + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJleHAiOjQxMDI0NDQ4MDB9.h4nrvZCtvX1OtRJMjID961FJGw2kkRM0zySLXN7VEtg" }, "body": "" }, From 5ba9814d1e0072209c5860dbcb16da8b31e07fb1 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Sat, 23 May 2026 09:00:28 +0100 Subject: [PATCH 08/12] copilot review --- examples/cdn/convert_image/src/lib.rs | 6 +- examples/cdn/hello_world/src/lib.rs | 5 - examples/cdn/properties/README.md | 2 +- examples/cdn/variables_and_secrets/src/lib.rs | 6 + fastedge-plugin-source/generate-docs.sh | 28 +- fastedge-plugin-source/manifest.json | 642 +++++++++++++++++- 6 files changed, 672 insertions(+), 17 deletions(-) diff --git a/examples/cdn/convert_image/src/lib.rs b/examples/cdn/convert_image/src/lib.rs index d1b74df..2909c20 100644 --- a/examples/cdn/convert_image/src/lib.rs +++ b/examples/cdn/convert_image/src/lib.rs @@ -32,7 +32,11 @@ impl HttpContext for ConvertImageContext { self.add_http_request_header("Image-Format", "original"); // get extension - let Some(ext) = self.get_property(vec!["request.extension"]) else { + 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; }; diff --git a/examples/cdn/hello_world/src/lib.rs b/examples/cdn/hello_world/src/lib.rs index 11e2bae..e6d6460 100644 --- a/examples/cdn/hello_world/src/lib.rs +++ b/examples/cdn/hello_world/src/lib.rs @@ -1,8 +1,3 @@ - - - - - use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; diff --git a/examples/cdn/properties/README.md b/examples/cdn/properties/README.md index e86747e..a7329b6 100644 --- a/examples/cdn/properties/README.md +++ b/examples/cdn/properties/README.md @@ -26,7 +26,7 @@ On every request, reads the following properties and adds each as a response hea | `request.region` | `request-country-region` | | `request.continent` | `request-continent` | -If any property is missing the handler sends a 55x error response and stops — each property has a unique status code to identify exactly which lookup failed. +If any property is missing the handler sends a 55x error response and stops — each property has a unique status code to identify exactly which lookup failed. `request.extension` is the exception: because a URL path often has no file extension, a missing value defaults to an empty string instead of triggering an error. ## Query-param overrides diff --git a/examples/cdn/variables_and_secrets/src/lib.rs b/examples/cdn/variables_and_secrets/src/lib.rs index 39a518f..16150b1 100644 --- a/examples/cdn/variables_and_secrets/src/lib.rs +++ b/examples/cdn/variables_and_secrets/src/lib.rs @@ -50,9 +50,15 @@ impl HttpContext for VariablesContext { .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 diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh index 79cc944..20a8f66 100755 --- a/fastedge-plugin-source/generate-docs.sh +++ b/fastedge-plugin-source/generate-docs.sh @@ -250,8 +250,9 @@ PROMPT tmpfile=$(mktemp "$DOCS_DIR/.${target}.XXXXXX") # Note: we do NOT trap-rm the tmpfile on RETURN. Failed-validation output is # preserved under $DOCS_DIR/.failures/ for prompt-debugging (see below) — - # the success path mv's the tmpfile out, the interrupt path rm's it explicitly, - # and after max_attempts we explicitly rm the working tmpfile. + # the success path writes stripped content directly to docs/$target and deletes + # the tmpfile, the interrupt path rm's it explicitly, and after max_attempts + # we explicitly rm the working tmpfile. # Failure-preservation directory. Accumulates across runs so you can compare # what the model emits on attempt 1 vs 2 vs 3 and across invocations. @@ -285,12 +286,21 @@ PROMPT # the doc. The original is still saved to .failures/ so the prompt can # be tuned later. local stripped - stripped=$(awk '/^# / { found=1 } found' "$tmpfile") - - if [ -n "$stripped" ]; then - # Detect preamble: anything before the first '# ' line is preamble. + # Fence-aware level-1-only salvage: skip lines inside ``` fences and only + # treat a lone `# ` heading (not `##`/`###` or `#include` lines) as the + # document start. This avoids false positives from sub-headings and + # code-fence-internal `#`-prefixed lines (e.g. shell comments, #include). + stripped=$(awk '/^```/ { in_fence = !in_fence; next } !in_fence && /^#[^#]/ { found=1 } found' "$tmpfile") + + # Post-strip validation: confirm the salvaged output actually starts with a + # level-1 heading. If awk matched something that looks like `#!` (shebang) + # or another edge case, reject it and retry rather than silently writing junk. + local first_nonempty + first_nonempty=$(printf '%s\n' "$stripped" | grep -m1 '.') + if [ -n "$stripped" ] && [[ "$first_nonempty" =~ ^#[^#] ]]; then + # Detect preamble: anything before the first level-1 heading is preamble. local first_heading_line - first_heading_line=$(grep -n -m1 '^# ' "$tmpfile" | cut -d: -f1) + first_heading_line=$(grep -n -m1 '^#[^#]' "$tmpfile" | cut -d: -f1) if [ "${first_heading_line:-1}" -gt 1 ]; then local preamble_copy="$failure_dir/${target}.preamble.attempt-${attempt}.$(date +%s).md" cp "$tmpfile" "$preamble_copy" @@ -302,10 +312,10 @@ PROMPT return 0 fi - # No level-1 heading anywhere — genuine failure. Save and retry. + # No valid level-1 heading found — genuine failure. Save and retry. local failed_copy="$failure_dir/${target}.attempt-${attempt}.$(date +%s).md" cp "$tmpfile" "$failed_copy" - echo " Attempt $attempt/$max_attempts failed for $target (no '# ' heading found in output) — saved to $failed_copy, retrying..." + echo " Attempt $attempt/$max_attempts failed for $target (no level-1 heading found in output) — saved to $failed_copy, retrying..." attempt=$((attempt + 1)) done diff --git a/fastedge-plugin-source/manifest.json b/fastedge-plugin-source/manifest.json index ceb6dd2..537e74f 100644 --- a/fastedge-plugin-source/manifest.json +++ b/fastedge-plugin-source/manifest.json @@ -1,7 +1,7 @@ { "$schema": "https://fastedge-plugin-source/manifest/v1", "repo_id": "fastedge-sdk-rust", - "version": "1.4.0", + "version": "1.5.0", "sources": { "sdk-api": { "files": ["docs/SDK_API.md"], @@ -337,6 +337,439 @@ ], "required": true, "description": "CDN Cache Control example — docs pattern extraction" + }, + + "cdn-convert-image-blueprint": { + "files": [ + "examples/cdn/convert_image/src/lib.rs", + "examples/cdn/convert_image/Cargo.toml", + "examples/cdn/convert_image/README.md" + ], + "required": true, + "description": "CDN Image Conversion example — scaffold blueprint extraction" + }, + "cdn-convert-image-pattern": { + "files": [ + "examples/cdn/convert_image/src/lib.rs", + "examples/cdn/convert_image/Cargo.toml", + "examples/cdn/convert_image/README.md" + ], + "required": true, + "description": "CDN Image Conversion example — docs pattern extraction" + }, + + "cdn-custom-blueprint": { + "files": [ + "examples/cdn/custom/src/lib.rs", + "examples/cdn/custom/Cargo.toml", + "examples/cdn/custom/README.md" + ], + "required": true, + "description": "CDN Custom Response example — scaffold blueprint extraction" + }, + "cdn-custom-pattern": { + "files": [ + "examples/cdn/custom/src/lib.rs", + "examples/cdn/custom/Cargo.toml", + "examples/cdn/custom/README.md" + ], + "required": true, + "description": "CDN Custom Response example — docs pattern extraction" + }, + + "cdn-log-time-blueprint": { + "files": [ + "examples/cdn/log_time/src/lib.rs", + "examples/cdn/log_time/Cargo.toml", + "examples/cdn/log_time/README.md" + ], + "required": true, + "description": "CDN Log Time example — scaffold blueprint extraction" + }, + "cdn-log-time-pattern": { + "files": [ + "examples/cdn/log_time/src/lib.rs", + "examples/cdn/log_time/Cargo.toml", + "examples/cdn/log_time/README.md" + ], + "required": true, + "description": "CDN Log Time example — docs pattern extraction" + }, + + "cdn-md2html-blueprint": { + "files": [ + "examples/cdn/md2html/src/lib.rs", + "examples/cdn/md2html/Cargo.toml", + "examples/cdn/md2html/README.md" + ], + "required": true, + "description": "CDN Markdown-to-HTML example — scaffold blueprint extraction" + }, + "cdn-md2html-pattern": { + "files": [ + "examples/cdn/md2html/src/lib.rs", + "examples/cdn/md2html/Cargo.toml", + "examples/cdn/md2html/README.md" + ], + "required": true, + "description": "CDN Markdown-to-HTML example — docs pattern extraction" + }, + + "http-basic-hello-world-blueprint": { + "files": [ + "examples/http/basic/hello_world/src/lib.rs", + "examples/http/basic/hello_world/Cargo.toml", + "examples/http/basic/hello_world/README.md" + ], + "required": true, + "description": "HTTP Basic Hello World example (sync handler) — scaffold blueprint extraction" + }, + "http-basic-hello-world-pattern": { + "files": [ + "examples/http/basic/hello_world/src/lib.rs", + "examples/http/basic/hello_world/Cargo.toml", + "examples/http/basic/hello_world/README.md" + ], + "required": true, + "description": "HTTP Basic Hello World example (sync handler) — docs pattern extraction" + }, + + "http-basic-api-wrapper-blueprint": { + "files": [ + "examples/http/basic/api_wrapper/src/lib.rs", + "examples/http/basic/api_wrapper/Cargo.toml", + "examples/http/basic/api_wrapper/README.md" + ], + "required": true, + "description": "HTTP Basic API Wrapper example — scaffold blueprint extraction" + }, + "http-basic-api-wrapper-pattern": { + "files": [ + "examples/http/basic/api_wrapper/src/lib.rs", + "examples/http/basic/api_wrapper/Cargo.toml", + "examples/http/basic/api_wrapper/README.md" + ], + "required": true, + "description": "HTTP Basic API Wrapper example — docs pattern extraction" + }, + + "http-basic-backend-blueprint": { + "files": [ + "examples/http/basic/backend/src/lib.rs", + "examples/http/basic/backend/Cargo.toml", + "examples/http/basic/backend/README.md" + ], + "required": true, + "description": "HTTP Basic Backend Proxy example — scaffold blueprint extraction" + }, + "http-basic-backend-pattern": { + "files": [ + "examples/http/basic/backend/src/lib.rs", + "examples/http/basic/backend/Cargo.toml", + "examples/http/basic/backend/README.md" + ], + "required": true, + "description": "HTTP Basic Backend Proxy example — docs pattern extraction" + }, + + "http-basic-cache-blueprint": { + "files": [ + "examples/http/basic/cache/src/lib.rs", + "examples/http/basic/cache/Cargo.toml" + ], + "required": true, + "description": "HTTP Basic Cache example — scaffold blueprint extraction" + }, + "http-basic-cache-pattern": { + "files": [ + "examples/http/basic/cache/src/lib.rs", + "examples/http/basic/cache/Cargo.toml" + ], + "required": true, + "description": "HTTP Basic Cache example — docs pattern extraction" + }, + + "http-basic-markdown-render-blueprint": { + "files": [ + "examples/http/basic/markdown_render/src/lib.rs", + "examples/http/basic/markdown_render/Cargo.toml", + "examples/http/basic/markdown_render/README.md" + ], + "required": true, + "description": "HTTP Basic Markdown Render example — scaffold blueprint extraction" + }, + "http-basic-markdown-render-pattern": { + "files": [ + "examples/http/basic/markdown_render/src/lib.rs", + "examples/http/basic/markdown_render/Cargo.toml", + "examples/http/basic/markdown_render/README.md" + ], + "required": true, + "description": "HTTP Basic Markdown Render example — docs pattern extraction" + }, + + "http-basic-outbound-fetch-blueprint": { + "files": [ + "examples/http/basic/outbound_fetch/src/lib.rs", + "examples/http/basic/outbound_fetch/Cargo.toml", + "examples/http/basic/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP Basic Outbound Fetch example — scaffold blueprint extraction" + }, + "http-basic-outbound-fetch-pattern": { + "files": [ + "examples/http/basic/outbound_fetch/src/lib.rs", + "examples/http/basic/outbound_fetch/Cargo.toml", + "examples/http/basic/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP Basic Outbound Fetch example — docs pattern extraction" + }, + + "http-basic-print-blueprint": { + "files": [ + "examples/http/basic/print/src/lib.rs", + "examples/http/basic/print/Cargo.toml", + "examples/http/basic/print/README.md" + ], + "required": true, + "description": "HTTP Basic Print / Logging example — scaffold blueprint extraction" + }, + "http-basic-print-pattern": { + "files": [ + "examples/http/basic/print/src/lib.rs", + "examples/http/basic/print/Cargo.toml", + "examples/http/basic/print/README.md" + ], + "required": true, + "description": "HTTP Basic Print / Logging example — docs pattern extraction" + }, + + "http-basic-s3upload-blueprint": { + "files": [ + "examples/http/basic/s3upload/src/lib.rs", + "examples/http/basic/s3upload/Cargo.toml", + "examples/http/basic/s3upload/README.md" + ], + "required": true, + "description": "HTTP Basic S3 Upload example — scaffold blueprint extraction" + }, + "http-basic-s3upload-pattern": { + "files": [ + "examples/http/basic/s3upload/src/lib.rs", + "examples/http/basic/s3upload/Cargo.toml", + "examples/http/basic/s3upload/README.md" + ], + "required": true, + "description": "HTTP Basic S3 Upload example — docs pattern extraction" + }, + + "http-basic-secret-blueprint": { + "files": [ + "examples/http/basic/secret/src/lib.rs", + "examples/http/basic/secret/Cargo.toml", + "examples/http/basic/secret/README.md" + ], + "required": true, + "description": "HTTP Basic Secrets example — scaffold blueprint extraction" + }, + "http-basic-secret-pattern": { + "files": [ + "examples/http/basic/secret/src/lib.rs", + "examples/http/basic/secret/Cargo.toml", + "examples/http/basic/secret/README.md" + ], + "required": true, + "description": "HTTP Basic Secrets example — docs pattern extraction" + }, + + "http-basic-smart-switch-blueprint": { + "files": [ + "examples/http/basic/smart_switch/src/lib.rs", + "examples/http/basic/smart_switch/Cargo.toml", + "examples/http/basic/smart_switch/README.md" + ], + "required": true, + "description": "HTTP Basic Smart Switch / Routing example — scaffold blueprint extraction" + }, + "http-basic-smart-switch-pattern": { + "files": [ + "examples/http/basic/smart_switch/src/lib.rs", + "examples/http/basic/smart_switch/Cargo.toml", + "examples/http/basic/smart_switch/README.md" + ], + "required": true, + "description": "HTTP Basic Smart Switch / Routing example — docs pattern extraction" + }, + + "http-basic-watermark-blueprint": { + "files": [ + "examples/http/basic/watermark/src/lib.rs", + "examples/http/basic/watermark/Cargo.toml", + "examples/http/basic/watermark/README.md" + ], + "required": true, + "description": "HTTP Basic Watermark example — scaffold blueprint extraction" + }, + "http-basic-watermark-pattern": { + "files": [ + "examples/http/basic/watermark/src/lib.rs", + "examples/http/basic/watermark/Cargo.toml", + "examples/http/basic/watermark/README.md" + ], + "required": true, + "description": "HTTP Basic Watermark example — docs pattern extraction" + }, + + "http-wasi-cache-blueprint": { + "files": [ + "examples/http/wasi/cache/src/lib.rs", + "examples/http/wasi/cache/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Cache example — scaffold blueprint extraction" + }, + "http-wasi-cache-pattern": { + "files": [ + "examples/http/wasi/cache/src/lib.rs", + "examples/http/wasi/cache/Cargo.toml" + ], + "required": true, + "description": "HTTP WASI Cache example — docs pattern extraction" + }, + + "http-wasi-geo-redirect-blueprint": { + "files": [ + "examples/http/wasi/geo_redirect/src/lib.rs", + "examples/http/wasi/geo_redirect/Cargo.toml", + "examples/http/wasi/geo_redirect/README.md" + ], + "required": true, + "description": "HTTP WASI Geo Redirect example — scaffold blueprint extraction" + }, + "http-wasi-geo-redirect-pattern": { + "files": [ + "examples/http/wasi/geo_redirect/src/lib.rs", + "examples/http/wasi/geo_redirect/Cargo.toml", + "examples/http/wasi/geo_redirect/README.md" + ], + "required": true, + "description": "HTTP WASI Geo Redirect example — docs pattern extraction" + }, + + "http-wasi-headers-blueprint": { + "files": [ + "examples/http/wasi/headers/src/lib.rs", + "examples/http/wasi/headers/Cargo.toml", + "examples/http/wasi/headers/README.md" + ], + "required": true, + "description": "HTTP WASI Headers Manipulation example — scaffold blueprint extraction" + }, + "http-wasi-headers-pattern": { + "files": [ + "examples/http/wasi/headers/src/lib.rs", + "examples/http/wasi/headers/Cargo.toml", + "examples/http/wasi/headers/README.md" + ], + "required": true, + "description": "HTTP WASI Headers Manipulation example — docs pattern extraction" + }, + + "http-wasi-large-env-variable-blueprint": { + "files": [ + "examples/http/wasi/large_env_variable/src/lib.rs", + "examples/http/wasi/large_env_variable/Cargo.toml", + "examples/http/wasi/large_env_variable/README.md" + ], + "required": true, + "description": "HTTP WASI Large Env Variable / Dictionary example — scaffold blueprint extraction" + }, + "http-wasi-large-env-variable-pattern": { + "files": [ + "examples/http/wasi/large_env_variable/src/lib.rs", + "examples/http/wasi/large_env_variable/Cargo.toml", + "examples/http/wasi/large_env_variable/README.md" + ], + "required": true, + "description": "HTTP WASI Large Env Variable / Dictionary example — docs pattern extraction" + }, + + "http-wasi-outbound-fetch-blueprint": { + "files": [ + "examples/http/wasi/outbound_fetch/src/lib.rs", + "examples/http/wasi/outbound_fetch/Cargo.toml", + "examples/http/wasi/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Outbound Fetch example — scaffold blueprint extraction" + }, + "http-wasi-outbound-fetch-pattern": { + "files": [ + "examples/http/wasi/outbound_fetch/src/lib.rs", + "examples/http/wasi/outbound_fetch/Cargo.toml", + "examples/http/wasi/outbound_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Outbound Fetch example — docs pattern extraction" + }, + + "http-wasi-secret-rollover-blueprint": { + "files": [ + "examples/http/wasi/secret_rollover/src/lib.rs", + "examples/http/wasi/secret_rollover/Cargo.toml", + "examples/http/wasi/secret_rollover/README.md" + ], + "required": true, + "description": "HTTP WASI Secret Rollover example — scaffold blueprint extraction" + }, + "http-wasi-secret-rollover-pattern": { + "files": [ + "examples/http/wasi/secret_rollover/src/lib.rs", + "examples/http/wasi/secret_rollover/Cargo.toml", + "examples/http/wasi/secret_rollover/README.md" + ], + "required": true, + "description": "HTTP WASI Secret Rollover example — docs pattern extraction" + }, + + "http-wasi-simple-fetch-blueprint": { + "files": [ + "examples/http/wasi/simple_fetch/src/lib.rs", + "examples/http/wasi/simple_fetch/Cargo.toml", + "examples/http/wasi/simple_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Simple Fetch example — scaffold blueprint extraction" + }, + "http-wasi-simple-fetch-pattern": { + "files": [ + "examples/http/wasi/simple_fetch/src/lib.rs", + "examples/http/wasi/simple_fetch/Cargo.toml", + "examples/http/wasi/simple_fetch/README.md" + ], + "required": true, + "description": "HTTP WASI Simple Fetch example — docs pattern extraction" + }, + + "http-wasi-variables-and-secrets-blueprint": { + "files": [ + "examples/http/wasi/variables_and_secrets/src/lib.rs", + "examples/http/wasi/variables_and_secrets/Cargo.toml", + "examples/http/wasi/variables_and_secrets/README.md" + ], + "required": true, + "description": "HTTP WASI Variables and Secrets example — scaffold blueprint extraction" + }, + "http-wasi-variables-and-secrets-pattern": { + "files": [ + "examples/http/wasi/variables_and_secrets/src/lib.rs", + "examples/http/wasi/variables_and_secrets/Cargo.toml", + "examples/http/wasi/variables_and_secrets/README.md" + ], + "required": true, + "description": "HTTP WASI Variables and Secrets example — docs pattern extraction" } }, "target_mapping": { @@ -510,6 +943,213 @@ "cdn-cache-control-pattern": { "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md", "section": null + }, + + "cdn-convert-image-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md", + "section": null + }, + "cdn-convert-image-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md", + "section": null + }, + + "cdn-custom-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md", + "section": null + }, + "cdn-custom-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md", + "section": null + }, + + "cdn-log-time-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md", + "section": null + }, + "cdn-log-time-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md", + "section": null + }, + + "cdn-md2html-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md", + "section": null + }, + "cdn-md2html-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md", + "section": null + }, + + "http-basic-hello-world-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/hello-world-basic-rust.md", + "section": null + }, + "http-basic-hello-world-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md", + "section": null + }, + + "http-basic-api-wrapper-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/api-wrapper-basic-rust.md", + "section": null + }, + "http-basic-api-wrapper-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md", + "section": null + }, + + "http-basic-backend-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/backend-basic-rust.md", + "section": null + }, + "http-basic-backend-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md", + "section": null + }, + + "http-basic-cache-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-rust.md", + "section": null + }, + "http-basic-cache-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md", + "section": null + }, + + "http-basic-markdown-render-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/markdown-render-basic-rust.md", + "section": null + }, + "http-basic-markdown-render-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md", + "section": null + }, + + "http-basic-outbound-fetch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-basic-rust.md", + "section": null + }, + "http-basic-outbound-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md", + "section": null + }, + + "http-basic-print-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/print-basic-rust.md", + "section": null + }, + "http-basic-print-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md", + "section": null + }, + + "http-basic-s3upload-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/s3upload-basic-rust.md", + "section": null + }, + "http-basic-s3upload-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md", + "section": null + }, + + "http-basic-secret-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/secret-basic-rust.md", + "section": null + }, + "http-basic-secret-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md", + "section": null + }, + + "http-basic-smart-switch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/smart-switch-basic-rust.md", + "section": null + }, + "http-basic-smart-switch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md", + "section": null + }, + + "http-basic-watermark-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/watermark-basic-rust.md", + "section": null + }, + "http-basic-watermark-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md", + "section": null + }, + + "http-wasi-cache-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md", + "section": null + }, + "http-wasi-cache-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md", + "section": null + }, + + "http-wasi-geo-redirect-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md", + "section": null + }, + "http-wasi-geo-redirect-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md", + "section": null + }, + + "http-wasi-headers-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md", + "section": null + }, + "http-wasi-headers-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md", + "section": null + }, + + "http-wasi-large-env-variable-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md", + "section": null + }, + "http-wasi-large-env-variable-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md", + "section": null + }, + + "http-wasi-outbound-fetch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md", + "section": null + }, + "http-wasi-outbound-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md", + "section": null + }, + + "http-wasi-secret-rollover-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md", + "section": null + }, + "http-wasi-secret-rollover-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md", + "section": null + }, + + "http-wasi-simple-fetch-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md", + "section": null + }, + "http-wasi-simple-fetch-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md", + "section": null + }, + + "http-wasi-variables-and-secrets-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md", + "section": null + }, + "http-wasi-variables-and-secrets-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md", + "section": null } }, "validation": { From 8c6e08d5d4f7e129099fb74e7af2f98ffb423d6e Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Sat, 23 May 2026 09:09:07 +0100 Subject: [PATCH 09/12] re-generated docs --- docs/CDN_APPS.md | 50 --------------- docs/HOST_SERVICES.md | 38 ------------ docs/INDEX.md | 2 +- docs/SDK_API.md | 138 +++++++++++++----------------------------- docs/quickstart.md | 34 ++--------- llms.txt | 2 +- 6 files changed, 50 insertions(+), 214 deletions(-) diff --git a/docs/CDN_APPS.md b/docs/CDN_APPS.md index 9ff7bcd..03e5de2 100644 --- a/docs/CDN_APPS.md +++ b/docs/CDN_APPS.md @@ -27,7 +27,6 @@ CDN apps come in two tiers depending on whether they need FastEdge host services **Tier 1 — Basic CDN app** (no FastEdge host services): -```toml [package] name = "my-cdn-app" version = "0.1.0" @@ -39,11 +38,9 @@ crate-type = ["cdylib"] [dependencies] proxy-wasm = "0.2" log = "0.4" -``` **Tier 2 — CDN app with FastEdge host services** (KV, secrets, dictionary): -```toml [package] name = "my-cdn-app" version = "0.1.0" @@ -55,7 +52,6 @@ crate-type = ["cdylib"] [dependencies] proxy-wasm = "0.2" fastedge = { version = "0.3", features = ["proxywasm"] } -``` The `proxywasm` feature flag is required to access `fastedge::proxywasm::*`. Without it, `fastedge` only exposes Component Model APIs, which are not available in the proxy-wasm environment. @@ -63,7 +59,6 @@ The `proxywasm` feature flag is required to access `fastedge::proxywasm::*`. Wit A complete CDN app that adds a response header and logs each lifecycle phase: -```rust,no_run use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -113,13 +108,10 @@ impl HttpContext for HelloWorld { Action::Continue } } -``` ### Build -```sh cargo build --target wasm32-wasip1 --release -``` CDN apps and basic HTTP apps share the same build target: `wasm32-wasip1`. Only async WASI HTTP apps using `#[wstd::http_server]` target `wasm32-wasip2`. @@ -131,7 +123,6 @@ The proxy-wasm lifecycle is the core concept for CDN app development. Every CDN The `proxy_wasm::main!` macro initializes the filter. It sets the log level and registers the root context factory function. -```rust,no_run use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -141,13 +132,11 @@ proxy_wasm::main! {{ Box::new(MyAppRoot) }); }} -``` ### Root Context The root context is a singleton created once when the filter loads. Its primary role is to create a new HTTP context for each lifecycle callback invocation. -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; struct MyAppRoot; @@ -163,7 +152,6 @@ impl RootContext for MyAppRoot { Some(Box::new(MyApp)) } } -``` `get_type()` must return `Some(ContextType::HttpContext)` for HTTP traffic interception. `create_http_context` is called once per lifecycle callback invocation and receives a unique `context_id`. @@ -171,7 +159,6 @@ impl RootContext for MyAppRoot { The HTTP context is where request and response processing happens. A new instance is created for each lifecycle callback invocation — not once per request. See [Hook State Isolation](#hook-state-isolation) for the consequences this has on state management. -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; struct MyApp; @@ -188,7 +175,6 @@ impl HttpContext for MyApp { Action::Continue } } -``` Both `Context` and `HttpContext` must be implemented. The `Context` impl can be empty if no shared context callbacks are needed. @@ -215,7 +201,6 @@ Every lifecycle callback returns an `Action` that controls what happens next. For body callbacks, return `Action::StopIterationAndBuffer` until `end_of_stream` is `true`, then process the full body and return `Action::Continue`. -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -229,7 +214,6 @@ impl HttpContext for MyApp { Action::Continue } } -``` ### Hook State Isolation @@ -243,7 +227,6 @@ This has critical consequences for application design: To pass data between callbacks, use `self.set_property` and `self.get_property` with a custom property path. The host preserves these values across callback invocations for the same logical request: -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -263,13 +246,11 @@ impl HttpContext for MyApp { Action::Continue } } -``` ## Request and Response Manipulation ### Reading Headers and Properties -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -291,13 +272,11 @@ impl HttpContext for MyApp { Action::Continue } } -``` Properties return `Option>`. Most properties are UTF-8 strings; see the Request Properties section for encoding details. ### Modifying Headers -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -319,7 +298,6 @@ impl HttpContext for MyApp { Action::Continue } } -``` **Known limitation**: On the FastEdge CDN platform, passing `None` to `set_http_request_header` or `set_http_response_header` sets the header value to an empty string rather than removing the header entirely. When checking for header absence, test for an empty string as well as a missing value. @@ -327,7 +305,6 @@ impl HttpContext for MyApp { To short-circuit the request and respond directly to the client without forwarding to origin, call `send_http_response` and return `Action::Pause`. -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -346,7 +323,6 @@ impl HttpContext for MyApp { Action::Continue } } -``` `send_http_response` signature: `fn send_http_response(&self, status_code: u32, headers: Vec<(&str, &str)>, body: Option<&[u8]>)` @@ -379,7 +355,6 @@ Most properties are UTF-8 strings decoded with `std::str::from_utf8()`. The `res Geo-IP properties (`request.country`, `request.country.name`, `request.city`, `request.region`, `request.continent`, `request.geo.lat`, `request.geo.long`) are derived from the client IP address. -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -396,9 +371,7 @@ impl HttpContext for MyApp { Action::Continue } } -``` -```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -417,7 +390,6 @@ impl HttpContext for MyApp { Action::Continue } } -``` ## Host Services for CDN Apps @@ -429,9 +401,7 @@ Provides persistent key-value storage. The API shape mirrors `fastedge::key_valu #### `Store` -```rust,ignore pub struct Store { /* ... */ } -``` | Method | Return Type | Description | | ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ | @@ -445,13 +415,11 @@ pub struct Store { /* ... */ } #### `Error` -```rust,ignore pub enum Error { NoSuchStore, AccessDenied, Other(String), } -``` | Variant | Description | | --------------- | ----------------------------------------------------------- | @@ -461,7 +429,6 @@ pub enum Error { #### Example — Bloom filter check in request headers phase -```rust,no_run use fastedge::proxywasm::key_value::Store; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -505,16 +472,13 @@ impl HttpContext for RateLimitFilter { } } } -``` ### Secret Management (`fastedge::proxywasm::secret`) Provides access to encrypted secrets stored in the FastEdge platform. -```rust,ignore pub fn get(key: &str) -> Result>, u32> pub fn get_effective_at(key: &str, at: u32) -> Result>, u32> -``` | Function | Return Type | Description | | -------------------------------------- | ------------------------------ | -------------------------------------------------- | @@ -529,7 +493,6 @@ Never log or expose secret values in application output. #### Example — JWT validation using a secret signing key -```rust,no_run use fastedge::proxywasm::secret; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -570,21 +533,17 @@ impl HttpContext for AuthFilter { Action::Continue } } -``` ### Dictionary (`fastedge::proxywasm::dictionary`) Provides read-only key-value lookups for configuration data. Values are returned as `String`. -```rust,ignore pub fn get(key: &str) -> Option -``` Returns `Some(value)` if the key exists and the value is valid UTF-8, `None` otherwise. #### Example — Reading upstream configuration -```rust,no_run use fastedge::proxywasm::dictionary; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -616,19 +575,15 @@ impl HttpContext for ConfigFilter { Action::Continue } } -``` ### Diagnostics (`fastedge::proxywasm::utils`) -```rust,ignore pub fn set_user_diag(value: &str) -``` Writes a diagnostic message visible in FastEdge platform logs. Panics if the host returns a non-zero status. Use for debugging and operational monitoring; do not log sensitive values. #### Example -```rust,no_run use fastedge::proxywasm::utils; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -661,13 +616,11 @@ impl HttpContext for DiagFilter { Action::Continue } } -``` ### Environment Variables CDN apps read non-secret configuration via `std::env::var()`. This works identically to HTTP apps — no proxy-wasm-specific API is involved. -```rust,no_run use std::env; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -709,7 +662,6 @@ impl HttpContext for EnvFilter { Action::Continue } } -``` For sensitive configuration, use `fastedge::proxywasm::secret::get()` instead of environment variables. @@ -717,7 +669,6 @@ For sensitive configuration, use `fastedge::proxywasm::secret::get()` instead of CDN apps can write log output using `println!` or the `proxy_wasm::hostcalls::log` function: -```rust,no_run use proxy_wasm::hostcalls; use proxy_wasm::types::LogLevel; @@ -726,7 +677,6 @@ println!("Request received"); // Proxy-wasm log API (routes through the configured log level) hostcalls::log(LogLevel::Info, "Request received").ok(); -``` The `log` crate macros (`info!`, `warn!`, `error!`, etc.) work when `proxy_wasm::set_log_level()` is configured in the entry point, which routes them through the proxy-wasm log infrastructure. diff --git a/docs/HOST_SERVICES.md b/docs/HOST_SERVICES.md index 5ce7587..ad007a0 100644 --- a/docs/HOST_SERVICES.md +++ b/docs/HOST_SERVICES.md @@ -12,14 +12,11 @@ The `fastedge::key_value` module provides persistent storage with support for si ### Opening a Store -```rust pub fn new() -> Result pub fn open(name: &str) -> Result -``` `Store::new()` opens the default store. `Store::open(name)` opens a named store. Both return `Err(Error::NoSuchStore)` if the store label is not recognized and `Err(Error::AccessDenied)` if the application is not authorized. -```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -36,17 +33,13 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from("ok"))?) } -``` ### Reading Values -```rust pub fn get(&self, key: &str) -> Result>, Error> -``` Returns `Ok(Some(bytes))` if the key exists, `Ok(None)` if it does not. -```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -64,13 +57,10 @@ async fn main(_request: Request) -> anyhow::Result> { .body(Body::from("not found"))?), } } -``` ### Pattern Scanning -```rust pub fn scan(&self, pattern: &str) -> Result, Error> -``` Scans the store for keys matching a glob-style pattern. Returns a list of matching key names. Returns an empty `Vec` if no keys match. @@ -82,7 +72,6 @@ Supported glob syntax: | `?` | Any single character | | `[abc]` | Any character in the set | -```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -100,16 +89,13 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("{} keys found", keys.len())))?) } -``` ### Sorted Sets Sorted sets store members associated with a `f64` score. Members are ordered from lowest to highest score. -```rust pub fn zrange_by_score(&self, key: &str, min: f64, max: f64) -> Result, f64)>, Error> pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Error> -``` `zrange_by_score` returns all members of the sorted set stored at `key` whose score falls in the inclusive range `[min, max]`. Use `f64::NEG_INFINITY` and `f64::INFINITY` for unbounded ranges. @@ -117,7 +103,6 @@ pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Err Both return an empty `Vec` when the key does not exist or no members fall within the specified range or pattern. -```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -141,19 +126,15 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("{} top players", top_players.len())))?) } -``` ### Bloom Filters -```rust pub fn bf_exists(&self, key: &str, item: &str) -> Result -``` Tests whether `item` is a member of the bloom filter stored at `key`. Returns `true` if the item was probably added to the filter (subject to the false-positive rate of the filter), or `false` if the key does not exist or the item was definitely not added. Bloom filters cannot produce false negatives: if `bf_exists` returns `false`, the item has not been added. -```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -174,7 +155,6 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } -``` ### Error Handling @@ -186,7 +166,6 @@ All `Store` methods return `Result<_, Error>`. The `Error` type has three varian | `Error::AccessDenied` | The application does not have permission to access the store | | `Error::Other(String)` | An implementation-specific error (I/O or internal host failure) | -```rust,no_run use fastedge::key_value::{Error, Store}; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -216,7 +195,6 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } -``` CDN apps use `fastedge::proxywasm::key_value` instead — see [CDN_APPS.md](CDN_APPS.md) for the ProxyWasm API surface and usage examples. @@ -228,13 +206,10 @@ The `fastedge::secret` module provides access to encrypted secrets such as API k ### Reading Secrets -```rust pub fn get(key: &str) -> Result>, Error> -``` Returns the currently effective value of the named secret. Returns `Ok(None)` if no secret with that name is configured. -```rust,no_run use fastedge::secret; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -257,19 +232,15 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } -``` ### Time-Based Retrieval -```rust pub fn get_effective_at(key: &str, at: u32) -> Result>, Error> -``` Returns the value of the named secret that was effective at the given Unix timestamp (`at`, seconds since epoch). This is useful during secret rotation: you can verify that both the old and new versions of a secret are accessible before completing a rotation. Returns `Ok(None)` if no version of the secret was configured at that time. -```rust,no_run use fastedge::secret; use std::time::{SystemTime, UNIX_EPOCH}; use wstd::http::body::Body; @@ -293,7 +264,6 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } -``` ### Security Notes @@ -312,15 +282,12 @@ The `fastedge::dictionary` module provides fast, read-only lookups for configura ### Configuration Lookups -```rust pub fn get(key: &str) -> Option -``` Returns `Some(value)` if the key exists and its value is valid UTF-8, or `None` if the key is not found or the value cannot be decoded as UTF-8. Dictionary values are environment variables set at deployment time via the platform — the same management mechanism as secrets, but without encryption. They are not writable from application code. -```rust,no_run use fastedge::dictionary; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -341,7 +308,6 @@ async fn main(_request: Request) -> anyhow::Result> { upstream, timeout_ms )))?) } -``` ### When to Use Dictionary vs Key-Value vs Secrets @@ -366,15 +332,12 @@ The `fastedge::utils` module provides diagnostic functions for monitoring and de ### Diagnostics -```rust pub fn set_user_diag(value: &str) -``` Writes a diagnostic string that appears in the FastEdge platform logs associated with the current request. This is intended for debugging and operational monitoring. There is no return value; the function panics if the host rejects the call. The FastEdge platform captures only **stdout** for application log output. `stderr` is silently discarded and will not appear in the platform's log viewer. Use `println!` (or logging crates that write to stdout) for any output you need to observe. Do not use `eprintln!` — it produces no visible output on the platform. -```rust,no_run use fastedge::key_value::Store; use fastedge::utils::set_user_diag; use wstd::http::body::Body; @@ -399,7 +362,6 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } -``` One diagnostic message per request is the typical pattern. If `set_user_diag` is called multiple times, the platform may record only the last value or concatenate them depending on runtime behavior. diff --git a/docs/INDEX.md b/docs/INDEX.md index a519dc0..7773ed2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,6 +1,6 @@ # FastEdge Rust SDK Documentation -Documentation for the `fastedge` crate (v0.3.5) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. +Documentation for the `fastedge` crate (v0.4.0) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. ## Documents diff --git a/docs/SDK_API.md b/docs/SDK_API.md index fb4ad9f..28dbf85 100644 --- a/docs/SDK_API.md +++ b/docs/SDK_API.md @@ -8,35 +8,30 @@ Reference for the `fastedge` crate. Covers the handler macros, body type, outbou ### Cargo.toml -The current crate version is `0.3.5` (from `[workspace.package]` in the repository's `Cargo.toml`). +The current crate version is `0.4.0` (from `[workspace.package]` in the repository's `Cargo.toml`). For `#[wstd::http_server]` (recommended): -```toml [dependencies] wstd = "0.6" anyhow = "1" [lib] crate-type = ["cdylib"] -``` For `#[fastedge::http]` (basic): -```toml [dependencies] -fastedge = "0.3.5" +fastedge = "0.4.0" anyhow = "1" [lib] crate-type = ["cdylib"] -``` ### Minimal Handler The recommended handler for new applications uses `#[wstd::http_server]` (async, WASI-HTTP): -```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -47,32 +42,25 @@ async fn main(_request: Request) -> anyhow::Result> { .body(Body::from("Hello, FastEdge!"))?; Ok(response) } -``` ### Build For `#[wstd::http_server]` (recommended): -```bash rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 --release -``` To avoid passing `--target` on every build, add `.cargo/config.toml` to your project: -```toml [build] target = "wasm32-wasip2" -``` Then `cargo build --release` is sufficient. For `#[fastedge::http]` (basic): -```bash rustup target add wasm32-wasip1 cargo build --target wasm32-wasip1 --release -``` The output `.wasm` file is located at `target//release/.wasm`. @@ -86,9 +74,7 @@ Provided by the [`wstd`](https://crates.io/crates/wstd) crate. Registers an asyn The decorated function must match the following signature: -```rust,ignore async fn (request: Request) -> anyhow::Result> -``` **Requirements:** @@ -99,7 +85,6 @@ async fn (request: Request) -> anyhow::Result> **Example — echo handler:** -```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -110,11 +95,9 @@ async fn main(request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("Method: {}", method)))?) } -``` **Example — outbound HTTP with `wstd::http::Client`:** -```rust,no_run use wstd::http::body::Body; use wstd::http::{Client, Request, Response}; @@ -127,22 +110,17 @@ async fn main(_request: Request) -> anyhow::Result> { let response = Client::new().send(upstream).await?; Ok(response) } -``` ### `#[fastedge::http]` (Basic) -```rust,ignore #[proc_macro_attribute] pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream -``` Provided by the `fastedge` crate. Registers a synchronous function as the HTTP request handler using the FastEdge-specific WIT interface. Use this for applications that require synchronous execution or the `fastedge::send_request` client. New projects should prefer `#[wstd::http_server]`. The decorated function must match the following signature: -```rust,ignore fn (req: fastedge::http::Request) -> anyhow::Result> -``` **Requirements:** @@ -157,7 +135,6 @@ If the function returns `Err(e)`, the macro converts it to an HTTP `500 Internal **Example — minimal handler:** -```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -169,11 +146,9 @@ fn main(_req: Request) -> Result> { .body(Body::from("Hello, FastEdge!")) .map_err(Into::into) } -``` **Example — method dispatch:** -```rust,no_run use anyhow::{anyhow, Result}; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -188,7 +163,6 @@ fn main(req: Request) -> Result> { _ => Err(anyhow!("method not allowed")), } } -``` ### Comparison @@ -205,9 +179,7 @@ fn main(req: Request) -> Result> { ## Body Type -```rust,ignore pub struct Body { /* private fields */ } -``` `fastedge::body::Body` wraps [`bytes::Bytes`](https://docs.rs/bytes) and carries a MIME content-type. The content-type is set at construction time based on the input data and cannot be changed after creation. @@ -215,25 +187,22 @@ pub struct Body { /* private fields */ } ### Constructors -| Constructor | Content-Type | Notes | -| -------------------------------------------- | --------------------------- | ------------------------------------------------------------------ | -| `Body::from(value: String)` | `text/plain; charset=utf-8` | | -| `Body::from(value: &'static str)` | `text/plain; charset=utf-8` | | -| `Body::from(value: Vec)` | `application/octet-stream` | | -| `Body::from(value: &'static [u8])` | `application/octet-stream` | | -| `Body::empty()` | `text/plain; charset=utf-8` | Zero-length body | -| `Body::try_from(value: serde_json::Value)` | `application/json` | Requires `json` feature; returns `Result` | +| Constructor | Content-Type | Notes | +| ------------------------------------------ | --------------------------- | ------------------------------------------------------------------ | +| `Body::from(value: String)` | `text/plain; charset=utf-8` | | +| `Body::from(value: &'static str)` | `text/plain; charset=utf-8` | | +| `Body::from(value: Vec)` | `application/octet-stream` | | +| `Body::from(value: &'static [u8])` | `application/octet-stream` | | +| `Body::empty()` | `text/plain; charset=utf-8` | Zero-length body | +| `Body::try_from(value: serde_json::Value)` | `application/json` | Requires `json` feature; returns `Result` | -```rust use fastedge::body::Body; let text = Body::from("hello"); let owned = Body::from(String::from("hello")); let bytes = Body::from(vec![0x48u8, 0x69]); let empty = Body::empty(); -``` -```rust // json feature required use fastedge::body::Body; use serde_json::json; @@ -243,40 +212,36 @@ let body = Body::try_from(json!({"status": "ok"}))?; assert_eq!(body.content_type(), "application/json"); # Ok(()) # } -``` ### Methods -| Method | Return Type | Description | -| -------------------------------- | ----------- | --------------------------------------------------- | -| `content_type(&self) -> String` | `String` | Returns the MIME type set when the body was created | -| `empty() -> Self` | `Body` | Constructs a zero-length body | +| Method | Return Type | Description | +| ------------------------------- | ----------- | --------------------------------------------------- | +| `content_type(&self) -> String` | `String` | Returns the MIME type set when the body was created | +| `empty() -> Self` | `Body` | Constructs a zero-length body | All methods from `bytes::Bytes` are available via `Deref`: -```rust use fastedge::body::Body; let body = Body::from("hello"); assert_eq!(body.len(), 5); assert!(!body.is_empty()); let slice: &[u8] = &body[..]; -``` ### Content-Type Detection Content-type is determined at construction time and cannot be changed after creation. -| Input type | Resulting content-type | -| --------------------- | --------------------------- | -| `String` / `&str` | `text/plain; charset=utf-8` | -| `Vec` / `&[u8]` | `application/octet-stream` | -| `serde_json::Value` | `application/json` | -| `Body::empty()` | `text/plain; charset=utf-8` | +| Input type | Resulting content-type | +| ------------------- | --------------------------- | +| `String` / `&str` | `text/plain; charset=utf-8` | +| `Vec` / `&[u8]` | `application/octet-stream` | +| `serde_json::Value` | `application/json` | +| `Body::empty()` | `text/plain; charset=utf-8` | To send a response with a content-type that does not match automatic detection, set the `Content-Type` header explicitly on the response builder: -```rust use fastedge::body::Body; use fastedge::http::{Response, StatusCode}; @@ -286,7 +251,6 @@ let response = Response::builder() .header("content-type", "text/html; charset=utf-8") .body(Body::from(html)) .unwrap(); -``` --- @@ -294,9 +258,7 @@ let response = Response::builder() ### `send_request` -```rust,ignore pub fn send_request(req: http::Request) -> Result, Error> -``` Sends a synchronous outbound HTTP request to a backend service and returns the response. Available when using `#[fastedge::http]`. For async outbound requests with `#[wstd::http_server]`, use `wstd::http::Client` instead. @@ -308,7 +270,6 @@ Sends a synchronous outbound HTTP request to a backend service and returns the r - `Error::BindgenHttpError` — the host runtime rejected or failed the request. - `Error::InvalidBody` — the response body could not be decoded. -```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -328,9 +289,7 @@ fn main(_req: Request) -> Result> { .body(upstream_resp.into_body()) .map_err(Into::into) } -``` -```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -351,7 +310,6 @@ fn main(_req: Request) -> Result> { .body(Body::empty()) .map_err(Into::into) } -``` --- @@ -359,7 +317,6 @@ fn main(_req: Request) -> Result> { ### Error Enum -```rust,ignore #[derive(thiserror::Error, Debug)] pub enum Error { UnsupportedMethod(http::Method), @@ -368,19 +325,17 @@ pub enum Error { InvalidBody, InvalidStatusCode(u16), } -``` -| Variant | When it occurs | -| ----------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `UnsupportedMethod(http::Method)` | `send_request` was called with a method other than GET, POST, PUT, DELETE, HEAD, PATCH, or OPTIONS | -| `BindgenHttpError` | The host runtime returned an error during request execution | -| `HttpError(http::Error)` | An error occurred constructing or parsing an HTTP message | -| `InvalidBody` | The request or response body could not be encoded or decoded | -| `InvalidStatusCode(u16)` | A status code outside the range 100–599 was encountered | +| Variant | When it occurs | +| --------------------------------- | -------------------------------------------------------------------------------------------------- | +| `UnsupportedMethod(http::Method)` | `send_request` was called with a method other than GET, POST, PUT, DELETE, HEAD, PATCH, or OPTIONS | +| `BindgenHttpError` | The host runtime returned an error during request execution | +| `HttpError(http::Error)` | An error occurred constructing or parsing an HTTP message | +| `InvalidBody` | The request or response body could not be encoded or decoded | +| `InvalidStatusCode(u16)` | A status code outside the range 100–599 was encountered | `Error` implements `std::error::Error` and `std::fmt::Display`. It is compatible with `anyhow` and `?` propagation. -```rust use fastedge::{Error, send_request}; use fastedge::body::Body; use fastedge::http::{Method, Request}; @@ -395,30 +350,25 @@ fn fetch(uri: &str) -> Result { let resp = send_request(req)?; Ok(format!("status: {}", resp.status())) } -``` --- ## Feature Flags -| Flag | Default | Effect | -| ------------- | ---------- | ----------------------------------------------------------------------------------- | -| `proxywasm` | enabled | Enables the `fastedge::proxywasm` module for ProxyWasm ABI compatibility | -| `json` | disabled | Enables `Body::try_from(serde_json::Value)` and adds `serde_json` as a dependency | +| Flag | Default | Effect | +| ----------- | -------- | ---------------------------------------------------------------------------------- | +| `proxywasm` | enabled | Enables the `fastedge::proxywasm` module for ProxyWasm ABI compatibility | +| `json` | disabled | Enables `Body::try_from(serde_json::Value)` and adds `serde_json` as a dependency | Enable non-default features in `Cargo.toml`: -```toml [dependencies] -fastedge = { version = "0.3.5", features = ["json"] } -``` +fastedge = { version = "0.4.0", features = ["json"] } Disable the default `proxywasm` feature if you do not need it: -```toml [dependencies] -fastedge = { version = "0.3.5", default-features = false } -``` +fastedge = { version = "0.4.0", default-features = false } --- @@ -426,21 +376,19 @@ fastedge = { version = "0.3.5", default-features = false } `fastedge` re-exports the [`http`](https://crates.io/crates/http) crate as `fastedge::http`. All standard HTTP types are available through this path without adding `http` as a direct dependency. -```rust,ignore use fastedge::http::{Method, Request, Response, StatusCode, HeaderMap, Uri}; -``` **Supported HTTP methods** (the complete set accepted by `send_request`): -| Constant | Method | -| ------------------- | ----------- | -| `Method::GET` | `GET` | -| `Method::POST` | `POST` | -| `Method::PUT` | `PUT` | -| `Method::DELETE` | `DELETE` | -| `Method::HEAD` | `HEAD` | -| `Method::PATCH` | `PATCH` | -| `Method::OPTIONS` | `OPTIONS` | +| Constant | Method | +| ----------------- | --------- | +| `Method::GET` | `GET` | +| `Method::POST` | `POST` | +| `Method::PUT` | `PUT` | +| `Method::DELETE` | `DELETE` | +| `Method::HEAD` | `HEAD` | +| `Method::PATCH` | `PATCH` | +| `Method::OPTIONS` | `OPTIONS` | --- @@ -448,7 +396,6 @@ use fastedge::http::{Method, Request, Response, StatusCode, HeaderMap, Uri}; The FastEdge platform captures **stdout only**. Output written to `stderr` is silently discarded and will not appear in the platform's log viewer. Use `print!` / `println!` for all diagnostic output. Do not use `eprint!` / `eprintln!` — those produce no visible output on the platform. -```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -462,7 +409,6 @@ fn main(req: Request) -> Result> { .body(Body::empty()) .map_err(Into::into) } -``` --- diff --git a/docs/quickstart.md b/docs/quickstart.md index 418dda5..f372f84 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -10,26 +10,20 @@ Build and deploy edge computing applications that compile to WebAssembly using t Install both targets: -```bash rustup target add wasm32-wasip1 rustup target add wasm32-wasip2 -``` ## Create a New Project Create a new library crate: -```bash cargo new --lib my-edge-app cd my-edge-app -``` The crate must be compiled as a `cdylib`. Add to `Cargo.toml`: -```toml [lib] crate-type = ["cdylib"] -``` ## Option A: Async Handler (Recommended) @@ -37,18 +31,15 @@ The async handler uses the standard WASI-HTTP interface via the [`wstd`](https:/ Add dependencies to `Cargo.toml`: -```toml [dependencies] wstd = "0.6" anyhow = "1" [lib] crate-type = ["cdylib"] -``` Write the handler in `src/lib.rs`: -```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -61,20 +52,15 @@ async fn main(request: Request) -> anyhow::Result> { .header("content-type", "text/plain;charset=UTF-8") .body(Body::from(format!("Hello, you made a request to {url}")))?) } -``` Build targeting `wasm32-wasip2`: -```bash cargo build --target wasm32-wasip2 --release -``` To avoid passing `--target` on every build, add a `.cargo/config.toml` to your project: -```toml [build] target = "wasm32-wasip2" -``` Then `cargo build --release` is sufficient. @@ -86,18 +72,15 @@ The sync handler uses the `fastedge` crate directly. It is synchronous and suite Add dependencies to `Cargo.toml`: -```toml [dependencies] -fastedge = "0.3.5" +fastedge = "0.4.0" anyhow = "1" [lib] crate-type = ["cdylib"] -``` Write the handler in `src/lib.rs`: -```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -112,13 +95,10 @@ fn main(req: Request) -> Result> { .body(Body::from(format!("Hello, you made a request to {url}"))) .map_err(Into::into) } -``` Build targeting `wasm32-wasip1`: -```bash cargo build --target wasm32-wasip1 --release -``` The compiled `.wasm` file is written to `target/wasm32-wasip1/release/`. @@ -133,17 +113,15 @@ Both commands produce a `.wasm` binary in the respective `target//releas ## Feature Flags -| Feature | Default | Description | -| ----------- | ------- | ----------------------------------------- | -| `proxywasm` | yes | Enable ProxyWasm compatibility layer | -| `json` | no | Enable JSON body support via `serde_json` | +| Feature | Default | Description | +| ------------- | ------- | ----------------------------------------- | +| `proxywasm` | yes | Enable ProxyWasm compatibility layer | +| `json` | no | Enable JSON body support via `serde_json` | Enable the `json` feature in `Cargo.toml`: -```toml [dependencies] -fastedge = { version = "0.3.5", features = ["json"] } -``` +fastedge = { version = "0.4.0", features = ["json"] } ## CDN Apps diff --git a/llms.txt b/llms.txt index 00742d5..865b16d 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # fastedge -> Documentation for the `fastedge` crate (v0.3.5) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. +> Documentation for the `fastedge` crate (v0.4.0) — a Rust SDK for building edge computing applications that compile to WebAssembly and run on the FastEdge platform. ## Documentation From f39133483825a316bda429e4faf18d2694f54daa Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Sat, 23 May 2026 09:12:34 +0100 Subject: [PATCH 10/12] copilot --- fastedge-plugin-source/generate-docs.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh index 20a8f66..141db2c 100755 --- a/fastedge-plugin-source/generate-docs.sh +++ b/fastedge-plugin-source/generate-docs.sh @@ -286,11 +286,12 @@ PROMPT # the doc. The original is still saved to .failures/ so the prompt can # be tuned later. local stripped - # Fence-aware level-1-only salvage: skip lines inside ``` fences and only - # treat a lone `# ` heading (not `##`/`###` or `#include` lines) as the - # document start. This avoids false positives from sub-headings and - # code-fence-internal `#`-prefixed lines (e.g. shell comments, #include). - stripped=$(awk '/^```/ { in_fence = !in_fence; next } !in_fence && /^#[^#]/ { found=1 } found' "$tmpfile") + # Fence-aware level-1-only salvage: track ``` fences so that #-prefixed + # lines inside a fence (shell comments, #include, etc.) don't trigger the + # heading-detection scan. Fence delimiter lines are NOT skipped — they fall + # through to `found { print }` so fenced code blocks are preserved intact + # in the output. Only a bare `# ` heading outside a fence sets found=1. + stripped=$(awk '/^```/ { in_fence = !in_fence } !in_fence && /^#[^#]/ { found=1 } found' "$tmpfile") # Post-strip validation: confirm the salvaged output actually starts with a # level-1 heading. If awk matched something that looks like `#!` (shebang) From 0f14523190ac0cd67ffeec3b10731f4b5b519ef4 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Sat, 23 May 2026 09:16:23 +0100 Subject: [PATCH 11/12] docs up to date --- docs/CDN_APPS.md | 58 ++++++++++++++++++++++++++++++++++++++++++- docs/HOST_SERVICES.md | 38 ++++++++++++++++++++++++++++ docs/SDK_API.md | 54 ++++++++++++++++++++++++++++++++++++++++ docs/quickstart.md | 38 ++++++++++++++++++++++------ 4 files changed, 179 insertions(+), 9 deletions(-) diff --git a/docs/CDN_APPS.md b/docs/CDN_APPS.md index 03e5de2..cb4edb0 100644 --- a/docs/CDN_APPS.md +++ b/docs/CDN_APPS.md @@ -27,6 +27,7 @@ CDN apps come in two tiers depending on whether they need FastEdge host services **Tier 1 — Basic CDN app** (no FastEdge host services): +```toml [package] name = "my-cdn-app" version = "0.1.0" @@ -38,9 +39,11 @@ crate-type = ["cdylib"] [dependencies] proxy-wasm = "0.2" log = "0.4" +``` **Tier 2 — CDN app with FastEdge host services** (KV, secrets, dictionary): +```toml [package] name = "my-cdn-app" version = "0.1.0" @@ -51,7 +54,8 @@ crate-type = ["cdylib"] [dependencies] proxy-wasm = "0.2" -fastedge = { version = "0.3", features = ["proxywasm"] } +fastedge = { version = "0.4", features = ["proxywasm"] } +``` The `proxywasm` feature flag is required to access `fastedge::proxywasm::*`. Without it, `fastedge` only exposes Component Model APIs, which are not available in the proxy-wasm environment. @@ -59,6 +63,7 @@ The `proxywasm` feature flag is required to access `fastedge::proxywasm::*`. Wit A complete CDN app that adds a response header and logs each lifecycle phase: +```rust,no_run use log::info; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -108,10 +113,13 @@ impl HttpContext for HelloWorld { Action::Continue } } +``` ### Build +```sh cargo build --target wasm32-wasip1 --release +``` CDN apps and basic HTTP apps share the same build target: `wasm32-wasip1`. Only async WASI HTTP apps using `#[wstd::http_server]` target `wasm32-wasip2`. @@ -123,6 +131,7 @@ The proxy-wasm lifecycle is the core concept for CDN app development. Every CDN The `proxy_wasm::main!` macro initializes the filter. It sets the log level and registers the root context factory function. +```rust,no_run use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -132,13 +141,21 @@ proxy_wasm::main! {{ Box::new(MyAppRoot) }); }} +# struct MyAppRoot; +# impl proxy_wasm::traits::Context for MyAppRoot {} +# impl proxy_wasm::traits::RootContext for MyAppRoot {} +``` ### Root Context The root context is a singleton created once when the filter loads. Its primary role is to create a new HTTP context for each lifecycle callback invocation. +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; +# struct MyApp; +# impl Context for MyApp {} +# impl HttpContext for MyApp {} struct MyAppRoot; impl Context for MyAppRoot {} @@ -152,6 +169,7 @@ impl RootContext for MyAppRoot { Some(Box::new(MyApp)) } } +``` `get_type()` must return `Some(ContextType::HttpContext)` for HTTP traffic interception. `create_http_context` is called once per lifecycle callback invocation and receives a unique `context_id`. @@ -159,6 +177,7 @@ impl RootContext for MyAppRoot { The HTTP context is where request and response processing happens. A new instance is created for each lifecycle callback invocation — not once per request. See [Hook State Isolation](#hook-state-isolation) for the consequences this has on state management. +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; struct MyApp; @@ -175,6 +194,7 @@ impl HttpContext for MyApp { Action::Continue } } +``` Both `Context` and `HttpContext` must be implemented. The `Context` impl can be empty if no shared context callbacks are needed. @@ -201,6 +221,7 @@ Every lifecycle callback returns an `Action` that controls what happens next. For body callbacks, return `Action::StopIterationAndBuffer` until `end_of_stream` is `true`, then process the full body and return `Action::Continue`. +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -214,6 +235,7 @@ impl HttpContext for MyApp { Action::Continue } } +``` ### Hook State Isolation @@ -227,6 +249,7 @@ This has critical consequences for application design: To pass data between callbacks, use `self.set_property` and `self.get_property` with a custom property path. The host preserves these values across callback invocations for the same logical request: +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -246,11 +269,13 @@ impl HttpContext for MyApp { Action::Continue } } +``` ## Request and Response Manipulation ### Reading Headers and Properties +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -272,11 +297,13 @@ impl HttpContext for MyApp { Action::Continue } } +``` Properties return `Option>`. Most properties are UTF-8 strings; see the Request Properties section for encoding details. ### Modifying Headers +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -298,6 +325,7 @@ impl HttpContext for MyApp { Action::Continue } } +``` **Known limitation**: On the FastEdge CDN platform, passing `None` to `set_http_request_header` or `set_http_response_header` sets the header value to an empty string rather than removing the header entirely. When checking for header absence, test for an empty string as well as a missing value. @@ -305,6 +333,7 @@ impl HttpContext for MyApp { To short-circuit the request and respond directly to the client without forwarding to origin, call `send_http_response` and return `Action::Pause`. +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -323,6 +352,7 @@ impl HttpContext for MyApp { Action::Continue } } +``` `send_http_response` signature: `fn send_http_response(&self, status_code: u32, headers: Vec<(&str, &str)>, body: Option<&[u8]>)` @@ -355,6 +385,7 @@ Most properties are UTF-8 strings decoded with `std::str::from_utf8()`. The `res Geo-IP properties (`request.country`, `request.country.name`, `request.city`, `request.region`, `request.continent`, `request.geo.lat`, `request.geo.long`) are derived from the client IP address. +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -371,7 +402,9 @@ impl HttpContext for MyApp { Action::Continue } } +``` +```rust,no_run # use proxy_wasm::traits::*; # use proxy_wasm::types::*; # struct MyApp; @@ -390,6 +423,7 @@ impl HttpContext for MyApp { Action::Continue } } +``` ## Host Services for CDN Apps @@ -401,7 +435,9 @@ Provides persistent key-value storage. The API shape mirrors `fastedge::key_valu #### `Store` +```rust,ignore pub struct Store { /* ... */ } +``` | Method | Return Type | Description | | ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ | @@ -415,11 +451,13 @@ pub struct Store { /* ... */ } #### `Error` +```rust,ignore pub enum Error { NoSuchStore, AccessDenied, Other(String), } +``` | Variant | Description | | --------------- | ----------------------------------------------------------- | @@ -429,6 +467,7 @@ pub enum Error { #### Example — Bloom filter check in request headers phase +```rust,no_run use fastedge::proxywasm::key_value::Store; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -472,13 +511,16 @@ impl HttpContext for RateLimitFilter { } } } +``` ### Secret Management (`fastedge::proxywasm::secret`) Provides access to encrypted secrets stored in the FastEdge platform. +```rust,ignore pub fn get(key: &str) -> Result>, u32> pub fn get_effective_at(key: &str, at: u32) -> Result>, u32> +``` | Function | Return Type | Description | | -------------------------------------- | ------------------------------ | -------------------------------------------------- | @@ -493,6 +535,7 @@ Never log or expose secret values in application output. #### Example — JWT validation using a secret signing key +```rust,no_run use fastedge::proxywasm::secret; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -533,17 +576,21 @@ impl HttpContext for AuthFilter { Action::Continue } } +``` ### Dictionary (`fastedge::proxywasm::dictionary`) Provides read-only key-value lookups for configuration data. Values are returned as `String`. +```rust,ignore pub fn get(key: &str) -> Option +``` Returns `Some(value)` if the key exists and the value is valid UTF-8, `None` otherwise. #### Example — Reading upstream configuration +```rust,no_run use fastedge::proxywasm::dictionary; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -575,15 +622,19 @@ impl HttpContext for ConfigFilter { Action::Continue } } +``` ### Diagnostics (`fastedge::proxywasm::utils`) +```rust,ignore pub fn set_user_diag(value: &str) +``` Writes a diagnostic message visible in FastEdge platform logs. Panics if the host returns a non-zero status. Use for debugging and operational monitoring; do not log sensitive values. #### Example +```rust,no_run use fastedge::proxywasm::utils; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -616,11 +667,13 @@ impl HttpContext for DiagFilter { Action::Continue } } +``` ### Environment Variables CDN apps read non-secret configuration via `std::env::var()`. This works identically to HTTP apps — no proxy-wasm-specific API is involved. +```rust,no_run use std::env; use proxy_wasm::traits::*; use proxy_wasm::types::*; @@ -662,6 +715,7 @@ impl HttpContext for EnvFilter { Action::Continue } } +``` For sensitive configuration, use `fastedge::proxywasm::secret::get()` instead of environment variables. @@ -669,6 +723,7 @@ For sensitive configuration, use `fastedge::proxywasm::secret::get()` instead of CDN apps can write log output using `println!` or the `proxy_wasm::hostcalls::log` function: +```rust,no_run use proxy_wasm::hostcalls; use proxy_wasm::types::LogLevel; @@ -677,6 +732,7 @@ println!("Request received"); // Proxy-wasm log API (routes through the configured log level) hostcalls::log(LogLevel::Info, "Request received").ok(); +``` The `log` crate macros (`info!`, `warn!`, `error!`, etc.) work when `proxy_wasm::set_log_level()` is configured in the entry point, which routes them through the proxy-wasm log infrastructure. diff --git a/docs/HOST_SERVICES.md b/docs/HOST_SERVICES.md index ad007a0..5ce7587 100644 --- a/docs/HOST_SERVICES.md +++ b/docs/HOST_SERVICES.md @@ -12,11 +12,14 @@ The `fastedge::key_value` module provides persistent storage with support for si ### Opening a Store +```rust pub fn new() -> Result pub fn open(name: &str) -> Result +``` `Store::new()` opens the default store. `Store::open(name)` opens a named store. Both return `Err(Error::NoSuchStore)` if the store label is not recognized and `Err(Error::AccessDenied)` if the application is not authorized. +```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -33,13 +36,17 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from("ok"))?) } +``` ### Reading Values +```rust pub fn get(&self, key: &str) -> Result>, Error> +``` Returns `Ok(Some(bytes))` if the key exists, `Ok(None)` if it does not. +```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -57,10 +64,13 @@ async fn main(_request: Request) -> anyhow::Result> { .body(Body::from("not found"))?), } } +``` ### Pattern Scanning +```rust pub fn scan(&self, pattern: &str) -> Result, Error> +``` Scans the store for keys matching a glob-style pattern. Returns a list of matching key names. Returns an empty `Vec` if no keys match. @@ -72,6 +82,7 @@ Supported glob syntax: | `?` | Any single character | | `[abc]` | Any character in the set | +```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -89,13 +100,16 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("{} keys found", keys.len())))?) } +``` ### Sorted Sets Sorted sets store members associated with a `f64` score. Members are ordered from lowest to highest score. +```rust pub fn zrange_by_score(&self, key: &str, min: f64, max: f64) -> Result, f64)>, Error> pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Error> +``` `zrange_by_score` returns all members of the sorted set stored at `key` whose score falls in the inclusive range `[min, max]`. Use `f64::NEG_INFINITY` and `f64::INFINITY` for unbounded ranges. @@ -103,6 +117,7 @@ pub fn zscan(&self, key: &str, pattern: &str) -> Result, f64)>, Err Both return an empty `Vec` when the key does not exist or no members fall within the specified range or pattern. +```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -126,15 +141,19 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("{} top players", top_players.len())))?) } +``` ### Bloom Filters +```rust pub fn bf_exists(&self, key: &str, item: &str) -> Result +``` Tests whether `item` is a member of the bloom filter stored at `key`. Returns `true` if the item was probably added to the filter (subject to the false-positive rate of the filter), or `false` if the key does not exist or the item was definitely not added. Bloom filters cannot produce false negatives: if `bf_exists` returns `false`, the item has not been added. +```rust,no_run use fastedge::key_value::Store; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -155,6 +174,7 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } +``` ### Error Handling @@ -166,6 +186,7 @@ All `Store` methods return `Result<_, Error>`. The `Error` type has three varian | `Error::AccessDenied` | The application does not have permission to access the store | | `Error::Other(String)` | An implementation-specific error (I/O or internal host failure) | +```rust,no_run use fastedge::key_value::{Error, Store}; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -195,6 +216,7 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } +``` CDN apps use `fastedge::proxywasm::key_value` instead — see [CDN_APPS.md](CDN_APPS.md) for the ProxyWasm API surface and usage examples. @@ -206,10 +228,13 @@ The `fastedge::secret` module provides access to encrypted secrets such as API k ### Reading Secrets +```rust pub fn get(key: &str) -> Result>, Error> +``` Returns the currently effective value of the named secret. Returns `Ok(None)` if no secret with that name is configured. +```rust,no_run use fastedge::secret; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -232,15 +257,19 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } +``` ### Time-Based Retrieval +```rust pub fn get_effective_at(key: &str, at: u32) -> Result>, Error> +``` Returns the value of the named secret that was effective at the given Unix timestamp (`at`, seconds since epoch). This is useful during secret rotation: you can verify that both the old and new versions of a secret are accessible before completing a rotation. Returns `Ok(None)` if no version of the secret was configured at that time. +```rust,no_run use fastedge::secret; use std::time::{SystemTime, UNIX_EPOCH}; use wstd::http::body::Body; @@ -264,6 +293,7 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } +``` ### Security Notes @@ -282,12 +312,15 @@ The `fastedge::dictionary` module provides fast, read-only lookups for configura ### Configuration Lookups +```rust pub fn get(key: &str) -> Option +``` Returns `Some(value)` if the key exists and its value is valid UTF-8, or `None` if the key is not found or the value cannot be decoded as UTF-8. Dictionary values are environment variables set at deployment time via the platform — the same management mechanism as secrets, but without encryption. They are not writable from application code. +```rust,no_run use fastedge::dictionary; use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -308,6 +341,7 @@ async fn main(_request: Request) -> anyhow::Result> { upstream, timeout_ms )))?) } +``` ### When to Use Dictionary vs Key-Value vs Secrets @@ -332,12 +366,15 @@ The `fastedge::utils` module provides diagnostic functions for monitoring and de ### Diagnostics +```rust pub fn set_user_diag(value: &str) +``` Writes a diagnostic string that appears in the FastEdge platform logs associated with the current request. This is intended for debugging and operational monitoring. There is no return value; the function panics if the host rejects the call. The FastEdge platform captures only **stdout** for application log output. `stderr` is silently discarded and will not appear in the platform's log viewer. Use `println!` (or logging crates that write to stdout) for any output you need to observe. Do not use `eprintln!` — it produces no visible output on the platform. +```rust,no_run use fastedge::key_value::Store; use fastedge::utils::set_user_diag; use wstd::http::body::Body; @@ -362,6 +399,7 @@ async fn main(_request: Request) -> anyhow::Result> { .status(200) .body(Body::empty())?) } +``` One diagnostic message per request is the typical pattern. If `set_user_diag` is called multiple times, the platform may record only the last value or concatenate them depending on runtime behavior. diff --git a/docs/SDK_API.md b/docs/SDK_API.md index 28dbf85..9d60fdb 100644 --- a/docs/SDK_API.md +++ b/docs/SDK_API.md @@ -12,26 +12,31 @@ The current crate version is `0.4.0` (from `[workspace.package]` in the reposito For `#[wstd::http_server]` (recommended): +```toml [dependencies] wstd = "0.6" anyhow = "1" [lib] crate-type = ["cdylib"] +``` For `#[fastedge::http]` (basic): +```toml [dependencies] fastedge = "0.4.0" anyhow = "1" [lib] crate-type = ["cdylib"] +``` ### Minimal Handler The recommended handler for new applications uses `#[wstd::http_server]` (async, WASI-HTTP): +```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -42,25 +47,32 @@ async fn main(_request: Request) -> anyhow::Result> { .body(Body::from("Hello, FastEdge!"))?; Ok(response) } +``` ### Build For `#[wstd::http_server]` (recommended): +```bash rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 --release +``` To avoid passing `--target` on every build, add `.cargo/config.toml` to your project: +```toml [build] target = "wasm32-wasip2" +``` Then `cargo build --release` is sufficient. For `#[fastedge::http]` (basic): +```bash rustup target add wasm32-wasip1 cargo build --target wasm32-wasip1 --release +``` The output `.wasm` file is located at `target//release/.wasm`. @@ -74,7 +86,9 @@ Provided by the [`wstd`](https://crates.io/crates/wstd) crate. Registers an asyn The decorated function must match the following signature: +```rust,ignore async fn (request: Request) -> anyhow::Result> +``` **Requirements:** @@ -85,6 +99,7 @@ async fn (request: Request) -> anyhow::Result> **Example — echo handler:** +```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -95,9 +110,11 @@ async fn main(request: Request) -> anyhow::Result> { .status(200) .body(Body::from(format!("Method: {}", method)))?) } +``` **Example — outbound HTTP with `wstd::http::Client`:** +```rust,no_run use wstd::http::body::Body; use wstd::http::{Client, Request, Response}; @@ -110,17 +127,22 @@ async fn main(_request: Request) -> anyhow::Result> { let response = Client::new().send(upstream).await?; Ok(response) } +``` ### `#[fastedge::http]` (Basic) +```rust,ignore #[proc_macro_attribute] pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream +``` Provided by the `fastedge` crate. Registers a synchronous function as the HTTP request handler using the FastEdge-specific WIT interface. Use this for applications that require synchronous execution or the `fastedge::send_request` client. New projects should prefer `#[wstd::http_server]`. The decorated function must match the following signature: +```rust,ignore fn (req: fastedge::http::Request) -> anyhow::Result> +``` **Requirements:** @@ -135,6 +157,7 @@ If the function returns `Err(e)`, the macro converts it to an HTTP `500 Internal **Example — minimal handler:** +```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -146,9 +169,11 @@ fn main(_req: Request) -> Result> { .body(Body::from("Hello, FastEdge!")) .map_err(Into::into) } +``` **Example — method dispatch:** +```rust,no_run use anyhow::{anyhow, Result}; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -163,6 +188,7 @@ fn main(req: Request) -> Result> { _ => Err(anyhow!("method not allowed")), } } +``` ### Comparison @@ -179,7 +205,9 @@ fn main(req: Request) -> Result> { ## Body Type +```rust,ignore pub struct Body { /* private fields */ } +``` `fastedge::body::Body` wraps [`bytes::Bytes`](https://docs.rs/bytes) and carries a MIME content-type. The content-type is set at construction time based on the input data and cannot be changed after creation. @@ -196,13 +224,16 @@ pub struct Body { /* private fields */ } | `Body::empty()` | `text/plain; charset=utf-8` | Zero-length body | | `Body::try_from(value: serde_json::Value)` | `application/json` | Requires `json` feature; returns `Result` | +```rust use fastedge::body::Body; let text = Body::from("hello"); let owned = Body::from(String::from("hello")); let bytes = Body::from(vec![0x48u8, 0x69]); let empty = Body::empty(); +``` +```rust,ignore // json feature required use fastedge::body::Body; use serde_json::json; @@ -212,6 +243,7 @@ let body = Body::try_from(json!({"status": "ok"}))?; assert_eq!(body.content_type(), "application/json"); # Ok(()) # } +``` ### Methods @@ -222,12 +254,14 @@ assert_eq!(body.content_type(), "application/json"); All methods from `bytes::Bytes` are available via `Deref`: +```rust use fastedge::body::Body; let body = Body::from("hello"); assert_eq!(body.len(), 5); assert!(!body.is_empty()); let slice: &[u8] = &body[..]; +``` ### Content-Type Detection @@ -242,6 +276,7 @@ Content-type is determined at construction time and cannot be changed after crea To send a response with a content-type that does not match automatic detection, set the `Content-Type` header explicitly on the response builder: +```rust,no_run use fastedge::body::Body; use fastedge::http::{Response, StatusCode}; @@ -251,6 +286,7 @@ let response = Response::builder() .header("content-type", "text/html; charset=utf-8") .body(Body::from(html)) .unwrap(); +``` --- @@ -258,7 +294,9 @@ let response = Response::builder() ### `send_request` +```rust,ignore pub fn send_request(req: http::Request) -> Result, Error> +``` Sends a synchronous outbound HTTP request to a backend service and returns the response. Available when using `#[fastedge::http]`. For async outbound requests with `#[wstd::http_server]`, use `wstd::http::Client` instead. @@ -270,6 +308,7 @@ Sends a synchronous outbound HTTP request to a backend service and returns the r - `Error::BindgenHttpError` — the host runtime rejected or failed the request. - `Error::InvalidBody` — the response body could not be decoded. +```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -289,7 +328,9 @@ fn main(_req: Request) -> Result> { .body(upstream_resp.into_body()) .map_err(Into::into) } +``` +```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Method, Request, Response, StatusCode}; @@ -310,6 +351,7 @@ fn main(_req: Request) -> Result> { .body(Body::empty()) .map_err(Into::into) } +``` --- @@ -317,6 +359,7 @@ fn main(_req: Request) -> Result> { ### Error Enum +```rust,ignore #[derive(thiserror::Error, Debug)] pub enum Error { UnsupportedMethod(http::Method), @@ -325,6 +368,7 @@ pub enum Error { InvalidBody, InvalidStatusCode(u16), } +``` | Variant | When it occurs | | --------------------------------- | -------------------------------------------------------------------------------------------------- | @@ -336,6 +380,7 @@ pub enum Error { `Error` implements `std::error::Error` and `std::fmt::Display`. It is compatible with `anyhow` and `?` propagation. +```rust,no_run use fastedge::{Error, send_request}; use fastedge::body::Body; use fastedge::http::{Method, Request}; @@ -350,6 +395,7 @@ fn fetch(uri: &str) -> Result { let resp = send_request(req)?; Ok(format!("status: {}", resp.status())) } +``` --- @@ -362,13 +408,17 @@ fn fetch(uri: &str) -> Result { Enable non-default features in `Cargo.toml`: +```toml [dependencies] fastedge = { version = "0.4.0", features = ["json"] } +``` Disable the default `proxywasm` feature if you do not need it: +```toml [dependencies] fastedge = { version = "0.4.0", default-features = false } +``` --- @@ -376,7 +426,9 @@ fastedge = { version = "0.4.0", default-features = false } `fastedge` re-exports the [`http`](https://crates.io/crates/http) crate as `fastedge::http`. All standard HTTP types are available through this path without adding `http` as a direct dependency. +```rust,ignore use fastedge::http::{Method, Request, Response, StatusCode, HeaderMap, Uri}; +``` **Supported HTTP methods** (the complete set accepted by `send_request`): @@ -396,6 +448,7 @@ use fastedge::http::{Method, Request, Response, StatusCode, HeaderMap, Uri}; The FastEdge platform captures **stdout only**. Output written to `stderr` is silently discarded and will not appear in the platform's log viewer. Use `print!` / `println!` for all diagnostic output. Do not use `eprint!` / `eprintln!` — those produce no visible output on the platform. +```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -409,6 +462,7 @@ fn main(req: Request) -> Result> { .body(Body::empty()) .map_err(Into::into) } +``` --- diff --git a/docs/quickstart.md b/docs/quickstart.md index f372f84..f1041b1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -10,20 +10,26 @@ Build and deploy edge computing applications that compile to WebAssembly using t Install both targets: +```bash rustup target add wasm32-wasip1 rustup target add wasm32-wasip2 +``` ## Create a New Project Create a new library crate: +```bash cargo new --lib my-edge-app cd my-edge-app +``` The crate must be compiled as a `cdylib`. Add to `Cargo.toml`: +```toml [lib] crate-type = ["cdylib"] +``` ## Option A: Async Handler (Recommended) @@ -31,15 +37,18 @@ The async handler uses the standard WASI-HTTP interface via the [`wstd`](https:/ Add dependencies to `Cargo.toml`: +```toml [dependencies] wstd = "0.6" anyhow = "1" [lib] crate-type = ["cdylib"] +``` Write the handler in `src/lib.rs`: +```rust,no_run use wstd::http::body::Body; use wstd::http::{Request, Response}; @@ -52,15 +61,20 @@ async fn main(request: Request) -> anyhow::Result> { .header("content-type", "text/plain;charset=UTF-8") .body(Body::from(format!("Hello, you made a request to {url}")))?) } +``` Build targeting `wasm32-wasip2`: +```bash cargo build --target wasm32-wasip2 --release +``` To avoid passing `--target` on every build, add a `.cargo/config.toml` to your project: +```toml [build] target = "wasm32-wasip2" +``` Then `cargo build --release` is sufficient. @@ -72,15 +86,18 @@ The sync handler uses the `fastedge` crate directly. It is synchronous and suite Add dependencies to `Cargo.toml`: +```toml [dependencies] fastedge = "0.4.0" anyhow = "1" [lib] crate-type = ["cdylib"] +``` Write the handler in `src/lib.rs`: +```rust,no_run use anyhow::Result; use fastedge::body::Body; use fastedge::http::{Request, Response, StatusCode}; @@ -95,33 +112,38 @@ fn main(req: Request) -> Result> { .body(Body::from(format!("Hello, you made a request to {url}"))) .map_err(Into::into) } +``` Build targeting `wasm32-wasip1`: +```bash cargo build --target wasm32-wasip1 --release +``` The compiled `.wasm` file is written to `target/wasm32-wasip1/release/`. ## Build -| Handler path | Build command | -| ----------------------- | ---------------------------------------------- | -| Async (`wstd`) | `cargo build --target wasm32-wasip2 --release` | -| Sync (`fastedge::http`) | `cargo build --target wasm32-wasip1 --release` | +| Handler path | Build command | +| ----------------------- | ------------------------------------------------ | +| Async (`wstd`) | `cargo build --target wasm32-wasip2 --release` | +| Sync (`fastedge::http`) | `cargo build --target wasm32-wasip1 --release` | Both commands produce a `.wasm` binary in the respective `target//release/` directory. Neither path requires `cargo-component`. ## Feature Flags -| Feature | Default | Description | -| ------------- | ------- | ----------------------------------------- | -| `proxywasm` | yes | Enable ProxyWasm compatibility layer | -| `json` | no | Enable JSON body support via `serde_json` | +| Feature | Default | Description | +| ----------- | ------- | ----------------------------------------- | +| `proxywasm` | yes | Enable ProxyWasm compatibility layer | +| `json` | no | Enable JSON body support via `serde_json` | Enable the `json` feature in `Cargo.toml`: +```toml [dependencies] fastedge = { version = "0.4.0", features = ["json"] } +``` ## CDN Apps From 8e7a40f534a035555afc4a83a3205d661293d29a Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Sat, 23 May 2026 09:18:55 +0100 Subject: [PATCH 12/12] copilot instruction fix --- .github/copilot-instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ce9f661..dd40ac1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -62,6 +62,8 @@ Files in `docs/` are **machine-generated** from source code by `./fastedge-plugi | `src/proxywasm/dictionary.rs` | `docs/HOST_SERVICES.md` | | `src/proxywasm/utils.rs` | `docs/HOST_SERVICES.md` | | `src/proxywasm/` (CDN lifecycle, FFI) | `docs/CDN_APPS.md` | +| `examples/http/wasi/hello_world/src/lib.rs` | `docs/quickstart.md` | +| `examples/http/basic/hello_world/src/lib.rs` | `docs/quickstart.md` | | `Cargo.toml` (version, features) | `docs/INDEX.md` | | `fastedge-plugin-source/manifest.json` | `.github/copilot-instructions.md` |