@fresho/router 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -6
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +2 -2
- package/dist/docs-B1IFBS_w.mjs +724 -0
- package/dist/docs-B1IFBS_w.mjs.map +1 -0
- package/dist/index.d.mts +90 -24
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +38 -684
- package/dist/index.mjs.map +1 -1
- package/dist/{jwt-DHM_nSwL.d.mts → jwt-B-Fqwu4E.d.mts} +52 -9
- package/dist/jwt-B-Fqwu4E.d.mts.map +1 -0
- package/dist/jwt-ikNuUBcp.mjs +373 -0
- package/dist/jwt-ikNuUBcp.mjs.map +1 -0
- package/dist/mcp/index.d.mts +63 -0
- package/dist/mcp/index.d.mts.map +1 -0
- package/dist/mcp/index.mjs +329 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/middleware/index.d.mts +10 -4
- package/dist/middleware/index.d.mts.map +1 -1
- package/dist/middleware/index.mjs +10 -5
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{middleware-XMbIevSv.mjs → middleware-B-_0PLA6.mjs} +1 -1
- package/dist/{middleware-XMbIevSv.mjs.map → middleware-B-_0PLA6.mjs.map} +1 -1
- package/dist/{middleware-Bxq0LHLS.d.mts → middleware-zRMBpTLe.d.mts} +23 -2
- package/dist/middleware-zRMBpTLe.d.mts.map +1 -0
- package/docs/authentication.md +89 -5
- package/package.json +21 -4
- package/dist/jwt-DHM_nSwL.d.mts.map +0 -1
- package/dist/jwt-Dab9kOC2.mjs +0 -208
- package/dist/jwt-Dab9kOC2.mjs.map +0 -1
- package/dist/middleware-Bxq0LHLS.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -39,12 +39,13 @@ export default { fetch: api.handler() };
|
|
|
39
39
|
|
|
40
40
|
## Features
|
|
41
41
|
|
|
42
|
-
- **Type-safe path params** — `$id` creates dynamic segments
|
|
42
|
+
- **Type-safe path params** — `$id` creates dynamic segments whose names are inferred by handlers and clients
|
|
43
43
|
- **Schema validation** — query and body validated at runtime, typed at compile time
|
|
44
44
|
- **Property-based routing** — property names become URL segments
|
|
45
45
|
- **Typed HTTP client** — call your API with full type safety
|
|
46
46
|
- **Typed local client** — test handlers directly without HTTP
|
|
47
47
|
- **OpenAPI generation** — auto-generate docs from your routes
|
|
48
|
+
- **MCP server** — expose routes as tools for AI agents
|
|
48
49
|
- **Middleware** — cors, auth, rate limiting, and more
|
|
49
50
|
- **Streaming** — SSE and JSON lines helpers
|
|
50
51
|
- **HTTP-compliant** — automatic HEAD support for GET routes
|
|
@@ -56,6 +57,16 @@ export default { fetch: api.handler() };
|
|
|
56
57
|
npm install @fresho/router
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
```bash
|
|
61
|
+
bun add @fresho/router
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Runtime Support
|
|
65
|
+
|
|
66
|
+
The full test suite runs in Node.js 22/24 and Bun 1.3.14. CI also deploys the built
|
|
67
|
+
package to a local Cloudflare `workerd` runtime and verifies routing, validation,
|
|
68
|
+
middleware, and streaming behavior.
|
|
69
|
+
|
|
59
70
|
## Path Convention
|
|
60
71
|
|
|
61
72
|
Property names become URL path segments:
|
|
@@ -219,7 +230,7 @@ See **[Middleware Documentation](docs/middleware.md)** for detailed usage.
|
|
|
219
230
|
For standalone auth utilities (JWT signing/verification, Basic auth, OAuth), import from `@fresho/router/auth`:
|
|
220
231
|
|
|
221
232
|
```typescript
|
|
222
|
-
import { jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";
|
|
233
|
+
import { createJwks, jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";
|
|
223
234
|
|
|
224
235
|
// Sign a JWT
|
|
225
236
|
const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });
|
|
@@ -227,12 +238,20 @@ const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });
|
|
|
227
238
|
// Verify a JWT
|
|
228
239
|
const payload = await jwtVerify(token, secret);
|
|
229
240
|
|
|
241
|
+
// Verify a third-party OAuth/OIDC token against the provider's JWKS
|
|
242
|
+
const jwks = createJwks("https://example.auth0.com/.well-known/jwks.json");
|
|
243
|
+
const claims = await jwtVerify(accessToken, jwks, {
|
|
244
|
+
algorithms: ["RS256"],
|
|
245
|
+
issuer: "https://example.auth0.com/",
|
|
246
|
+
audience: "https://api.example.com",
|
|
247
|
+
});
|
|
248
|
+
|
|
230
249
|
// Parse Basic auth header
|
|
231
250
|
const creds = parseBasicAuth(request.headers.get("Authorization"));
|
|
232
251
|
```
|
|
233
252
|
|
|
234
253
|
Available utilities:
|
|
235
|
-
- **JWT**: `jwtSign`, `jwtVerify`
|
|
254
|
+
- **JWT**: `jwtSign`, `jwtVerify`, `createJwks` (HS*, RS*, PS*, ES256/384)
|
|
236
255
|
- **Basic Auth**: `parseBasicAuth`, `encodeBasicAuth`, `extractBasicAuthToken`
|
|
237
256
|
- **OAuth**: `encodeOAuthState`, `decodeOAuthState`, `exchangeCode`, `refreshAccessToken`, `revokeToken`, `buildAuthorizationUrl`, `OAUTH_PROVIDERS`
|
|
238
257
|
|
|
@@ -278,6 +297,26 @@ await client.users.$get(); // GET /users
|
|
|
278
297
|
await client.users.$post({ body: { name: "Bob" } }); // POST /users
|
|
279
298
|
```
|
|
280
299
|
|
|
300
|
+
The `$id` segment determines the `id` key in the client's `path` option, so missing or misspelled
|
|
301
|
+
parameter names are caught at compile time.
|
|
302
|
+
|
|
303
|
+
### Routers Built by a Factory
|
|
304
|
+
|
|
305
|
+
If your router is created inside a factory so its handlers can close over injected dependencies,
|
|
306
|
+
derive the API type from the factory's return type. The type-only import keeps server code out of the
|
|
307
|
+
client bundle:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { createHttpClient } from "@fresho/router";
|
|
311
|
+
import type { createServer } from "./server";
|
|
312
|
+
|
|
313
|
+
type Api = ReturnType<typeof createServer>["api"];
|
|
314
|
+
|
|
315
|
+
const client = createHttpClient<Api>({
|
|
316
|
+
baseUrl: "https://api.example.com",
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
281
320
|
### HTTP Method Syntax
|
|
282
321
|
|
|
283
322
|
HTTP methods are prefixed with `$` to distinguish them from path navigation:
|
|
@@ -297,7 +336,9 @@ This allows routes with path segments named after HTTP methods (e.g., `/api/get`
|
|
|
297
336
|
|
|
298
337
|
### Building URLs Without Fetching
|
|
299
338
|
|
|
300
|
-
Use `$url` to derive a URL string from the typed routes without firing a request. It accepts the
|
|
339
|
+
Use `$url` to derive a URL string from the typed routes without firing a request. It accepts the
|
|
340
|
+
same `path`/`query` options as a real call, including the parameter names inferred from `$id`
|
|
341
|
+
segments:
|
|
301
342
|
|
|
302
343
|
```typescript
|
|
303
344
|
// Absolute URL when baseUrl is configured:
|
|
@@ -348,6 +389,52 @@ const docs = router({
|
|
|
348
389
|
});
|
|
349
390
|
```
|
|
350
391
|
|
|
392
|
+
## MCP Server
|
|
393
|
+
|
|
394
|
+
Expose routes as [Model Context Protocol](https://modelcontextprotocol.io) tools, so agents can use the same API as your other clients. Requires the optional peer dependency `@modelcontextprotocol/sdk`:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
npm install @modelcontextprotocol/sdk
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { router } from "@fresho/router";
|
|
402
|
+
import { mcp } from "@fresho/router/mcp";
|
|
403
|
+
|
|
404
|
+
const api = router({
|
|
405
|
+
users,
|
|
406
|
+
billing,
|
|
407
|
+
mcp: mcp({ users }, { name: "my-api", version: "1.0.0" }, bearerAuth({ verify })),
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`mcp()` requires endpoint middleware (usually auth). Pass `public: true` instead when the endpoint is open, or already guarded by middleware on its parent routers.
|
|
412
|
+
|
|
413
|
+
- **Tools** — one per route, named from the method and path (`get_users`, `post_users`, `get_users_by_id`, ...) unless the route sets `operationId` (also used in OpenAPI). Descriptions come from `route.description`.
|
|
414
|
+
- **Arguments** — flat: path params, query fields and body fields side by side (`{ id, text }`), as JSON Schema derived from the route's schemas. Names must be unique across the three; a clash throws when `mcp()` is called. `descriptions: { field: '...', 'address.city': '...' }` on a route documents fields for agents and OpenAPI. Array query params are left out, since the router can't parse them from query strings. Without a body schema, `post`/`put`/`patch` tools send any other arguments as the body.
|
|
415
|
+
- **Annotations** — `get` tools are marked `readOnlyHint`, `delete` tools `destructiveHint` and `idempotentHint`, and `put` tools `idempotentHint`, so clients can decide when to ask for confirmation. Override per route with `mcp: { annotations: { ... } }`.
|
|
416
|
+
- **Calls** — each tool call is dispatched as an HTTP request through the given routers with the caller's headers forwarded. Middleware on the MCP endpoint (passed to `mcp()` or on its parent routers) runs once per MCP request, and what it adds to the context (e.g. `c.user`) reaches handlers; middleware on the given routers and below runs on every call. Middleware on the given routers' *own* parents elsewhere in your app does **not** run, so guard the endpoint or the routers themselves. Responses become text content, plus `structuredContent` when the body is a JSON object; a non-2xx status sets `isError`, and streaming (SSE, NDJSON) responses are rejected.
|
|
417
|
+
- **Safety** — requests with an `Origin` other than the endpoint's get 403 (as the MCP spec requires against DNS rebinding), so browser-hosted clients on other origins can't connect. Path parameters can't be `.` or `..`, and handler exceptions become a generic `Internal error`.
|
|
418
|
+
- **Transport** — stateless Streamable HTTP with JSON responses; `GET` and `DELETE` return 405.
|
|
419
|
+
- Pass a router to expose all of its routes, or a record of routers to mount them under their keys.
|
|
420
|
+
|
|
421
|
+
### OAuth
|
|
422
|
+
|
|
423
|
+
Put auth middleware on the endpoint so unauthenticated clients get an HTTP 401, and set `oauth` to advertise your authorization server (RFC 9728):
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const oauth = { authorizationServers: ["https://auth.example.com"], scopes: ["users:read"] };
|
|
427
|
+
|
|
428
|
+
const app = router({
|
|
429
|
+
users,
|
|
430
|
+
mcp: mcp({ users }, { name: "my-api", version: "1.0.0", oauth }, jwtAuth({ ... })),
|
|
431
|
+
// Optional: metadata at the origin root, for clients that probe it before calling the endpoint.
|
|
432
|
+
".well-known": oauthProtectedResource(oauth),
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
401s from the endpoint gain `WWW-Authenticate: Bearer resource_metadata="https://host/mcp/.well-known/oauth-protected-resource"`, and that URL serves the metadata without the endpoint's middleware (keep auth off the endpoint's parent routers, or the metadata is guarded too). `oauthProtectedResource` only handles single-segment endpoint paths such as `/mcp`. Validate that tokens were issued for this server (the `aud` claim), not just by your authorization server.
|
|
437
|
+
|
|
351
438
|
## Streaming
|
|
352
439
|
|
|
353
440
|
### Server-Sent Events
|
|
@@ -355,16 +442,36 @@ const docs = router({
|
|
|
355
442
|
```typescript
|
|
356
443
|
import { sseResponse } from "@fresho/router";
|
|
357
444
|
|
|
445
|
+
export interface ItemEvents {
|
|
446
|
+
message: { connected: boolean };
|
|
447
|
+
update: { count: number };
|
|
448
|
+
removed: { id: string };
|
|
449
|
+
}
|
|
450
|
+
|
|
358
451
|
events: router({
|
|
359
452
|
get: async () =>
|
|
360
|
-
sseResponse(async (send, close) => {
|
|
361
|
-
send({ data:
|
|
453
|
+
sseResponse<ItemEvents>(async (send, close) => {
|
|
454
|
+
send({ data: { connected: true } });
|
|
362
455
|
send({ event: "update", data: { count: 1 } });
|
|
456
|
+
send({ event: "removed", data: { id: "item-1" } });
|
|
363
457
|
close();
|
|
364
458
|
}),
|
|
365
459
|
});
|
|
366
460
|
```
|
|
367
461
|
|
|
462
|
+
The event map checks event names and their corresponding payloads. The reserved `message` key types
|
|
463
|
+
events that omit the `event` field. Export the map for consumers, or extract it from a typed client
|
|
464
|
+
method:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import type { SSEEventsOf } from "@fresho/router";
|
|
468
|
+
|
|
469
|
+
type ClientEvents = SSEEventsOf<ReturnType<typeof client.events>>;
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
HTTP and local clients return streaming routes as raw `Response` objects instead of parsing their
|
|
473
|
+
bodies as JSON, so callers can consume `response.body` as data arrives.
|
|
474
|
+
|
|
368
475
|
### JSON Lines
|
|
369
476
|
|
|
370
477
|
```typescript
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as SignJwtOptions, c as jwtSign, i as JwtSecret, l as jwtVerify, n as JwtAlgorithm, o as VerifyJwtOptions, r as JwtPayload, s as createJwks, t as Jwks } from "../jwt-B-Fqwu4E.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/auth/basic.d.ts
|
|
4
4
|
|
|
@@ -323,5 +323,5 @@ interface AuthorizationUrlOptions {
|
|
|
323
323
|
*/
|
|
324
324
|
declare function buildAuthorizationUrl(options: AuthorizationUrlOptions): string;
|
|
325
325
|
//#endregion
|
|
326
|
-
export { type AuthorizationUrlOptions, type BasicCredentials, type ExchangeCodeOptions, type JwtAlgorithm, type JwtPayload, type JwtSecret, OAUTH_PROVIDERS, type OAuthProvider, type OAuthTokenResponse, type RevokeTokenOptions, type SignJwtOptions, type TokenAuthMethod, type VerifyJwtOptions, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
326
|
+
export { type AuthorizationUrlOptions, type BasicCredentials, type ExchangeCodeOptions, type Jwks, type JwtAlgorithm, type JwtPayload, type JwtSecret, OAUTH_PROVIDERS, type OAuthProvider, type OAuthTokenResponse, type RevokeTokenOptions, type SignJwtOptions, type TokenAuthMethod, type VerifyJwtOptions, buildAuthorizationUrl, createJwks, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
327
327
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as extractBasicAuthToken, i as encodeBasicAuth, n as jwtSign, o as parseBasicAuth, r as jwtVerify, t as createJwks } from "../jwt-ikNuUBcp.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/auth/oauth.ts
|
|
4
4
|
/** Common OAuth 2.0 providers with their endpoints. */
|
|
@@ -275,5 +275,5 @@ function buildAuthorizationUrl(options) {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
//#endregion
|
|
278
|
-
export { OAUTH_PROVIDERS, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
278
|
+
export { OAUTH_PROVIDERS, buildAuthorizationUrl, createJwks, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
279
279
|
//# sourceMappingURL=index.mjs.map
|