weifuwu 0.2.2 → 0.2.3

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.
Files changed (45) hide show
  1. package/README.md +56 -5
  2. package/dist/compress.d.ts +6 -0
  3. package/dist/cookie.d.ts +12 -0
  4. package/dist/index.d.ts +21 -0
  5. package/dist/index.js +1420 -0
  6. package/dist/middleware.d.ts +21 -0
  7. package/dist/rate-limit.d.ts +8 -0
  8. package/dist/router.d.ts +55 -0
  9. package/dist/serve.d.ts +19 -0
  10. package/dist/static.d.ts +7 -0
  11. package/dist/tsx.d.ts +17 -0
  12. package/dist/types.d.ts +9 -0
  13. package/dist/upload.d.ts +14 -0
  14. package/dist/validate.d.ts +9 -0
  15. package/package.json +14 -2
  16. package/AGENTS.md +0 -105
  17. package/compress.ts +0 -69
  18. package/cookie.ts +0 -58
  19. package/index.ts +0 -21
  20. package/middleware.ts +0 -178
  21. package/rate-limit.ts +0 -68
  22. package/router.ts +0 -701
  23. package/serve.ts +0 -126
  24. package/static.ts +0 -113
  25. package/test/compress.test.ts +0 -106
  26. package/test/cookie.test.ts +0 -79
  27. package/test/fixtures/pages/about/page.tsx +0 -3
  28. package/test/fixtures/pages/blog/[slug]/load.ts +0 -3
  29. package/test/fixtures/pages/blog/[slug]/page.tsx +0 -3
  30. package/test/fixtures/pages/blog/[slug]/route.ts +0 -7
  31. package/test/fixtures/pages/blog/layout.tsx +0 -3
  32. package/test/fixtures/pages/layout.tsx +0 -12
  33. package/test/fixtures/pages/page.tsx +0 -3
  34. package/test/middleware.test.ts +0 -407
  35. package/test/rate-limit.test.ts +0 -94
  36. package/test/static.test.ts +0 -93
  37. package/test/tsx.test.ts +0 -285
  38. package/test/unode.test.ts +0 -401
  39. package/test/upload.test.ts +0 -130
  40. package/test/validate.test.ts +0 -133
  41. package/tsconfig.json +0 -13
  42. package/tsx.ts +0 -374
  43. package/types.ts +0 -23
  44. package/upload.ts +0 -101
  45. package/validate.ts +0 -88
