From 7a1139bef4f19a202668bceed487d86d8b76f0ca Mon Sep 17 00:00:00 2001 From: "fastedge-plugin-sync[bot]" Date: Thu, 20 Aug 2026 12:51:05 +0000 Subject: [PATCH] auto: update reference docs from fastedge-sdk-js (main) --- .../reference/http/examples-ab-testing-js.md | 31 +- .../reference/http/examples-auth-js.md | 2 +- .../reference/http/examples-cache-js.md | 278 ++++++++++++++++- .../reference/http/examples-fetch-js.md | 2 +- .../http/examples-geo-redirect-js.md | 2 +- .../reference/http/examples-headers-js.md | 2 +- .../reference/http/examples-hono-js.md | 2 +- .../reference/http/examples-kv-store-js.md | 2 +- .../reference/http/examples-proxy-js.md | 177 ++++++++++- .../fastedge-docs/reference/js-runtime.md | 2 +- .../fastedge-docs/reference/quickstart-js.md | 2 +- .../reference/sdk-reference-js.md | 2 +- .../skills/scaffold/reference/build-cli.md | 2 +- .../scaffold/reference/http/ab-testing-ts.md | 2 +- .../skills/scaffold/reference/http/base-ts.md | 4 +- .../http/bloom-filter-denylist-ts.md | 66 +--- .../scaffold/reference/http/cache-basic-ts.md | 116 +------- .../scaffold/reference/http/cache-ts.md | 280 +---------------- .../reference/http/crypto-hmac-jwt-ts.md | 99 +----- .../scaffold/reference/http/fetch-ts.md | 2 +- .../reference/http/geo-redirect-ts.md | 2 +- .../scaffold/reference/http/headers-ts.md | 2 +- .../reference/http/kv-store-basic-ts.md | 46 +-- .../scaffold/reference/http/kv-store-ts.md | 2 +- .../scaffold/reference/http/mcp-server-ts.md | 281 +----------------- .../http/outbound-modify-response-ts.md | 48 +-- .../http/react-with-hono-server-ts.md | 2 +- .../reference/http/request-inspection-ts.md | 2 +- .../reference/http/secret-rotation-ts.md | 73 ++++- .../reference/http/static-assets-ts.md | 2 +- .../scaffold/reference/http/streaming-ts.md | 49 ++- .../http/template-invoice-ab-testing-ts.md | 2 +- .../reference/http/template-invoice-ts.md | 85 +++++- .../http/variables-and-secrets-ts.md | 2 +- .../skills/scaffold/reference/init-cli.md | 2 +- .../skills/scaffold/reference/static-sites.md | 2 +- 36 files changed, 687 insertions(+), 990 deletions(-) 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 d1e2c6e..4c0c294 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # A/B Testing — FastEdge Example @@ -208,9 +208,9 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **Empty cookie after strip**: If `x-fastedge-abid` was the only cookie, the `cookie` header is deleted entirely rather than set to an empty string. - **Last variant not assigned**: If `xid * 100` falls exactly at or beyond the sum of all normalized percentages (due to floating-point rounding), no variant header is set for that test. In practice this is extremely rare given `xid` is always `< 1`. -## Source Material +--- -### FILE: examples/ab-testing/src/index.js +## Source ```js import { getEnv } from 'fastedge::env'; @@ -232,8 +232,6 @@ async function eventHandler({ request }) { const headers = createAbTestHeaders(slicedHeaders, testConfig, xid); - // This is the URL of the outbound service - i.e. could be a url to your origin - // e.g. https://template-invoice-ab-test-123456.fastedge.cdn.gc.onl/ const outboundUrl = getEnv('OUTBOUND_URL'); if (!outboundUrl || !String(outboundUrl).trim()) { return new Response('OUTBOUND_URL environment variable is not configured', { @@ -243,7 +241,6 @@ async function eventHandler({ request }) { const response = await fetch(outboundUrl, { headers }); - // Request/Response Headers are immutable, so we need to create a new Headers object const resHeaders = new Headers(response.headers); resHeaders.set( 'set-cookie', @@ -261,13 +258,10 @@ addEventListener('fetch', (event) => { }); const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { - // Request/Response Headers are immutable, so we need to create a new Headers object const headers = new Headers(reqHeaders); const cookie = headers.get('cookie') || ''; - // Read the existing `xid` cookie value. const xid = (cookie.match(/(?:^|;) *x-fastedge-abid=((0|1|)\.\d+) *(?:;|$)/u) || [])[1]; if (xid) { - // Request contains A/B cookie, hide it from the origin const newCookie = cookie.replace(/x-fastedge-abid=[^;]+;?\s*/gu, ''); if (newCookie) { headers.set('cookie', newCookie); @@ -277,7 +271,6 @@ const sliceAbTestIdFromCookie = ({ headers: reqHeaders }) => { return [xid, headers]; } const randomXid = `${Math.random()}`.slice(1, 5); - // Request does not contain A/B cookie, return random number return [randomXid, headers]; }; @@ -307,21 +300,3 @@ const createAbTestHeaders = (reqHeaders, testConfig, xid) => { return headers; }; ``` - -### FILE: examples/ab-testing/package.json - -```json -{ - "name": "fastedge-example-ab-testing", - "version": "1.0.0", - "description": "FastEdge JS example: cookie-based A/B testing", - "type": "module", - "main": "src/index.js", - "scripts": { - "build": "fastedge-build src/index.js dist/ab-testing.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-auth-js.md index 7993351..e4a5086 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 141ee81..c3bc2f1 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # FastEdge Cache — JavaScript Examples @@ -331,3 +331,279 @@ Validation errors (e.g., conflicting `WriteOptions`) are thrown synchronously. H - platform-overview (POP topology, request lifecycle) - deploy skill reference (fastedge-build CLI, binary upload) - best-practices (error handling patterns, response construction) + +## Source Material + +### FILE: examples/cache/src/index.ts + +```ts +// FastEdge Cache — flagship patterns +// +// This example demonstrates the three highest-value uses of the +// `fastedge::cache` module: +// +// 1. Per-IP rate limiting (atomic counters) +// 2. Origin-cache proxy (manual get/set with conditional caching) +// 3. JSON memoisation (getOrSet with a computed populator) +// +// All three patterns rely on the cache being: +// - **Strongly consistent within a POP** — atomic `incr` returns a +// correct count under concurrent load, which `fastedge::kv` cannot. +// - **Fast for both reads and writes** — sub-millisecond on the hot +// path, so caching is cheaper than recomputing or refetching. +// - **POP-local** — values do not replicate across data centers. +// This is acceptable (and often desirable) for transient state. + +import { Cache } from 'fastedge::cache'; + +// --------------------------------------------------------------------------- +// Pattern 1 — Rate limiting via atomic incr + expire +// --------------------------------------------------------------------------- +// +// Increment a per-IP counter. On the first hit (count === 1) we attach +// a TTL to create a fixed 60-second window anchored to that request: +// the counter resets 60 seconds after the user's *first* request, not +// after every request. +// +// `Cache.incr` is atomic: under concurrent load, two simultaneous +// requests cannot both see "count === 1" and double-set the expiry. +// This is the property that makes the cache suitable for limiting, +// quotas, locks, and other counter primitives. + +const RATE_LIMIT_MAX = 10; // Requests per window. +const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. + +async function rateLimit(event: FetchEvent): Promise { + // `event.client.address` is the trusted-edge client IP. Sourced from + // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by + // the FastEdge POP, not the client, so they're safe to key on. + const ip = event.client.address || 'unknown'; + + const key = `rl:${ip}`; + + const count = await Cache.incr(key); + + // Only set the expiry on the first hit of a new window. If we set it + // on every request, the window would never close — each new request + // would push the deadline another 60 seconds out. + if (count === 1) { + await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); + } + + if (count > RATE_LIMIT_MAX) { + return Response.json( + { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, + { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, + ); + } + + return Response.json({ + pattern: 'rate-limit', + ip, + count, + remaining: RATE_LIMIT_MAX - count, + windowSeconds: RATE_LIMIT_WINDOW_S, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 2 — Origin-cache proxy with conditional caching +// --------------------------------------------------------------------------- +// +// Cache successful upstream responses for PROXY_TTL_S seconds; pass +// non-2xx and redirects through *without* caching, so a transient 404 +// or 500 doesn't get pinned for the rest of the window. The cache is +// a byte cache (no status/headers), so we only cache when "200 OK with +// application/octet-stream" is a faithful replay of the upstream. +// +// `getOrSet` is not used here because its populator can't signal +// "fetched, but don't cache" — we need that distinction to handle +// error responses safely. See Pattern 3 for `getOrSet` in a context +// where every populator output is cacheable. + +const PROXY_TTL_S = 30; + +async function proxy(url: string): Promise { + // Validate the URL before we use it as a cache key. + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); + } + + // Strip the fragment: fetch() never sends it to the origin, so + // `https://example.com/#a` and `#b` are the same upstream resource + // and must share one cache entry. + parsed.hash = ''; + + const key = `proxy:${parsed.toString()}`; + + // Cache hit — replay the bytes as 200 OK. Status/headers from the + // original response are not preserved by the byte cache. + const cached = await Cache.get(key); + if (cached !== null) { + return new Response(await cached.arrayBuffer(), { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'hit', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); + } + + // Cache miss — fetch upstream and only cache successful responses. + // Non-2xx and redirects flow through unchanged so callers see the + // real status code instead of a synthetic 200. + const upstream = await fetch(parsed.toString()); + if (!upstream.ok) { + return upstream; + } + + const bytes = await upstream.arrayBuffer(); + await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); + return new Response(bytes, { + headers: { + 'content-type': 'application/octet-stream', + 'x-cache': 'miss', + 'x-cache-ttl': String(PROXY_TTL_S), + }, + }); +} + +// --------------------------------------------------------------------------- +// Pattern 3 — JSON memoisation via getOrSet with a computed populator +// --------------------------------------------------------------------------- +// +// Same shape as the proxy pattern, but the populator does CPU work +// instead of network I/O. Use this whenever you compute the same +// expensive answer many times in a row — search index lookups, +// signed-token verification, derived report rollups, JSON +// transformations of slow-changing source data. +// +// We embed `generatedAt` in the result so a client refreshing the +// page can see the timestamp stay constant within the cache window +// and update once it expires. + +const MEMO_TTL_S = 60; + +async function memo(): Promise { + const entry = await Cache.getOrSet( + 'memo:report', + () => { + // Stand-in for "expensive computation". The populator can be + // synchronous or async — both are accepted. + const report = { + generatedAt: new Date().toISOString(), + topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ + name, + score: Math.round(Math.random() * 1000) / 10, + rank: i + 1, + })), + }; + // The populator returns the value to store. Because we want + // structured JSON back later, we serialise here and re-parse + // via `entry.json()` on read. + return JSON.stringify(report); + }, + { ttl: MEMO_TTL_S }, + ); + + // `entry.json()` parses the cached UTF-8 bytes as JSON. Use + // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. + const report = await entry.json(); + + return Response.json({ + pattern: 'memo', + note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, + report, + }); +} + +// --------------------------------------------------------------------------- +// Default landing — usage menu when no action is supplied +// --------------------------------------------------------------------------- + +function landing(): Response { + return Response.json({ + name: 'FastEdge Cache patterns', + actions: { + 'rate-limit': '/?action=rate-limit', + proxy: '/?action=proxy&url=https://www.example.com', + memo: '/?action=memo', + }, + }); +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +async function eventHandler(event: FetchEvent): Promise { + try { + const url = new URL(event.request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'rate-limit': + return await rateLimit(event); + case 'proxy': + return await proxy(url.searchParams.get('url') ?? ''); + case 'memo': + return await memo(); + case null: + return landing(); + default: + return Response.json( + { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, + { status: 400 }, + ); + } + } catch (error: unknown) { + // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions + // fields) are synchronous; host errors arrive as Promise rejections. + // Both are caught by this single handler. + return Response.json({ error: (error as Error).message }, { status: 500 }); + } +} + +addEventListener('fetch', (event: FetchEvent) => { + event.respondWith(eventHandler(event)); +}); +``` + +### FILE: examples/cache/package.json + +```json +{ + "name": "fastedge-example-cache", + "version": "1.0.0", + "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", + "type": "module", + "scripts": { + "build": "fastedge-build -c" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0" + } +} +``` + +### FILE: examples/cache/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "lib": ["ES2023"], + "types": ["@gcoredev/fastedge-sdk-js"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-fetch-js.md index 733b49d..4db84f6 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## 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 b812025..b0f32ea 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## 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 42d3004..7add540 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## 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 0779d91..7dd139b 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 2b8dbeb..eb54e64 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## 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 6490c05..e9a2822 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) @@ -250,3 +250,178 @@ Returns: `ArrayBuffer | null` — `null` if the key does not exist. - examples/kv-store — KV-backed caching patterns - platform-overview reference — plan limits, execution budget, and body size constraints - sdk-reference-js reference — full KvStore API + +## Source Material + +### FILE: docs/PROXY_PATTERNS.md + +``` + + +# Proxy and Response Transform Patterns + +FastEdge HTTP apps can act as a thin proxy in front of an origin or upstream API — fetching a response, transforming it, and returning the result. This document covers the common proxy and transform patterns. + +## Simple Proxy + +Fetch from an upstream and return the body unchanged: + +```typescript +async function handle(request) { + const url = new URL(request.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: request.method, + headers: request.headers, + body: request.method !== "GET" && request.method !== "HEAD" + ? await request.arrayBuffer() + : undefined, + }); + + return response; +} + +addEventListener("fetch", (event) => { + event.respondWith(handle(event.request)); +}); +``` + +`fetch()` returns a streamable `Response`; returning it directly forwards body chunks without buffering everything in memory. + +## Proxy with JSON Transform + +Modify the response body before returning it. This is the pattern from `examples/outbound-modify-response/`: + +```typescript +async function handle() { + const upstream = await fetch("https://jsonplaceholder.typicode.com/users"); + const users = await upstream.json(); + + const transformed = { + users: users.slice(0, 5), + total: 5, + skip: 0, + limit: 30, + }; + + return new Response(JSON.stringify(transformed), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +addEventListener("fetch", (event) => { + event.respondWith(handle()); +}); +``` + +Reading `await upstream.json()` consumes the body; you cannot read it again. Read the body once, transform what you need, then return. + +## Hono Proxy with Transform + +Inside a Hono app, use `c.req.raw.headers` to forward the inbound headers and `c.req.arrayBuffer()` for the body: + +```typescript +import { Hono } from "hono"; + +const app = new Hono(); + +app.all("/api/*", async (c) => { + const url = new URL(c.req.url); + const upstream = `https://backend.example.com${url.pathname}${url.search}`; + + const response = await fetch(upstream, { + method: c.req.method, + headers: c.req.raw.headers, + body: c.req.method !== "GET" && c.req.method !== "HEAD" + ? await c.req.arrayBuffer() + : undefined, + }); + + const data = await response.json(); + data.processedAt = new Date().toISOString(); + return c.json(data, response.status); +}); + +addEventListener("fetch", (event) => { + event.respondWith(app.fetch(event.request)); +}); +``` + +## Header Manipulation in Proxies + +Strip hop-by-hop headers before forwarding to upstream, and add diagnostic headers on the way back: + +```typescript +async function handle(request) { + const upstreamHeaders = new Headers(request.headers); + // Hop-by-hop headers should not be forwarded + upstreamHeaders.delete("connection"); + upstreamHeaders.delete("keep-alive"); + upstreamHeaders.delete("transfer-encoding"); + + const upstream = await fetch("https://backend.example.com", { + method: request.method, + headers: upstreamHeaders, + }); + + const responseHeaders = new Headers(upstream.headers); + responseHeaders.set("X-Proxied-By", "FastEdge"); + + return new Response(upstream.body, { + status: upstream.status, + headers: responseHeaders, + }); +} +``` + +`new Response(upstream.body, ...)` streams the body through without reading it into memory — preferred for large responses. + +## Cache-aware Proxy with KV + +Cache upstream responses in the KV store to avoid repeated outbound calls. Note: KV is read-only from app code; writes happen via the portal/API: + +```typescript +import { KvStore } from "fastedge::kv"; + +async function handle(request) { + const url = new URL(request.url); + + try { + const cache = KvStore.open("api-cache"); + const cached = cache.get(url.pathname); + if (cached !== null) { + return new Response(cached, { + status: 200, + headers: { "content-type": "application/json", "x-cache": "hit" }, + }); + } + } catch { + // KV store unavailable — fall through to upstream fetch + } + + const upstream = await fetch(`https://backend.example.com${url.pathname}`); + return new Response(await upstream.arrayBuffer(), { + status: upstream.status, + headers: { ...Object.fromEntries(upstream.headers), "x-cache": "miss" }, + }); +} +``` + +`KvStore.open(name)` returns a `KvStoreInstance` (it does not return null) but can throw if the named store is not provisioned — wrap the open call in `try/catch`. `cache.get(key)` returns `ArrayBuffer | null`; check for `null`, not falsy, since an empty buffer is a valid value. + +## Operational Notes + +- **Outbound fetch budget.** Each invocation has a limited number of outbound requests (5 on Basic, 20 on Pro). Parallelise where possible with `Promise.all([...])` instead of sequential `await fetch(...)`. +- **Execution time budget.** Proxying upstream + transforming counts against the 50ms (Basic) / 200ms (Pro) execution budget. Slow upstreams will trip 532 timeouts. +- **Body size limits.** Inbound and outbound bodies are subject to the configured request/response size limits. Stream where possible rather than buffering with `arrayBuffer()` / `text()` / `json()`. + +## See Also + +- `examples/outbound-modify-response/` — JSON transform of upstream response +- `examples/outbound-fetch/` — basic outbound `fetch()` patterns +- `examples/headers/` — request/response header manipulation +- `examples/kv-store/` — KV-backed caching patterns + +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md index 0c82fbe..d97f55f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/js-runtime.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 b14550a..8daded8 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-js.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 80f5974..05fe7e0 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 7a3b373..e260a0e 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/build-cli.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## 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 71565f9..c8ebe17 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 558d43c..0be58fa 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -14,7 +14,7 @@ languages: [typescript, javascript] template_origin: http-base source_repo: https://github.com/G-Core/FastEdge-sdk-js source_ref: 81145a9a43ec499240c687bd49376ab20c72b11c -updated: 2026-08-17 +updated: 2026-08-20 --- # 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 f81acb2..ba67275 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -176,67 +176,3 @@ Bloom filters guarantee no false negatives (an IP absent from the set is never b - fastedge::env reference - fastedge-build CLI reference - KV store setup and bloom-filter payload upload guide - -## Source Material - -### FILE: examples/bloom-filter-denylist/src/index.js - -```js -import { getEnv } from 'fastedge::env'; -import { KvStore } from 'fastedge::kv'; - -const BLOOM_KEY = 'blocked-ips'; - -function app(event) { - const storeName = getEnv('DENYLIST_STORE'); - if (!storeName) { - return Response.json( - { error: 'DENYLIST_STORE environment variable is not configured' }, - { status: 500 }, - ); - } - - const ip = event.client.address; - if (!ip) { - return Response.json({ error: 'client address unavailable' }, { status: 500 }); - } - - let blocked; - try { - const store = KvStore.open(storeName); - blocked = store.bfExists(BLOOM_KEY, ip); - } catch (error) { - return Response.json({ error: `KV lookup failed: ${error.message}` }, { status: 500 }); - } - - if (blocked) { - // Bloom filter says "maybe in set" — a small fraction of hits will be false positives. - // Acceptable for a denylist (you over-block some legitimate users); not acceptable for - // allowlists or anything requiring exact membership — use KvStore.get() for that. - return Response.json({ allowed: false, ip }, { status: 403 }); - } - - return Response.json({ allowed: true, ip }); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/bloom-filter-denylist/package.json - -```json -{ - "name": "fastedge-example-bloom-filter-denylist", - "version": "1.0.0", - "description": "FastEdge JS example: IP denylist using a KV Store bloom filter", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/bloom-filter-denylist.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-basic-ts.md index 8e33d7f..914ed37 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -231,117 +231,3 @@ SDK version constraint: `@gcoredev/fastedge-sdk-js ^2.3.0` - `fastedge::cache` full API reference — advanced patterns, streaming values, CacheEntry interface - http-base skeleton — base HTTP handler structure this feature extends - deploy skill reference — building and uploading the compiled WASM binary - -## Source Material - -### FILE: examples/cache-basic/src/index.js - -```js -// FastEdge Cache — basic operations -// -// The `fastedge::cache` module gives you a fast, data-center-scoped -// key/value store. Values written here are stored in the same point of -// presence (POP) that runs the worker, so reads and writes are very fast, -// and writes from one POP are not visible to others. -// -// Use this for transient, request-time state — short-lived caches, hit -// counters, rate limit windows, deduplicated work. For globally -// replicated storage, use the `fastedge::kv` module instead. -// -// This example demonstrates the four most common operations: -// -// GET /?action=set&key=foo&value=bar -> Cache.set -// GET /?action=get&key=foo -> Cache.get -// GET /?action=exists&key=foo -> Cache.exists -// GET /?action=delete&key=foo -> Cache.delete - -import { Cache } from 'fastedge::cache'; - -async function eventHandler(event) { - try { - const url = new URL(event.request.url); - const action = url.searchParams.get('action'); - const key = url.searchParams.get('key'); - - if (!key) { - throw new Error('Missing required query parameter: "key"'); - } - - switch (action) { - case 'set': { - // Cache.set writes a value under `key`. Accepts strings, - // ArrayBuffers, ArrayBufferViews, ReadableStreams, and Response - // objects (the body is consumed; status and headers are not stored). - // - // The `{ ttl: 60 }` option means "expire 60 seconds from now". You - // can also use `ttlMs` for sub-second precision, or `expiresAt` for - // a fixed Unix-epoch deadline. Omit options entirely for no expiry. - const value = url.searchParams.get('value') ?? ''; - await Cache.set(key, value, { ttl: 60 }); - return Response.json({ action, key, value, ttl: 60 }); - } - - case 'get': { - // Cache.get returns a CacheEntry on a hit, or `null` on a miss - // (key absent or expired). The cache stores raw bytes, so on read - // you choose how to decode using one of: - // entry.text() -> Promise (UTF-8) - // entry.json() -> Promise (parsed JSON) - // entry.arrayBuffer() -> Promise - const entry = await Cache.get(key); - if (entry === null) { - return Response.json({ action, key, hit: false }); - } - const value = await entry.text(); - return Response.json({ action, key, hit: true, value }); - } - - case 'exists': { - // Cache.exists is a cheap presence check — useful when you only - // need to know whether a key is set without transferring its value - // (e.g. idempotency-key checks, "have we seen this token?"). - const present = await Cache.exists(key); - return Response.json({ action, key, present }); - } - - case 'delete': { - // Cache.delete removes the entry. It is a no-op if the key is - // already absent — no error is thrown. - await Cache.delete(key); - return Response.json({ action, key, deleted: true }); - } - - default: - throw new Error( - `Unknown action: "${action}". Use one of: set, get, exists, delete.`, - ); - } - } catch (error) { - // Validation errors (e.g. wrong types, conflicting WriteOptions fields) - // are thrown synchronously; host errors (access denied, internal error) - // arrive as Promise rejections. Both are caught by this single handler. - return Response.json({ error: error.message }, { status: 500 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/cache-basic/package.json - -```json -{ - "name": "fastedge-example-cache-basic", - "version": "1.0.0", - "description": "FastEdge JS example: simple Cache set/get/exists/delete operations", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/cache-basic.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md index 5579117..4a59ac4 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -315,281 +315,3 @@ Build command: `fastedge-build -c` - http-base skeleton (base event listener and fetch handler structure) - platform-overview (POP-local vs. global state trade-offs) - best-practices (key naming conventions, TTL selection, error handling) - -## Source Material - -### FILE: examples/cache/src/index.ts - -```ts -// FastEdge Cache — flagship patterns -// -// This example demonstrates the three highest-value uses of the -// `fastedge::cache` module: -// -// 1. Per-IP rate limiting (atomic counters) -// 2. Origin-cache proxy (manual get/set with conditional caching) -// 3. JSON memoisation (getOrSet with a computed populator) -// -// All three patterns rely on the cache being: -// - **Strongly consistent within a POP** — atomic `incr` returns a -// correct count under concurrent load, which `fastedge::kv` cannot. -// - **Fast for both reads and writes** — sub-millisecond on the hot -// path, so caching is cheaper than recomputing or refetching. -// - **POP-local** — values do not replicate across data centers. -// This is acceptable (and often desirable) for transient state. - -import { Cache } from 'fastedge::cache'; - -// --------------------------------------------------------------------------- -// Pattern 1 — Rate limiting via atomic incr + expire -// --------------------------------------------------------------------------- -// -// Increment a per-IP counter. On the first hit (count === 1) we attach -// a TTL to create a fixed 60-second window anchored to that request: -// the counter resets 60 seconds after the user's *first* request, not -// after every request. -// -// `Cache.incr` is atomic: under concurrent load, two simultaneous -// requests cannot both see "count === 1" and double-set the expiry. -// This is the property that makes the cache suitable for limiting, -// quotas, locks, and other counter primitives. - -const RATE_LIMIT_MAX = 10; // Requests per window. -const RATE_LIMIT_WINDOW_S = 60; // Window length, seconds. - -async function rateLimit(event: FetchEvent): Promise { - // `event.client.address` is the trusted-edge client IP. Sourced from - // `x-real-ip` (with fallback to `x-forwarded-for`); both are set by - // the FastEdge POP, not the client, so they're safe to key on. - const ip = event.client.address || 'unknown'; - - const key = `rl:${ip}`; - - const count = await Cache.incr(key); - - // Only set the expiry on the first hit of a new window. If we set it - // on every request, the window would never close — each new request - // would push the deadline another 60 seconds out. - if (count === 1) { - await Cache.expire(key, { ttl: RATE_LIMIT_WINDOW_S }); - } - - if (count > RATE_LIMIT_MAX) { - return Response.json( - { error: 'Too Many Requests', limit: RATE_LIMIT_MAX, count }, - { status: 429, headers: { 'retry-after': String(RATE_LIMIT_WINDOW_S) } }, - ); - } - - return Response.json({ - pattern: 'rate-limit', - ip, - count, - remaining: RATE_LIMIT_MAX - count, - windowSeconds: RATE_LIMIT_WINDOW_S, - }); -} - -// --------------------------------------------------------------------------- -// Pattern 2 — Origin-cache proxy with conditional caching -// --------------------------------------------------------------------------- -// -// Cache successful upstream responses for PROXY_TTL_S seconds; pass -// non-2xx and redirects through *without* caching, so a transient 404 -// or 500 doesn't get pinned for the rest of the window. The cache is -// a byte cache (no status/headers), so we only cache when "200 OK with -// application/octet-stream" is a faithful replay of the upstream. -// -// `getOrSet` is not used here because its populator can't signal -// "fetched, but don't cache" — we need that distinction to handle -// error responses safely. See Pattern 3 for `getOrSet` in a context -// where every populator output is cacheable. - -const PROXY_TTL_S = 30; - -async function proxy(url: string): Promise { - // Validate the URL before we use it as a cache key. - let parsed: URL; - try { - parsed = new URL(url); - } catch { - return Response.json({ error: `Invalid url: "${url}"` }, { status: 400 }); - } - - // Strip the fragment: fetch() never sends it to the origin, so - // `https://example.com/#a` and `#b` are the same upstream resource - // and must share one cache entry. - parsed.hash = ''; - - const key = `proxy:${parsed.toString()}`; - - // Cache hit — replay the bytes as 200 OK. Status/headers from the - // original response are not preserved by the byte cache. - const cached = await Cache.get(key); - if (cached !== null) { - return new Response(await cached.arrayBuffer(), { - headers: { - 'content-type': 'application/octet-stream', - 'x-cache': 'hit', - 'x-cache-ttl': String(PROXY_TTL_S), - }, - }); - } - - // Cache miss — fetch upstream and only cache successful responses. - // Non-2xx and redirects flow through unchanged so callers see the - // real status code instead of a synthetic 200. - const upstream = await fetch(parsed.toString()); - if (!upstream.ok) { - return upstream; - } - - const bytes = await upstream.arrayBuffer(); - await Cache.set(key, bytes, { ttl: PROXY_TTL_S }); - return new Response(bytes, { - headers: { - 'content-type': 'application/octet-stream', - 'x-cache': 'miss', - 'x-cache-ttl': String(PROXY_TTL_S), - }, - }); -} - -// --------------------------------------------------------------------------- -// Pattern 3 — JSON memoisation via getOrSet with a computed populator -// --------------------------------------------------------------------------- -// -// Same shape as the proxy pattern, but the populator does CPU work -// instead of network I/O. Use this whenever you compute the same -// expensive answer many times in a row — search index lookups, -// signed-token verification, derived report rollups, JSON -// transformations of slow-changing source data. -// -// We embed `generatedAt` in the result so a client refreshing the -// page can see the timestamp stay constant within the cache window -// and update once it expires. - -const MEMO_TTL_S = 60; - -async function memo(): Promise { - const entry = await Cache.getOrSet( - 'memo:report', - () => { - // Stand-in for "expensive computation". The populator can be - // synchronous or async — both are accepted. - const report = { - generatedAt: new Date().toISOString(), - topItems: ['alpha', 'beta', 'gamma'].map((name, i) => ({ - name, - score: Math.round(Math.random() * 1000) / 10, - rank: i + 1, - })), - }; - // The populator returns the value to store. Because we want - // structured JSON back later, we serialise here and re-parse - // via `entry.json()` on read. - return JSON.stringify(report); - }, - { ttl: MEMO_TTL_S }, - ); - - // `entry.json()` parses the cached UTF-8 bytes as JSON. Use - // `entry.text()` for a string, or `entry.arrayBuffer()` for bytes. - const report = await entry.json(); - - return Response.json({ - pattern: 'memo', - note: `Cached for ${MEMO_TTL_S}s. Refresh to confirm 'generatedAt' stays the same until expiry.`, - report, - }); -} - -// --------------------------------------------------------------------------- -// Default landing — usage menu when no action is supplied -// --------------------------------------------------------------------------- - -function landing(): Response { - return Response.json({ - name: 'FastEdge Cache patterns', - actions: { - 'rate-limit': '/?action=rate-limit', - proxy: '/?action=proxy&url=https://www.example.com', - memo: '/?action=memo', - }, - }); -} - -// --------------------------------------------------------------------------- -// Router -// --------------------------------------------------------------------------- - -async function eventHandler(event: FetchEvent): Promise { - try { - const url = new URL(event.request.url); - const action = url.searchParams.get('action'); - - switch (action) { - case 'rate-limit': - return await rateLimit(event); - case 'proxy': - return await proxy(url.searchParams.get('url') ?? ''); - case 'memo': - return await memo(); - case null: - return landing(); - default: - return Response.json( - { error: `Unknown action: "${action}". Use one of: rate-limit, proxy, memo.` }, - { status: 400 }, - ); - } - } catch (error: unknown) { - // Validation errors thrown by Cache.* (e.g. conflicting WriteOptions - // fields) are synchronous; host errors arrive as Promise rejections. - // Both are caught by this single handler. - return Response.json({ error: (error as Error).message }, { status: 500 }); - } -} - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(eventHandler(event)); -}); -``` - - -### FILE: examples/cache/package.json - -```json -{ - "name": "fastedge-example-cache", - "version": "1.0.0", - "description": "FastEdge JS example: Cache patterns — rate limiting, origin-cache proxy, memoisation", - "type": "module", - "scripts": { - "build": "fastedge-build -c" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` - - -### FILE: examples/cache/tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2023", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "lib": ["ES2023"], - "types": ["@gcoredev/fastedge-sdk-js"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/crypto-hmac-jwt-ts.md index 3331544..10afb0e 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -260,100 +260,3 @@ Configure via the FastEdge secrets API or the manage skill before deploying. - WebCrypto API (crypto.subtle) — available as a global in the FastEdge JS runtime - http-base skeleton (base event listener and Response patterns) - FastEdge deploy skill (building and uploading WASM, configuring secrets) - -## Source Material - -### FILE: examples/crypto-hmac-jwt/src/index.js - -```js -import { getSecret } from 'fastedge::secret'; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - -function base64urlToBytes(str) { - const padded = str.replace(/-/gu, '+').replace(/_/gu, '/') + '='.repeat((4 - (str.length % 4)) % 4); - const binary = atob(padded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.codePointAt(i); - } - return bytes; -} - -async function verifyJwtHs256(token, secret) { - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('malformed token: expected three segments'); - } - const [encodedHeader, encodedPayload, encodedSignature] = parts; - - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(secret), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['verify'], - ); - - const signature = base64urlToBytes(encodedSignature); - const signedData = encoder.encode(`${encodedHeader}.${encodedPayload}`); - - const valid = await crypto.subtle.verify('HMAC', key, signature, signedData); - if (!valid) { - throw new Error('invalid signature'); - } - - const claims = JSON.parse(decoder.decode(base64urlToBytes(encodedPayload))); - - if (typeof claims.exp === 'number' && Math.floor(Date.now() / 1000) >= claims.exp) { - throw new Error('token expired'); - } - - return claims; -} - -async function app(event) { - const auth = event.request.headers.get('authorization') ?? ''; - const match = auth.match(/^Bearer\s+(.+)$/iu); - if (!match) { - return Response.json( - { ok: false, error: 'missing or malformed Authorization header' }, - { status: 401 }, - ); - } - - const secret = getSecret('JWT_SECRET'); - if (!secret) { - return Response.json({ ok: false, error: 'JWT_SECRET is not configured' }, { status: 500 }); - } - - try { - const claims = await verifyJwtHs256(match[1], secret); - return Response.json({ ok: true, claims }); - } catch (error) { - return Response.json({ ok: false, error: error.message }, { status: 401 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/crypto-hmac-jwt/package.json - -```json -{ - "name": "fastedge-example-crypto-hmac-jwt", - "version": "1.0.0", - "description": "FastEdge JS example: verify HS256 JWTs with the Web Crypto API", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/crypto-hmac-jwt.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md index f8dd25a..ae4a6d4 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/fetch-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 daf9cda..ed691d5 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- type: feature 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 ea38fe3..0a587fb 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 e5691d5..54b3f87 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -127,47 +127,3 @@ Build script from `package.json`: - fastedge-sdk-js SDK reference - FastEdge app configuration (store attachment) - BUILD_CLI reference (fastedge-build options) - -## Source Material - -### FILE: examples/kv-store-basic/src/index.js - -```js -import { KvStore } from 'fastedge::kv'; - -async function eventHandler(event) { - try { - const myStore = KvStore.open('kv-store-name-as-defined-on-app'); - const entry = await myStore.getEntry('key'); - - if (entry === null) { - return new Response('Key not found', { status: 404 }); - } - - return new Response(`The KV Store responded with: ${await entry.text()}`); - } catch (error) { - return Response.json({ error: error.message }, { status: 500 }); - } -} - -addEventListener('fetch', (event) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store-basic/package.json - -```json -{ - "name": "fastedge-example-kv-store-basic", - "version": "1.0.0", - "description": "FastEdge JS example: simple KV Store get operation", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/kv-store-basic.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-ts.md index bd5f092..65a287d 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 54b0eef..8a36751 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -400,282 +400,3 @@ The MCP server listens at `/mcp` on whatever domain the FastEdge app is deployed - Model Context Protocol specification (MCP HTTP transport, tool registration) - FastEdge build CLI reference (fastedge-build) - http-base blueprint (base HTTP worker skeleton) - -## Source Material - -### FILE: examples/mcp-server/src/index.ts - -```ts -import { StreamableHTTPTransport } from '@hono/mcp'; -import { Hono } from 'hono'; - -import server from './server.js'; - -const router = new Hono(); - -router.all('/mcp', async (c) => { - const transport = new StreamableHTTPTransport(); - await server.connect(transport); - return transport.handleRequest(c); -}); - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(router.fetch(event.request)); -}); -``` - - -### FILE: examples/mcp-server/src/server.ts - -```ts -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { z } from 'zod'; - -import type { - AlertFeature, - AlertsResponse, - ForecastPeriod, - ForecastResponse, - PointsResponse, -} from './types.js'; - -const NWS_API_BASE = 'https://api.weather.gov'; -const USER_AGENT = 'weather-app/1.0'; - -// Helper function for making NWS API requests -async function makeNWSRequest(url: string): Promise { - const headers = { - 'User-Agent': USER_AGENT, - Accept: 'application/geo+json', - }; - - try { - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return (await response.json()) as T; - } catch (error) { - console.error('Error making NWS request:', error); - return null; - } -} - -// Format alert data -function formatAlert(feature: AlertFeature): string { - const props = feature.properties; - return [ - `Event: ${props.event || 'Unknown'}`, - `Area: ${props.areaDesc || 'Unknown'}`, - `Severity: ${props.severity || 'Unknown'}`, - `Status: ${props.status || 'Unknown'}`, - `Headline: ${props.headline || 'No headline'}`, - '---', - ].join('\n'); -} - -// Create server instance -const server = new McpServer({ - name: 'weather', - version: '1.0.0', -}); - -// Register weather tools -server.registerTool( - 'get-alerts', - { - title: 'Get Weather Alerts', - description: 'Get weather alerts for a US state', - inputSchema: z.object({ - state: z.string().length(2).describe('Two-letter state code (e.g. CA, NY)'), - }), - }, - async ({ state }) => { - const stateCode = state.toUpperCase(); - - const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`; - const alertsData = await makeNWSRequest(alertsUrl); - - if (!alertsData) { - return { - content: [ - { - type: 'text', - text: 'Failed to retrieve alerts data', - }, - ], - }; - } - - const features = alertsData.features || []; - if (features.length === 0) { - return { - content: [ - { - type: 'text', - text: `No active alerts for ${stateCode}`, - }, - ], - }; - } - - const formattedAlerts = features.map(formatAlert); - const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`; - - return { - content: [ - { - type: 'text', - text: alertsText, - }, - ], - }; - }, -); - -server.registerTool( - 'get-forecast', - { - title: 'Get Weather Forecast', - description: 'Get weather forecast for a location', - inputSchema: z.object({ - latitude: z.number().min(-90).max(90).describe('Latitude of the location'), - longitude: z.number().min(-180).max(180).describe('Longitude of the location'), - }), - }, - async ({ latitude, longitude }) => { - // Get grid point data - const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`; - const pointsData = await makeNWSRequest(pointsUrl); - - if (!pointsData) { - return { - content: [ - { - type: 'text', - text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`, - }, - ], - }; - } - - const forecastUrl = pointsData.properties?.forecast; - if (!forecastUrl) { - return { - content: [ - { - type: 'text', - text: 'Failed to get forecast URL from grid point data', - }, - ], - }; - } - - // Get forecast data - const forecastData = await makeNWSRequest(forecastUrl); - if (!forecastData) { - return { - content: [ - { - type: 'text', - text: 'Failed to retrieve forecast data', - }, - ], - }; - } - - const periods = forecastData.properties?.periods || []; - if (periods.length === 0) { - return { - content: [ - { - type: 'text', - text: 'No forecast periods available', - }, - ], - }; - } - - // Format forecast periods - const formattedForecast = periods.map((period: ForecastPeriod) => - [ - `${period.name || 'Unknown'}:`, - `Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`, - `Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`, - `${period.shortForecast || 'No forecast available'}`, - '---', - ].join('\n'), - ); - - const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join( - '\n', - )}`; - - return { - content: [ - { - type: 'text', - text: forecastText, - }, - ], - }; - }, -); - -export default server; -``` - - -### FILE: examples/mcp-server/package.json - -```json -{ - "name": "fastedge-example-mcp-server", - "version": "1.0.0", - "description": "Basic Example of running MCP Server on FastEdge", - "main": "src/index.ts", - "type": "module", - "bin": { - "weather": "./build/index.js" - }, - "scripts": { - "build": "npm run transpile && npm run build-wasm", - "build-wasm": "npx fastedge-build --input build/index.js --output build/weather.wasm --tsconfig tsconfig.json", - "transpile": "tsc" - }, - "files": [ - "build" - ], - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0", - "@hono/mcp": "^0.2.5", - "@modelcontextprotocol/sdk": "^1.29.0", - "hono": "^4.12.25", - "zod": "^4.3.6" - }, - "devDependencies": { - "typescript": "^5.9.2" - } -} -``` - - -### FILE: examples/mcp-server/tsconfig.json - -```json -{ - "compilerOptions": { - "target": "ES2023", - "module": "Node16", - "moduleResolution": "Node16", - "outDir": "./build", - "rootDir": "./src", - "strict": true, - "skipLibCheck": true, - "lib": ["ES2023"], - "types": ["@gcoredev/fastedge-sdk-js"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-ts.md index f802a94..8c88e7f 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -145,49 +145,3 @@ From `package.json`: - sdk-reference-js (fetch API, Response constructor, addEventListener) - deploy skill reference (uploading and registering the compiled WASM binary) - outbound-fetch feature blueprint (fetch without body transformation) - -## Source Material - -### FILE: examples/outbound-modify-response/src/index.js - -```js -async function app(event) { - const outboundResponse = await fetch('http://jsonplaceholder.typicode.com/users'); - const users = await outboundResponse.json(); - return new Response( - JSON.stringify({ - users: users.slice(0, 5), - total: 5, - skip: 0, - limit: 30, - }), - { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }, - ); -} - -addEventListener('fetch', (event) => { - event.respondWith(app(event)); -}); -``` - -### FILE: examples/outbound-modify-response/package.json - -```json -{ - "name": "fastedge-example-outbound-modify-response", - "version": "1.0.0", - "description": "FastEdge JS example: fetch and modify outbound response", - "type": "module", - "scripts": { - "build": "fastedge-build src/index.js dist/outbound-modify-response.wasm" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.2.2" - } -} -``` diff --git a/plugins/gcore-fastedge/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 5a0a7b2..7eb3746 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 a9cb616..d4f9186 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 497a726..5e3fdf9 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -45,13 +45,14 @@ import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; #### `getSecretEffectiveAt(name, slot)` -| Parameter | Type | Description | -|-----------|--------|----------------------------------------------| -| `name` | string | Secret name (key) | -| `slot` | number | Slot index or unix timestamp (non-negative integer) | +| Parameter | Type | Description | +|-----------|--------|---------------------------------------------------------------------| +| `name` | string | Secret name (key) | +| `slot` | number | Slot index or unix timestamp (non-negative integer) | - **Returns**: `string | null` — the value from the highest slot `<= slot`, or `null` if no slot satisfies the constraint. - **Slot model**: slots are interpreted as either indices (`0, 1, 2, …`) or unix timestamps. The host resolves by returning the value stored at the highest slot number that is less than or equal to the supplied slot number. +- Both functions are synchronous — no `await` required. --- @@ -207,3 +208,65 @@ This shape is the diagnostic surface for verifying that rotation is working corr - http-base skeleton - deploy skill (for uploading secrets via the API before deploying) - manage skill (`secrets` subcommand for secret CRUD operations) + +## Source Material + +### FILE: examples/secret-rotation/src/index.js + +```js +import { getSecret, getSecretEffectiveAt } from 'fastedge::secret'; + +function app(event) { + const { request } = event; + + // Read the slot from the x-slot header, defaulting to the current unix timestamp. + // Slots can be interpreted either as indices (0, 1, 2...) or as unix timestamps; + // the host returns the value from the highest slot <= this number. + const slotHeader = request.headers.get('x-slot'); + const slot = + slotHeader !== null ? Number.parseInt(slotHeader, 10) : Math.floor(Date.now() / 1000); + + if (!Number.isFinite(slot) || slot < 0) { + return new Response('x-slot header must be a non-negative integer', { status: 400 }); + } + + const secretName = request.headers.get('x-secret-name') ?? 'TOKEN_SECRET'; + + const current = getSecret(secretName); + const effective = getSecretEffectiveAt(secretName, slot); + + const body = JSON.stringify({ + secret_name: secretName, + slot, + current, + effective_at_slot: effective, + is_same: current === effective, + }); + + return new Response(body, { + status: 200, + headers: { 'content-type': 'application/json' }, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/secret-rotation/package.json + +```json +{ + "name": "fastedge-example-secret-rotation", + "version": "1.0.0", + "description": "FastEdge JS example: slot-based secret retrieval for rotation", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/secret-rotation.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-ts.md index ae130e1..5857dc3 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 8155e9b..053da59 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-ts.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -115,3 +115,50 @@ Entry point: `src/index.js`. Output: `dist/streaming.wasm`. Requires `@gcoredev/ - deploy skill reference - fastedge-build CLI reference - FastEdge-sdk-js SDK reference + +## Source Material + +### FILE: examples/streaming/src/index.js + +```js +function app(event) { + const encoder = new TextEncoder(); + + const stream = new ReadableStream({ + async start(controller) { + for (let i = 0; i < 5; i++) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { setTimeout(resolve, 200); }); + controller.enqueue(encoder.encode(`chunk ${i}\n`)); + } + controller.close(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { 'content-type': 'text/plain; charset=utf-8' }, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(app(event)); +}); +``` + +### FILE: examples/streaming/package.json + +```json +{ + "name": "fastedge-example-streaming", + "version": "1.0.0", + "description": "FastEdge JS example: streaming response with ReadableStream", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/streaming.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.2.2" + } +} +``` diff --git a/plugins/gcore-fastedge/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 758ad7d..04a1fa2 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- 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 5d7002e..36b8b19 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -202,3 +202,86 @@ Output binary: `dist/template-invoice.wasm` - static-assets blueprint (contrast: uses asset pipeline, this blueprint does not) - fastedge-build CLI reference - FastEdge SDK JS reference + +## Source Material + +### FILE: examples/template-invoice/src/index.js + +```js +import Handlebars from 'handlebars'; + +import { cssStyles } from './css-styles.js'; +import { htmlTemplate } from './html-template.js'; +import { logoBrand } from './logo.js'; + +const invoiceData = { + createdDate: 'March 4, 2024', + dueDate: 'April 19, 2024', + invoiceNumber: '1729', + recipientAddress: { + name: 'Homer Simpson', + address1: '742 Evergreen Terrace', + address2: 'Springfield, United States.', + }, + paymentMethod: 'PayPal', + paymentId: '8915648', + items: [ + { + description: '1x Keg of Duff Beer', + price: 250, + }, + { + description: '3x Crate of Duff Beer', + price: 85, + }, + { + description: '2x Duff Football Finger', + price: 20, + }, + ], +}; + +const getTotalPrice = (items) => items.reduce((total, item) => total + item.price, 0).toFixed(2); + +async function eventHandler() { + const rawHtmlTemplate = htmlTemplate(); + + const template = Handlebars.compile(rawHtmlTemplate); + + const html = template({ + cssStyles, + logoBrand, + ...invoiceData, + totalPrice: getTotalPrice(invoiceData.items), + }); + + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler()); +}); +``` + +### FILE: examples/template-invoice/package.json + +```json +{ + "name": "fastedge-example-template-invoice", + "version": "1.0.0", + "description": "FastEdge JS example: HTML invoice rendered via Handlebars templates", + "type": "module", + "scripts": { + "build": "fastedge-build src/index.js dist/template-invoice.wasm" + }, + "dependencies": { + "@gcoredev/fastedge-sdk-js": "^2.3.0", + "handlebars": "^4.7.9" + } +} +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-ts.md index 889bb88..232d331 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 @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md b/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md index 0c4a451..270689c 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> # 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 273f457..f5e4834 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/static-sites.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-js ref: main commit: 81145a9a43ec499240c687bd49376ab20c72b11c - updated: 2026-08-17 + updated: 2026-08-20 --> ## How It Works