From 24edeb9d52ef625e87bd09946f180462ce57903a Mon Sep 17 00:00:00 2001
From: Quim T <26749475+espetro@users.noreply.github.com>
Date: Mon, 19 Oct 2026 07:46:28 +0000
Subject: [PATCH] docs: annotate handlers with (c: CtxCore), the
scriptc-compilable form
scriptc refuses unannotated handler params ('any') and the Ctx
generic interface (SC2009); CtxCore is the runtime class Handler is
typed on. Fixes the @chttp/auth README quickstart (cutils#29) and the
same handler idiom across the docs site and package READMEs.
---
README.md | 22 +++++++++++-----------
docs/deploy.md | 4 ++--
docs/html.md | 3 ++-
docs/index.md | 4 ++--
docs/middleware.md | 26 +++++++++++++-------------
docs/multipart.md | 4 ++--
docs/quickstart.md | 12 ++++++------
docs/routing.md | 6 ++++--
docs/static.md | 4 ++--
docs/streaming.md | 6 +++---
docs/unplugin.md | 6 +++---
packages/auth/README.md | 5 +++--
packages/router/README.md | 12 ++++++------
packages/stream/README.md | 4 ++--
14 files changed, 61 insertions(+), 57 deletions(-)
diff --git a/README.md b/README.md
index 302bbc7..4c940d3 100644
--- a/README.md
+++ b/README.md
@@ -22,14 +22,14 @@ npm i @chttp/router
Import:
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
const app = createRouter();
-app.get("/", (c) => c.text("hi"));
-app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
-app.post("/echo", (c) => c.text(c.req.body()));
-app.notFound((c) => c.status(404).text("not found"));
+app.get("/", (c: CtxCore) => c.text("hi"));
+app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
+app.post("/echo", (c: CtxCore) => c.text(c.req.body()));
+app.notFound((c: CtxCore) => c.status(404).text("not found"));
app.serve({ port: 3000 });
```
@@ -39,8 +39,8 @@ app.serve({ port: 3000 });
Static segments, `:param`, and a tail `*` wildcard:
```ts
-app.get("/user/:id", (c) => c.req.param("id")); // 'id' is literal-typed
-app.get("/wild/*", (c) => c.req.param("*"));
+app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // 'id' is literal-typed
+app.get("/wild/*", (c: CtxCore) => c.req.param("*"));
app.all("/any-method", handler);
app.on("m-search", "/custom", handler);
```
@@ -88,13 +88,13 @@ c.redirect("/elsewhere", 302);
registered after it (snapshot semantics):
```ts
-app.use((c, next) => {
+app.use((c: CtxCore, next) => {
c.header("x-start", Date.now().toString());
next();
});
// path-scoped: `*` matches every path, `/api/*` is a prefix
-app.use("/api/*", async (c, next) => {
+app.use("/api/*", async (c: CtxCore, next) => {
await auth(c.req);
await next();
});
@@ -116,7 +116,7 @@ at 100% static).
```ts
const app = createRouter();
-app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
+app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
// Cloudflare Workers / Deno Deploy / Bun / Service Worker
export default { async fetch(req, env, ctx) { return app.fetch(req, env, ctx); } };
@@ -201,7 +201,7 @@ same band as [valibot](https://github.com/open-circle/valibot).
import * as v from "@chttp/val";
const Order = v.strictObject({ id: v.pipe(v.string(), v.uuid()) });
-app.post("/order", (c) => {
+app.post("/order", (c: CtxCore) => {
const r = v.safeParse(Order, JSON.parse(c.req.body()));
return r.success
? c.json({ ok: true })
diff --git a/docs/deploy.md b/docs/deploy.md
index 7f1aa54..5bfdfee 100644
--- a/docs/deploy.md
+++ b/docs/deploy.md
@@ -47,10 +47,10 @@ Use this when you need npm packages outside the scriptc subset. See [Run targets
The canonical entry is `app.fetch(req: Request) → Promise`. The same `createRouter()` symbol works on Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, Netlify Edge, Supabase, and as a Service Worker. `app.handler()` is a thin Node adapter around `fetch()` for `node:http` users.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
const app = createRouter();
-app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
+app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
// Cloudflare Workers / Deno Deploy / Bun / Vercel Edge / Service Worker
export default {
diff --git a/docs/html.md b/docs/html.md
index 586663a..f0d4de6 100644
--- a/docs/html.md
+++ b/docs/html.md
@@ -16,13 +16,14 @@
| `isVoidElement`, `isValidTag`, `isValidAttrName` | `(s: string) => boolean` | Exposed helpers |
```ts
+import { CtxCore } from "@chttp/router";
import { BARE, escapeHtml, h, html } from "@chttp/html";
function Todo(props: { text: string; done: boolean }): string {
return h("li", ["class", props.done ? "done" : "open"], [escapeHtml(props.text)]);
}
-app.get("/todos", (c) =>
+app.get("/todos", (c: CtxCore) =>
c.html(
h("ul", [], [Todo({ text: "ship", done: false })]) +
h("input", ["disabled", BARE, "value", "x"], []),
diff --git a/docs/index.md b/docs/index.md
index e1b9aa4..cf7eea3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,10 +3,10 @@
chttp is a minimal async-first HTTP router for `node:http`, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset so the same source also compiles to a single native ELF.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
const app = createRouter();
-app.get("/user/:id", async (c) => c.json(await fetchUser(c.req.param("id"))));
+app.get("/user/:id", async (c: CtxCore) => c.json(await fetchUser(c.req.param("id"))));
app.serve({ port: 3000 });
```
diff --git a/docs/middleware.md b/docs/middleware.md
index 63264c1..18bb14a 100644
--- a/docs/middleware.md
+++ b/docs/middleware.md
@@ -3,7 +3,7 @@
Middleware is `(c, next) => void | Promise` — sync or async. `next()` returns the tail's result, so `await next()` in an async middleware waits for the rest of the chain to settle.
```ts
-app.use((c, next) => {
+app.use((c: CtxCore, next) => {
c.header("x-start", Date.now().toString());
next();
});
@@ -26,16 +26,16 @@ Every snippet below is written in the scriptc subset (indexed `for` loops, `| nu
Two pieces, because of how dispatch orders things: an `OPTIONS` request to a path that only has `GET`/`POST` registered fails with 405 *before any middleware runs*, so a middleware alone cannot answer preflight. The working shape is a `use()` middleware for the response headers plus one explicit `app.options()` route per path (or `app.all()` with an in-handler method check) that answers the preflight with 204.
```ts
-import type { Ctx } from "@chttp/router";
+import type { CtxCore } from "@chttp/router";
// 1. Response headers on every actual request (apply to routes after this use()).
-app.use((c: Ctx, next: () => unknown): unknown => {
+app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("access-control-allow-origin", "*");
return next();
});
// 2. Explicit preflight answer: 204, no body.
-function preflight(c: Ctx): void {
+function preflight(c: CtxCore): void {
c.status(204)
.header("access-control-allow-origin", "*")
.header("access-control-allow-methods", "GET,POST,PUT,DELETE,OPTIONS")
@@ -48,11 +48,11 @@ function preflight(c: Ctx): void {
c.sent = true; // tells dispatch this middleware answered
}
-app.options("/users", (c: Ctx) => {
+app.options("/users", (c: CtxCore) => {
preflight(c);
});
-app.get("/users", (c: Ctx) => c.json({ users: 1 }));
-app.post("/users", (c: Ctx) => c.json({ ok: true }), { body: true });
+app.get("/users", (c: CtxCore) => c.json({ users: 1 }));
+app.post("/users", (c: CtxCore) => c.json({ ok: true }), { body: true });
```
With credentials instead of `*`: echo the request's `origin` header and add `access-control-allow-credentials: true`. For many paths, a shared `app.all("/*", ...)` fallback also answers preflight (a splat route matches paths with no other registration), but paths that have any method registered need their own `app.options()` entry or the 405 wins.
@@ -71,7 +71,7 @@ function fnv1a(s: string): string {
return h.toString(16);
}
-app.get("/users", (c: Ctx) => {
+app.get("/users", (c: CtxCore) => {
const body: string = JSON.stringify({ users: [1, 2] });
const etag: string = '"' + fnv1a(body) + '"';
const inm: string | null = c.req.header("if-none-match");
@@ -101,7 +101,7 @@ The comparison is exact equality against a single etag. Browsers may send a comm
Async middleware: `await next()` waits until the chain settles, so `Date.now()` after it includes the handler's async work. Duration goes to stdout, not a response header, because the response may already be flushed (the flush rule above).
```ts
-app.use(async (c: Ctx, next: () => unknown): Promise => {
+app.use(async (c: CtxCore, next: () => unknown): Promise => {
const t0: number = Date.now();
await next();
console.log(c.req.method + " " + c.req.path + " " + String(Date.now() - t0) + "ms");
@@ -115,7 +115,7 @@ Register it before the routes you want logged; routes registered earlier carry n
Sync middleware that sets headers before `next()`; they land on every response from routes registered after it.
```ts
-app.use((c: Ctx, next: () => unknown): unknown => {
+app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("x-content-type-options", "nosniff");
c.header("x-frame-options", "DENY");
c.header("referrer-policy", "no-referrer");
@@ -141,7 +141,7 @@ const WINDOW_MS: number = 60000; // window length
const buckets: { [k: string]: Bucket } = {};
-function rateLimit(c: Ctx, next: () => unknown): unknown {
+function rateLimit(c: CtxCore, next: () => unknown): unknown {
const key: string = c.req.header("x-api-key") === null ? "anon" : String(c.req.header("x-api-key"));
const now: number = Date.now();
const cur: Bucket | null = buckets[key] === undefined ? null : buckets[key];
@@ -177,13 +177,13 @@ A true `server-timing: app;dur=N` header needs the duration *before* the body is
```ts
// start stamp: header lands (set before the handler can flush)
-app.use((c: Ctx, next: () => unknown): unknown => {
+app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("x-request-start", String(Date.now()));
return next();
});
// duration: log it, the response is already flushed by the time you know it
-app.use(async (c: Ctx, next: () => unknown): Promise => {
+app.use(async (c: CtxCore, next: () => unknown): Promise => {
const t0: number = Date.now();
await next();
console.log(c.req.path + " " + String(Date.now() - t0) + "ms");
diff --git a/docs/multipart.md b/docs/multipart.md
index c5c8b68..801fa52 100644
--- a/docs/multipart.md
+++ b/docs/multipart.md
@@ -3,13 +3,13 @@
`multipart/form-data` uploads are opt-in per route — the same seam as `{ body: true }`. Declare `multipart` in the route options and dispatch parses the stream before the handler chain runs, so `c.req` stays synchronous.
```ts
-import { createRouter, diskStorage, memoryStorage } from "@chttp/router";
+import { CtxCore, createRouter, diskStorage, memoryStorage } from "@chttp/router";
const app = createRouter();
app.post(
"/api/entries/:entryId/media",
- (c) => {
+ (c: CtxCore) => {
const file = c.req.file("file"); // UploadedFile | null
const title = c.req.field("title"); // string | null
db.run([file!.path, file!.mimeType, file!.size, title]);
diff --git a/docs/quickstart.md b/docs/quickstart.md
index cba2b86..d54aada 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -5,7 +5,7 @@ pnpm add @chttp/router
```
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
const app = createRouter();
@@ -16,18 +16,18 @@ function sleep(ms: number): Promise {
}
// Sync handler: returns immediately, no microtask cost.
-app.get("/", (c) => c.text("hi"));
-app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
+app.get("/", (c: CtxCore) => c.text("hi"));
+app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
// Async handler: await anything, the router waits for it to settle.
-app.get("/slow", async (c) => {
+app.get("/slow", async (c: CtxCore) => {
await sleep(100);
c.text("done");
});
// Declare { body: true } or c.req.body() returns "".
-app.post("/echo", (c) => c.text(c.req.body()), { body: true });
-app.notFound((c) => c.status(404).text("not found"));
+app.post("/echo", (c: CtxCore) => c.text(c.req.body()), { body: true });
+app.notFound((c: CtxCore) => c.status(404).text("not found"));
app.serve({ port: 3000 });
```
diff --git a/docs/routing.md b/docs/routing.md
index 2b1cbaf..213ab05 100644
--- a/docs/routing.md
+++ b/docs/routing.md
@@ -5,8 +5,10 @@ Routes are registered per method: `app.get | post | put | delete | patch | head
Paths support static segments, `:param`, and a tail `*` wildcard.
```ts
-app.get("/user/:id", (c) => c.req.param("id")); // literal-typed: 'id'
-app.get("/wild/*", (c) => c.req.param("*"));
+import { CtxCore } from "@chttp/router";
+
+app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // literal-typed: 'id'
+app.get("/wild/*", (c: CtxCore) => c.req.param("*"));
```
## Semantics
diff --git a/docs/static.md b/docs/static.md
index db87110..a6d9070 100644
--- a/docs/static.md
+++ b/docs/static.md
@@ -3,11 +3,11 @@
`@chttp/static` serves files and single page apps from a directory or from bytes baked into the binary. It depends on `@chttp/router` only and is authored in the scriptc subset, so it compiles into the same native ELF as the rest of your app.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
import { DiskAssets, serveStatic } from "@chttp/static";
const app = createRouter();
-app.get("/api/ping", (c) => c.json({ ok: true }));
+app.get("/api/ping", (c: CtxCore) => c.json({ ok: true }));
serveStatic(app, "/", new DiskAssets("./web/dist"), { spa: true });
app.serve({ port: 3000 });
```
diff --git a/docs/streaming.md b/docs/streaming.md
index b997527..fe3cebc 100644
--- a/docs/streaming.md
+++ b/docs/streaming.md
@@ -3,12 +3,12 @@
`@chttp/stream` adds Server-Sent Events and chunked text responses to `@chttp/router`. It is authored in the scriptc subset, so it compiles into the same native binary as your app.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
import { streamSSE } from "@chttp/stream";
const app = createRouter();
-app.get("/clock", (c) =>
+app.get("/clock", (c: CtxCore) =>
streamSSE(c, (s) => {
const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000);
s.onAbort(() => clearInterval(t));
@@ -43,7 +43,7 @@ Multi-line data is split on LF, CRLF or CR into one `data:` line each, as the SS
`streamText(c, fn)` is the same shape for `text/plain` chunked output. The stream has `write(s)`, `writeln(s)`, `close()` and `onAbort(fn)`.
```ts
-app.get("/log", (c) =>
+app.get("/log", (c: CtxCore) =>
streamText(c, (s) => {
s.writeln("starting");
s.writeln("done");
diff --git a/docs/unplugin.md b/docs/unplugin.md
index c6d115b..946edf6 100644
--- a/docs/unplugin.md
+++ b/docs/unplugin.md
@@ -58,12 +58,12 @@ Mark a routes module:
```ts
// src/routes.chttp.ts
-import { createRouter } from '@chttp/router'
+import { CtxCore, createRouter } from '@chttp/router'
export const app = createRouter()
-app.get('/user/:id', (c) => c.json({ id: c.req.param('id') }))
-app.post('/echo', (c) => c.text(c.req.body()), { body: true })
+app.get('/user/:id', (c: CtxCore) => c.json({ id: c.req.param('id') }))
+app.post('/echo', (c: CtxCore) => c.text(c.req.body()), { body: true })
```
Wire the generated dispatch:
diff --git a/packages/auth/README.md b/packages/auth/README.md
index b7da27c..6633d91 100644
--- a/packages/auth/README.md
+++ b/packages/auth/README.md
@@ -7,6 +7,7 @@ Every function is string-in/string-out or boolean-out; none of them touches a re
## Quickstart
```ts
+import { CtxCore } from "@chttp/router";
import {
MemorySessionStore,
buildSessionCookie,
@@ -18,14 +19,14 @@ import {
const store = new MemorySessionStore(); // dev/test only — see below
-app.post("/login", async (c) => {
+app.post("/login", async (c: CtxCore) => {
const token = generateSessionToken();
await createSession(store, token, "user-1", { role: "admin" });
c.header("set-cookie", buildSessionCookie(token, { secure: false }));
return c.status(201).text("ok");
});
-app.get("/me", async (c) => {
+app.get("/me", async (c: CtxCore) => {
const tok = readCookie(c.req.header("cookie"), "session");
const rec = tok === null ? null : await validateSessionToken(store, tok);
if (rec === null) return c.status(401).text("unauthorized");
diff --git a/packages/router/README.md b/packages/router/README.md
index c629f6b..4c41980 100644
--- a/packages/router/README.md
+++ b/packages/router/README.md
@@ -3,20 +3,20 @@
Minimal async-first HTTP router for `node:http`, zero dependencies, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset. The same source lowers to a native ELF directly, no plugin required.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
const app = createRouter();
-app.get("/", (c) => c.text("hi"));
-app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
-app.post("/echo", (c) => c.text(c.req.body()));
+app.get("/", (c: CtxCore) => c.text("hi"));
+app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
+app.post("/echo", (c: CtxCore) => c.text(c.req.body()));
-app.get("/slow", async (c) => {
+app.get("/slow", async (c: CtxCore) => {
const user = await fetchUser(c.req.param("id"));
return c.json(user);
});
-app.notFound((c) => c.status(404).text("not found"));
+app.notFound((c: CtxCore) => c.status(404).text("not found"));
app.serve({ port: 3000 });
```
diff --git a/packages/stream/README.md b/packages/stream/README.md
index 94b6991..9227bc9 100644
--- a/packages/stream/README.md
+++ b/packages/stream/README.md
@@ -3,12 +3,12 @@
Server-Sent Events and chunked text streaming for [@chttp/router](https://github.com/sigilco/chttp), authored inside the scriptc compilable subset so it compiles into the same native binary.
```ts
-import { createRouter } from "@chttp/router";
+import { CtxCore, createRouter } from "@chttp/router";
import { streamSSE } from "@chttp/stream";
const app = createRouter();
-app.get("/clock", (c) =>
+app.get("/clock", (c: CtxCore) =>
streamSSE(c, (s) => {
const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000);
s.onAbort(() => clearInterval(t));