weifuwu 0.22.1 → 0.22.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/README.md CHANGED
@@ -60,10 +60,20 @@ await server.ready
60
60
  | `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
61
61
 
62
62
  ```ts
63
- interface Server { stop: () => Promise<void>; readonly port: number; readonly hostname: string; ready: Promise<void> }
63
+ interface Server {
64
+ stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
65
+ readonly port: number
66
+ readonly hostname: string
67
+ ready: Promise<void>
68
+ }
64
69
  const { server, url } = await createTestServer(handler)
65
70
  ```
66
71
 
72
+ `server.stop()` performs a graceful shutdown: stops accepting new connections,
73
+ closes idle keep-alive sockets, then waits for in-flight requests to complete.
74
+ If they don't finish within `timeoutMs` (default 10 seconds), remaining connections
75
+ are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
76
+
67
77
  ### Router
68
78
 
69
79
  ```ts
@@ -404,6 +414,16 @@ app.use(auth({ token: 'sk-123' })) // static token
404
414
  app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
405
415
  app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
406
416
  app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
417
+
418
+ // Session-based auth (must be placed after session() middleware)
419
+ app.use(session())
420
+ app.use(auth({
421
+ session: true,
422
+ resolveUser: async (userId) => { // load user from DB
423
+ const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
424
+ return user ?? null // null → destroy stale session
425
+ },
426
+ }))
407
427
  ```
408
428
 
409
429
  | Option | Type | Default | Description |
@@ -412,6 +432,13 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
412
432
  | `header` | `string` | `'Authorization'` | Header name |
413
433
  | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
414
434
  | `proxy` | `string` | — | Auth service URL to proxy requests to |
435
+ | `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
436
+ | `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
437
+
438
+ When `session: true`, auth checks `ctx.session.userId` before the
439
+ Authorization header. This lets logged-in users authenticate via their
440
+ session cookie without sending a token. Falls back to header/token auth
441
+ if no session userId is present.
415
442
 
416
443
  ### compress [α]
417
444
 
@@ -699,6 +726,64 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
699
726
 
700
727
 
701
728
 
729
+ ### knowledgeBase [β] — RAG with pgvector
730
+
731
+ ```ts
732
+ import { knowledgeBase } from 'weifuwu'
733
+ import { embed } from 'ai'
734
+
735
+ const kb = knowledgeBase({
736
+ sql: ctx.sql,
737
+ embedding: (text) =>
738
+ embed({ model: openai.embedding('text-embedding-3-small'), value: text })
739
+ .then(r => r.embedding),
740
+ dimensions: 1536,
741
+ })
742
+
743
+ // Create table + HNSW index (safe to call multiple times)
744
+ await kb.migrate()
745
+
746
+ // Ingest a document (auto chunk → embed → store)
747
+ await kb.ingest('docs/intro.md', `# Welcome\n\nThis is the introduction...`, {
748
+ title: 'Introduction',
749
+ metadata: { source: 'docs', author: 'alice' },
750
+ })
751
+
752
+ // Semantic search
753
+ const results = await kb.search('how to get started?', { limit: 5 })
754
+ // → [{ key, title, content, score: 0.92, metadata }, ...]
755
+
756
+ // Delete
757
+ await kb.delete('docs/outdated.md')
758
+
759
+ // List all documents
760
+ const entries = await kb.list()
761
+ // → [{ key, title, chunks: 3 }, ...]
762
+
763
+ // Use as middleware (injects ctx.kb.search)
764
+ app.use(kb.middleware())
765
+ app.get('/search', async (req, ctx) => {
766
+ const results = await ctx.kb.search(ctx.query.q)
767
+ return Response.json(results)
768
+ })
769
+ ```
770
+
771
+ | Option | Type | Default | Description |
772
+ |--------|------|---------|-------------|
773
+ | `sql` | `Sql<{}>` | — | **Required.** Postgres client with pgvector |
774
+ | `embedding` | `(text) => Promise<number[]>` | — | **Required.** Embedding function |
775
+ | `dimensions` | `number` | `1536` | Vector dimensions |
776
+ | `table` | `string` | `'_kb_docs'` | Database table name |
777
+ | `chunkSize` | `number` | `512` | Max characters per chunk |
778
+ | `chunkOverlap` | `number` | `64` | Overlap between chunks |
779
+ | `searchLimit` | `number` | `5` | Default search result count |
780
+ | `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
781
+
782
+ Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
783
+ replaces old chunks. The HNSW index enables fast approximate nearest-neighbor
784
+ search (cosine distance).
785
+
786
+
702
787
  ### logdb [β]
703
788
 
704
789
  PostgreSQL structured event logging with monthly partitioning.
