weifuwu 0.27.2 → 0.27.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.
- package/dist/ai/provider.d.ts +45 -0
- package/dist/ai/stream.d.ts +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +131 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +72 -0
- package/dist/core/serve.d.ts +38 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +3963 -0
- package/dist/mailer.d.ts +51 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +47 -0
- package/dist/middleware/flash.d.ts +90 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/module.d.ts +16 -0
- package/dist/postgres/schema/columns.d.ts +99 -0
- package/dist/postgres/schema/index.d.ts +6 -0
- package/dist/postgres/schema/sql.d.ts +22 -0
- package/dist/postgres/schema/table.d.ts +141 -0
- package/dist/postgres/schema/where.d.ts +29 -0
- package/dist/postgres/types.d.ts +49 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/redis/client.d.ts +2 -0
- package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +50 -0
- package/package.json +10 -10
- package/ai/provider.ts +0 -129
- package/ai/stream.ts +0 -63
- package/cli.ts +0 -147
- package/core/cookie.ts +0 -114
- package/core/env.ts +0 -142
- package/core/logger.ts +0 -72
- package/core/router.ts +0 -795
- package/core/serve.ts +0 -294
- package/core/sse.ts +0 -85
- package/core/trace.ts +0 -146
- package/graphql.ts +0 -267
- package/hub.ts +0 -133
- package/index.ts +0 -71
- package/mailer.ts +0 -81
- package/middleware/compress.ts +0 -103
- package/middleware/cors.ts +0 -81
- package/middleware/csrf.ts +0 -112
- package/middleware/flash.ts +0 -144
- package/middleware/health.ts +0 -44
- package/middleware/helmet.ts +0 -98
- package/middleware/i18n.ts +0 -175
- package/middleware/rate-limit.ts +0 -167
- package/middleware/request-id.ts +0 -60
- package/middleware/static.ts +0 -149
- package/middleware/theme.ts +0 -84
- package/middleware/upload.ts +0 -168
- package/middleware/validate.ts +0 -186
- package/postgres/client.ts +0 -132
- package/postgres/index.ts +0 -4
- package/postgres/module.ts +0 -37
- package/postgres/schema/columns.ts +0 -186
- package/postgres/schema/index.ts +0 -36
- package/postgres/schema/sql.ts +0 -39
- package/postgres/schema/table.ts +0 -548
- package/postgres/schema/where.ts +0 -99
- package/postgres/types.ts +0 -48
- package/queue/cron.ts +0 -90
- package/queue/index.ts +0 -654
- package/queue/types.ts +0 -60
- package/redis/client.ts +0 -24
- package/redis/types.ts +0 -28
- package/types.ts +0 -78
package/middleware/cors.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { Middleware, Context } from '../types.ts'
|
|
2
|
-
|
|
3
|
-
/** Options for {@link cors}. */
|
|
4
|
-
export interface CORSOptions {
|
|
5
|
-
/** Allowed origin(s). Default `'*'`. If `credentials: true`, reflects the request origin. */
|
|
6
|
-
origin?: string | string[] | ((origin: string) => string | boolean | undefined)
|
|
7
|
-
/** Allowed HTTP methods. Default: `GET, HEAD, PUT, PATCH, POST, DELETE`. */
|
|
8
|
-
methods?: string[]
|
|
9
|
-
/** Allowed request headers. Default: `Content-Type, Authorization`. */
|
|
10
|
-
allowedHeaders?: string[]
|
|
11
|
-
/** Exposed response headers. */
|
|
12
|
-
exposedHeaders?: string[]
|
|
13
|
-
/** Whether to expose `Access-Control-Allow-Credentials`. */
|
|
14
|
-
credentials?: boolean
|
|
15
|
-
/** `Access-Control-Max-Age` in seconds. */
|
|
16
|
-
maxAge?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* CORS middleware.
|
|
21
|
-
*
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { cors } from 'weifuwu'
|
|
24
|
-
* app.use(cors({ origin: 'https://myapp.com', credentials: true }))
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export function cors(options?: CORSOptions): Middleware<Context, Context> {
|
|
28
|
-
const opts: Required<Pick<CORSOptions, 'origin' | 'methods' | 'allowedHeaders'>> & CORSOptions = {
|
|
29
|
-
origin: '*',
|
|
30
|
-
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
|
31
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
32
|
-
...options,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function resolveOrigin(requestOrigin: string): string {
|
|
36
|
-
if (typeof opts.origin === 'string') {
|
|
37
|
-
if (opts.origin === '*') {
|
|
38
|
-
return opts.credentials ? requestOrigin : '*'
|
|
39
|
-
}
|
|
40
|
-
return opts.origin
|
|
41
|
-
}
|
|
42
|
-
if (Array.isArray(opts.origin)) {
|
|
43
|
-
return opts.origin.includes(requestOrigin) ? requestOrigin : ''
|
|
44
|
-
}
|
|
45
|
-
const result = opts.origin(requestOrigin)
|
|
46
|
-
if (typeof result === 'boolean') return result ? requestOrigin : ''
|
|
47
|
-
if (typeof result === 'string') return result
|
|
48
|
-
return ''
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function setCORSHeaders(res: Response, acao: string): Response {
|
|
52
|
-
if (!acao) return res
|
|
53
|
-
const headers = new Headers(res.headers)
|
|
54
|
-
headers.set('Access-Control-Allow-Origin', acao)
|
|
55
|
-
if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
56
|
-
if (opts.exposedHeaders?.length)
|
|
57
|
-
headers.set('Access-Control-Expose-Headers', opts.exposedHeaders.join(', '))
|
|
58
|
-
if (acao !== '*') headers.set('Vary', 'Origin')
|
|
59
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (req, ctx, next) => {
|
|
63
|
-
const requestOrigin = req.headers.get('origin') ?? ''
|
|
64
|
-
const acao = resolveOrigin(requestOrigin)
|
|
65
|
-
|
|
66
|
-
if (req.method === 'OPTIONS' && acao) {
|
|
67
|
-
const headers = new Headers()
|
|
68
|
-
headers.set('Access-Control-Allow-Origin', acao)
|
|
69
|
-
headers.set('Access-Control-Allow-Methods', opts.methods.join(', '))
|
|
70
|
-
headers.set('Access-Control-Allow-Headers', opts.allowedHeaders.join(', '))
|
|
71
|
-
if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
72
|
-
if (opts.maxAge != null) headers.set('Access-Control-Max-Age', String(opts.maxAge))
|
|
73
|
-
if (acao !== '*') headers.set('Vary', 'Origin')
|
|
74
|
-
return new Response(null, { status: 204, headers })
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!acao) return next(req, ctx)
|
|
78
|
-
|
|
79
|
-
return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao))
|
|
80
|
-
}
|
|
81
|
-
}
|
package/middleware/csrf.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars */
|
|
2
|
-
import type { Context, Middleware } from '../types.ts'
|
|
3
|
-
import { getCookies, setCookie } from '../core/cookie.ts'
|
|
4
|
-
|
|
5
|
-
// Augment Context with csrf property
|
|
6
|
-
declare module '../types.ts' {
|
|
7
|
-
interface Context {
|
|
8
|
-
csrf: CsrfInjected
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface CsrfInjected {
|
|
13
|
-
token: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Options for {@link csrf}. */
|
|
17
|
-
/** CSRF protection module — a {@link Middleware} that injects `ctx.csrf`. */
|
|
18
|
-
export type CsrfModule = Middleware<Context, Context & CsrfInjected>
|
|
19
|
-
|
|
20
|
-
export interface CsrfOptions {
|
|
21
|
-
/** Cookie name for CSRF token (default: `'_csrf'`). */
|
|
22
|
-
cookie?: string
|
|
23
|
-
/** Request header name for CSRF token (default: `'x-csrf-token'`). */
|
|
24
|
-
header?: string
|
|
25
|
-
/** Form body key for CSRF token (default: `'_csrf'`). */
|
|
26
|
-
key?: string
|
|
27
|
-
/** HTTP methods to exclude from CSRF protection (default: `['GET', 'HEAD', 'OPTIONS']`). */
|
|
28
|
-
excludeMethods?: string[]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* CSRF protection middleware.
|
|
33
|
-
*
|
|
34
|
-
* On excluded methods (GET, HEAD, OPTIONS), generates a token and stores it
|
|
35
|
-
* in a cookie. On other methods, validates the token from header or body
|
|
36
|
-
* against the cookie.
|
|
37
|
-
*
|
|
38
|
-
* Injects `ctx.csrf.token` for use in forms.
|
|
39
|
-
*
|
|
40
|
-
* ```ts
|
|
41
|
-
* import { csrf } from 'weifuwu'
|
|
42
|
-
* app.use(csrf())
|
|
43
|
-
*
|
|
44
|
-
* // In a form:
|
|
45
|
-
* app.get('/form', (req, ctx) => {
|
|
46
|
-
* return new Response(`
|
|
47
|
-
* <form method="POST">
|
|
48
|
-
* <input type="hidden" name="_csrf" value="${ctx.csrf.token}" />
|
|
49
|
-
* <input type="submit" />
|
|
50
|
-
* </form>
|
|
51
|
-
* `, { headers: { 'content-type': 'text/html' } })
|
|
52
|
-
* })
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export function csrf(options?: CsrfOptions): Middleware<Context, Context & CsrfInjected> {
|
|
56
|
-
const cookieName = options?.cookie ?? '_csrf'
|
|
57
|
-
const headerName = options?.header ?? 'x-csrf-token'
|
|
58
|
-
const bodyKey = options?.key ?? '_csrf'
|
|
59
|
-
const excluded = new Set(options?.excludeMethods ?? ['GET', 'HEAD', 'OPTIONS'])
|
|
60
|
-
|
|
61
|
-
const mw: Middleware<Context, Context & CsrfInjected> = async (req, ctx, next) => {
|
|
62
|
-
const method = req.method.toUpperCase()
|
|
63
|
-
|
|
64
|
-
if (excluded.has(method)) {
|
|
65
|
-
let token = getCookies(req)[cookieName]
|
|
66
|
-
if (!token) {
|
|
67
|
-
token = crypto.randomUUID()
|
|
68
|
-
;(ctx as any).csrf = { token }
|
|
69
|
-
} else {
|
|
70
|
-
;(ctx as any).csrf = { token }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const res = await next(req, ctx as Context & CsrfInjected)
|
|
74
|
-
const tokenToSet = (ctx as any).csrf?.token
|
|
75
|
-
if (tokenToSet && !getCookies(req)[cookieName]) {
|
|
76
|
-
return setCookie(res, cookieName, tokenToSet, {
|
|
77
|
-
httpOnly: true,
|
|
78
|
-
sameSite: 'strict',
|
|
79
|
-
path: '/',
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
return res
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const cookieToken = getCookies(req)[cookieName]
|
|
86
|
-
|
|
87
|
-
let headerToken = req.headers.get(headerName) ?? ''
|
|
88
|
-
// Fallback: try to extract from request body
|
|
89
|
-
if (
|
|
90
|
-
!headerToken &&
|
|
91
|
-
(req.method === 'POST' ||
|
|
92
|
-
req.method === 'PUT' ||
|
|
93
|
-
req.method === 'PATCH' ||
|
|
94
|
-
req.method === 'DELETE')
|
|
95
|
-
) {
|
|
96
|
-
try {
|
|
97
|
-
const body = await req.clone().json()
|
|
98
|
-
headerToken = body[bodyKey] ?? ''
|
|
99
|
-
} catch (e) {
|
|
100
|
-
return new Response('Invalid request body', { status: 400 })
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
105
|
-
return new Response('CSRF token mismatch', { status: 403 })
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return next(req, ctx as Context & CsrfInjected)
|
|
109
|
-
}
|
|
110
|
-
mw.__meta = { injects: ['csrf'], depends: [] }
|
|
111
|
-
return mw
|
|
112
|
-
}
|
package/middleware/flash.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Flash message middleware.
|
|
3
|
-
*
|
|
4
|
-
* Provides a cookie-based flash message system:
|
|
5
|
-
* - Read: `ctx.flash.value` parses the incoming flash cookie
|
|
6
|
-
* - Write: `ctx.flash.set(data, location)` sets a new flash and redirects
|
|
7
|
-
* - Auto-clear: after reading, the flash cookie is cleared from the response
|
|
8
|
-
*
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { flash } from 'weifuwu'
|
|
11
|
-
*
|
|
12
|
-
* app.use(flash())
|
|
13
|
-
*
|
|
14
|
-
* // Read flash
|
|
15
|
-
* app.get('/', (req, ctx) => {
|
|
16
|
-
* const msg = ctx.flash.value // e.g. { type: 'success', text: 'Saved!' }
|
|
17
|
-
* })
|
|
18
|
-
*
|
|
19
|
-
* // Set flash + redirect
|
|
20
|
-
* app.post('/save', async (req, ctx) => {
|
|
21
|
-
* await save()
|
|
22
|
-
* return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
|
|
23
|
-
* })
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
import type { Context, Middleware } from '../types.ts'
|
|
27
|
-
import { getCookies } from '../core/cookie.ts'
|
|
28
|
-
|
|
29
|
-
// Augment Context with flash property
|
|
30
|
-
declare module '../types.ts' {
|
|
31
|
-
interface Context {
|
|
32
|
-
flash: FlashInjected
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Flash message module — a {@link Middleware} that injects `ctx.flash`. */
|
|
37
|
-
export type FlashModule = Middleware<Context, Context & FlashInjected>
|
|
38
|
-
|
|
39
|
-
/** Options for {@link flash}. */
|
|
40
|
-
export interface FlashOptions {
|
|
41
|
-
/**
|
|
42
|
-
* Cookie name to store the flash message.
|
|
43
|
-
* @default 'flash'
|
|
44
|
-
*/
|
|
45
|
-
name?: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Flash message object injected into `ctx.flash`.
|
|
50
|
-
*
|
|
51
|
-
* Access the current flash with `.value`, or set a new flash with `.set()`.
|
|
52
|
-
* The `.value` is automatically cleared after being read.
|
|
53
|
-
*/
|
|
54
|
-
export interface FlashInjected {
|
|
55
|
-
/**
|
|
56
|
-
* The flash value read from the incoming cookie.
|
|
57
|
-
* `undefined` if no flash cookie is present.
|
|
58
|
-
* Automatically cleared after the response is sent.
|
|
59
|
-
*/
|
|
60
|
-
value: unknown
|
|
61
|
-
/**
|
|
62
|
-
* Set a flash message and return a 302 redirect response.
|
|
63
|
-
*
|
|
64
|
-
* @param data - Any JSON-serializable value to store as the flash message.
|
|
65
|
-
* @param location - Redirect location (defaults to the `Referer` header).
|
|
66
|
-
* @returns A 302 Response with a `Set-Cookie` header.
|
|
67
|
-
*
|
|
68
|
-
* ```ts
|
|
69
|
-
* return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
set: (data: unknown, location?: string) => Response
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function makeSetFlash(name: string, location: string) {
|
|
76
|
-
return (data: unknown, loc?: string) => {
|
|
77
|
-
const finalLoc = loc ?? location
|
|
78
|
-
const value = encodeURIComponent(JSON.stringify(data))
|
|
79
|
-
return new Response(null, {
|
|
80
|
-
status: 302,
|
|
81
|
-
headers: {
|
|
82
|
-
Location: finalLoc,
|
|
83
|
-
'Set-Cookie': `${name}=${value}; Path=/; SameSite=Lax`,
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Flash message middleware — injects `ctx.flash`.
|
|
91
|
-
*
|
|
92
|
-
* @param options - Cookie name configuration.
|
|
93
|
-
* @returns Middleware that injects `ctx.flash` (`FlashInjected`).
|
|
94
|
-
*
|
|
95
|
-
* ```ts
|
|
96
|
-
* app.use(flash())
|
|
97
|
-
*
|
|
98
|
-
* // Read
|
|
99
|
-
* app.get('/', (req, ctx) => {
|
|
100
|
-
* const msg = ctx.flash.value
|
|
101
|
-
* })
|
|
102
|
-
*
|
|
103
|
-
* // Write + redirect
|
|
104
|
-
* app.post('/save', async (req, ctx) => {
|
|
105
|
-
* return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
|
|
106
|
-
* })
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
export function flash(
|
|
110
|
-
options?: FlashOptions,
|
|
111
|
-
): Middleware<Context, Context & { flash: FlashInjected }> {
|
|
112
|
-
const name = options?.name ?? 'flash'
|
|
113
|
-
|
|
114
|
-
const mw: Middleware<Context, Context & { flash: FlashInjected }> = async (req, ctx, next) => {
|
|
115
|
-
const raw = getCookies(req)[name] ?? null
|
|
116
|
-
const referer = req.headers.get('referer') || '/'
|
|
117
|
-
|
|
118
|
-
let value: unknown = undefined
|
|
119
|
-
if (raw) {
|
|
120
|
-
try {
|
|
121
|
-
value = JSON.parse(decodeURIComponent(raw))
|
|
122
|
-
} catch {
|
|
123
|
-
value = raw
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
ctx.flash = {
|
|
128
|
-
value,
|
|
129
|
-
set: makeSetFlash(name, referer),
|
|
130
|
-
} as FlashInjected
|
|
131
|
-
|
|
132
|
-
const res = await next(req, ctx as Context & { flash: FlashInjected })
|
|
133
|
-
|
|
134
|
-
if (raw) {
|
|
135
|
-
const headers = new Headers(res.headers)
|
|
136
|
-
headers.append('Set-Cookie', `${name}=; Path=/; Max-Age=0`)
|
|
137
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return res
|
|
141
|
-
}
|
|
142
|
-
mw.__meta = { injects: ['flash'], depends: [] }
|
|
143
|
-
return mw
|
|
144
|
-
}
|
package/middleware/health.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Handler } from '../types.ts'
|
|
2
|
-
import { Router } from '../core/router.ts'
|
|
3
|
-
|
|
4
|
-
/** Options for {@link health}. */
|
|
5
|
-
export interface HealthOptions {
|
|
6
|
-
/** Health check endpoint path (default: `'/__health'`). */
|
|
7
|
-
path?: string
|
|
8
|
-
/** Async function that throws if the service is unhealthy. Called on each request. */
|
|
9
|
-
check?: () => Promise<void>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Health check endpoint.
|
|
14
|
-
*
|
|
15
|
-
* Returns 200 with `'OK'` if the check passes, 503 if it fails.
|
|
16
|
-
*
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { health } from 'weifuwu'
|
|
19
|
-
*
|
|
20
|
-
* app.use(health({
|
|
21
|
-
* check: async () => {
|
|
22
|
-
* await db.query('SELECT 1')
|
|
23
|
-
* },
|
|
24
|
-
* }))
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export function health(options?: HealthOptions): Router {
|
|
28
|
-
const path = options?.path ?? '/__health'
|
|
29
|
-
const r = new Router()
|
|
30
|
-
|
|
31
|
-
const handler: Handler = async () => {
|
|
32
|
-
try {
|
|
33
|
-
await options?.check?.()
|
|
34
|
-
return new Response('OK', { status: 200 })
|
|
35
|
-
} catch {
|
|
36
|
-
return new Response('Service Unavailable', { status: 503 })
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
r.get(path, handler)
|
|
41
|
-
r.head(path, handler)
|
|
42
|
-
|
|
43
|
-
return r
|
|
44
|
-
}
|
package/middleware/helmet.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { Middleware, Context } from '../types.ts'
|
|
2
|
-
|
|
3
|
-
/** Options for {@link helmet}. Set any header to `false` to omit it. */
|
|
4
|
-
export interface HelmetOptions {
|
|
5
|
-
/** `Content-Security-Policy` header value. */
|
|
6
|
-
contentSecurityPolicy?: string | false
|
|
7
|
-
/** `Cross-Origin-Embedder-Policy` header value. */
|
|
8
|
-
crossOriginEmbedderPolicy?: string | false
|
|
9
|
-
/** `Cross-Origin-Opener-Policy` header value. */
|
|
10
|
-
crossOriginOpenerPolicy?: string | false
|
|
11
|
-
/** `Cross-Origin-Resource-Policy` header value. */
|
|
12
|
-
crossOriginResourcePolicy?: string | false
|
|
13
|
-
/** `Origin-Agent-Cluster` header value. */
|
|
14
|
-
originAgentCluster?: string | false
|
|
15
|
-
/** `Referrer-Policy` header value. */
|
|
16
|
-
referrerPolicy?: string | false
|
|
17
|
-
/** `Strict-Transport-Security` header value. */
|
|
18
|
-
strictTransportSecurity?: string | false
|
|
19
|
-
/** `X-Content-Type-Options` header value. */
|
|
20
|
-
xContentTypeOptions?: string | false
|
|
21
|
-
/** `X-DNS-Prefetch-Control` header value. */
|
|
22
|
-
xDnsPrefetchControl?: string | false
|
|
23
|
-
/** `X-Download-Options` header value. */
|
|
24
|
-
xDownloadOptions?: string | false
|
|
25
|
-
/** `X-Frame-Options` header value. */
|
|
26
|
-
xFrameOptions?: string | false
|
|
27
|
-
/** `X-Permitted-Cross-Domain-Policies` header value. */
|
|
28
|
-
xPermittedCrossDomainPolicies?: string | false
|
|
29
|
-
/** `X-XSS-Protection` header value. */
|
|
30
|
-
xXssProtection?: string | false
|
|
31
|
-
/** `Permissions-Policy` header value. */
|
|
32
|
-
permissionsPolicy?: string | false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Security headers middleware. Sets sensible defaults for all major security headers.
|
|
37
|
-
*
|
|
38
|
-
* ```ts
|
|
39
|
-
* import { helmet } from 'weifuwu'
|
|
40
|
-
* app.use(helmet())
|
|
41
|
-
*
|
|
42
|
-
* // Customize or disable specific headers
|
|
43
|
-
* app.use(helmet({ contentSecurityPolicy: false, xFrameOptions: 'DENY' }))
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
const HEADER_MAP: Record<string, keyof HelmetOptions> = {
|
|
47
|
-
'Content-Security-Policy': 'contentSecurityPolicy',
|
|
48
|
-
'Cross-Origin-Embedder-Policy': 'crossOriginEmbedderPolicy',
|
|
49
|
-
'Cross-Origin-Opener-Policy': 'crossOriginOpenerPolicy',
|
|
50
|
-
'Cross-Origin-Resource-Policy': 'crossOriginResourcePolicy',
|
|
51
|
-
'Origin-Agent-Cluster': 'originAgentCluster',
|
|
52
|
-
'Referrer-Policy': 'referrerPolicy',
|
|
53
|
-
'Strict-Transport-Security': 'strictTransportSecurity',
|
|
54
|
-
'X-Content-Type-Options': 'xContentTypeOptions',
|
|
55
|
-
'X-DNS-Prefetch-Control': 'xDnsPrefetchControl',
|
|
56
|
-
'X-Download-Options': 'xDownloadOptions',
|
|
57
|
-
'X-Frame-Options': 'xFrameOptions',
|
|
58
|
-
'X-Permitted-Cross-Domain-Policies': 'xPermittedCrossDomainPolicies',
|
|
59
|
-
'X-XSS-Protection': 'xXssProtection',
|
|
60
|
-
'Permissions-Policy': 'permissionsPolicy',
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function helmet(options?: HelmetOptions): Middleware<Context, Context> {
|
|
64
|
-
const opts = { ...DEFAULTS, ...options } as HelmetOptions
|
|
65
|
-
|
|
66
|
-
const headers = new Headers()
|
|
67
|
-
for (const [header, key] of Object.entries(HEADER_MAP)) {
|
|
68
|
-
const val = opts[key]
|
|
69
|
-
if (val !== false && val !== undefined) headers.set(header, val)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return async (req, ctx, next) => {
|
|
73
|
-
const res = await next(req, ctx)
|
|
74
|
-
const h = new Headers(res.headers)
|
|
75
|
-
for (const [k, v] of headers) {
|
|
76
|
-
if (!h.has(k)) h.set(k, v)
|
|
77
|
-
}
|
|
78
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const DEFAULTS: HelmetOptions = {
|
|
83
|
-
contentSecurityPolicy:
|
|
84
|
-
"default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
|
|
85
|
-
crossOriginEmbedderPolicy: 'require-corp',
|
|
86
|
-
crossOriginOpenerPolicy: 'same-origin',
|
|
87
|
-
crossOriginResourcePolicy: 'same-origin',
|
|
88
|
-
originAgentCluster: '?1',
|
|
89
|
-
referrerPolicy: 'no-referrer',
|
|
90
|
-
strictTransportSecurity: 'max-age=15552000; includeSubDomains',
|
|
91
|
-
xContentTypeOptions: 'nosniff',
|
|
92
|
-
xDnsPrefetchControl: 'off',
|
|
93
|
-
xDownloadOptions: 'noopen',
|
|
94
|
-
xFrameOptions: 'SAMEORIGIN',
|
|
95
|
-
xPermittedCrossDomainPolicies: 'none',
|
|
96
|
-
xXssProtection: '0',
|
|
97
|
-
permissionsPolicy: 'camera=(),display-capture=(),fullscreen=(),geolocation=(),microphone=()',
|
|
98
|
-
}
|
package/middleware/i18n.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
3
|
-
import { join, resolve } from 'node:path'
|
|
4
|
-
import type { Context, Middleware } from '../types.ts'
|
|
5
|
-
import { getCookies } from '../core/cookie.ts'
|
|
6
|
-
import { Router } from '../core/router.ts'
|
|
7
|
-
|
|
8
|
-
// Augment Context with i18n property
|
|
9
|
-
declare module '../types.ts' {
|
|
10
|
-
interface Context {
|
|
11
|
-
i18n: I18nInjected
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface I18nInjected {
|
|
16
|
-
locale: string
|
|
17
|
-
messages?: Record<string, unknown>
|
|
18
|
-
t: (key: string, params?: Record<string, string>, fallback?: string) => string
|
|
19
|
-
set?: (value: string, loc?: string) => Response
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface I18nOptions {
|
|
23
|
-
/** Default locale (default: 'en'). */
|
|
24
|
-
default?: string
|
|
25
|
-
/** Directory containing `{locale}.json` translation files. */
|
|
26
|
-
dir?: string
|
|
27
|
-
/** Inline translation messages keyed by locale. */
|
|
28
|
-
messages?: Record<string, Record<string, unknown>>
|
|
29
|
-
/** Cookie name for locale (default: 'locale'). Set empty to disable. */
|
|
30
|
-
cookie?: string
|
|
31
|
-
/** Whether to detect locale from Accept-Language header (default: true). */
|
|
32
|
-
fromAcceptLanguage?: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const DEFAULTS = {
|
|
36
|
-
default: 'en',
|
|
37
|
-
cookie: 'locale',
|
|
38
|
-
fromAcceptLanguage: true,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function translate(
|
|
42
|
-
msgs: Record<string, unknown>,
|
|
43
|
-
key: string,
|
|
44
|
-
params?: Record<string, string>,
|
|
45
|
-
fallback?: string,
|
|
46
|
-
): string {
|
|
47
|
-
const msg = key.split('.').reduce((o: any, k: string) => o?.[k], msgs)
|
|
48
|
-
if (msg === undefined || msg === null) return fallback ?? key
|
|
49
|
-
if (!params) return String(msg)
|
|
50
|
-
let result = String(msg)
|
|
51
|
-
for (const [k, v] of Object.entries(params)) {
|
|
52
|
-
result = result.replace(`{${k}}`, v)
|
|
53
|
-
}
|
|
54
|
-
return result
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* i18n module. Returns a Router with an attached `.middleware()` method.
|
|
59
|
-
*
|
|
60
|
-
* ```ts
|
|
61
|
-
* const l = i18n({ dir: './locales' })
|
|
62
|
-
* app.use(l.middleware()) // → ctx.i18n = { locale, t, set }
|
|
63
|
-
* app.use('/', l) // → GET /__lang/:locale (switch route)
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export interface I18nModule extends Router {
|
|
67
|
-
/** Middleware that injects `ctx.i18n = { locale, t, set }`. */
|
|
68
|
-
middleware: () => Middleware<Context, Context & I18nInjected>
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function i18n(options?: I18nOptions): I18nModule {
|
|
72
|
-
const opts = { ...DEFAULTS, ...options }
|
|
73
|
-
const dir = opts.dir ? resolve(opts.dir) : undefined
|
|
74
|
-
const cache = new Map<string, Record<string, unknown>>()
|
|
75
|
-
|
|
76
|
-
function validLocale(locale: string): boolean {
|
|
77
|
-
return /^[\w-]+$/.test(locale) && !locale.includes('..')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function loadMessages(locale: string): Promise<Record<string, unknown>> {
|
|
81
|
-
// Check inline messages first
|
|
82
|
-
if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
|
|
83
|
-
cache.set(locale, opts.messages[locale])
|
|
84
|
-
return opts.messages[locale]
|
|
85
|
-
}
|
|
86
|
-
// Then check file system
|
|
87
|
-
if (!dir || !validLocale(locale)) return {}
|
|
88
|
-
const cached = cache.get(locale)
|
|
89
|
-
if (cached) return cached
|
|
90
|
-
const filePath = join(dir, `${locale}.json`)
|
|
91
|
-
try {
|
|
92
|
-
await stat(filePath)
|
|
93
|
-
const content = await readFile(filePath, 'utf-8')
|
|
94
|
-
const data = JSON.parse(content) as Record<string, unknown>
|
|
95
|
-
cache.set(locale, data)
|
|
96
|
-
return data
|
|
97
|
-
} catch {
|
|
98
|
-
/* file not found */
|
|
99
|
-
}
|
|
100
|
-
// Fallback: zh-CN → zh
|
|
101
|
-
const short = locale.split('-')[0]
|
|
102
|
-
if (short !== locale) {
|
|
103
|
-
const fallback = cache.get(short) || (await loadMessages(short))
|
|
104
|
-
if (fallback && Object.keys(fallback).length > 0) {
|
|
105
|
-
cache.set(locale, fallback)
|
|
106
|
-
return fallback
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function detectLocale(req: Request): string {
|
|
113
|
-
if (opts.cookie) {
|
|
114
|
-
const fromCookie = getCookies(req)[opts.cookie]
|
|
115
|
-
if (fromCookie && validLocale(fromCookie)) return fromCookie
|
|
116
|
-
}
|
|
117
|
-
if (opts.fromAcceptLanguage) {
|
|
118
|
-
const fromHeader = req.headers.get('Accept-Language')?.split(',')[0]?.trim()
|
|
119
|
-
if (fromHeader && validLocale(fromHeader)) return fromHeader
|
|
120
|
-
}
|
|
121
|
-
return opts.default
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const mw: Middleware<Context, Context & I18nInjected> = async (req, ctx, next) => {
|
|
125
|
-
const locale = detectLocale(req)
|
|
126
|
-
const msgs = await loadMessages(locale)
|
|
127
|
-
|
|
128
|
-
;(ctx as Context & I18nInjected).i18n = {
|
|
129
|
-
locale,
|
|
130
|
-
messages: msgs,
|
|
131
|
-
t: (key: string, params?: Record<string, string>, fallback?: string) =>
|
|
132
|
-
translate(msgs, key, params, fallback),
|
|
133
|
-
set: (value: string, loc?: string) => {
|
|
134
|
-
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`
|
|
135
|
-
const location = loc ?? (req.headers.get('referer') || '/')
|
|
136
|
-
return new Response(null, {
|
|
137
|
-
status: 302,
|
|
138
|
-
headers: { Location: location, 'Set-Cookie': cookie },
|
|
139
|
-
})
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return next(req, ctx as Context & I18nInjected)
|
|
144
|
-
}
|
|
145
|
-
mw.__meta = { injects: ['i18n'], depends: [] }
|
|
146
|
-
|
|
147
|
-
class I18nRouter extends Router {
|
|
148
|
-
middleware() {
|
|
149
|
-
return mw
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const router = new I18nRouter()
|
|
154
|
-
router.get('/__lang/:locale', async (req) => {
|
|
155
|
-
const url = new URL(req.url)
|
|
156
|
-
const value = url.pathname.split('/__lang/')[1] ?? ''
|
|
157
|
-
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`
|
|
158
|
-
const messages = await loadMessages(value)
|
|
159
|
-
const accept = req.headers.get('accept') ?? ''
|
|
160
|
-
if (accept.includes('application/json')) {
|
|
161
|
-
return Response.json(
|
|
162
|
-
{
|
|
163
|
-
ok: true,
|
|
164
|
-
locale: value,
|
|
165
|
-
messages: Object.keys(messages).length > 0 ? messages : undefined,
|
|
166
|
-
},
|
|
167
|
-
{ headers: { 'Set-Cookie': cookie } },
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
const referer = req.headers.get('referer') || '/'
|
|
171
|
-
return new Response(null, { status: 302, headers: { Location: referer, 'Set-Cookie': cookie } })
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
return router
|
|
175
|
-
}
|