From 4018717ccb4b29f86dfbb8dcfe6e8a8cfbe263ff Mon Sep 17 00:00:00 2001 From: "fastedge-plugin-sync[bot]" Date: Mon, 17 Aug 2026 13:21:16 +0000 Subject: [PATCH] auto: update reference docs from fastedge-sdk-js (main) --- .../reference/http/examples-ab-testing-js.md | 120 +++++++- .../reference/http/examples-auth-js.md | 2 +- .../reference/http/examples-cache-js.md | 2 +- .../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 | 32 +- .../reference/http/examples-proxy-js.md | 2 +- .../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 | 120 +++++++- .../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 | 4 +- .../scaffold/reference/http/fetch-ts.md | 2 +- .../reference/http/geo-redirect-ts.md | 2 +- .../scaffold/reference/http/headers-ts.md | 3 +- .../reference/http/kv-store-basic-ts.md | 2 +- .../scaffold/reference/http/kv-store-ts.md | 185 +----------- .../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 | 2 +- .../reference/http/static-assets-ts.md | 2 +- .../scaffold/reference/http/streaming-ts.md | 51 +--- .../http/template-invoice-ab-testing-ts.md | 2 +- .../reference/http/template-invoice-ts.md | 86 +----- .../http/variables-and-secrets-ts.md | 2 +- .../skills/scaffold/reference/init-cli.md | 2 +- .../skills/scaffold/reference/static-sites.md | 2 +- 36 files changed, 1086 insertions(+), 354 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 0d5fa22..d1e2c6e 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-07-23 + updated: 2026-08-17 --> # A/B Testing — FastEdge Example @@ -207,3 +207,121 @@ Iterates each test in `testConfig`, maps `xid * 100` into the normalized variant - **`xid` range**: `Math.random()` can return `0` but not `1`. `slice(1, 5)` on `"0.473..."` yields `".473"` — always a 4-character string starting with `.`. - **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 + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(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', { + status: 500, + }); + } + + 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', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(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); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + 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 bd47ddb..7993351 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-07-23 + updated: 2026-08-17 --> # 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 7032040..141ee81 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-07-23 + updated: 2026-08-17 --> # FastEdge Cache — JavaScript Examples 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 9ad49e5..733b49d 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-07-23 + updated: 2026-08-17 --> ## 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 156db75..b812025 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-07-23 + updated: 2026-08-17 --> ## 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 b6535a5..42d3004 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-07-23 + updated: 2026-08-17 --> ## 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 d3612a3..0779d91 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-07-23 + updated: 2026-08-17 --> # 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 a906fce..2b8dbeb 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-07-23 + updated: 2026-08-17 --> ## KV Store — Example Reference @@ -132,6 +132,14 @@ Validation is performed by `validateQueryParams(queryParams: URLSearchParams)` i - `min` and `max` are required for: `zrange`. - `item` is required for: `bfExists`. +**Type definitions (from `utils.ts`):** +```ts +const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; +export type Action = (typeof ALL_ACTIONS)[number]; +type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; +type Params = { [key in ParamKey]: string }; +``` + --- ### Error Handling @@ -246,10 +254,31 @@ addEventListener('fetch', (event: FetchEvent) => { Decodes an `ArrayBuffer` to a UTF-8 string. Returns `''` if `arrVal` is `null`. +```ts +export const decodeValueArray = (arrVal: ArrayBuffer | null) => { + if (arrVal) { + const decoder = new TextDecoder(); + return decoder.decode(arrVal); + } + return ''; +}; +``` + #### `stringifyValueScoreTuples(tupleList: Array<[ArrayBuffer, number]>): string` Formats sorted-set result tuples as a string: `[{ Value: , Score: }, ...]`. +```ts +export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { + let strResponse = '['; + for (const tuple of tupleList) { + strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; + } + strResponse += ']'; + return strResponse; +}; +``` + --- ### Build Configuration @@ -299,3 +328,4 @@ TypeScript types for FastEdge globals (`FetchEvent`, etc.) are provided by `@gco - `"type": "module"` must be set in `package.json` for ESM compatibility with `fastedge-build`. - The SDK dependency version is `^2.3.0`. - `tsconfig.json` `target` is `ES2023`; `moduleResolution` is `Bundler`; `lib` is `["ES2023"]`; `types` is `["@gcoredev/fastedge-sdk-js"]`. +- Empty string values for required query parameters are treated as missing — validation rejects them the same as absent params. 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 a1963fe..6490c05 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-07-23 + updated: 2026-08-17 --> # Proxy and Response Transform Patterns (JavaScript/TypeScript) 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 fb7264b..0c82fbe 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-07-23 + updated: 2026-08-17 --> # 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 7e3acc8..b14550a 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-07-23 + updated: 2026-08-17 --> # 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 5827670..80f5974 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-07-23 + updated: 2026-08-17 --> # 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 f82d262..7a3b373 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-07-23 + updated: 2026-08-17 --> ## 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 1e1f8f7..71565f9 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-07-23 + updated: 2026-08-17 --> --- @@ -252,3 +252,121 @@ Output: Headers - fastedge-sdk-js SDK reference - http-base skeleton - FastEdge build CLI reference + +## Source Material + +### FILE: examples/ab-testing/src/index.js + +```js +import { getEnv } from 'fastedge::env'; + +const testConfig = { + logo: [ + { variant: 'hops', weight: 50 }, + { variant: 'bottle', weight: 50 }, + ], + font: [ + { variant: 'exo2', weight: 40 }, + { variant: 'gloria', weight: 65 }, + { variant: 'standard', weight: 45 }, + ], +}; + +async function eventHandler({ request }) { + const [xid, slicedHeaders] = sliceAbTestIdFromCookie(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', { + status: 500, + }); + } + + 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', + `x-fastedge-abid=${xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax;`, + ); + + return new Response(response.body, { + status: response.status, + headers: resHeaders, + }); +} + +addEventListener('fetch', (event) => { + event.respondWith(eventHandler(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); + } else { + headers.delete('cookie'); + } + return [xid, headers]; + } + const randomXid = `${Math.random()}`.slice(1, 5); + // Request does not contain A/B cookie, return random number + return [randomXid, headers]; +}; + +const forceWeightsToPercentages = (testValues) => { + const total = testValues.reduce((acc, { weight }) => acc + weight, 0); + return testValues.map(({ variant, weight }) => ({ + variant, + percentage: (weight / total) * 100, + })); +}; + +const createAbTestHeaders = (reqHeaders, testConfig, xid) => { + const headers = new Headers(reqHeaders); + for (const testName of Object.keys(testConfig)) { + const xidPercentage = Number.parseFloat(xid) * 100; + const testValues = forceWeightsToPercentages(testConfig[testName]); + let start = 0; + for (const { variant, percentage } of testValues) { + const end = start + percentage; + if (xidPercentage >= start && xidPercentage < end) { + headers.set(`ab-test-${testName}`, variant); + break; + } + start = end; + } + } + 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/scaffold/reference/http/base-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-ts.md index 7d20330..558d43c 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-07-23 + updated: 2026-08-17 --> --- @@ -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-07-23 +updated: 2026-08-17 --- # 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 6b889c9..f81acb2 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-07-23 + updated: 2026-08-17 --> --- @@ -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 ec77b72..8e33d7f 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-07-23 + updated: 2026-08-17 --> --- @@ -231,3 +231,117 @@ 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 c8c4e8a..5579117 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-07-23 + updated: 2026-08-17 --> --- @@ -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 6d6aa86..3331544 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-07-23 + updated: 2026-08-17 --> --- @@ -40,7 +40,7 @@ Build script: `fastedge-build src/index.js dist/crypto-hmac-jwt.wasm` import { getSecret } from 'fastedge::secret'; ``` -`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons: +`TextEncoder` and `TextDecoder` are globals available in the FastEdge runtime. Instantiate them once as top-level singletons reused across requests: ```js const encoder = new TextEncoder(); 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 7b17509..f8dd25a 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-07-23 + updated: 2026-08-17 --> --- 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 b4ffbb2..daf9cda 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-07-23 + updated: 2026-08-17 --> --- 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 b14c60e..ea38fe3 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-07-23 + updated: 2026-08-17 --> --- @@ -154,7 +154,6 @@ addEventListener('fetch', (event) => { }); ``` - ### FILE: examples/headers/package.json ```json 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 a21b2d4..e5691d5 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-07-23 + updated: 2026-08-17 --> --- 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 f11cd59..bd5f092 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-07-23 + updated: 2026-08-17 --> --- @@ -286,186 +286,3 @@ Missing required parameters return HTTP 500 with JSON `{ "error": "..." }`. - The KV store must be pre-created in the Gcore dashboard or API before the app can use it. The store name is passed at runtime (e.g., as a query parameter). - Available KV operations: `get`, `scan`, `zrangeByScore`, `zscan`, `bfExists`. - `tsconfig.json` uses `"moduleResolution": "Bundler"` and `"target": "ES2023"`. Do not use `"moduleResolution": "Node"` or older ES targets with this SDK version. - -## Source Material - -### FILE: examples/kv-store/src/index.ts - -```ts -import { KvStore } from 'fastedge::kv'; - -import { Action, decodeValueArray, stringifyValueScoreTuples, validateQueryParams } from './utils'; - -async function eventHandler(event: FetchEvent): Promise { - try { - const { request: req } = event; - const url = new URL(req.url); - - const params = validateQueryParams(url.searchParams); - if (params.error) { - throw new Error(params.error); - } - - const myStore = KvStore.open(params.store); - const action = params.action as Action; - - const responseObj: Record = { - Store: params.store, - Action: action, - }; - - switch (action) { - case 'get': { - const response = myStore.get(params.key); - responseObj.Key = params.key; - responseObj.Response = decodeValueArray(response); - break; - } - case 'scan': { - const response = myStore.scan(params.match); - responseObj.Match = params.match; - responseObj.Response = response.join(', '); - break; - } - case 'zrange': { - const { key, min, max } = params; - const response = myStore.zrangeByScore(key, Number.parseFloat(min), Number.parseFloat(max)); - responseObj.Key = key; - responseObj.Min = min; - responseObj.Max = max; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'zscan': { - const { key, match } = params; - const response = myStore.zscan(key, match); - responseObj.Key = key; - responseObj.Match = match; - responseObj.Response = stringifyValueScoreTuples(response); - break; - } - case 'bfExists': { - const { key, item } = params; - const exists = myStore.bfExists(key, item); - responseObj.Key = key; - responseObj.Item = item; - responseObj.Response = exists ? 'true' : 'false'; - break; - } - default: - break; - } - - return Response.json(responseObj); - } catch (error: Error | unknown) { - return Response.json({ error: `${(error as Error).message}` }, { status: 500 }); - } -} - -addEventListener('fetch', (event: FetchEvent) => { - event.respondWith(eventHandler(event)); -}); -``` - -### FILE: examples/kv-store/src/utils.ts - -```ts -const ALL_ACTIONS = ['get', 'scan', 'zscan', 'zrange', 'bfExists'] as const; - -export type Action = (typeof ALL_ACTIONS)[number]; - -type ParamKey = 'action' | 'store' | 'key' | 'match' | 'min' | 'max' | 'item' | 'error'; - -type Params = { [key in ParamKey]: string }; - -export function validateQueryParams(queryParams: URLSearchParams): Params { - const validParams = {} as Params; - - // Validate 'action' parameter - const action = queryParams.get('action') ?? 'get'; - if (ALL_ACTIONS.includes(action as Action)) { - validParams.action = action; - } else { - validParams.error = `Invalid action '${action}'. Supported actions are: ${ALL_ACTIONS.join( - ', ', - )}`; - return validParams; - } - - const requiredParameters = { - store: [...ALL_ACTIONS], - key: ['get', 'zrange', 'zscan', 'bfExists'], - match: ['scan', 'zscan'], - min: ['zrange'], - max: ['zrange'], - item: ['bfExists'], - } as Record>; - - for (const [key, actions] of Object.entries(requiredParameters)) { - if (actions.includes(action)) { - const value = queryParams.get(key); - if (value && value !== '') { - validParams[key as ParamKey] = value; - } else { - validParams.error = `Query parameters must provide '${key}' for a '${action}' action.`; - return validParams; - } - } - } - - return validParams; -} - -export const decodeValueArray = (arrVal: ArrayBuffer | null) => { - if (arrVal) { - const decoder = new TextDecoder(); - return decoder.decode(arrVal); - } - return ''; -}; - -export const stringifyValueScoreTuples = (tupleList: Array<[ArrayBuffer, number]>): string => { - let strResponse = '['; - for (const tuple of tupleList) { - strResponse += `{ Value: ${decodeValueArray(tuple[0])}, Score: ${tuple[1]} }, `; - } - strResponse += ']'; - return strResponse; -}; -``` - -### FILE: examples/kv-store/package.json - -```json -{ - "name": "fastedge-example-kv-store", - "version": "1.0.0", - "description": "FastEdge JS example: KV Store operations via query params", - "type": "module", - "scripts": { - "build": "fastedge-build -c" - }, - "dependencies": { - "@gcoredev/fastedge-sdk-js": "^2.3.0" - } -} -``` - -### FILE: examples/kv-store/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/mcp-server-ts.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/mcp-server-ts.md index 46c119b..54b0eef 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-07-23 + updated: 2026-08-17 --> --- @@ -400,3 +400,282 @@ 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 7fd769f..f802a94 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-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,49 @@ 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 3882cd6..5a0a7b2 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-07-23 + updated: 2026-08-17 --> --- 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 c7ddc04..a9cb616 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-07-23 + updated: 2026-08-17 --> --- 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 61b3fbb..497a726 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-07-23 + updated: 2026-08-17 --> --- 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 9141628..ae130e1 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-07-23 + updated: 2026-08-17 --> --- 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 5fe2b27..8155e9b 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-07-23 + updated: 2026-08-17 --> --- @@ -73,6 +73,7 @@ function app(event) { 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`)); } @@ -114,51 +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 ee50bab..758ad7d 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-07-23 + updated: 2026-08-17 --> --- 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 43e1a3d..5d7002e 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-07-23 + updated: 2026-08-17 --> --- @@ -202,87 +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 fffcaaa..889bb88 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-07-23 + updated: 2026-08-17 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md b/plugins/gcore-fastedge/skills/scaffold/reference/init-cli.md index 3d72c6f..0c4a451 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-07-23 + updated: 2026-08-17 --> # 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 316ceb2..273f457 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-07-23 + updated: 2026-08-17 --> ## How It Works