@@ -746,6 +831,72 @@ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p
746
831
  | `from` | `string` | — | Default sender address |
747
832
  | `send` | `function` | — | Custom send function (alternative to transport) |
748
833
 
834
+
835
+
836
+ ### oauthClient [β] — Social login (OAuth 2.0 client)
837
+
838
+ ```ts
839
+ import { oauthClient } from 'weifuwu'
840
+
841
+ app.use(session()) // required — stores OAuth state
842
+ app.use(user({ pg, jwtSecret })) // required — user management
843
+ app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/callback
844
+ pg,
845
+ jwtSecret,
846
+ redirectUrl: '/dashboard',
847
+ providers: {
848
+ google: {
849
+ clientId: process.env.GOOGLE_CLIENT_ID,
850
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
851
+ },
852
+ github: {
853
+ clientId: process.env.GITHUB_CLIENT_ID,
854
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
855
+ },
856
+ },
857
+ }))
858
+ ```
859
+
860
+ **Flow:** User clicks "Login with Google" → redirected to Google → back to app → user created/linked in database → JWT signed → session created → redirected to `redirectUrl` with `?token=` (or JSON response for API clients).
861
+
862
+ Supports custom providers via `authUrl`, `tokenUrl`, `userUrl`, and `parseUser`:
863
+
864
+ ```ts
865
+ app.use('/auth', oauthClient({
866
+ pg,
867
+ jwtSecret,
868
+ providers: {
869
+ discord: {
870
+ clientId: process.env.DISCORD_CLIENT_ID,
871
+ clientSecret: process.env.DISCORD_CLIENT_SECRET,
872
+ authUrl: 'https://discord.com/api/oauth2/authorize',
873
+ tokenUrl: 'https://discord.com/api/oauth2/token',
874
+ userUrl: 'https://discord.com/api/users/@me',
875
+ parseUser: (data) => ({
876
+ id: data.id,
877
+ email: data.email ?? '',
878
+ name: data.global_name ?? data.username,
879
+ avatarUrl: data.avatar
880
+ ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
881
+ : '',
882
+ }),
883
+ },
884
+ },
885
+ }))
886
+ ```
887
+
888
+ | Option | Type | Default | Description |
889
+ |--------|------|---------|-------------|
890
+ | `pg` | `PostgresClient` | — | **Required.** Database connection |
891
+ | `jwtSecret` | `string` | — | **Required.** Must match `user()` module's secret |
892
+ | `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
893
+ | `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
894
+ | `expiresIn` | `string \| number` | `'24h'` | JWT expiry |
895
+ | `table` | `string` | `'_auth_providers'` | Provider-user link table name |
896
+
897
+ The module auto-creates a `_auth_providers` table (`user_id`, `provider`, `provider_id`, `email`, `name`, `avatar_url`) on first request. Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`.
898
+
899
+
749
900
  ### messager [β]
750
901
 
751
902
  Real-time chat with channels, WebSocket, agent routing.
@@ -1099,6 +1250,85 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
1099
1250
 
1100
1251
 
1101
1252
 
1253
+ ### s3 [α] — S3-compatible object storage
1254
+
1255
+ ```ts
1256
+ import { s3 } from 'weifuwu'
1257
+
1258
+ app.use(s3({
1259
+ bucket: 'my-app',
1260
+ region: 'us-east-1',
1261
+ endpoint: process.env.S3_URL, // MinIO / R2 / AWS
1262
+ forcePathStyle: true, // required for MinIO
1263
+ credentials: {
1264
+ accessKeyId: process.env.S3_ACCESS_KEY,
1265
+ secretAccessKey: process.env.S3_SECRET_KEY,
1266
+ },
1267
+ publicUrl: 'https://cdn.example.com', // for unsigned public URLs
1268
+ }))
1269
+ ```
1270
+
1271
+ Injects `ctx.s3` with methods for S3-compatible object storage.
1272
+
1273
+ ```ts
1274
+ // Upload
1275
+ await ctx.s3.put('images/logo.png', buffer, { contentType: 'image/png' })
1276
+
1277
+ // Download
1278
+ const buf = await ctx.s3.get('images/logo.png') // Buffer | null
1279
+
1280
+ // Delete
1281
+ await ctx.s3.delete('images/logo.png')
1282
+
1283
+ // Check existence
1284
+ if (await ctx.s3.exists('images/logo.png')) { ... }
1285
+
1286
+ // Signed URL (expires in 1 hour by default)
1287
+ const url = await ctx.s3.url('images/logo.png')
1288
+ const shortUrl = await ctx.s3.url('images/logo.png', { expiresIn: 60 })
1289
+
1290
+ // Public URL (requires publicUrl in options)
1291
+ const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
1292
+
1293
+ // List objects under a prefix
1294
+ const keys = await ctx.s3.list('images/')
1295
+ ```
1296
+
1297
+ | Option | Type | Default | Description |
1298
+ |--------|------|---------|-------------|
1299
+ | `bucket` | `string` | — | **Required.** S3 bucket name |
1300
+ | `region` | `string` | `'us-east-1'` | AWS region |
1301
+ | `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
1302
+ | `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
1303
+ | `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
1304
+ | `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
1305
+
1306
+ Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
1307
+ `AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
1308
+
1309
+ The module can also be used standalone without the middleware:
1310
+
1311
+ ```ts
1312
+ const storage = s3({ bucket: 'my-app', endpoint: '...' })
1313
+ await storage.put('key', body)
1314
+ const data = await storage.get('key')
1315
+ ```
1316
+
1317
+ Requires MinIO or another S3-compatible service for local development.
1318
+ Add to `docker-compose.yml`:
1319
+
1320
+ ```yml
1321
+ minio:
1322
+ image: minio/minio
1323
+ ports:
1324
+ - '9000:9000'
1325
+ environment:
1326
+ MINIO_ROOT_USER: minioadmin
1327
+ MINIO_ROOT_PASSWORD: minioadmin
1328
+ command: server /data
1329
+ ```
1330
+
1331
+
1102
1332
  ### seo [β] + seoMiddleware [α]
