Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions context/CONTEXT_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ Start here. Read only what your task requires.

### Post-Onboarding Cleanup

- **`sources.json`: fastedge-sdk-js and fastedge-sdk-rust refs are temporarily `"main"`** — changed from `"latest-release"` during onboarding because the `fastedge-plugin-source/` contracts didn't exist at the latest tagged releases. Once contracts are merged and new releases are cut, reset both to `"latest-release"` so doc syncs are release-gated.
- **`sources.json`: proxy-wasm-sdk-as ref is `"latest-release"`** — added April 2026. Pipeline test (AS-07) pending. Deploy workflow dispatch (AS-09) not yet wired.
- **`sources.json`: all SDK repos + fastedge-test are on `"latest-release"`** (Sep 2026) — every latest release now contains `fastedge-plugin-source/`, so doc syncs are release-gated. fastedge-templates stays on `"main"` (no releases; the portal deploys from main).

### MCP Integration — Planned (after 002-scaffold-redesign)

Expand All @@ -114,6 +113,4 @@ Build/deploy delegation to FastEdge-mcp-server. Phase 1 (delegate build + deploy
- Intent skills with hierarchical inheritance: root bases → appType bases → per-example files. 4th intent directory added for proxy-wasm-sdk-as (20 files).
- Pipeline PRs merged for all 3 source repos (fastedge-test #35, fastedge-sdk-js #34, fastedge-sdk-rust #33/#36). proxy-wasm-sdk-as pending first run (AS-07).
- Blueprint format contract: `specs/002-scaffold-redesign/contracts/blueprint-format.md`
- Dual-intent manifest pattern: `specs/002-scaffold-redesign/contracts/manifest-dual-intent.md`

**Note:** `sources.json` refs for fastedge-sdk-js and fastedge-sdk-rust are still `"main"` — reset to `"latest-release"` once new releases are cut.
- Dual-intent manifest pattern: `specs/002-scaffold-redesign/contracts/manifest-dual-intent.md`
3 changes: 3 additions & 0 deletions plugins/gcore-fastedge-codex/skills/fastedge-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Answer FastEdge questions with high precision and low token usage using local in
- Before proposing to hand-build a capability from scratch, check for a matching `templates`
topic in the index first — it may already exist as a maintained, ready-to-deploy bolt-on
installed from the Gcore portal, not something to scaffold.
- Before designing, writing, or reviewing a CDN app (proxy-wasm filter), read the
`platform-cdn-filter-runtime` topic. Before designing anything that uses KV or Cache, read
`platform-storage`. Both record host behaviour that local tests cannot reveal.

## Output rules

Expand Down
4 changes: 3 additions & 1 deletion plugins/gcore-fastedge/skills/fastedge-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ The reference directory is organised in two layers:

- `./reference/platform/overview.md` — Architecture, PoPs, app types, request lifecycle, resource limits
- `./reference/platform/error-codes.md` — 530–533 debugging strategies
- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), public-route disable pattern
- `./reference/platform/cdn-filter-runtime.md` — **MANDATORY before designing, writing, or reviewing any CDN app (proxy-wasm filter).** Filter-vs-HTTP-app capability boundary, which request properties/headers are reliable, path non-normalisation (auth-bypass risk), filters-before-cache, what local tests cannot prove.
- `./reference/platform/storage.md` — **Read before designing anything that uses KV or Cache.** KV per-node read caching, write API shape, missing operations, Cache TTL/`incr` rules, failure rate, measured performance, preprod-vs-production.
- `./reference/platform/cdn-integration.md` — How CDN apps attach to CDN resources via `options.fastedge`, lifecycle hook configuration, ruleset-based path overrides (replace-not-merge), API merge semantics, purge after rule changes, public-route disable pattern
- `./reference/platform/operations.md` — Operational knobs with time-bounded behaviour (the 30-min `debug` logging toggle)
- `./reference/platform/best-practices.md` — Agent-quality guidance: confirmation discipline, scaffold-first, TDD loop, resource preconditions, observation vs. request, ask-don't-guess
- `./reference/platform/as-constraints.md` — **MANDATORY before writing or reviewing any AssemblyScript CDN app code.** Hard compile-time and runtime constraints where AssemblyScript diverges from TypeScript. Violating these produces wasm that traps at runtime or silently returns wrong values — not a compiler error.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# CDN Filter Runtime — Capabilities and Traps

What a proxy-wasm filter (CDN app) can and cannot do on FastEdge, and the host behaviours that silently differ from generic proxy-wasm or from HTTP apps. **Read this before designing or reviewing a CDN app** — most items here are invisible in local tests and only surface on a deployed app.

Every claim carries its provenance:

| Tag | Meaning |
|---|---|
| `[live]` | Measured against a deployed app on a real CDN resource |
| `[source]` | Read from the FastEdge runtime or SDK source |

Last verified: 2026-08-25 (SDK surface re-checked 2026-09-23).

---

## Capability boundary — filter vs HTTP app

A filter is a core wasm module linked against the proxy-wasm `env` ABI. It is a different execution world from an HTTP app, not a subset of it. Designs that assume symmetry with the HTTP-app SDK fail at deploy.

| Capability | HTTP app | CDN filter |
|---|---|---|
| KV read | ✅ | ✅ same six operations: `open · get · scan · zrange_by_score · zscan · bf_exists` `[source]` |
| KV write | ❌ (API only — see [storage.md](./storage.md)) | ❌ (API only) |
| Cache (`get/set/delete/exists/incr/expire/purge/purgePrefix`) | ✅ | ✅ host-supported; **SDK support varies — check the SDK reference for your language** |
| Secrets / dictionary | ✅ | ✅ via the SDK's proxy-wasm module |
| Outbound `fetch` | ✅ | proxy-wasm `http_call` only |
| Deferred / background work | JS `waitUntil` only (not a timer) | ❌ none |
| State between requests | ❌ | ❌ — consecutive requests hit different nodes `[live]` |

### Use only the SDK's proxy-wasm modules

HTTP-app host APIs are component-model (WIT) imports. A proxy-wasm host cannot resolve them, so **one reference anywhere in a filter makes the app fail to instantiate — HTTP 530 on every path**, including paths that never reach the call `[live]`. It compiles and links with no warning.

- **Rust:** in a filter import only from `fastedge::proxywasm::*`. Never use a top-level `fastedge::` module (`fastedge::cache`, `fastedge::secret`, …) — each has a `fastedge::proxywasm::` counterpart.
- **Check before deploying** — any `gcore:fastedge/...` import in a filter build means a 530 at runtime:
```bash
wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u
```

---

## Reading the request — use properties, not headers

Several headers a proxy-wasm developer would reach for are mangled or absent. **All failures are silent** `[live]`.

| Read via | Result |
|---|---|
| property `request.host` | ✅ clean client-requested host (before any origin host rewrite) on the edge tier; on shield nodes it may carry a `shield_` prefix |
| property `request.x_real_ip` | ✅ true client IP, IPv4 and IPv6 |
| property `request.path` | ⚠️ **includes the query string** — see below |
| property `request.query` | ✅ query without `?`; not set by the host when there is no query (how "not set" surfaces — `None`, empty buffer — depends on the SDK) |
| property `request.asn` / `.country` / `.city` / `.region` / `.continent` | ✅ |
| property `source.address` | ❌ does not exist (Envoy's name) — returns nothing |
| header `host` | ❌ mangled to `<domain>_cache_sharded` |
| headers `:path`, `:authority` | ❌ absent |
| headers `x-real-ip`, `x-forwarded-for`, `cookie`, `user-agent` | ✅ intact (`cookie` is visible even with `ignore_cookie: true`) |

### `request.path` includes the query string

```
GET /x?a=1&b=2
request.path = "/x?a=1&b=2"
request.query = "a=1&b=2"
```

Prefix checks are unaffected; **equality checks silently fail on any request with a query** — auth callbacks are exactly where equality checks live. Split on `?` first. Corollary: because it is path+query, `request.path` works directly as a relative `Location` for a same-origin redirect, which avoids the mangled `host` header.

---

## Security — gating filters

### The CDN does not normalise the path before the filter runs

`[live]` Traversal and separator sequences arrive verbatim:

```bash
curl --path-as-is ".../app/x/../test" # request.path = "/app/x/../test"
curl --path-as-is ".../app//test" # request.path = "/app//test"
```

A filter that bypasses auth on a prefix (`/auth/`, `/public/`) will wave through `/auth/../admin`, which the origin may normalise back to `/admin`. **That is an authentication bypass**, and it is the natural way to write the code.

Reject suspicious sequences before any prefix bypass, and **fail closed** — fall through to the auth check. Never normalise the path yourself and proceed; you will not match the origin's rules.

```rust
fn has_suspicious_sequence(path: &str) -> bool {
if path.contains("..") || path.contains("//") || path.contains('\\') { return true; }
let lower = path.to_ascii_lowercase();
lower.contains("%2e") || lower.contains("%2f")
|| lower.contains("%5c") || lower.contains("%25") // %25 catches double-encoding
}
```

### Filters run before the CDN cache lookup

`[live, edge tier only]` A request-phase filter runs on **every** request, including cache hits.

- **Security:** a cached response cannot bypass a request-phase gate.
- **Cost:** every request pays for whatever the filter does — KV reads, cache ops — even on cache hits.
- A `no-store` response from a filter is not cached. Varying the cache key on a session cookie fragments the cache per visitor.

Only `execute_on_edge` was measured; ordering on the shield tier is unverified.

---

## Synthetic responses

`[live]` A filter can serve a complete response via `send_http_response`: any status, arbitrary headers, multiple `Set-Cookie` headers on one response, and bodies up to at least 1 MB without truncation. Single-app designs (challenge pages, interstitials, error pages) do not need a second HTTP app.

---

## What local testing cannot prove

`@gcoredev/fastedge-test` runs proxy-wasm filters locally and proves **flow logic** — routing, header manipulation, gate decisions. It cannot prove **host behaviour**: property semantics, header mangling, path non-normalisation, instantiation failures, cache interaction. Every trap on this page passes a local suite green. Confirm host-dependent behaviour on a deployed app attached to a real CDN resource.

---

## See Also

- [storage.md](./storage.md) — KV and Cache semantics, limits, measured performance
- [cdn-integration.md](./cdn-integration.md) — attaching filters to CDN resources
- [error-codes.md](./error-codes.md) — 530 diagnosis
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,25 @@ curl -s -X PATCH "https://api.gcore.com/cdn/resources/<resource-id>" \
}'
```

**`PATCH /cdn/resources/{id}` merges `options`** — keys you omit (e.g. `hostHeader`) are kept `[live]`. Do not confuse this with the rule-vs-resource replace semantics above, which govern request-time override, not the API.

Rules are managed through their own endpoints under `/cdn/resources/{resource_id}/rules`. The full rule API is in the Gcore CDN documentation.

### Cached content after a routing change

Request-phase filters run before the cache lookup (see [cdn-filter-runtime.md](./cdn-filter-runtime.md)), so attaching or changing an `on_request_headers` filter takes effect on cached paths without a purge. What does **not** change on its own is content already cached from the previous configuration — e.g. after a rule that points a path at a different origin, or a change to response-phase output. That content keeps being served until it expires (observed ~15 min).

In that case, purge only the affected paths, then verify:

```bash
curl -s -X POST "https://api.gcore.com/cdn/resources/<resource-id>/purge" \
-H "Authorization: APIKey $GCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"paths": ["/affected/path/*"]}'
```

`{"paths": []}` purges **the entire resource** and sends all traffic back to origin — use it only with explicit user confirmation. The FastEdge MCP server's default access policy blocks this endpoint; when it is blocked, give the user the command to run themselves.

## Operational Notes

- **`app_id` is a string.** The FastEdge apps API (`GET /fastedge/v1/apps`) returns `id` as an integer, but in the `options.fastedge.<hook>.app_id` field it must be quoted as a string. Mixing types is a common source of validation errors.
Expand All @@ -191,7 +208,7 @@ Rules are managed through their own endpoints under `/cdn/resources/{resource_id
- **Hook absence vs. `enabled: false`.** Both result in the hook not running. Removing the key entirely is cleaner; toggling `enabled: false` preserves the hook's other config for easy re-enable.
- **`execute_on_shield` only matters when origin shielding is on.** If `shielded: false` on the resource, the shield-layer setting has no effect — the hook only ever runs at the edge.
- **Rule weight ordering.** When multiple rules match a request path, only the rule with the lowest weight applies. Rules don't compose.
- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update.
- **Changes propagate.** CDN configuration changes can take a few minutes to propagate to all PoPs. The resource may briefly be in `status: "processed"` after an update. Content cached under the old configuration may also need a path-scoped purge (above).

## See Also

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco

**Meaning:** The Wasm module failed to start up before processing the request.

**Signal:** a CDN app that fails to instantiate also returns `x-cdn-internal-status: 3100`. That header distinguishes "failed to start" from "the app ran and errored" (531).

**Common Causes:**
- **Unresolved imports (CDN apps)** — the binary imports a host function the proxy-wasm host does not provide, most commonly an HTTP-app (component-model) API used in a filter, e.g. top-level `fastedge::cache` in Rust instead of `fastedge::proxywasm::cache`. **Every path returns 530**, including paths that never reach the call, and the build gives no warning. See [cdn-filter-runtime.md](./cdn-filter-runtime.md).
- Missing required environment variables that the app reads during initialization
- Corrupted or invalid Wasm binary
- Binary was compiled for the wrong target (not `wasm32-wasip1`)
Expand All @@ -21,9 +24,15 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco
- JS: `fastedge-build ./src/index.js ./<name>.wasm` completed without errors
- Rust: `cargo build --release --target wasm32-wasip1` succeeded
2. Check binary size: `ls -la *.wasm` or `ls -la target/wasm32-wasip1/release/*.wasm`
3. Test locally: `fastedge-run http -w ./app.wasm --port 8080`
4. Verify all required env vars are set via API or portal
5. Re-upload the binary and update the app
3. **CDN apps:** inspect the import table — any `gcore:fastedge/...` import in a proxy-wasm build is unresolvable:
```bash
wasm-tools print app.wasm | grep -o '(import "[^"]*"' | sort -u
```
4. Run locally:
- HTTP apps: `fastedge-run http -w ./app.wasm --port 8080`
- CDN apps: `fastedge-run` does not run proxy-wasm — use `@gcoredev/fastedge-test`. Note a local run does not reproduce host instantiation, so a clean local run does not rule out step 3.
5. Verify all required env vars are set via API or portal
6. Re-upload the binary and update the app

---

Expand All @@ -32,6 +41,7 @@ FastEdge returns specific HTTP status codes (530-533) when the Wasm runtime enco
**Meaning:** The app started successfully but threw an unhandled exception during request processing.

**Common Causes:**
- Calling `getEnv()` / `getSecret()` at module top level (JS) — they are request-time only; call them inside the handler
Comment thread
godronus marked this conversation as resolved.
- Uncaught JavaScript exception (TypeError, ReferenceError, etc.)
- Rust panic (`unwrap()` on `None` or `Err`)
- Failed `fetch()` call without error handling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ What is appropriate: per-end-user or per-session key prefixes within your app's
5. **Response Delivery** — Response sent back to client
6. **Cleanup** — Wasm instance destroyed, memory freed

### No scheduled or deferred work

There is no scheduler or durable background execution. Timers exist within a request (e.g. JS `setTimeout`) but live only as long as that instance. JS `event.waitUntil()` extends the instance until a promise settles, starting immediately after the response — `waitUntil(sleep(30_000).then(work))` just pins one instance per request for 30 s. Rust has no post-response work API. Rewrite "do X in L seconds" as "on each request, do X if L has elapsed"; periodic jobs need an external cron.

### PoP identity

`getEnv("dc")` returns the PoP's lowercase short code (e.g. `ls1`, `am3`) — the only PoP identifier available to an app. **It can be empty**; an empty-string fallback silently merges every PoP's keys into one namespace. Fail loudly instead. Node count per PoP varies, and the number of active PoPs is not discoverable from inside an app.

## Resource Limits

| Resource | Basic Plan | Pro Plan |
Expand Down
Loading
Loading