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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,45 @@ The public API surface is defined by:

Changes to these surfaces require updated `docs/`, updated tests, and a semver-appropriate version bump.

## Documentation Freshness
## Content in `docs/`

`docs/` contains two distinct types of files with different authoring rules:

### Machine-generated docs (most files)

These files are generated from source code by `./fastedge-plugin-source/generate-docs.sh` and **must not be edited by hand** — manual changes will be silently overwritten on the next generation run:

- `docs/BUILD_CLI.md`, `docs/INIT_CLI.md`, `docs/ASSETS_CLI.md`, `docs/STATIC_SITES.md`, `docs/SDK_API.md`
- `docs/quickstart.md`, `docs/INDEX.md`

### Hand-curated docs (exception)

These files contain knowledge and best practices with no single code-source equivalent. They are **authored directly** and are not produced by `generate-docs.sh`:

`docs/` is the single source of truth for public API documentation. When code changes affect the public API or user-facing behavior, **request changes** if the corresponding doc file was not updated in the same PR.
- `docs/AUTH_PATTERNS.md` — JWT/HMAC auth patterns, `crypto.subtle` usage guidance
- `docs/HONO_PATTERNS.md` — Hono framework integration patterns for FastEdge
- `docs/PROXY_PATTERNS.md` — Proxy and response transformation patterns
- `docs/RUNTIME_CONSTRAINTS.md` — StarlingMonkey JS runtime capabilities and constraints

**For hand-curated docs:** Edit them directly. Do not run `generate-docs.sh` for these files — it will not affect them.

### When reviewing PRs that touch `docs/`:

- For **generated** docs: never suggest manual edits. If stale or incorrect, suggest: **Run `./fastedge-plugin-source/generate-docs.sh`**
- For **hand-curated** docs (`AUTH_PATTERNS.md`, `HONO_PATTERNS.md`, `PROXY_PATTERNS.md`, `RUNTIME_CONSTRAINTS.md`): direct edits are correct and expected
- If the generated output itself is wrong (e.g., wrong structure, missing section), the fix belongs in `fastedge-plugin-source/.generation-config.md`, not in the generated `docs/` file directly
- If a PR modifies a **generated** `docs/` file without a corresponding source code change, flag it — the change should come from the generation script, not a hand-edit

### When reviewing PRs that change source code covered by `docs/`:

- Check whether the change affects the public API or user-facing behavior
- If yes, and `docs/` was not regenerated in the same PR, **request changes** with:
> Source code affecting public API was changed but docs/ was not regenerated.
> Run: `./fastedge-plugin-source/generate-docs.sh`

## Documentation Freshness

### Public API changes (must update docs/)
### Public API changes (must regenerate docs/)
- New, modified, or removed CLI flags in `src/cli/fastedge-build/build.ts`
- Changes to `BuildConfig` or `AssetCacheConfig` interfaces in `src/cli/fastedge-build/types.ts`
- Changes to scaffold wizard behavior in `src/cli/fastedge-init/`
Expand All @@ -53,19 +87,23 @@ Changes to these surfaces require updated `docs/`, updated tests, and a semver-a
| `types/globals.d.ts` | `docs/SDK_API.md` |
| `package.json` (exports, bin) | `docs/INDEX.md` |
| `types/`, `src/cli/`, `README.md` (quickstart examples) | `docs/quickstart.md` |
| hand-curated — JWT/HMAC auth patterns, `crypto.subtle` usage guidance | `docs/AUTH_PATTERNS.md` |
| hand-curated — Hono framework integration patterns for FastEdge | `docs/HONO_PATTERNS.md` |
| hand-curated — proxy and response transformation patterns | `docs/PROXY_PATTERNS.md` |
| hand-curated — StarlingMonkey JS runtime capabilities and constraints | `docs/RUNTIME_CONSTRAINTS.md` |
| `fastedge-plugin-source/manifest.json` | `.github/copilot-instructions.md` |

### Violation example

> PR changes `BuildConfig` interface in `types.ts` but `docs/BUILD_CLI.md` still shows the old field names → **request changes**. The config interface must be documented before merge.
> PR changes `BuildConfig` interface in `types.ts` but `docs/BUILD_CLI.md` still shows the old field names → **request changes**. Run `./fastedge-plugin-source/generate-docs.sh` before merge.

### Quickstart protection

If any public API signature or CLI behavior changes, check whether `docs/quickstart.md` examples are still accurate. Request changes if examples would no longer work against the updated code.
If any public API signature or CLI behavior changes, check whether `docs/quickstart.md` examples are still accurate. Request regeneration if examples would no longer work against the updated code.

### Pipeline source contract

If `fastedge-plugin-source/manifest.json` lists source files that overlap with files changed in this PR, request that `docs/` is updated to keep the plugin pipeline's source material current.
If `fastedge-plugin-source/manifest.json` lists source files that overlap with files changed in this PR, request that `docs/` is regenerated (run `./fastedge-plugin-source/generate-docs.sh`) to keep the plugin pipeline's source material current.