@@ -0,0 +1,21 @@
1
+ import type { Middleware } from './types.ts';
2
+ export interface LoggerOptions {
3
+ format?: 'short' | 'combined';
4
+ }
5
+ export declare function logger(options?: LoggerOptions): Middleware;
6
+ export interface CORSOptions {
7
+ origin?: string | string[] | ((origin: string) => string | boolean | undefined);
8
+ methods?: string[];
9
+ allowedHeaders?: string[];
10
+ exposedHeaders?: string[];
11
+ credentials?: boolean;
12
+ maxAge?: number;
13
+ }
14
+ export declare function cors(options?: CORSOptions): Middleware;
15
+ export interface AuthOptions {
16
+ token?: string;
17
+ verify?: (token: string, req: Request) => unknown | Promise<unknown>;
18
+ proxy?: string | URL;
19
+ header?: string;
20
+ }
21
+ export declare function auth(options: AuthOptions): Middleware;
@@ -0,0 +1,8 @@
1
+ import type { Middleware } from './types.ts';
2
+ export interface RateLimitOptions {
3
+ max?: number;
4
+ window?: number;
5
+ key?: (req: Request) => string;
6
+ message?: string;
7
+ }
8
+ export declare function rateLimit(options?: RateLimitOptions): Middleware;
@@ -0,0 +1,55 @@
1
+ import { type WebSocket } from 'ws';
2
+ import type { IncomingMessage } from 'node:http';
3
+ import type { Duplex } from 'node:stream';
4
+ import { type GraphQLSchema } from 'graphql';
5
+ import { streamText } from 'ai';
6
+ import type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
7
+ type StreamTextParams = Parameters<typeof streamText>[0];
8
+ export type WebSocketHandler = {
9
+ open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
10
+ message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
11
+ close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
12
+ error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
13
+ };
14
+ export type AIOptions = Omit<StreamTextParams, 'model'> & {
15
+ model: string | (() => any);
16
+ };
17
+ export type AIHandler = (req: Request, ctx: Context) => AIOptions | Promise<AIOptions>;
18
+ export type GraphQLOptions = {
19
+ schema: string | GraphQLSchema;
20
+ rootValue?: any;
21
+ resolvers?: any;
22
+ context?: (req: Request, ctx: Context) => Record<string, any> | Promise<Record<string, any>>;
23
+ graphiql?: boolean;
24
+ };
25
+ type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
26
+ export declare class Router {
27
+ private root;
28
+ private wsRoot;
29
+ private globalMws;
30
+ private errorHandler?;
31
+ use(mw: Middleware): this;
32
+ use(path: string, router: Router): this;
33
+ use(path: string, mw: Middleware): this;
34
+ get(path: string, ...args: [...Middleware[], Handler]): this;
35
+ post(path: string, ...args: [...Middleware[], Handler]): this;
36
+ put(path: string, ...args: [...Middleware[], Handler]): this;
37
+ delete(path: string, ...args: [...Middleware[], Handler]): this;
38
+ patch(path: string, ...args: [...Middleware[], Handler]): this;
39
+ head(path: string, ...args: [...Middleware[], Handler]): this;
40
+ options(path: string, ...args: [...Middleware[], Handler]): this;
41
+ all(path: string, ...args: [...Middleware[], Handler]): this;
42
+ onError(handler: ErrorHandler): this;
43
+ route(method: string, path: string, ...args: [...Middleware[], Handler]): this;
44
+ ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this;
45
+ graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this;
46
+ ai(path: string, ...args: [...Middleware[], AIHandler]): this;
47
+ handler(): Handler;
48
+ websocketHandler(): WsUpgradeHandler;
49
+ private splitPath;
50
+ private matchTrie;
51
+ private matchWsTrie;
52
+ private handle;
53
+ private runChain;
54
+ }
55
+ export {};
@@ -0,0 +1,19 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ import type { Handler } from './types.ts';
4
+ export interface ServeOptions {
5
+ port?: number;
6
+ hostname?: string;
7
+ signal?: AbortSignal;
8
+ websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
9
+ }
10
+ export interface Server {
11
+ stop: () => void;
12
+ readonly port: number;
13
+ readonly hostname: string;
14
+ ready: Promise<void>;
15
+ }
16
+ export declare function readBody(req: IncomingMessage): Promise<Buffer>;
17
+ export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
18
+ export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
19
+ export declare function serve(handler: Handler, options?: ServeOptions): Server;
@@ -0,0 +1,7 @@
1
+ import type { Handler } from './types.ts';
2
+ export interface ServeStaticOptions {
3
+ index?: string;
4
+ maxAge?: number;
5
+ immutable?: boolean;
6
+ }
7
+ export declare function serveStatic(root: string, options?: ServeStaticOptions): Handler;
package/dist/tsx.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { Router } from './router.ts';
2
+ export interface TsxOptions {
3
+ dir: string;
4
+ }
5
+ export declare const TsxContext: import("react").Context<{
6
+ params: Record<string, string>;
7
+ query: Record<string, string>;
8
+ user?: unknown;
9
+ parsed?: Record<string, unknown>;
10
+ }>;
11
+ export declare function useTsx(): {
12
+ params: Record<string, string>;
13
+ query: Record<string, string>;
14
+ user?: unknown;
15
+ parsed?: Record<string, unknown>;
16
+ };
17
+ export declare function tsx(options: TsxOptions): Promise<Router>;
@@ -0,0 +1,9 @@
1
+ export interface Context {
2
+ params: Record<string, string>;
3
+ query: Record<string, string>;
4
+ user?: unknown;
5
+ parsed?: Record<string, unknown>;
6
+ }
7
+ export type Handler = (req: Request, ctx: Context) => Response | Promise<Response>;
8
+ export type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>;
9
+ export type ErrorHandler = (error: Error, req: Request, ctx: Context) => Response;
@@ -0,0 +1,14 @@
1
+ import type { Middleware } from './types.ts';
2
+ export interface UploadedFile {
3
+ name: string;
4
+ type: string;
5
+ size: number;
6
+ path?: string;
7
+ buffer?: Buffer;
8
+ }
9
+ export interface UploadOptions {
10
+ dir?: string;
11
+ maxFileSize?: number;
12
+ allowedTypes?: string[];
13
+ }
14
+ export declare function upload(options?: UploadOptions): Middleware;
@@ -0,0 +1,9 @@
1
+ import type { ZodSchema } from 'zod';
2
+ import type { Middleware } from './types.ts';
3
+ export interface ValidationSchemas {
4
+ body?: ZodSchema;
5
+ query?: ZodSchema;
6
+ params?: ZodSchema;
7
+ headers?: ZodSchema;
8
+ }
9
+ export declare function validate(schemas: ValidationSchemas): Middleware;
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
- "main": "index.ts",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./tsx": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
6
16
  "scripts": {
17
+ "build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai",
18
+ "prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
7
19
  "test": "node --test 'test/**/*.test.ts'"
8
20
  },
