diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md index 4c0c294..2400018 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # A/B Testing — FastEdge Example diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md index e4a5086..aa6fc7e 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # Authentication Patterns (JavaScript) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md index c3bc2f1..46d3c9c 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # FastEdge Cache — JavaScript Examples @@ -331,279 +331,3 @@ Validation errors (e.g., conflicting `WriteOptions`) are thrown synchronously. H - platform-overview (POP topology, request lifecycle) - deploy skill reference (fastedge-build CLI, binary upload) - best-practices (error handling patterns, response construction) - -## Source Material - -### FILE: examples/cache/src/index.ts - -```ts -// FastEdge Cache — flagship patterns -// -// This example demonstrates the three highest-value uses of the -// `fastedge::cache` module: -// -// 1. Per-IP rate limiting (atomic counters) -// 2. Origin-cache proxy (manual get/set with conditional caching) -// 3. JSON memoisation (getOrSet with a computed populator) -// -// All three patterns rely on the cache being: -// - **Strongly consistent within a POP** — atomic `incr` returns a -// correct count under concurrent load, which `fastedge::kv` cannot. -// - **Fast for both reads and writes** — sub-millisecond on the hot -// path, so caching is cheaper than recomputing or refetching. -// - **POP-local** — values do not replicate across data centers. -// This is acceptable (and often desirable) for transient state. - -import { Cache } from 'fastedge::cache'; - -// --------------------------------------------------------------------------- -// Pattern 1 — Rate limiting via atomic incr + expire -// --------------------------------------------------------------------------- -// -// Increment a per-IP counter. On the first hit (count === 1) we attach -// a TTL to create a fixed 60-second window anchored to that request: -// the counter resets 60 seconds after the user's *first* request, not -// after every request. -// -// `Cache.incr` is atomic: under concurrent load, two simultaneous -// requests cannot both see "count === 1" and double-set the expiry. -// This is the property that makes the cache suitable for limiting, -// quotas, locks, and other counter primitives. - -const RATE_LIMIT_MAX = 10; // Requests per window. -const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. - -async function rateLimit(event: FetchEvent): Promise { - // `event.client.address` is the trusted-edge client IP. Sourced from - // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by - // the FastEdge POP, not the client, so they're safe to key on. - const ip = event.client.address || 'unknown'; - - const key = `rl:${ip}`; - - const count = await Cache.incr(key); - - // Only set the expiry on the first hit of a new window. If we set it - // on every request, the window would never close — each new request - // would push the deadline another 60 seconds out. - if (count === 1) { - await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); - } - - if (count > RATE_LIMIT_MAX) { - return Response.json( - { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, - { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, - ); - } - - return Response.json({ - pattern: 'rate-limit', - ip, - count, - remaining: RATE_LIMIT_MAX - count, - windowSeconds: RATE_LIMIT_WINDOW_S, - }); -} - -// --------------------------------------------------------------------------- -// Pattern 2 — Origin-cache proxy with conditional caching -// --------------------------------------------------------------------------- -// -// Cache successful upstream responses for PROXY_TTL_S seconds; pass -// non-2xx and redirects through *without* caching, so a transient 404 -// or 500 doesn't get pinned for the rest of the window. The cache is -// a byte cache (no status/headers), so we only cache when "200 OK with -// application/octet-stream" is a faithful replay of the upstream. -// -// `getOrSet` is not used here because its populator can't signal -// "fetched, but don't cache" — we need that distinction to handle -// error responses safely. See Pattern 3 for `getOrSet` in a context -// where every populator output is cacheable. - -const PROXY_TTL_S = 30; - -async function proxy(url: string): Promise { - // Validate the URL before we use it as a cache key. - let parsed: URL; - try { - parsed = new URL(url); - } catch { - return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); - } - - // Strip the fragment: fetch() never sends it to the origin, so - // `https://example.com/#a` and `#b` are the same upstream resource - // and must share one cache entry. - parsed.hash = ''; - - const key = `proxy:${parsed.toString()}`; - - // Cache hit — replay the bytes as 200 OK. Status/headers from the - // original response are not preserved by the byte cache. - const cached = await Cache.get(key); - if (cached !== null) { - return new Response(await cached.arrayBuffer(), { - headers: { - 'content-type': 'application/octet-stream', - 'x-cache': 'hit', - 'x-cache-ttl': String(PROXY_TTL_S), - }, - }); - } - - // Cache miss — fetch upstream and only cache successful responses. - // Non-2xx and redirects flow through unchanged so callers see the - // real status code instead of a synthetic 200. - const upstream = await fetch(parsed.toString()); - if (!upstream.ok) { - return upstream; - } - - const bytes = await upstream.arrayBuffer(); - await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); - return new Response(bytes, { - headers: { - 'content-type': 'application/octet-stream', - 'x-cache': 'miss', - 'x-cache-ttl': String(PROXY_TTL_S), - }, - }); -} - -// --------------------------------------------------------------------------- -// Pattern 3 — JSON memoisation via getOrSet with a computed populator -// --------------------------------------------------------------------------- -// -// Same shape as the proxy pattern, but the populator does CPU work -// instead of network I/O. Use this whenever you compute the same -// expensive answer many times in a row — search index lookups, -// signed-token verification, derived report rollups, JSON -// transformations of slow-changing source data. -// -// We embed `generatedAt` in the result so a client refreshing the -// page can see the timestamp stay constant within the cache window -// and update once it expires. - -const MEMO_TTL_S = 60; - -async function memo(): Promise { - const entry = await Cache.getOrSet( - 'memo:report', - () => { - // Stand-in for "expensive computation". The populator can be - // synchronous or async — both are accepted. - const report = { - generatedAt: new Date().toISOString(), - topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ - name, - score: Math.round(Math.random() * 1000) / 10, - rank: i + 1, - })), - }; - // The populator returns the value to store. Because we want - // structured JSON back later, we serialise here and re-parse - // via `entry.json()` on read. - return JSON.stringify(report); - }, - { ttl: MEMO_TTL_S }, - ); - - // `entry.json()` parses the cached UTF-8 bytes as JSON. Use - // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. - const report = await entry.json(); - - return Response.json({ - pattern: 'memo', - note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, - report, - }); -} - -// --------------------------------------------------------------------------- -// Default landing — usage menu when no action is supplied -// --------------------------------------------------------------------------- - -function landing(): Response { - return Response.json({ - name: 'FastEdge Cache patterns', - actions: { - 'rate-limit': '/?action=rate-limit', - proxy: '/?action=proxy&url=https://www.example.com', - memo: '/?action=memo', - }, - }); -} - -// --------------------------------------------------------------------------- -// Router -// --------------------------------------------------------------------------- - -async function eventHandler(event: FetchEvent): Promise { - try { - const url = new URL(event.request.url); - const action = url.searchParams.get('action'); - - switch (action) { - case 'rate-limit': - return await rateLimit(event); - case 'proxy': - return await proxy(url.searchParams.get('url') ?? ''); - case 'memo': - return await memo(); - case null: - return landing(); - default: - return Response.json( - { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, - { status: 400 }, - ); - } - } catch (error: unknown) { - // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions - // fields) are synchronous; host errors arrive as Promise rejections. - // Both are caught by this single handler. - return Response.json({ error: (error as Error).message }, { status: 500 }); - } -} - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/cache/package.json - -```json -{ - "name": "fastedge-example-cache", - "version": "1.0.0", - "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", - "type": "module", - "scripts": { - "build": "fastedge-build -c" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` - -### FILE: examples/cache/tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2023", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "lib": ["ES2023"], - "types": ["@gcoredev/fastedge-sdk-js"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md index 4db84f6..7b5cf8c 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## fetch — Outbound HTTP Requests diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md index b0f32ea..dad6840 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## Example: Geo-Redirect diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md index 7add540..dc5cdf6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## Headers Example — FastEdge JS diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md index 7dd139b..61364a2 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hono-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # Hono Patterns on FastEdge (JavaScript/TypeScript) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md index eb54e64..76aae72 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## KV Store — Example Reference diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md index e9a2822..b61ea98 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-proxy-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,178 +250,3 @@ Returns: `ArrayBuffer | null` — `null` if the key does not exist. - examples/kv-store — KV-backed caching patterns - platform-overview reference — plan limits, execution budget, and body size constraints - sdk-reference-js reference — full KvStore API - -## Source Material - -### FILE: docs/PROXY_PATTERNS.md - -``` - - -# Proxy and Response Transform Patterns - -FastEdge HTTP apps can act as a thin proxy in front of an origin or upstream API — fetching a response, transforming it, and returning the result. This document covers the common proxy and transform patterns. - -## Simple Proxy - -Fetch from an upstream and return the body unchanged: - -```typescript -async function handle(request) { - const url = new URL(request.url); - const upstream = `https://backend.example.com${url.pathname}${url.search}`; - - const response = await fetch(upstream, { - method: request.method, - headers: request.headers, - body: request.method !== "GET" && request.method !== "HEAD" - ? await request.arrayBuffer() - : undefined, - }); - - return response; -} - -addEventListener("fetch", (event) => { - event.respondWith(handle(event.request)); -}); -``` - -`fetch()` returns a streamable `Response`; returning it directly forwards body chunks without buffering everything in memory. - -## Proxy with JSON Transform - -Modify the response body before returning it. This is the pattern from `examples/outbound-modify-response/`: - -```typescript -async function handle() { - const upstream = await fetch("https://jsonplaceholder.typicode.com/users"); - const users = await upstream.json(); - - const transformed = { - users: users.slice(0, 5), - total: 5, - skip: 0, - limit: 30, - }; - - return new Response(JSON.stringify(transformed), { - status: 200, - headers: { "content-type": "application/json" }, - }); -} - -addEventListener("fetch", (event) => { - event.respondWith(handle()); -}); -``` - -Reading `await upstream.json()` consumes the body; you cannot read it again. Read the body once, transform what you need, then return. - -## Hono Proxy with Transform - -Inside a Hono app, use `c.req.raw.headers` to forward the inbound headers and `c.req.arrayBuffer()` for the body: - -```typescript -import { Hono } from "hono"; - -const app = new Hono(); - -app.all("/api/*", async (c) => { - const url = new URL(c.req.url); - const upstream = `https://backend.example.com${url.pathname}${url.search}`; - - const response = await fetch(upstream, { - method: c.req.method, - headers: c.req.raw.headers, - body: c.req.method !== "GET" && c.req.method !== "HEAD" - ? await c.req.arrayBuffer() - : undefined, - }); - - const data = await response.json(); - data.processedAt = new Date().toISOString(); - return c.json(data, response.status); -}); - -addEventListener("fetch", (event) => { - event.respondWith(app.fetch(event.request)); -}); -``` - -## Header Manipulation in Proxies - -Strip hop-by-hop headers before forwarding to upstream, and add diagnostic headers on the way back: - -```typescript -async function handle(request) { - const upstreamHeaders = new Headers(request.headers); - // Hop-by-hop headers should not be forwarded - upstreamHeaders.delete("connection"); - upstreamHeaders.delete("keep-alive"); - upstreamHeaders.delete("transfer-encoding"); - - const upstream = await fetch("https://backend.example.com", { - method: request.method, - headers: upstreamHeaders, - }); - - const responseHeaders = new Headers(upstream.headers); - responseHeaders.set("X-Proxied-By", "FastEdge"); - - return new Response(upstream.body, { - status: upstream.status, - headers: responseHeaders, - }); -} -``` - -`new Response(upstream.body, ...)` streams the body through without reading it into memory — preferred for large responses. - -## Cache-aware Proxy with KV - -Cache upstream responses in the KV store to avoid repeated outbound calls. Note: KV is read-only from app code; writes happen via the portal/API: - -```typescript -import { KvStore } from "fastedge::kv"; - -async function handle(request) { - const url = new URL(request.url); - - try { - const cache = KvStore.open("api-cache"); - const cached = cache.get(url.pathname); - if (cached !== null) { - return new Response(cached, { - status: 200, - headers: { "content-type": "application/json", "x-cache": "hit" }, - }); - } - } catch { - // KV store unavailable — fall through to upstream fetch - } - - const upstream = await fetch(`https://backend.example.com${url.pathname}`); - return new Response(await upstream.arrayBuffer(), { - status: upstream.status, - headers: { ...Object.fromEntries(upstream.headers), "x-cache": "miss" }, - }); -} -``` - -`KvStore.open(name)` returns a `KvStoreInstance` (it does not return null) but can throw if the named store is not provisioned — wrap the open call in `try/catch`. `cache.get(key)` returns `ArrayBuffer | null`; check for `null`, not falsy, since an empty buffer is a valid value. - -## Operational Notes - -- **Outbound fetch budget.** Each invocation has a limited number of outbound requests (5 on Basic, 20 on Pro). Parallelise where possible with `Promise.all([...])` instead of sequential `await fetch(...)`. -- **Execution time budget.** Proxying upstream + transforming counts against the 50ms (Basic) / 200ms (Pro) execution budget. Slow upstreams will trip 532 timeouts. -- **Body size limits.** Inbound and outbound bodies are subject to the configured request/response size limits. Stream where possible rather than buffering with `arrayBuffer()` / `text()` / `json()`. - -## See Also - -- `examples/outbound-modify-response/` — JSON transform of upstream response -- `examples/outbound-fetch/` — basic outbound `fetch()` patterns -- `examples/headers/` — request/response header manipulation -- `examples/kv-store/` — KV-backed caching patterns - -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md index d97f55f..c37305a 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # FastEdge JS Runtime — Constraints & Compatibility diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md index 8daded8..8bd2b3e 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # FastEdge JavaScript Quickstart diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md index 05fe7e0..e52332f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-js.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # JavaScript SDK Reference (`@gcoredev/fastedge-sdk-js`) diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md b/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md index e260a0e..7912f54 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## fastedge-build CLI Reference diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-ts.md index c8ebe17..363e4cb 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md index 0be58fa..35e28bd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -13,8 +13,8 @@ app_type: http languages: [typescript, javascript] template_origin: http-base source_repo: https://github.com/G-Core/FastEdge-sdk-js -source_ref: 81145a9a43ec499240c687bd49376ab20c72b11c -updated: 2026-08-20 +source_ref: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 +updated: 2026-09-22 --- # Base Skeleton: HTTP TypeScript/JavaScript diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-ts.md index ba67275..eaa4402 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -176,3 +176,67 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide + +## Source Material + +### FILE: examples/bloom-filter-denylist/src/index.js + +```js +import { getEnv } from 'fastedge::env'; +import { KvStore } from 'fastedge::kv'; + +const BLOOM_KEY = 'blocked-ips'; + +function app(event) { + const storeName = getEnv('DENYLIST_STORE'); + if (!storeName) { + return Response.json( + { error: 'DENYLIST_STORE environment variable is not configured' }, + { status: 500 }, + ); + } + + const ip = event.client.address; + if (!ip) { + return Response.json({ error: 'client address unavailable' }, { status: 500 }); + } + + let blocked; + try { + const store = KvStore.open(storeName); + blocked = store.bfExists(BLOOM_KEY, ip); + } catch (error) { + return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); + } + + if (blocked) { + // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. + // Acceptable for a denylist (you over-block some legitimate users); not acceptable for + // allowlists or anything requiring exact membership — use KvStore.get() for that. + return Response.json({ allowed: false, ip }, { status: 403 }); + } + + return Response.json({ allowed: true, ip }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/bloom-filter-denylist/package.json + +```json +{ + "name": "fastedge-example-bloom-filter-denylist", + "version": "1.0.0", + "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md index 914ed37..c10ff23 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -227,7 +227,121 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` ## See Also -- `fastedge::kv` reference — globally replicated key/value storage (use when cross-POP visibility is required) -- `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface +- fastedge::kv reference — globally replicated key/value storage (use when cross-POP visibility is required) +- fastedge::cache full API reference — advanced patterns, streaming values, CacheEntry interface - http-base skeleton — base HTTP handler structure this feature extends - deploy skill reference — building and uploading the compiled WASM binary + +## Source Material + +### FILE: examples/cache-basic/src/index.js + +```js +// FastEdge Cache — basic operations +// +// The `fastedge::cache` module gives you a fast, data-center-scoped +// key/value store. Values written here are stored in the same point of +// presence (POP) that runs the worker, so reads and writes are very fast, +// and writes from one POP are not visible to others. +// +// Use this for transient, request-time state — short-lived caches, hit +// counters, rate limit windows, deduplicated work. For globally +// replicated storage, use the `fastedge::kv` module instead. +// +// This example demonstrates the four most common operations: +// +// GET /?action=set&key=foo&value=bar -> Cache.set +// GET /?action=get&key=foo -> Cache.get +// GET /?action=exists&key=foo -> Cache.exists +// GET /?action=delete&key=foo -> Cache.delete + +import { Cache } from 'fastedge::cache'; + +async function eventHandler(event) { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + const key = url.searchParams.get('key'); + + if (!key) { + throw new Error('Missing required query parameter: "key"'); + } + + switch (action) { + case 'set': { + // Cache.set writes a value under `key`. Accepts strings, + // ArrayBuffers, ArrayBufferViews, ReadableStreams, and Response + // objects (the body is consumed; status and headers are not stored). + // + // The `{ ttl: 60 }` option means "expire 60 seconds from now". You + // can also use `ttlMs` for sub-second precision, or `expiresAt` for + // a fixed Unix-epoch deadline. Omit options entirely for no expiry. + const value = url.searchParams.get('value') ?? ''; + await Cache.set(key, value, { ttl: 60 }); + return Response.json({ action, key, value, ttl: 60 }); + } + + case 'get': { + // Cache.get returns a CacheEntry on a hit, or `null` on a miss + // (key absent or expired). The cache stores raw bytes, so on read + // you choose how to decode using one of: + // entry.text() -> Promise (UTF-8) + // entry.json() -> Promise (parsed JSON) + // entry.arrayBuffer() -> Promise + const entry = await Cache.get(key); + if (entry === null) { + return Response.json({ action, key, hit: false }); + } + const value = await entry.text(); + return Response.json({ action, key, hit: true, value }); + } + + case 'exists': { + // Cache.exists is a cheap presence check — useful when you only + // need to know whether a key is set without transferring its value + // (e.g. idempotency-key checks, "have we seen this token?"). + const present = await Cache.exists(key); + return Response.json({ action, key, present }); + } + + case 'delete': { + // Cache.delete removes the entry. It is a no-op if the key is + // already absent — no error is thrown. + await Cache.delete(key); + return Response.json({ action, key, deleted: true }); + } + + default: + throw new Error( + `Unknown action: "${action}". Use one of: set, get, exists, delete.`, + ); + } + } catch (error) { + // Validation errors (e.g. wrong types, conflicting WriteOptions fields) + // are thrown synchronously; host errors (access denied, internal error) + // arrive as Promise rejections. Both are caught by this single handler. + return Response.json({ error: error.message }, { status: 500 }); + } +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); +``` + +### FILE: examples/cache-basic/package.json + +```json +{ + "name": "fastedge-example-cache-basic", + "version": "1.0.0", + "description": "FastEdge JS example: simple Cache set/get/exists/delete operations", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/cache-basic.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md index 4a59ac4..82141f5 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -315,3 +315,281 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise { + // `event.client.address` is the trusted-edge client IP. Sourced from + // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by + // the FastEdge POP, not the client, so they're safe to key on. + const ip = event.client.address || 'unknown'; + + const key = `rl:${ip}`; + + const count = await Cache.incr(key); + + // Only set the expiry on the first hit of a new window. If we set it + // on every request, the window would never close — each new request + // would push the deadline another 60 seconds out. + if (count === 1) { + await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); + } + + if (count > RATE_LIMIT_MAX) { + return Response.json( + { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, + { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, + ); + } + + return Response.json({ + pattern: 'rate-limit', + ip, + count, + remaining: RATE_LIMIT_MAX - count, + windowSeconds: RATE_LIMIT_WINDOW_S, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 2 — Origin-cache proxy with conditional caching +// --------------------------------------------------------------------------- +// +// Cache successful upstream responses for PROXY_TTL_S seconds; pass +// non-2xx and redirects through *without* caching, so a transient 404 +// or 500 doesn't get pinned for the rest of the window. The cache is +// a byte cache (no status/headers), so we only cache when "200 OK with +// application/octet-stream" is a faithful replay of the upstream. +// +// `getOrSet` is not used here because its populator can't signal +// "fetched, but don't cache" — we need that distinction to handle +// error responses safely. See Pattern 3 for `getOrSet` in a context +// where every populator output is cacheable. + +const PROXY_TTL_S = 30; + +async function proxy(url: string): Promise { + // Validate the URL before we use it as a cache key. + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); + } + + // Strip the fragment: fetch() never sends it to the origin, so + // `https://example.com/#a` and `#b` are the same upstream resource + // and must share one cache entry. + parsed.hash = ''; + + const key = `proxy:${parsed.toString()}`; + + // Cache hit — replay the bytes as 200 OK. Status/headers from the + // original response are not preserved by the byte cache. + const cached = await Cache.get(key); + if (cached !== null) { + return new Response(await cached.arrayBuffer(), { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'hit', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); + } + + // Cache miss — fetch upstream and only cache successful responses. + // Non-2xx and redirects flow through unchanged so callers see the + // real status code instead of a synthetic 200. + const upstream = await fetch(parsed.toString()); + if (!upstream.ok) { + return upstream; + } + + const bytes = await upstream.arrayBuffer(); + await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); + return new Response(bytes, { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'miss', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 3 — JSON memoisation via getOrSet with a computed populator +// --------------------------------------------------------------------------- +// +// Same shape as the proxy pattern, but the populator does CPU work +// instead of network I/O. Use this whenever you compute the same +// expensive answer many times in a row — search index lookups, +// signed-token verification, derived report rollups, JSON +// transformations of slow-changing source data. +// +// We embed `generatedAt` in the result so a client refreshing the +// page can see the timestamp stay constant within the cache window +// and update once it expires. + +const MEMO_TTL_S = 60; + +async function memo(): Promise { + const entry = await Cache.getOrSet( + 'memo:report', + () => { + // Stand-in for "expensive computation". The populator can be + // synchronous or async — both are accepted. + const report = { + generatedAt: new Date().toISOString(), + topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ + name, + score: Math.round(Math.random() * 1000) / 10, + rank: i + 1, + })), + }; + // The populator returns the value to store. Because we want + // structured JSON back later, we serialise here and re-parse + // via `entry.json()` on read. + return JSON.stringify(report); + }, + { ttl: MEMO_TTL_S }, + ); + + // `entry.json()` parses the cached UTF-8 bytes as JSON. Use + // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. + const report = await entry.json(); + + return Response.json({ + pattern: 'memo', + note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, + report, + }); +} + +// --------------------------------------------------------------------------- +// Default landing — usage menu when no action is supplied +// --------------------------------------------------------------------------- + +function landing(): Response { + return Response.json({ + name: 'FastEdge Cache patterns', + actions: { + 'rate-limit': '/?action=rate-limit', + proxy: '/?action=proxy&url=https://www.example.com', + memo: '/?action=memo', + }, + }); +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +async function eventHandler(event: FetchEvent): Promise { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'rate-limit': + return await rateLimit(event); + case 'proxy': + return await proxy(url.searchParams.get('url') ?? ''); + case 'memo': + return await memo(); + case null: + return landing(); + default: + return Response.json( + { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, + { status: 400 }, + ); + } + } catch (error: unknown) { + // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions + // fields) are synchronous; host errors arrive as Promise rejections. + // Both are caught by this single handler. + return Response.json({ error: (error as Error).message }, { status: 500 }); + } +} + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(eventHandler(event)); +}); +``` + + +### FILE: examples/cache/package.json + +```json +{ + "name": "fastedge-example-cache", + "version": "1.0.0", + "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", + "type": "module", + "scripts": { + "build": "fastedge-build -c" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` + + +### FILE: examples/cache/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md index 10afb0e..643e5bd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -260,3 +260,100 @@ Configure via the FastEdge secrets API or the manage skill before deploying. - WebCrypto API (crypto.subtle) — available as a global in the FastEdge JS runtime - http-base skeleton (base event listener and Response patterns) - FastEdge deploy skill (building and uploading WASM, configuring secrets) + +## Source Material + +### FILE: examples/crypto-hmac-jwt/src/index.js + +```js +import { getSecret } from 'fastedge::secret'; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function base64urlToBytes(str) { + const padded = str.replace(/-/gu, '+').replace(/_/gu, '/') + '='.repeat((4 - (str.length % 4)) % 4); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.codePointAt(i); + } + return bytes; +} + +async function verifyJwtHs256(token, secret) { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('malformed token: expected three segments'); + } + const [encodedHeader, encodedPayload, encodedSignature] = parts; + + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['verify'], + ); + + const signature = base64urlToBytes(encodedSignature); + const signedData = encoder.encode(`${encodedHeader}.${encodedPayload}`); + + const valid = await crypto.subtle.verify('HMAC', key, signature, signedData); + if (!valid) { + throw new Error('invalid signature'); + } + + const claims = JSON.parse(decoder.decode(base64urlToBytes(encodedPayload))); + + if (typeof claims.exp === 'number' && Math.floor(Date.now() / 1000) >= claims.exp) { + throw new Error('token expired'); + } + + return claims; +} + +async function app(event) { + const auth = event.request.headers.get('authorization') ?? ''; + const match = auth.match(/^Bearer\s+(.+)$/iu); + if (!match) { + return Response.json( + { ok: false, error: 'missing or malformed Authorization header' }, + { status: 401 }, + ); + } + + const secret = getSecret('JWT_SECRET'); + if (!secret) { + return Response.json({ ok: false, error: 'JWT_SECRET is not configured' }, { status: 500 }); + } + + try { + const claims = await verifyJwtHs256(match[1], secret); + return Response.json({ ok: true, claims }); + } catch (error) { + return Response.json({ ok: false, error: error.message }, { status: 401 }); + } +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/crypto-hmac-jwt/package.json + +```json +{ + "name": "fastedge-example-crypto-hmac-jwt", + "version": "1.0.0", + "description": "FastEdge JS example: verify HS256 JWTs with the Web Crypto API", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/crypto-hmac-jwt.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md index ae4a6d4..c24356c 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- type: feature diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-ts.md index ed691d5..1fba134 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-ts.md @@ -3,14 +3,14 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- type: feature app_type: http languages: [typescript, javascript] -capabilities: [geo-routing, geo-redirect] +capabilities: [geo-routing] base_skeleton: http-base source_example: FastEdge-sdk-js/examples/geo-redirect --- @@ -113,7 +113,7 @@ addEventListener('fetch', (event) => { - Environment variables are set in the Gcore dashboard or via the API when creating or updating the FastEdge app. - Country-specific origins are configured as environment variables using ISO 3166-1 alpha-2 codes. If no matching variable exists, `BASE_ORIGIN` is used as the fallback. - If `BASE_ORIGIN` is not set, the handler returns HTTP 500 with a descriptive error message. -- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `81145a9a43ec499240c687bd49376ab20c72b11c`). +- SDK dependency: `@gcoredev/fastedge-sdk-js` `^2.3.0` (as of source commit `9c8c7886f0d1ec5ac2296b4080805966a96ca817`). ## Source Material diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md index 0a587fb..dad0d40 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-basic-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-basic-ts.md index 54b3f87..108a855 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-basic-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-basic-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -127,3 +127,47 @@ Build script from `package.json`: - fastedge-sdk-js SDK reference - FastEdge app configuration (store attachment) - BUILD_CLI reference (fastedge-build options) + +## Source Material + +### FILE: examples/kv-store-basic/src/index.js + +```js +import { KvStore } from 'fastedge::kv'; + +async function eventHandler(event) { + try { + const myStore = KvStore.open('kv-store-name-as-defined-on-app'); + const entry = await myStore.getEntry('key'); + + if (entry === null) { + return new Response('Key not found', { status: 404 }); + } + + return new Response(`The KV Store responded with: ${await entry.text()}`); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(event)); +}); +``` + +### FILE: examples/kv-store-basic/package.json + +```json +{ + "name": "fastedge-example-kv-store-basic", + "version": "1.0.0", + "description": "FastEdge JS example: simple KV Store get operation", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/kv-store-basic.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md index 65a287d..4b23e8a 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/mcp-server-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/mcp-server-ts.md index 8a36751..055d200 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/mcp-server-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/mcp-server-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -291,7 +291,7 @@ Include verbatim: "@gcoredev/fastedge-sdk-js": "^2.3.0", "@hono/mcp": "^0.2.5", "@modelcontextprotocol/sdk": "^1.29.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "zod": "^4.3.6" ``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md index 8c88e7f..45cb96b 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/react-with-hono-server-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/react-with-hono-server-ts.md index 7eb3746..50dc4c2 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/react-with-hono-server-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/react-with-hono-server-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -35,7 +35,7 @@ Over `http-base`, add the following to `package.json`. **Runtime dependencies:** ```json "@gcoredev/fastedge-sdk-js": "^2.3.0", -"hono": "^4.12.25", +"hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" ``` @@ -43,7 +43,7 @@ Over `http-base`, add the following to `package.json`. **Dev dependencies:** ```json "@eslint/js": "^9.36.0", -"@hono/node-server": "^1.19.14", +"@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -125,13 +125,13 @@ Script semantics: }, "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25", + "hono": "^4.13.5", "react": "^19.1.1", "react-dom": "^19.1.1" }, "devDependencies": { "@eslint/js": "^9.36.0", - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^1.19.15", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/request-inspection-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/request-inspection-ts.md index d4f9186..2f1f255 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/request-inspection-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/request-inspection-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rotation-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rotation-ts.md index 5e3fdf9..4da7354 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rotation-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rotation-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -208,65 +208,3 @@ This shape is the diagnostic surface for verifying that rotation is working corr - http-base skeleton - deploy skill (for uploading secrets via the API before deploying) - manage skill (`secrets` subcommand for secret CRUD operations) - -## Source Material - -### FILE: examples/secret-rotation/src/index.js - -```js -import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; - -function app(event) { - const { request } = event; - - // Read the slot from the x-slot header, defaulting to the current unix timestamp. - // Slots can be interpreted either as indices (0, 1, 2...) or as unix timestamps; - // the host returns the value from the highest slot <= this number. - const slotHeader = request.headers.get('x-slot'); - const slot = - slotHeader !== null ? Number.parseInt(slotHeader, 10) : Math.floor(Date.now() / 1000); - - if (!Number.isFinite(slot) || slot < 0) { - return new Response('x-slot header must be a non-negative integer', { status: 400 }); - } - - const secretName = request.headers.get('x-secret-name') ?? 'TOKEN_SECRET'; - - const current = getSecret(secretName); - const effective = getSecretEffectiveAt(secretName, slot); - - const body = JSON.stringify({ - secret_name: secretName, - slot, - current, - effective_at_slot: effective, - is_same: current === effective, - }); - - return new Response(body, { - status: 200, - headers: { 'content-type': 'application/json' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/secret-rotation/package.json - -```json -{ - "name": "fastedge-example-secret-rotation", - "version": "1.0.0", - "description": "FastEdge JS example: slot-based secret retrieval for rotation", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/secret-rotation.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md index 5857dc3..dbd2a9b 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -312,7 +312,7 @@ footer a { "license": "ISC", "dependencies": { "@gcoredev/fastedge-sdk-js": "^2.3.0", - "hono": "^4.12.25" + "hono": "^4.13.5" }, "devDependencies": { "npm-run-all2": "^9.0.2" @@ -348,7 +348,7 @@ Additions over `http-base`: | Package | Type | Version | |---|---|---| | `@gcoredev/fastedge-sdk-js` | runtime | `^2.3.0` | -| `hono` | runtime | `^4.12.25` | +| `hono` | runtime | `^4.13.5` | | `npm-run-all2` | devDependency | `^9.0.2` | ## Key API Patterns diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md index 053da59..4d314c9 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -115,50 +115,3 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference - -## Source Material - -### FILE: examples/streaming/src/index.js - -```js -function app(event) { - const encoder = new TextEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { setTimeout(resolve, 200); }); - controller.enqueue(encoder.encode(`chunk ${i}\n`)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status: 200, - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/streaming/package.json - -```json -{ - "name": "fastedge-example-streaming", - "version": "1.0.0", - "description": "FastEdge JS example: streaming response with ReadableStream", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/streaming.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md index 04a1fa2..eb66618 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ab-testing-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,91 +202,3 @@ addEventListener('fetch', (event) => { - http-base skeleton (base event handler structure and build setup) - fastedge-build CLI reference (WASM compilation) - Handlebars documentation (template syntax and compilation API) - -## Source Material - -### FILE: examples/template-invoice-ab-testing/src/index.js - -```javascript -import Handlebars from 'handlebars'; - -import { getStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { getLogoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler({ request }) { - const isAbTestLogo = request.headers.get('ab-test-logo') === 'bottle'; - const logo = getLogoBrand(isAbTestLogo); - - const abTestFont = request.headers.get('ab-test-font'); - const cssStyles = getStyles(abTestFont); - - const rawHtmlTemplate = htmlTemplate(abTestFont); - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logo, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/template-invoice-ab-testing/package.json - -```json -{ - "name": "fastedge-example-template-invoice-ab-testing", - "version": "1.0.0", - "description": "FastEdge JS example: Handlebars invoice with A/B test header variants", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice-ab-testing.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ts.md index 36b8b19..8538296 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/template-invoice-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- @@ -202,86 +202,3 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference - -## Source Material - -### FILE: examples/template-invoice/src/index.js - -```js -import Handlebars from 'handlebars'; - -import { cssStyles } from './css-styles.js'; -import { htmlTemplate } from './html-template.js'; -import { logoBrand } from './logo.js'; - -const invoiceData = { - createdDate: 'March 4, 2024', - dueDate: 'April 19, 2024', - invoiceNumber: '1729', - recipientAddress: { - name: 'Homer Simpson', - address1: '742 Evergreen Terrace', - address2: 'Springfield, United States.', - }, - paymentMethod: 'PayPal', - paymentId: '8915648', - items: [ - { - description: '1x Keg of Duff Beer', - price: 250, - }, - { - description: '3x Crate of Duff Beer', - price: 85, - }, - { - description: '2x Duff Football Finger', - price: 20, - }, - ], -}; - -const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); - -async function eventHandler() { - const rawHtmlTemplate = htmlTemplate(); - - const template = Handlebars.compile(rawHtmlTemplate); - - const html = template({ - cssStyles, - logoBrand, - ...invoiceData, - totalPrice: getTotalPrice(invoiceData.items), - }); - - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler()); -}); -``` - -### FILE: examples/template-invoice/package.json - -```json -{ - "name": "fastedge-example-template-invoice", - "version": "1.0.0", - "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/template-invoice.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "handlebars": "^4.7.9" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md index 232d331..ccabded 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md b/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md index 270689c..cab5326 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> # fastedge-init CLI diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md b/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md index f5e4834..7f41d5e 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md @@ -3,8 +3,8 @@ sources: - id: fastedge-sdk-js ref: main - commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-20 + commit: 9c8c7886f0d1ec5ac2296b4080805966a96ca817 + updated: 2026-09-22 --> ## How It Works