weifuwu 0.25.2 → 0.27.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.
Files changed (208) hide show
  1. package/README.md +291 -2489
  2. package/ai/provider.ts +129 -0
  3. package/ai/stream.ts +63 -0
  4. package/cli.ts +55 -257
  5. package/core/cookie.ts +114 -0
  6. package/core/env.ts +142 -0
  7. package/core/logger.ts +72 -0
  8. package/core/router.ts +795 -0
  9. package/core/serve.ts +294 -0
  10. package/core/sse.ts +85 -0
  11. package/core/trace.ts +146 -0
  12. package/graphql.ts +267 -0
  13. package/hub.ts +133 -0
  14. package/index.ts +71 -0
  15. package/mailer.ts +81 -0
  16. package/middleware/compress.ts +103 -0
  17. package/middleware/cors.ts +81 -0
  18. package/middleware/csrf.ts +112 -0
  19. package/middleware/flash.ts +144 -0
  20. package/middleware/health.ts +44 -0
  21. package/middleware/helmet.ts +98 -0
  22. package/middleware/i18n.ts +175 -0
  23. package/middleware/rate-limit.ts +167 -0
  24. package/middleware/request-id.ts +60 -0
  25. package/middleware/static.ts +149 -0
  26. package/middleware/theme.ts +84 -0
  27. package/middleware/upload.ts +168 -0
  28. package/middleware/validate.ts +186 -0
  29. package/package.json +14 -36
  30. package/postgres/client.ts +132 -0
  31. package/postgres/index.ts +4 -0
  32. package/postgres/module.ts +37 -0
  33. package/postgres/schema/columns.ts +186 -0
  34. package/postgres/schema/index.ts +36 -0
  35. package/postgres/schema/sql.ts +39 -0
  36. package/postgres/schema/table.ts +548 -0
  37. package/postgres/schema/where.ts +99 -0
  38. package/postgres/types.ts +48 -0
  39. package/queue/cron.ts +90 -0
  40. package/queue/index.ts +654 -0
  41. package/queue/types.ts +60 -0
  42. package/redis/client.ts +24 -0
  43. package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
  44. package/redis/types.ts +28 -0
  45. package/types.ts +78 -0
  46. package/cli/template/app.ts +0 -22
  47. package/cli/template/index.ts +0 -10
  48. package/cli/template/locales/en.json +0 -13
  49. package/cli/template/locales/zh-CN.json +0 -13
  50. package/cli/template/locales/zh-TW.json +0 -13
  51. package/cli/template/locales/zh.json +0 -13
  52. package/cli/template/ui/app/globals.css +0 -2
  53. package/cli/template/ui/app/layout.tsx +0 -15
  54. package/cli/template/ui/app/page.tsx +0 -124
  55. package/cli/template/ui/components/Greeting.tsx +0 -3
  56. package/dist/agent/client.d.ts +0 -2
  57. package/dist/agent/index.d.ts +0 -2
  58. package/dist/agent/rest.d.ts +0 -14
  59. package/dist/agent/run.d.ts +0 -19
  60. package/dist/agent/types.d.ts +0 -55
  61. package/dist/ai/provider.d.ts +0 -45
  62. package/dist/ai/utils.d.ts +0 -5
  63. package/dist/ai/workflow.d.ts +0 -17
  64. package/dist/ai-sdk.d.ts +0 -2
  65. package/dist/ai.d.ts +0 -13
  66. package/dist/analytics.d.ts +0 -45
  67. package/dist/auth.d.ts +0 -22
  68. package/dist/cache.d.ts +0 -74
  69. package/dist/cli.d.ts +0 -2
  70. package/dist/cli.js +0 -302
  71. package/dist/client-locale.d.ts +0 -25
  72. package/dist/client-pref.d.ts +0 -3
  73. package/dist/client-router.d.ts +0 -300
  74. package/dist/client-state.d.ts +0 -22
  75. package/dist/client-theme.d.ts +0 -36
  76. package/dist/compile.d.ts +0 -15
  77. package/dist/compress.d.ts +0 -20
  78. package/dist/cookie.d.ts +0 -36
  79. package/dist/cors.d.ts +0 -25
  80. package/dist/cron-utils.d.ts +0 -73
  81. package/dist/csrf.d.ts +0 -47
  82. package/dist/deploy/config.d.ts +0 -2
  83. package/dist/deploy/gateway.d.ts +0 -2
  84. package/dist/deploy/index.d.ts +0 -4
  85. package/dist/deploy/manager.d.ts +0 -16
  86. package/dist/deploy/process.d.ts +0 -14
  87. package/dist/deploy/types.d.ts +0 -53
  88. package/dist/env.d.ts +0 -69
  89. package/dist/error-boundary.d.ts +0 -2
  90. package/dist/flash.d.ts +0 -90
  91. package/dist/fts.d.ts +0 -36
  92. package/dist/graphql.d.ts +0 -16
  93. package/dist/head.d.ts +0 -6
  94. package/dist/health.d.ts +0 -24
  95. package/dist/helmet.d.ts +0 -33
  96. package/dist/html-shell.d.ts +0 -1
  97. package/dist/hub.d.ts +0 -37
  98. package/dist/i18n.d.ts +0 -39
  99. package/dist/iii/client.d.ts +0 -2
  100. package/dist/iii/index.d.ts +0 -4
  101. package/dist/iii/register-worker.d.ts +0 -9
  102. package/dist/iii/rest.d.ts +0 -3
  103. package/dist/iii/stream.d.ts +0 -82
  104. package/dist/iii/types.d.ts +0 -121
  105. package/dist/iii/worker.d.ts +0 -2
  106. package/dist/iii/ws.d.ts +0 -22
  107. package/dist/index.d.ts +0 -101
  108. package/dist/index.js +0 -12752
  109. package/dist/kb/index.d.ts +0 -3
  110. package/dist/kb/types.d.ts +0 -72
  111. package/dist/layout.d.ts +0 -2
  112. package/dist/live.d.ts +0 -7
  113. package/dist/logdb/client.d.ts +0 -2
  114. package/dist/logdb/index.d.ts +0 -2
  115. package/dist/logdb/rest.d.ts +0 -5
  116. package/dist/logdb/types.d.ts +0 -27
  117. package/dist/logger.d.ts +0 -16
  118. package/dist/mailer.d.ts +0 -51
  119. package/dist/mcp.d.ts +0 -34
  120. package/dist/messager/agent.d.ts +0 -11
  121. package/dist/messager/client.d.ts +0 -2
  122. package/dist/messager/index.d.ts +0 -2
  123. package/dist/messager/rest.d.ts +0 -15
  124. package/dist/messager/types.d.ts +0 -57
  125. package/dist/messager/ws.d.ts +0 -14
  126. package/dist/module-server.d.ts +0 -9
  127. package/dist/not-found.d.ts +0 -2
  128. package/dist/notifier/client.d.ts +0 -2
  129. package/dist/notifier/index.d.ts +0 -2
  130. package/dist/notifier/types.d.ts +0 -105
  131. package/dist/opencode/client.d.ts +0 -2
  132. package/dist/opencode/index.d.ts +0 -2
  133. package/dist/opencode/permissions.d.ts +0 -5
  134. package/dist/opencode/prompt.d.ts +0 -8
  135. package/dist/opencode/rest.d.ts +0 -16
  136. package/dist/opencode/run.d.ts +0 -13
  137. package/dist/opencode/session.d.ts +0 -26
  138. package/dist/opencode/skills.d.ts +0 -4
  139. package/dist/opencode/tools/bash.d.ts +0 -6
  140. package/dist/opencode/tools/edit.d.ts +0 -19
  141. package/dist/opencode/tools/glob.d.ts +0 -9
  142. package/dist/opencode/tools/grep.d.ts +0 -17
  143. package/dist/opencode/tools/index.d.ts +0 -12
  144. package/dist/opencode/tools/question.d.ts +0 -5
  145. package/dist/opencode/tools/read.d.ts +0 -16
  146. package/dist/opencode/tools/skill.d.ts +0 -18
  147. package/dist/opencode/tools/web.d.ts +0 -18
  148. package/dist/opencode/tools/write.d.ts +0 -13
  149. package/dist/opencode/types.d.ts +0 -90
  150. package/dist/opencode/ws.d.ts +0 -21
  151. package/dist/permissions.d.ts +0 -51
  152. package/dist/postgres/client.d.ts +0 -4
  153. package/dist/postgres/index.d.ts +0 -4
  154. package/dist/postgres/module.d.ts +0 -17
  155. package/dist/postgres/schema/columns.d.ts +0 -99
  156. package/dist/postgres/schema/index.d.ts +0 -6
  157. package/dist/postgres/schema/sql.d.ts +0 -22
  158. package/dist/postgres/schema/table.d.ts +0 -141
  159. package/dist/postgres/schema/where.d.ts +0 -29
  160. package/dist/postgres/types.d.ts +0 -50
  161. package/dist/queue/index.d.ts +0 -2
  162. package/dist/queue/types.d.ts +0 -62
  163. package/dist/rate-limit.d.ts +0 -45
  164. package/dist/react.d.ts +0 -14
  165. package/dist/react.js +0 -751
  166. package/dist/redis/client.d.ts +0 -2
  167. package/dist/redis/types.d.ts +0 -18
  168. package/dist/request-id.d.ts +0 -40
  169. package/dist/router.d.ts +0 -73
  170. package/dist/s3.d.ts +0 -68
  171. package/dist/seo.d.ts +0 -104
  172. package/dist/serve.d.ts +0 -38
  173. package/dist/server-registry.d.ts +0 -10
  174. package/dist/session.d.ts +0 -117
  175. package/dist/sse.d.ts +0 -47
  176. package/dist/ssr-entries.d.ts +0 -4
  177. package/dist/ssr.d.ts +0 -11
  178. package/dist/static.d.ts +0 -23
  179. package/dist/stream.d.ts +0 -24
  180. package/dist/tailwind.d.ts +0 -15
  181. package/dist/tenant/client.d.ts +0 -2
  182. package/dist/tenant/graphql.d.ts +0 -3
  183. package/dist/tenant/index.d.ts +0 -2
  184. package/dist/tenant/rest.d.ts +0 -3
  185. package/dist/tenant/schema.d.ts +0 -5
  186. package/dist/tenant/types.d.ts +0 -48
  187. package/dist/tenant/utils.d.ts +0 -9
  188. package/dist/test-utils.d.ts +0 -194
  189. package/dist/theme.d.ts +0 -31
  190. package/dist/trace.d.ts +0 -95
  191. package/dist/tsx-context.d.ts +0 -32
  192. package/dist/types.d.ts +0 -47
  193. package/dist/upload.d.ts +0 -55
  194. package/dist/use-action.d.ts +0 -42
  195. package/dist/use-agent-stream.d.ts +0 -49
  196. package/dist/use-flash-message.d.ts +0 -17
  197. package/dist/use-websocket.d.ts +0 -42
  198. package/dist/user/client.d.ts +0 -30
  199. package/dist/user/index.d.ts +0 -2
  200. package/dist/user/oauth-login.d.ts +0 -21
  201. package/dist/user/oauth2.d.ts +0 -31
  202. package/dist/user/types.d.ts +0 -178
  203. package/dist/validate.d.ts +0 -32
  204. package/dist/vendor.d.ts +0 -7
  205. package/dist/webhook.d.ts +0 -79
  206. package/opencode/ui/app/globals.css +0 -1
  207. package/opencode/ui/app/layout.tsx +0 -13
  208. package/opencode/ui/app/page.tsx +0 -523