## Quality Rules

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/copilot-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- fastedge-plugin-source/check-copilot-sync.sh
- .github/copilot-instructions.md
- .github/workflows/copilot-sync.yml
- examples/**
- docs/**

jobs:
check-sync:
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ package-lock.json

# FastEdge debugger artifacts
**/.fastedge-debug/

# example project lock files
examples/**/pnpm-lock.yaml
examples/**/package-lock.json

# Doc-generator failure artifacts — rejected/preamble-leaked claude -p
# outputs preserved for prompt-debugging. Prune manually.
docs/.failures/
9 changes: 9 additions & 0 deletions context/CONTEXT_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ Items that need attention. Surface these when asked "what's next" or "what needs
- **semantic-release** 23 → 25: Two major versions, needs CI pipeline testing. Upgrade with `conventional-changelog-eslint` 5 → 6.
- **TypeScript** 5.8 → 6.0: High risk, wait for ecosystem (`typescript-eslint`, tooling) to stabilize. Run `npx @andrewbranch/ts5to6` migration tool when ready.

### Doc verification backlog (build a test example, then add to `docs/`)

These are runtime/Web-API behaviors that have been *requested* in patterns docs but cannot yet be verified against an existing example or runtime test. Build a minimal example app proving each works on FastEdge before adding it to a `docs/` file. If a behavior is **not** supported, capture that here too — negative findings are also documentation.

- **`Response.clone()`** — Standard Web Fetch API. Used by patterns where the upstream body needs to be read twice (e.g. log full response while transforming a copy). No example currently exercises this. Build: a small handler that clones a `fetch()` response and reads both copies. Verify both bodies decode to the same bytes. If it works, the `docs/PROXY_PATTERNS.md` "JSON Transform" section can be expanded to document `clone()` for dual-read patterns.
- **`fetch(url, { redirect: "manual" })`** — Standard Web Fetch option, returns the upstream redirect response without following it. Used by patterns where the app needs to inspect or rewrite the `Location` header. Runtime test harness includes WPT `redirect-mode.any.js` but that does not confirm FastEdge's outbound `fetch` honors the option in production. Build: a handler that issues a `fetch()` to a known 302 endpoint with `redirect: "manual"` and asserts the response is the 302 itself, not the followed target. If it works, the `docs/PROXY_PATTERNS.md` operational notes can call out manual redirect handling.

When adding either to docs, also update the manifest source description so reviewers know the content is now grounded in an example.

---

## Search Tips
Expand Down
168 changes: 168 additions & 0 deletions docs/AUTH_PATTERNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<!-- maintenance: hand-authored. Not produced by fastedge-plugin-source/generate-docs.sh. Edit this file directly. -->

# Authentication Patterns

Authentication on FastEdge typically combines `getSecret` (for signing keys and shared secrets) with `crypto.subtle` (for HMAC and signature verification). This document covers Bearer-token validation and HMAC-SHA256 JWT verification — the two most common patterns.

## Secrets Setup

Auth credentials must never be hardcoded. Store them as FastEdge secrets and read at request time via `getSecret`:

```typescript
import { getSecret } from "fastedge::secret";

const token = getSecret("API_TOKEN"); // string | null
if (token === null) {
return new Response("Server misconfigured", { status: 500 });
}
```

`getSecret` returns `null` when the secret is not provisioned. Always handle the null case before using the value.

## Bearer Token Pattern

Extract a Bearer token from the `Authorization` header and compare against a configured shared secret:

```typescript
import { getSecret } from "fastedge::secret";

addEventListener("fetch", (event) => {
event.respondWith(handle(event.request));
});

async function handle(request) {
const authHeader = request.headers.get("Authorization") ?? "";
const match = authHeader.match(/^Bearer\s+(.+)$/iu);
if (!match) {
return Response.json(
{ error: "missing or malformed Authorization header" },
{ status: 401 },
);
}

const expected = getSecret("API_TOKEN");
if (expected === null) {
return Response.json({ error: "server misconfigured" }, { status: 500 });
}

if (match[1] !== expected) {
return Response.json({ error: "invalid token" }, { status: 403 });
}

return Response.json({ ok: true });
}
```

For Hono apps, this pattern is wrapped as middleware:

```typescript
import { Hono } from "hono";
import { getSecret } from "fastedge::secret";

const app = new Hono();

app.use("/api/*", async (c, next) => {
const auth = c.req.header("Authorization") ?? "";
const match = auth.match(/^Bearer\s+(.+)$/iu);
if (!match) return c.json({ error: "missing bearer token" }, 401);

const expected = getSecret("API_TOKEN");
if (expected === null) return c.json({ error: "server misconfigured" }, 500);
if (match[1] !== expected) return c.json({ error: "invalid token" }, 403);

await next();
});
```