9
21
  "dependencies": {
package/AGENTS.md DELETED
@@ -1,105 +0,0 @@
1
- This is the weifuwu HTTP framework — pure Node.js, no build step.
2
-
3
- ## Commands
4
-
5
- - `node --test` — run all tests
6
- - `npm install` — install dependencies
7
- - `npx tsc --noEmit` — type-check without emitting
8
-
9
- ## TypeScript rules
10
-
11
- - All imports must use explicit `.ts` extensions (e.g. `import { x } from './foo.ts'`)
12
- - Node.js v24+ supports TypeScript natively (no `--experimental-strip-types` needed)
13
- - No `tsc` compiler needed for runtime (native TS via Node.js)
14
-
15
- ## Code conventions
16
-
17
- - Read the full file before editing — context matters
18
- - Follow existing patterns: `Handler = (req, ctx) => Response | Promise<Response>`
19
- - All middleware returns a `Middleware` — `(req, ctx, next) => Response | Promise<Response>`
20
- - Import types from `./types.ts`, source from individual files
21
- - New modules get their own file, exported from `index.ts`
22
- - Every module needs tests in `test/`
23
- - All `ctx` mutations (like `ctx.parsed` or `ctx.user`) should be additive, never overwrite
24
-
25
- ## Dependencies
26
-
27
- - `ws` for WebSocket server
28
- - `graphql` + `@graphql-tools/schema` for GraphQL
29
- - `ai` (Vercel AI SDK) for AI streaming
30
- - `zod` for request validation
31
- - `react` + `react-dom` for `.tsx()` SSR + hydration
32
- - `esbuild` for hydration bundle compilation
33
- - Node.js built-in `WebSocket` for WebSocket clients
34
- - Node.js built-in `zlib` for response compression
35
-
36
- ## tsx() — React SSR + Auto Hydration
37
-
38
- `tsx({ dir })` — creates a Router from a React pages directory:
39
-
40
- ```ts
41
- type TsxRoute = {
42
- component: React.ComponentType<any>
43
- props?: Record<string, any> // custom props (merged with params + query)
44
- source?: string // component source path → enables hydration
45
- }
46
-
47
- type TsxHandler = (
48
- req: Request,
49
- ctx: Context,
50
- ) => TsxRoute | Promise<TsxRoute>
51
- ```
52
-
53
- - SSR via `react-dom/server` `renderToReadableStream`
54
- - Props are serialized as `window.__WEIFUWU_PROPS` in HTML
55
- - Hydration: esbuild lazily compiles source → client bundle served at `/_wfw/client/`
56
- - Props passed to component: `{ ...props, params, query }` (never `req`/`ctx`)
57
-
58
- ### File conventions
59
-
60
- ```
61
- pages/
62
- page.tsx → GET / (React component, default export)
63
- layout.tsx → root layout (wraps all pages)
64
- about/page.tsx → GET /about
65
- blog/[slug]/
66
- page.tsx → GET /blog/:slug
67
- load.ts → data fetching (server-only, default export)
68
- route.ts → POST /blog/:slug (API, named exports GET/POST/...)
69
- blog/layout.tsx → /blog/* layout (auto-wraps blog pages)
70
- ```
71
-
72
- - `page.tsx` — default export = React component, receives `{ params, query }` + load data
73
- - `load.ts` — default export = async function `({ params, query }) => props`, server-only
74
- - `layout.tsx` — default export = React component with `{ children }`, auto-nested by directory level
75
- - `route.ts` — named exports `GET`/`POST`/`PUT`/`DELETE`/`PATCH`, standard Handler signature
76
-
77
- ### Usage
78
-
79
- ```ts
80
- import { serve, Router } from 'weifuwu'
81
- import { tsx } from 'weifuwu/tsx'
82
-
83
- const r = new Router()
84
- r.use('/', await tsx({ dir: './pages/' }))
85
-
86
- // Other features coexist
87
- r.ws('/chat', { message(ws, _, data) { ws.send(data) } })
88
-
89
- serve(r.handler())
90
- ```
91
-
92
- ## Testing
93
-
94
- ```ts#test/example.test.ts
95
- import { describe, it } from 'node:test'
96
- import assert from 'node:assert/strict'
97
-
98
- describe('example', () => {
99
- it('works', () => {
100
- assert.equal(1 + 1, 2)
101
- })
102
- })
103
- ```
104
-
105
- Tests live in `test/` and follow the pattern: create a `Router`, call `r.handler()(request, ctx)`, assert on the response. For end-to-end tests, use `serve()`.
package/compress.ts DELETED
@@ -1,69 +0,0 @@
1
- import { gzipSync, brotliCompressSync, constants } from 'node:zlib'
2
- import type { Middleware } from './types.ts'
3
-
4
- export interface CompressOptions {
5
- level?: number
6
- threshold?: number
7
- }
8
-
9
- export function compress(options?: CompressOptions): Middleware {
10
- const level = options?.level ?? 6
11
- const threshold = options?.threshold ?? 1024
12
-
13
- return async (req, ctx, next) => {
14
- const accept = req.headers.get('accept-encoding') ?? ''
15
-
16
- const useBrotli = accept.includes('br')
17
- const useGzip = !useBrotli && accept.includes('gzip')
18
- const useDeflate = !useBrotli && !useGzip && accept.includes('deflate')
19
-
20
- if (!useBrotli && !useGzip && !useDeflate) {
21
- return next(req, ctx)
22
- }
23
-
24
- const res = await next(req, ctx)
25
-
26
- if (res.status === 304 || res.status === 204 || res.status < 200 || res.status >= 300) {
27
- return res
28
- }
29
-
30
- const ce = res.headers.get('content-encoding')
31
- if (ce) return res
32
-
33
- const ct = res.headers.get('content-type') ?? ''
34
- if (!ct || ct.startsWith('audio/') || ct.startsWith('video/') || ct.startsWith('image/') || ct === 'application/zip') {
35
- return res
36
- }
37
-
38
- const body = await res.bytes()
39
- if (body.byteLength < threshold) return res
40
-
41
- let compressed: Buffer
42
- let encoding: string
43
-
44
- if (useBrotli) {
45
- compressed = brotliCompressSync(body, {
46
- params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) },
47
- })
48
- encoding = 'br'
49
- } else if (useGzip) {
50
- compressed = gzipSync(body, { level: Math.min(level, 9) })
51
- encoding = 'gzip'
52
- } else {
53
- compressed = gzipSync(body, { level: Math.min(level, 9) })
54
- encoding = 'deflate'
55
- }
56
-
57
- const headers = new Headers(res.headers)
58
- headers.set('Content-Encoding', encoding)
59
- headers.set('Content-Length', String(compressed.byteLength))
60
- headers.delete('Content-Range')
61
- headers.set('Vary', 'Accept-Encoding')
62
-
63
- return new Response(compressed as BodyInit, {
64
- status: res.status,
65
- statusText: res.statusText,
66
- headers,
67
- })
68
- }
69
- }
package/cookie.ts DELETED
@@ -1,58 +0,0 @@
1
- export interface CookieOptions {
2
- domain?: string
3
- path?: string
4
- maxAge?: number
5
- expires?: Date
6
- httpOnly?: boolean
7
- secure?: boolean
8
- sameSite?: 'strict' | 'lax' | 'none'
9
- }
10
-
11
- export function getCookies(req: Request): Record<string, string> {
12
- const header = req.headers.get('cookie')
13
- if (!header) return {}
14
-
15
- const cookies: Record<string, string> = {}
16
- for (const pair of header.split(';')) {
17
- const idx = pair.indexOf('=')
18
- if (idx === -1) continue
19
- const name = pair.slice(0, idx).trim()
20
- const value = pair.slice(idx + 1).trim()
21
- if (name) {
22
- cookies[name] = decodeURIComponent(value)
23
- }
24
- }
25
- return cookies
26
- }
27
-
28
- function serializeCookie(name: string, value: string, options?: CookieOptions): string {
29
- const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
30
- if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`)
31
- if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`)
32
- if (options?.domain) parts.push(`Domain=${options.domain}`)
33
- if (options?.path) parts.push(`Path=${options.path}`)
34
- if (options?.httpOnly) parts.push('HttpOnly')
35
- if (options?.secure) parts.push('Secure')
36
- if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`)
37
- return parts.join('; ')
38
- }
39
-
40
- export function setCookie(res: Response, name: string, value: string, options?: CookieOptions): Response {
41
- const headers = new Headers(res.headers)
42
- headers.append('Set-Cookie', serializeCookie(name, value, options))
43
- return new Response(res.body, {
44
- status: res.status,
45
- statusText: res.statusText,
46
- headers,
47
- })
48
- }
49
-
50
- export function deleteCookie(res: Response, name: string, options?: Omit<CookieOptions, 'maxAge'>): Response {
51
- const headers = new Headers(res.headers)
52
- headers.append('Set-Cookie', serializeCookie(name, '', { ...options, maxAge: 0 }))
53
- return new Response(res.body, {
54
- status: res.status,
55
- statusText: res.statusText,
56
- headers,
57
- })
58
- }
package/index.ts DELETED
@@ -1,21 +0,0 @@
1
- export type { Context, Handler, Middleware, ErrorHandler } from './types.ts'
2
- export { serve } from './serve.ts'
3
- export type { ServeOptions, Server } from './serve.ts'
4
- export { Router } from './router.ts'
5
- export type { WebSocketHandler, GraphQLOptions, AIHandler } from './router.ts'
6
- export { tsx, TsxContext, useTsx } from './tsx.ts'
7
- export type { TsxOptions } from './tsx.ts'
8
- export { auth, cors, logger } from './middleware.ts'
9
- export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts'
10
- export { serveStatic } from './static.ts'
11
- export type { ServeStaticOptions } from './static.ts'
12
- export { validate } from './validate.ts'
13
- export type { ValidationSchemas } from './validate.ts'
14
- export { getCookies, setCookie, deleteCookie } from './cookie.ts'
15
- export type { CookieOptions } from './cookie.ts'
16
- export { upload } from './upload.ts'
17
- export type { UploadOptions, UploadedFile } from './upload.ts'
18
- export { rateLimit } from './rate-limit.ts'
19
- export type { RateLimitOptions } from './rate-limit.ts'
20
- export { compress } from './compress.ts'
21
- export type { CompressOptions } from './compress.ts'
package/middleware.ts DELETED
@@ -1,178 +0,0 @@
1
- import type { Middleware } from './types.ts'
2
-
3
- // ── Logger ────────────────────────────────────────────────────────────────────
4
-
5
- export interface LoggerOptions {
6
- format?: 'short' | 'combined'
7
- }
8
-
9
- export function logger(options?: LoggerOptions): Middleware {
10
- return async (req, ctx, next) => {
11
- const start = Date.now()
12
- const url = new URL(req.url)
13
- const res = await next(req, ctx)
14
- const ms = Date.now() - start
15
-
16
- if (options?.format === 'combined') {
17
- console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`)
18
- } else {
19
- console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`)
20
- }
21
-
22
- return res
23
- }
24
- }
25
-
26
- // ── CORS ──────────────────────────────────────────────────────────────────────
27
-
28
- export interface CORSOptions {
29
- origin?: string | string[] | ((origin: string) => string | boolean | undefined)
30
- methods?: string[]
31
- allowedHeaders?: string[]
32
- exposedHeaders?: string[]
33
- credentials?: boolean
34
- maxAge?: number
35
- }
36
-
37
- export function cors(options?: CORSOptions): Middleware {
38
- const opts: Required<Pick<CORSOptions, 'origin' | 'methods' | 'allowedHeaders'>> & CORSOptions = {
39
- origin: '*',
40
- methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
41
- allowedHeaders: ['Content-Type', 'Authorization'],
42
- ...options,
43
- }
44
-
45
- function resolveOrigin(requestOrigin: string): string {
46
- if (typeof opts.origin === 'string') return opts.origin === '*' ? '*' : opts.origin
47
- if (Array.isArray(opts.origin)) {
48
- return opts.origin.includes(requestOrigin) ? requestOrigin : ''
49
- }
50
- const result = opts.origin(requestOrigin)
51
- if (typeof result === 'boolean') return result ? requestOrigin : ''
52
- if (typeof result === 'string') return result
53
- return ''
54
- }
55
-
56
- function setCORSHeaders(res: Response, acao: string): Response {
57
- if (!acao) return res
58
- const headers = new Headers(res.headers)
59
- headers.set('Access-Control-Allow-Origin', acao)
60
- if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
61
- if (opts.exposedHeaders?.length) headers.set('Access-Control-Expose-Headers', opts.exposedHeaders.join(', '))
62
- headers.set('Vary', 'Origin')
63
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
64
- }
65
-
66
- return (req, ctx, next) => {
67
- const requestOrigin = req.headers.get('origin') ?? ''
68
- const acao = resolveOrigin(requestOrigin)
69
-
70
- if (req.method === 'OPTIONS' && acao) {
71
- const headers = new Headers()
72
- headers.set('Access-Control-Allow-Origin', acao)
73
- headers.set('Access-Control-Allow-Methods', opts.methods.join(', '))
74
- headers.set('Access-Control-Allow-Headers', opts.allowedHeaders.join(', '))
75
- if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
76
- if (opts.maxAge != null) headers.set('Access-Control-Max-Age', String(opts.maxAge))
77
- headers.set('Vary', 'Origin')
78
- return new Response(null, { status: 204, headers })
79
- }
80
-
81
- if (!acao) return next(req, ctx)
82
-
83
- return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao))
84
- }
85
- }
86
-
87
- // ── Auth ───────────────────────────────────────────────────────────────────────
88
-
89
- export interface AuthOptions {
90
- token?: string
91
- verify?: (token: string, req: Request) => unknown | Promise<unknown>
92
- proxy?: string | URL
93
- header?: string
94
- }
95
-
96
- export function auth(options: AuthOptions): Middleware {
97
- return async (req, ctx, next) => {
98
- const headerName = options.header ?? 'Authorization'
99
- const header = req.headers.get(headerName)
100
-
101
- if (!header) {
102
- return new Response('Unauthorized', {
103
- status: 401,
104
- headers: headerName.toLowerCase() === 'authorization'
105
- ? { 'WWW-Authenticate': 'Bearer' }
106
- : undefined,
107
- })
108
- }
109
-
110
- let token = header
111
- if (headerName.toLowerCase() === 'authorization') {
112
- const parts = header.split(' ')
113
- if (parts[0]?.toLowerCase() === 'bearer') {
114
- token = parts.slice(1).join(' ')
115
- }
116
- }
117
-
118
- // ── Proxy mode ──────────────────────────────────────────────────────────
119
- if (options.proxy) {
120
- const proxyUrl = typeof options.proxy === 'string'
121
- ? new URL(options.proxy)
122
- : options.proxy
123
-
124
- const proxyHeaders: Record<string, string> = {}
125
-
126
- if (headerName.toLowerCase() === 'authorization') {
127
- proxyHeaders['Authorization'] = header
128
- } else {
129
- proxyUrl.searchParams.set('access_token', token)
130
- }
131
-
132
- for (const name of ['x-forwarded-for', 'x-real-ip', 'user-agent', 'content-type']) {
133
- const v = req.headers.get(name)
134
- if (v) proxyHeaders[name] = v
135
- }
136
-
137
- const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders })
138
-
139
- if (proxyRes.status >= 400) {
140
- return new Response(await proxyRes.text() || 'Forbidden', { status: proxyRes.status })
141
- }
142
-
143
- let userData: unknown = undefined
144
- if (proxyRes.status === 200) {
145
- const ct = proxyRes.headers.get('content-type')
146
- if (ct?.includes('application/json')) {
147
- try { userData = await proxyRes.json() } catch {}
148
- }
149
- }
150
-
151
- ctx.user = userData
152
- return next(req, ctx)
153
- }
154
-
155
- // ── Static token mode ───────────────────────────────────────────────────
156
- if (options.token) {
157
- if (token !== options.token) {
158
- return new Response('Forbidden', { status: 403 })
159
- }
160
- return next(req, ctx)
161
- }
162
-
163
- // ── Verify mode ─────────────────────────────────────────────────────────
164
- if (options.verify) {
165
- const result = await options.verify(token, req)
166
- if (!result) {
167
- return new Response('Forbidden', { status: 403 })
168
- }
169
- if (typeof result === 'object' && result !== null) {
170
- ctx.user = result
171
- }
172
- return next(req, ctx)
173
- }
174
-
175
- // ── Trust any token (no validation configured) ─────────────────────────
176
- return next(req, ctx)
177
- }
178
- }