package/graphql.ts ADDED
@@ -0,0 +1,267 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any, no-console */
2
+ import {
3
+ buildSchema,
4
+ graphql as executeGraphQL,
5
+ type GraphQLSchema,
6
+ validate as validateQuery,
7
+ parse,
8
+ type DocumentNode,
9
+ } from 'graphql'
10
+ import { makeExecutableSchema } from '@graphql-tools/schema'
11
+ import type { Context } from './types.ts'
12
+ import { Router } from './core/router.ts'
13
+ import { currentTraceId } from './core/trace.ts'
14
+
15
+ export interface GraphQLOptions {
16
+ schema: string | GraphQLSchema
17
+ rootValue?: any
18
+ resolvers?: any
19
+ context?: (req: Request, ctx: Context) => Record<string, any> | Promise<Record<string, any>>
20
+ graphiql?: boolean
21
+ /** Max query depth (nesting). Default: 10. Set 0 to disable. */
22
+ maxDepth?: number
23
+ /** Execution timeout in ms. Default: 30_000. */
24
+ timeout?: number
25
+ }
26
+
27
+ export type GraphQLHandler = (
28
+ req: Request,
29
+ ctx: Context,
30
+ ) => GraphQLOptions | Promise<GraphQLOptions>
31
+
32
+ type GraphQLParams = {
33
+ query: string
34
+ variables: Record<string, any>
35
+ operationName?: string
36
+ }
37
+
38
+ function parseParamsFromGet(url: URL): GraphQLParams | null {
39
+ const query = url.searchParams.get('query')
40
+ if (!query) return null
41
+ let variables = {}
42
+ const variablesStr = url.searchParams.get('variables')
43
+ if (variablesStr) {
44
+ try {
45
+ variables = JSON.parse(variablesStr)
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+ return { query, variables, operationName: url.searchParams.get('operationName') || undefined }
51
+ }
52
+
53
+ async function parseParamsFromPost(req: Request): Promise<GraphQLParams | null> {
54
+ try {
55
+ const body = (await req.json()) as {
56
+ query?: string
57
+ variables?: Record<string, any>
58
+ operationName?: string
59
+ }
60
+ if (!body.query) return null
61
+ return { query: body.query, variables: body.variables || {}, operationName: body.operationName }
62
+ } catch {
63
+ return null
64
+ }
65
+ }
66
+
67
+ function buildSchemaFromOptions(options: GraphQLOptions): GraphQLSchema {
68
+ try {
69
+ if (typeof options.schema === 'string') {
70
+ return options.resolvers
71
+ ? makeExecutableSchema({ typeDefs: options.schema, resolvers: options.resolvers })
72
+ : buildSchema(options.schema)
73
+ }
74
+ return options.schema
75
+ } catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err)
77
+ console.error(`[graphql] schema build failed: ${msg}`)
78
+ throw err
79
+ }
80
+ }
81
+
82
+ /** Count max nesting depth of a GraphQL query. */
83
+ function queryDepth(doc: DocumentNode): number {
84
+ let max = 0
85
+ function walk(node: any, depth: number) {
86
+ if (depth > max) max = depth
87
+ if (node.selectionSet) {
88
+ for (const sel of node.selectionSet.selections) {
89
+ walk(sel, depth + 1)
90
+ }
91
+ }
92
+ }
93
+ for (const def of doc.definitions) {
94
+ if (def.kind === 'OperationDefinition') {
95
+ walk(def, 0)
96
+ }
97
+ }
98
+ return max
99
+ }
100
+
101
+ async function executeQuery(
102
+ schema: GraphQLSchema,
103
+ params: GraphQLParams,
104
+ options: GraphQLOptions,
105
+ req: Request,
106
+ ctx: Context,
107
+ ): Promise<Response> {
108
+ // Depth limit
109
+ const maxDepth = options.maxDepth ?? 10
110
+ if (maxDepth > 0) {
111
+ try {
112
+ const doc = parse(params.query)
113
+ const depth = queryDepth(doc)
114
+ if (depth > maxDepth) {
115
+ return Response.json(
116
+ { errors: [{ message: `Query depth ${depth} exceeds limit ${maxDepth}` }] },
117
+ { status: 400 },
118
+ )
119
+ }
120
+ const validationErrors = validateQuery(schema, doc)
121
+ if (validationErrors.length > 0) {
122
+ return Response.json(
123
+ { errors: validationErrors.map((e) => ({ message: e.message })) },
124
+ { status: 400 },
125
+ )
126
+ }
127
+ } catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err)
129
+ return Response.json({ errors: [{ message: `Parse error: ${msg}` }] }, { status: 400 })
130
+ }
131
+ }
132
+
133
+ // Timeout
134
+ const timeout = options.timeout ?? 30_000
135
+ const contextValue = options.context ? await options.context(req, ctx) : ctx
136
+
137
+ try {
138
+ const resultPromise = executeGraphQL({
139
+ schema,
140
+ source: params.query,
141
+ rootValue: options.rootValue,
142
+ contextValue,
143
+ variableValues: params.variables,
144
+ operationName: params.operationName,
145
+ }) as any
146
+
147
+ let result
148
+ if (timeout > 0) {
149
+ let timer: ReturnType<typeof setTimeout> | null = null
150
+ const timeoutPromise = new Promise<never>((_, reject) => {
151
+ timer = setTimeout(() => reject(new Error('Query timeout')), timeout)
152
+ })
153
+ result = await Promise.race([resultPromise, timeoutPromise])
154
+ if (timer) clearTimeout(timer)
155
+ } else {
156
+ result = await resultPromise
157
+ }
158
+
159
+ return Response.json(result, { status: result.errors ? 400 : 200 })
160
+ } catch (err) {
161
+ const msg = err instanceof Error ? err.message : String(err)
162
+ console.error(`[${currentTraceId()}] graphql execution failed: ${msg}`)
163
+ return Response.json({ errors: [{ message: msg }] }, { status: 500 })
164
+ }
165
+ }
166
+
167
+ function graphiqlHTML(endpoint: string): string {
168
+ const safeEndpoint = endpoint.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\x3C')
169
+ return `<!doctype html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8" />
173
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
174
+ <title>GraphiQL</title>
175
+ <style>body { margin: 0; } #graphiql { height: 100dvh; }</style>
176
+ <link rel="stylesheet" href="https://esm.sh/graphiql@5.2.2/dist/style.css" />
177
+ <script type="importmap">
178
+ {
179
+ "imports": {
180
+ "react": "https://esm.sh/react@19.2.5",
181
+ "react/": "https://esm.sh/react@19.2.5/",
182
+ "react-dom": "https://esm.sh/react-dom@19.2.5",
183
+ "react-dom/": "https://esm.sh/react-dom@19.2.5/",
184
+ "graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
185
+ "graphiql/": "https://esm.sh/graphiql@5.2.2/",
186
+ "@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
187
+ "@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
188
+ "graphql": "https://esm.sh/graphql@16.13.2",
189
+ "@emotion/is-prop-valid": "data:text/javascript,"
190
+ }
191
+ }
192
+ </script>
193
+ <script type="module">
194
+ import React from 'react';
195
+ import ReactDOM from 'react-dom/client';
196
+ import { GraphiQL } from 'graphiql';
197
+ import { createGraphiQLFetcher } from '@graphiql/toolkit';
198
+ import 'graphiql/setup-workers/esm.sh';
199
+
200
+ const fetcher = createGraphiQLFetcher({ url: "${safeEndpoint}" });
201
+
202
+ function App() {
203
+ return React.createElement(GraphiQL, { fetcher });
204
+ }
205
+
206
+ const container = document.getElementById('graphiql');
207
+ const root = ReactDOM.createRoot(container);
208
+ root.render(React.createElement(App));
209
+ </script>
210
+ </head>
211
+ <body>
212
+ <div id="graphiql">Loading\u2026</div>
213
+ </body>
214
+ </html>`
215
+ }
216
+
217
+ export function graphql(handler: GraphQLHandler): Router {
218
+ const r = new Router()
219
+ let cachedOptions: GraphQLOptions | null = null
220
+ let cachedSchema: GraphQLSchema | null = null
221
+
222
+ async function getSchema(
223
+ req: Request,
224
+ ctx: Context,
225
+ ): Promise<{ options: GraphQLOptions; schema: GraphQLSchema }> {
226
+ const options = await handler(req, ctx)
227
+ // Cache schema — handler must return the same schema reference for cache to work.
228
+ // If schema changes (e.g. hot-reload), return a different object reference.
229
+ if (cachedSchema && cachedOptions === options) {
230
+ return { options, schema: cachedSchema }
231
+ }
232
+ const schema = buildSchemaFromOptions(options)
233
+ cachedOptions = options
234
+ cachedSchema = schema
235
+ return { options, schema }
236
+ }
237
+
238
+ r.get('/', async (req, ctx) => {
239
+ const { options, schema } = await getSchema(req, ctx)
240
+ const url = new URL(req.url)
241
+
242
+ if (options.graphiql && !url.searchParams.has('query')) {
243
+ return new Response(graphiqlHTML(url.pathname), {
244
+ status: 200,
245
+ headers: { 'Content-Type': 'text/html' },
246
+ })
247
+ }
248
+
249
+ const params = parseParamsFromGet(url)
250
+ if (!params) {
251
+ return Response.json({ errors: [{ message: 'Missing query' }] }, { status: 400 })
252
+ }
253
+
254
+ return executeQuery(schema, params, options, req, ctx)
255
+ })
256
+
257
+ r.post('/', async (req, ctx) => {
258
+ const { options, schema } = await getSchema(req, ctx)
259
+ const params = await parseParamsFromPost(req)
260
+ if (!params) {
261
+ return Response.json({ errors: [{ message: 'Missing query' }] }, { status: 400 })
262
+ }
263
+ return executeQuery(schema, params, options, req, ctx)
264
+ })
265
+
266
+ return r
267
+ }
package/hub.ts ADDED
@@ -0,0 +1,133 @@
1
+ import type { Redis, WebSocket, Closeable } from './types.ts'
2
+
3
+ /** Options for {@link createHub}. */
4
+ export interface HubOptions {
5
+ /** Optional Redis client for cross-process pub/sub broadcast. */
6
+ redis?: Redis
7
+ /** Key prefix for Redis channels (default: `'hub:'`). */
8
+ prefix?: string
9
+ }
10
+
11
+ /**
12
+ * In-memory (and optionally Redis-backed) pub/sub hub for WebSocket rooms.
13
+ *
14
+ * Used internally by the WebSocket handler to implement `ctx.ws.join()` / `ctx.ws.sendRoom()`. */
15
+ export interface Hub extends Closeable {
16
+ /** Subscribe a WebSocket to a room/group. */
17
+ join(key: string, ws: WebSocket): void
18
+ /** Unsubscribe a WebSocket from all rooms. */
19
+ leave(ws: WebSocket): void
20
+ /** Send a JSON message to all members of a room. */
21
+ broadcast(key: string, data: unknown): void
22
+ /** Close the hub, disconnect Redis subscribers, clear all rooms. */
23
+ close(): Promise<void>
24
+ }
25
+
26
+ /**
27
+ * Create a pub/sub hub for WebSocket room management.
28
+ *
29
+ * In-memory by default. Pass `redis` to enable cross-process broadcasting.
30
+ *
31
+ * ```ts
32
+ * import { createHub } from 'weifuwu'
33
+ *
34
+ * const hub = createHub()
35
+ * hub.join('room:general', ws)
36
+ * hub.broadcast('room:general', { type: 'chat', text: 'Hello' })
37
+ * ```
38
+ */
39
+ export function createHub(opts?: HubOptions): Hub {
40
+ const prefix = opts?.prefix ?? 'hub:'
41
+ const channels = new Map<string, Set<WebSocket>>()
42
+ const wsKeys = new Map<WebSocket, Set<string>>()
43
+
44
+ let redisPub: Redis | undefined
45
+ let redisSub: Redis | null = null
46
+
47
+ if (opts?.redis) {
48
+ redisPub = opts.redis
49
+ redisSub = opts.redis.duplicate()
50
+ redisSub.on('message', (rawChannel: string, rawData: string) => {
51
+ if (!rawChannel.startsWith(prefix)) return
52
+ const key = rawChannel.slice(prefix.length)
53
+ const members = channels.get(key)
54
+ if (!members) return
55
+ for (const ws of members) {
56
+ try {
57
+ ws.send(rawData)
58
+ } catch {}
59
+ }
60
+ })
61
+ }
62
+
63
+ function join(key: string, ws: WebSocket): void {
64
+ if (!channels.has(key)) {
65
+ channels.set(key, new Set())
66
+ redisSub?.subscribe(`${prefix}${key}`)
67
+ }
68
+ channels.get(key)!.add(ws)
69
+ let keys = wsKeys.get(ws)
70
+ if (!keys) {
71
+ keys = new Set()
72
+ wsKeys.set(ws, keys)
73
+ }
74
+ keys.add(key)
75
+ // Auto-cleanup on close (if WebSocket supports addEventListener)
76
+ if (typeof ws.addEventListener === 'function') {
77
+ ws.addEventListener('close', () => removeFromChannels(ws))
78
+ ws.addEventListener('error', () => removeFromChannels(ws))
79
+ }
80
+ }
81
+
82
+ function removeFromChannels(ws: WebSocket): void {
83
+ const keys = wsKeys.get(ws)
84
+ if (keys) {
85
+ for (const key of keys) {
86
+ const members = channels.get(key)
87
+ if (members) {
88
+ members.delete(ws)
89
+ if (members.size === 0) channels.delete(key)
90
+ }
91
+ }
92
+ wsKeys.delete(ws)
93
+ }
94
+ }
95
+
96
+ function leave(ws: WebSocket): void {
97
+ removeFromChannels(ws)
98
+ }
99
+
100
+ function broadcast(key: string, data: unknown): void {
101
+ const msg = JSON.stringify(data)
102
+ const members = channels.get(key)
103
+ if (members) {
104
+ const dead: WebSocket[] = []
105
+ for (const ws of members) {
106
+ try {
107
+ ws.send(msg)
108
+ } catch {
109
+ dead.push(ws)
110
+ }
111
+ }
112
+ for (const ws of dead) removeFromChannels(ws)
113
+ }
114
+ redisPub?.publish(`${prefix}${key}`, msg)
115
+ }
116
+
117
+ async function close(): Promise<void> {
118
+ // Disconnect stale WebSocket listeners
119
+ for (const ws of wsKeys.keys()) {
120
+ removeFromChannels(ws)
121
+ }
122
+ channels.clear()
123
+ wsKeys.clear()
124
+ if (redisSub) {
125
+ redisSub.removeAllListeners('message')
126
+ await redisSub.quit()
127
+ }
128
+ redisPub = undefined
129
+ redisSub = null
130
+ }
131
+
132
+ return { join, leave, broadcast, close }
133
+ }
package/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ export type { Context, Handler, Middleware, ErrorHandler } from './types.ts'
2
+ export { HttpError } from './types.ts'
3
+ export { currentTraceId, currentTrace, runWithTrace, traceElapsed, trace } from './core/trace.ts'
4
+ export type { TraceContext, TraceInjected, TraceOptions } from './core/trace.ts'
5
+ export { loadEnv, isDev, isProd, isBundled, getPublicEnv, env } from './core/env.ts'
6
+ export { serve, createTestServer, DEFAULT_MAX_BODY } from './core/serve.ts'
7
+ export type { ServeOptions, Server } from './core/serve.ts'
8
+ export { Router } from './core/router.ts'
9
+ export type { WebSocketHandler } from './core/router.ts'
10
+ export { logger } from './core/logger.ts'
11
+ export type { LoggerOptions } from './core/logger.ts'
12
+ export { cors } from './middleware/cors.ts'
13
+ export type { CORSOptions } from './middleware/cors.ts'
14
+
15
+ export { serveStatic } from './middleware/static.ts'
16
+ export type { ServeStaticOptions } from './middleware/static.ts'
17
+ export { validate } from './middleware/validate.ts'
18
+ export type { ValidationSchemas, ValidateModule } from './middleware/validate.ts'
19
+ export { getCookies, setCookie, deleteCookie } from './core/cookie.ts'
20
+ export type { CookieOptions } from './core/cookie.ts'
21
+ export { upload } from './middleware/upload.ts'
22
+ export type { UploadOptions, UploadedFile, UploadModule } from './middleware/upload.ts'
23
+ export { rateLimit } from './middleware/rate-limit.ts'
24
+ export type { RateLimitOptions } from './middleware/rate-limit.ts'
25
+ export { compress } from './middleware/compress.ts'
26
+ export type { CompressOptions } from './middleware/compress.ts'
27
+ export { helmet } from './middleware/helmet.ts'
28
+ export type { HelmetOptions } from './middleware/helmet.ts'
29
+ export { requestId } from './middleware/request-id.ts'
30
+ export type { RequestIdOptions, RequestIdModule } from './middleware/request-id.ts'
31
+ export { createSSEStream, formatSSE, formatSSEData } from './core/sse.ts'
32
+ export type { SSEEvent } from './core/sse.ts'
33
+ export { testApp, TestApp, TestRequest, createTestDb, withTestDb } from './test/test-utils.ts'
34
+ export type { TestResponse, TestDb } from './test/test-utils.ts'
35
+ export { graphql } from './graphql.ts'
36
+ export type { GraphQLOptions, GraphQLHandler } from './graphql.ts'
37
+ export { aiStream } from './ai/stream.ts'
38
+ export type { AIHandler } from './ai/stream.ts'
39
+ export { aiProvider } from './ai/provider.ts'
40
+ export type { AIProvider, AIProviderOptions, AIProviderInjected } from './ai/provider.ts'
41
+ export {
42
+ streamText,
43
+ generateText,
44
+ generateObject,
45
+ streamObject,
46
+ tool,
47
+ embed,
48
+ embedMany,
49
+ smoothStream,
50
+ } from 'ai'
51
+ export { openai, createOpenAI } from '@ai-sdk/openai'
52
+ export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts'
53
+ export type { PostgresOptions, PostgresClient, PostgresInjected } from './postgres/types.ts'
54
+ export { redis } from './redis/index.ts'
55
+ export type { RedisOptions, RedisClient, RedisInjected } from './redis/types.ts'
56
+ export { createHub } from './hub.ts'
57
+ export type { Hub, HubOptions } from './hub.ts'
58
+ export { queue } from './queue/index.ts'
59
+ export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types.ts'
60
+ export { health } from './middleware/health.ts'
61
+ export type { HealthOptions } from './middleware/health.ts'
62
+ export { theme } from './middleware/theme.ts'
63
+ export type { ThemeOptions, ThemeInjected } from './middleware/theme.ts'
64
+ export { i18n } from './middleware/i18n.ts'
65
+ export type { I18nOptions, I18nInjected } from './middleware/i18n.ts'
66
+ export { flash } from './middleware/flash.ts'
67
+ export type { FlashOptions, FlashInjected, FlashModule } from './middleware/flash.ts'
68
+ export { mailer } from './mailer.ts'
69
+ export type { MailerOptions, MailOptions, Mailer } from './mailer.ts'
70
+ export { csrf } from './middleware/csrf.ts'
71
+ export type { CsrfOptions, CsrfInjected, CsrfModule } from './middleware/csrf.ts'
package/mailer.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { createTransport, type Transporter } from 'nodemailer'
2
+ import type { Closeable } from './types.ts'
3
+
4
+ /** Options for sending an email. */
5
+ export interface MailOptions {
6
+ /** Recipient address(es). */
7
+ to: string | string[]
8
+ /** Email subject. */
9
+ subject: string
10
+ /** Plain text body. */
11
+ text?: string
12
+ /** HTML body. */
13
+ html?: string
14
+ /** Sender address (overrides `MailerOptions.from`). */
15
+ from?: string
16
+ /** CC recipient(s). */
17
+ cc?: string | string[]
18
+ /** BCC recipient(s). */
19
+ bcc?: string | string[]
20
+ }
21
+
22
+ /** Options for {@link mailer}. */
23
+ export interface MailerOptions {
24
+ /** Nodemailer transport string or pre-built transporter object. */
25
+ transport?: string | Transporter
26
+ /** Default sender address. */
27
+ from?: string
28
+ /** Custom send function (bypasses nodemailer). */
29
+ send?: (opts: MailOptions) => Promise<void>
30
+ }
31
+
32
+ /** Mailer instance returned by {@link mailer}. */
33
+ export interface Mailer extends Closeable {
34
+ /** Send an email. */
35
+ send: (opts: MailOptions) => Promise<void>
36
+ /** Close the nodemailer transport. */
37
+ close: () => Promise<void>
38
+ }
39
+
40
+ /**
41
+ * Create a mailer instance.
42
+ *
43
+ * ```ts
44
+ * import { mailer } from 'weifuwu'
45
+ *
46
+ * const email = mailer({ transport: 'smtp://user:pass@smtp.example.com' })
47
+ * await email.send({
48
+ * to: 'user@example.com',
49
+ * subject: 'Hello',
50
+ * text: 'Hello from weifuwu!',
51
+ * })
52
+ * await email.close()
53
+ * ```
54
+ */
55
+ export function mailer(options: MailerOptions): Mailer {
56
+ const sender = options.send
57
+ const from = options.from
58
+
59
+ let transporter: Transporter | null = null
60
+ if (!sender && options.transport) {
61
+ transporter =
62
+ typeof options.transport === 'string' ? createTransport(options.transport) : options.transport
63
+ }
64
+
65
+ async function send(opts: MailOptions): Promise<void> {
66
+ if (sender) {
67
+ await sender(opts)
68
+ return
69
+ }
70
+ if (!transporter) {
71
+ throw new Error('mailer: no transport configured — provide `transport` or `send` option')
72
+ }
73
+ await transporter.sendMail({ ...opts, from: opts.from ?? from })
74
+ }
75
+
76
+ async function close(): Promise<void> {
77
+ transporter?.close()
78
+ }
79
+
80
+ return { send, close }
81
+ }
@@ -0,0 +1,103 @@
1
+ import { constants, brotliCompress, gzip, deflate } from 'node:zlib'
2
+ import { promisify } from 'node:util'
3
+ import type { Middleware } from '../types.ts'
4
+
5
+ const brotliCompressAsync = promisify(brotliCompress)
6
+ const gzipAsync = promisify(gzip)
7
+ const deflateAsync = promisify(deflate)
8
+
9
+ /** Options for {@link compress}. */
10
+ export interface CompressOptions {
11
+ /** Compression level (1-9, default: 6). */
12
+ level?: number
13
+ /** Minimum response body size in bytes to compress (default: 1024). */
14
+ threshold?: number
15
+ }
16
+
17
+ /**
18
+ * Response compression middleware (brotli, gzip, deflate).
19
+ *
20
+ * Automatically selects the best encoding based on `Accept-Encoding` header.
21
+ * Skips compression for small responses, images, audio, video, and already-encoded responses.
22
+ *
23
+ * ```ts
24
+ * import { compress } from 'weifuwu'
25
+ * app.use(compress())
26
+ * ```
27
+ */
28
+ export function compress(options?: CompressOptions): Middleware {
29
+ const level = options?.level ?? 6
30
+ const threshold = options?.threshold ?? 1024
31
+
32
+ return async (req, ctx, next) => {
33
+ const accept = req.headers.get('accept-encoding') ?? ''
34
+
35
+ const encoding = accept.includes('br')
36
+ ? 'br'
37
+ : accept.includes('gzip')
38
+ ? 'gzip'
39
+ : accept.includes('deflate')
40
+ ? 'deflate'
41
+ : ''
42
+
43
+ if (!encoding) return next(req, ctx)
44
+
45
+ const res = await next(req, ctx)
46
+
47
+ if (
48
+ res.status === 304 ||
49
+ res.status === 204 ||
50
+ res.status === 206 ||
51
+ res.status < 200 ||
52
+ res.status >= 300
53
+ ) {
54
+ return res
55
+ }
56
+
57
+ if (res.headers.get('content-encoding')) return res
58
+
59
+ const ct = res.headers.get('content-type') ?? ''
60
+ if (
61
+ !ct ||
62
+ ct.startsWith('audio/') ||
63
+ ct.startsWith('video/') ||
64
+ ct.startsWith('image/') ||
65
+ ct === 'application/zip'
66
+ ) {
67
+ return res
68
+ }
69
+
70
+ if (!res.body) return res
71
+
72
+ const body = await res.bytes()
73
+ if (body.byteLength < threshold) return res
74
+
75
+ let compressed: Buffer
76
+ try {
77
+ if (encoding === 'br') {
78
+ compressed = await brotliCompressAsync(body, {
79
+ params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) },
80
+ })
81
+ } else if (encoding === 'gzip') {
82
+ compressed = await gzipAsync(body, { level: Math.min(level, 9) })
83
+ } else {
84
+ compressed = await deflateAsync(body, { level: Math.min(level, 9) })
85
+ }
86
+ } catch {
87
+ return res
88
+ }
89
+
90
+ const headers = new Headers(res.headers)
91
+ headers.set('Content-Encoding', encoding)
92
+ headers.set('Content-Length', String(compressed.byteLength))
93
+ headers.delete('Content-Range')
94
+ const existingVary = headers.get('Vary')
95
+ headers.set('Vary', existingVary ? `${existingVary}, Accept-Encoding` : 'Accept-Encoding')
96
+
97
+ return new Response(compressed as BodyInit, {
98
+ status: res.status,
99
+ statusText: res.statusText,
100
+ headers,
101
+ })
102
+ }
103
+ }