## HMAC-SHA256 JWT Verification

For signed tokens, use `crypto.subtle` to verify the HMAC. The signing secret is loaded via `getSecret`. This is the pattern from `examples/crypto-hmac-jwt/`:

```typescript
import { getSecret } from "fastedge::secret";

const encoder = new TextEncoder();
const decoder = new TextDecoder();

function base64urlToBytes(str) {
const padded = str.replace(/-/g, "+").replace(/_/g, "/")
+ "=".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.charCodeAt(i);
return bytes;
}

async function verifyJwtHs256(token, secret) {
const parts = token.split(".");
if (parts.length !== 3) throw new Error("malformed token");
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;
}
```

Use it inside a request handler:

```typescript
async function handle(request) {
const auth = request.headers.get("Authorization") ?? "";
const match = auth.match(/^Bearer\s+(.+)$/iu);
if (!match) {
return Response.json({ ok: false, error: "missing bearer" }, { status: 401 });
}

const secret = getSecret("JWT_SECRET");
if (!secret) {
return Response.json({ ok: false, error: "JWT_SECRET not configured" }, { status: 500 });
}

try {
const claims = await verifyJwtHs256(match[1], secret);
return Response.json({ ok: true, claims });
} catch (err) {
return Response.json({ ok: false, error: err.message }, { status: 401 });
}
}
```

## Crypto Capabilities

The FastEdge JS runtime supports a subset of `crypto.subtle`:

| Operation | Algorithms supported |
|---|---|
| `digest` | SHA-1, SHA-256, SHA-384, SHA-512 |
| `sign` / `verify` | RSASSA-PKCS1-v1_5, ECDSA, HMAC |
| `importKey` | JWK, PKCS#8, SPKI, raw (HMAC) |

`encrypt`, `decrypt`, `generateKey`, `deriveKey`, `deriveBits`, and `exportKey` are not implemented. For details on runtime constraints and SAML library compatibility, see the runtime constraints reference.
Comment thread
godronus marked this conversation as resolved.

## Operational Notes

- **Never log secret values.** `console.log` output is captured in app logs.
- **Treat `getSecret` as request-time only.** It is not available during module initialization — call it inside the request handler.
- **Always check for `null`.** A misconfigured app should return 500, not crash with a 531 runtime error.
- **Rotate secrets via the API or portal**, not by redeploying the binary.

## See Also

- `examples/crypto-hmac-jwt/` — complete HMAC JWT verification example with fixtures
- `examples/secret-rotation/` — `getSecretEffectiveAt` slot-based rotation patterns
28 changes: 14 additions & 14 deletions docs/BUILD_CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ npx fastedge-build --version

## Options

| Flag | Alias | Type | Description |
| -------------- | ----- | ------------ | -------------------------------- |
| `--input` | `-i` | `String` | Input JavaScript/TypeScript file |
| `--output` | `-o` | `String` | Output WebAssembly file path |
| `--tsconfig` | `-t` | `String` | Path to tsconfig.json |
| `--config` | `-c` | `String[]` | Path(s) to build config files |
| `--help` | `-h` | `Boolean` | Show help |
| `--version` | `-v` | `Boolean` | Show version |
| Flag | Alias | Type | Description |
| ------------- | ----- | ---------- | -------------------------------- |
| `--input` | `-i` | `String` | Input JavaScript/TypeScript file |
| `--output` | `-o` | `String` | Output WebAssembly file path |
| `--tsconfig` | `-t` | `String` | Path to tsconfig.json |
| `--config` | `-c` | `String[]` | Path(s) to build config files |
| `--help` | `-h` | `Boolean` | Show help |
| `--version` | `-v` | `Boolean` | Show version |

## Build Modes

Expand Down Expand Up @@ -123,12 +123,12 @@ export { config };

### BuildConfig Fields

| Field | Type | Required | Description |
| -------------- | ---------------------- | -------- | ---------------------------------------------------- |
| `type` | `'http' \| 'static'` | No | Build type; must be `http` or `static` if provided |
| `entryPoint` | `string` | Yes | Input JavaScript/TypeScript file |
| `wasmOutput` | `string` | Yes | Output WASM file path |
| `tsConfigPath` | `string` | No | Path to tsconfig.json |
| Field | Type | Required | Description |
| -------------- | --------------------- | -------- | -------------------------------------------------- |
| `type` | `'http' \| 'static'` | No | Build type; must be `http` or `static` if provided |
| `entryPoint` | `string` | Yes | Input JavaScript/TypeScript file |
| `wasmOutput` | `string` | Yes | Output WASM file path |
| `tsConfigPath` | `string` | No | Path to tsconfig.json |

### Static-Only Fields

Expand Down
Loading
Loading