1103
1333
 
1104
1334
  ```ts
@@ -1155,6 +1385,16 @@ app.get('/logout', async (req, ctx) => {
1155
1385
  | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
1156
1386
  | `cookie.path` | `string` | `'/'` | Cookie path |
1157
1387
  | `cookie.domain` | `string` | — | Cookie domain |
1388
+ | `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
1389
+ | `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
1390
+
1391
+ When `secret` is set, the cookie value is signed with HMAC-SHA256:
1392
+ `uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
1393
+ sessions (no error message, no data leak).
1394
+
1395
+ Session ID auto-rotation copies data to a new ID and deletes the old one
1396
+ from the store. Rotation happens transparently on the next request after
1397
+ `rotateInterval` has elapsed.
1158
1398
 
1159
1399
  **Stores** are also exported for standalone use:
1160
1400
 
package/dist/auth.d.ts CHANGED
@@ -4,5 +4,19 @@ export interface AuthOptions {
4
4
  verify?: (token: string, req: Request) => unknown | Promise<unknown>;
5
5
  proxy?: string | URL;
6
6
  header?: string;
7
+ /**
8
+ * Enable session-based authentication.
9
+ * When true, auth() checks `ctx.session.userId` first.
10
+ * If found, the user is authenticated via session (no token required).
11
+ * Falls back to header-based auth if no session userId is present.
12
+ */
13
+ session?: boolean;
14
+ /**
15
+ * Function to load user data from a user ID stored in the session.
16
+ * Called when `session: true` and `ctx.session.userId` is present.
17
+ * Return a falsy value to reject (e.g. if the user was deleted).
18
+ * If not provided, `ctx.user` is set to `{ id: ctx.session.userId }`.
19
+ */
20
+ resolveUser?: (userId: unknown) => unknown | Promise<unknown>;
7
21
  }
8
22
  export declare function auth(options: AuthOptions): Middleware;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { cors } from './cors.ts';
13
13
  export type { CORSOptions } from './cors.ts';
14
14
  export { auth } from './auth.ts';
15
15
  export type { AuthOptions } from './auth.ts';
16
+ export { oauthClient } from './oauth-client.ts';
17
+ export type { OAuthClientOptions, OAuthProviderConfig } from './oauth-client.ts';
16
18
  export { serveStatic } from './static.ts';
17
19
  export type { ServeStaticOptions } from './static.ts';
18
20
  export { validate } from './validate.ts';
@@ -84,3 +86,7 @@ export type { CacheOptions, CacheStore, CacheMiddleware, CachedResponse } from '
84
86
  export { webhook } from './webhook.ts';
85
87
  export type { WebhookOptions, WebhookModule, WebhookEvent, WebhookHandler, PlatformConfig, CustomVerifierConfig } from './webhook.ts';
86
88
  export * as fts from './fts.ts';
89
+ export { s3 } from './s3.ts';
90
+ export type { S3Options, S3PutOptions, S3UrlOptions, S3Module, S3Body } from './s3.ts';
91
+ export { knowledgeBase } from './kb.ts';
92
+ export type { KBOptions, KBIngestOptions, KBSearchResult, KBSearchOptions, KBListEntry, KBModule } from './kb.ts';