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 +241 -1
- package/dist/auth.d.ts +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +662 -69
- package/dist/kb.d.ts +70 -0
- package/dist/oauth-client.d.ts +41 -0
- package/dist/s3.d.ts +68 -0
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +12 -0
- package/dist/test-utils.d.ts +10 -10
- package/package.json +3 -1
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 {
|
|
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';
|