@a_jackie_z/fastify 1.0.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.
- package/README.md +492 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# @a_jackie_z/fastify
|
|
2
|
+
|
|
3
|
+
A collection of Fastify plugins and utilities for building robust web applications with built-in support for JWT authentication, rate limiting, Swagger documentation, and TypeBox validation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @a_jackie_z/fastify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Basic Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createFastify } from '@a_jackie_z/fastify'
|
|
17
|
+
import { Type } from '@sinclair/typebox'
|
|
18
|
+
|
|
19
|
+
const app = await createFastify()
|
|
20
|
+
|
|
21
|
+
app.route({
|
|
22
|
+
method: 'GET',
|
|
23
|
+
url: '/hello',
|
|
24
|
+
schema: {
|
|
25
|
+
response: {
|
|
26
|
+
200: Type.Object({
|
|
27
|
+
message: Type.String(),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
handler: async () => {
|
|
32
|
+
return { message: 'Hello World!' }
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await app.listen({ port: 3000 })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **JWT Authentication** - Built-in JWT support with global and route-level configuration
|
|
42
|
+
- **Rate Limiting** - Flexible rate limiting at global and route levels
|
|
43
|
+
- **Swagger Documentation** - Auto-generated API documentation with TypeBox schemas
|
|
44
|
+
- **TypeBox Integration** - Type-safe request/response validation
|
|
45
|
+
- **Logger Integration** - Uses `@a_jackie_z/logger` for consistent logging
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### 1. Complete Setup with All Features
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { createFastify } from '@a_jackie_z/fastify'
|
|
53
|
+
import { Type } from '@sinclair/typebox'
|
|
54
|
+
|
|
55
|
+
const app = await createFastify({
|
|
56
|
+
// Rate limiting
|
|
57
|
+
rateLimit: {
|
|
58
|
+
global: {
|
|
59
|
+
max: 100,
|
|
60
|
+
timeWindow: '1 minute',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
// JWT authentication
|
|
64
|
+
jwt: {
|
|
65
|
+
secret: 'your-secret-key',
|
|
66
|
+
sign: {
|
|
67
|
+
expiresIn: '1h',
|
|
68
|
+
},
|
|
69
|
+
global: true, // Require JWT for all routes by default
|
|
70
|
+
},
|
|
71
|
+
// Swagger documentation
|
|
72
|
+
swagger: {
|
|
73
|
+
title: 'My API',
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
description: 'API documentation',
|
|
76
|
+
routePrefix: '/docs',
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
await app.listen({ port: 3000 })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Authentication Flow
|
|
84
|
+
|
|
85
|
+
#### Login Route (Public)
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
app.route({
|
|
89
|
+
method: 'POST',
|
|
90
|
+
url: '/auth/login',
|
|
91
|
+
config: {
|
|
92
|
+
jwt: false, // Bypass JWT check for login
|
|
93
|
+
},
|
|
94
|
+
schema: {
|
|
95
|
+
body: Type.Object({
|
|
96
|
+
username: Type.String(),
|
|
97
|
+
password: Type.String(),
|
|
98
|
+
}),
|
|
99
|
+
response: {
|
|
100
|
+
200: Type.Object({
|
|
101
|
+
token: Type.String(),
|
|
102
|
+
}),
|
|
103
|
+
401: Type.Object({
|
|
104
|
+
error: Type.String(),
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
handler: async (request, reply) => {
|
|
109
|
+
const { username, password } = request.body
|
|
110
|
+
|
|
111
|
+
// Validate credentials
|
|
112
|
+
if (username === 'admin' && password === 'secret') {
|
|
113
|
+
const token = app.jwt.sign({
|
|
114
|
+
username,
|
|
115
|
+
role: 'admin',
|
|
116
|
+
permissions: ['read', 'write', 'delete'],
|
|
117
|
+
})
|
|
118
|
+
return { token }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
reply.status(401)
|
|
122
|
+
return { error: 'Invalid credentials' }
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Public Route
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
app.route({
|
|
131
|
+
method: 'GET',
|
|
132
|
+
url: '/public',
|
|
133
|
+
config: {
|
|
134
|
+
jwt: false, // No authentication required
|
|
135
|
+
},
|
|
136
|
+
schema: {
|
|
137
|
+
response: {
|
|
138
|
+
200: Type.Object({
|
|
139
|
+
message: Type.String(),
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
handler: async () => {
|
|
144
|
+
return { message: 'Public endpoint - no auth required' }
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Protected Route (Auto-Protected by Global JWT)
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
app.route({
|
|
153
|
+
method: 'GET',
|
|
154
|
+
url: '/protected',
|
|
155
|
+
// No config needed - global JWT setting applies
|
|
156
|
+
schema: {
|
|
157
|
+
response: {
|
|
158
|
+
200: Type.Object({
|
|
159
|
+
message: Type.String(),
|
|
160
|
+
user: Type.Unknown(),
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
handler: async (request) => {
|
|
165
|
+
return {
|
|
166
|
+
message: 'Protected data',
|
|
167
|
+
user: request.user, // JWT payload
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3. Role-Based Authorization
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
app.route({
|
|
177
|
+
method: 'GET',
|
|
178
|
+
url: '/admin',
|
|
179
|
+
config: {
|
|
180
|
+
jwt: {
|
|
181
|
+
authorize: async (_request, _reply, user) => {
|
|
182
|
+
// Check if user has admin role
|
|
183
|
+
return user?.role === 'admin'
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
schema: {
|
|
188
|
+
response: {
|
|
189
|
+
200: Type.Object({
|
|
190
|
+
message: Type.String(),
|
|
191
|
+
}),
|
|
192
|
+
403: Type.Object({
|
|
193
|
+
error: Type.String(),
|
|
194
|
+
message: Type.String(),
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
handler: async () => {
|
|
199
|
+
return { message: 'Admin-only content' }
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 4. Permission-Based Authorization
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
app.route({
|
|
208
|
+
method: 'DELETE',
|
|
209
|
+
url: '/operations/:id',
|
|
210
|
+
config: {
|
|
211
|
+
jwt: {
|
|
212
|
+
authorize: async (_request, _reply, user) => {
|
|
213
|
+
// Check if user has delete permission
|
|
214
|
+
return user?.permissions?.includes('delete') === true
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
schema: {
|
|
219
|
+
params: Type.Object({
|
|
220
|
+
id: Type.String(),
|
|
221
|
+
}),
|
|
222
|
+
response: {
|
|
223
|
+
200: Type.Object({
|
|
224
|
+
message: Type.String(),
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
handler: async (request) => {
|
|
229
|
+
const { id } = request.params
|
|
230
|
+
return { message: `Operation ${id} deleted` }
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 5. Custom Authorization Logic
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
app.route({
|
|
239
|
+
method: 'POST',
|
|
240
|
+
url: '/custom-auth',
|
|
241
|
+
config: {
|
|
242
|
+
jwt: {
|
|
243
|
+
authorize: async (request, _reply, user) => {
|
|
244
|
+
// Custom logic: only allow specific users
|
|
245
|
+
if (user?.username === 'special-user') {
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
// Check request headers
|
|
249
|
+
if (request.headers['x-custom-header'] === 'allowed') {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
handler: async () => {
|
|
257
|
+
return { message: 'Custom authorization passed' }
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 6. Rate Limiting
|
|
263
|
+
|
|
264
|
+
#### Route-Specific Rate Limit
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
app.route({
|
|
268
|
+
method: 'GET',
|
|
269
|
+
url: '/limited',
|
|
270
|
+
config: {
|
|
271
|
+
rateLimit: {
|
|
272
|
+
max: 5,
|
|
273
|
+
timeWindow: '1 minute',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
handler: async () => {
|
|
277
|
+
return { message: 'Strict rate limiting applied' }
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Disable Rate Limit for Specific Route
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
app.route({
|
|
286
|
+
method: 'GET',
|
|
287
|
+
url: '/unlimited',
|
|
288
|
+
config: {
|
|
289
|
+
rateLimit: false,
|
|
290
|
+
},
|
|
291
|
+
handler: async () => {
|
|
292
|
+
return { message: 'No rate limiting' }
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 7. TypeBox Schema Validation
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
app.route({
|
|
301
|
+
method: 'POST',
|
|
302
|
+
url: '/users',
|
|
303
|
+
schema: {
|
|
304
|
+
body: Type.Object({
|
|
305
|
+
name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
306
|
+
email: Type.String({ format: 'email' }),
|
|
307
|
+
age: Type.Optional(Type.Number({ minimum: 0, maximum: 150 })),
|
|
308
|
+
}),
|
|
309
|
+
response: {
|
|
310
|
+
201: Type.Object({
|
|
311
|
+
id: Type.String(),
|
|
312
|
+
name: Type.String(),
|
|
313
|
+
email: Type.String(),
|
|
314
|
+
}),
|
|
315
|
+
400: Type.Object({
|
|
316
|
+
error: Type.String(),
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
handler: async (request, reply) => {
|
|
321
|
+
const user = request.body
|
|
322
|
+
reply.status(201)
|
|
323
|
+
return {
|
|
324
|
+
id: 'generated-id',
|
|
325
|
+
...user,
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Configuration Options
|
|
332
|
+
|
|
333
|
+
### `CreateFastifyOptions`
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface CreateFastifyOptions {
|
|
337
|
+
rateLimit?: {
|
|
338
|
+
global?: RateLimitPluginOptions
|
|
339
|
+
}
|
|
340
|
+
jwt?: {
|
|
341
|
+
secret: string
|
|
342
|
+
sign?: FastifyJWTOptions['sign']
|
|
343
|
+
verify?: FastifyJWTOptions['verify']
|
|
344
|
+
global?: boolean // Enable JWT check globally for all routes
|
|
345
|
+
}
|
|
346
|
+
swagger?: {
|
|
347
|
+
title: string
|
|
348
|
+
version: string
|
|
349
|
+
description: string
|
|
350
|
+
routePrefix?: string // Default: '/documentation'
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Route JWT Configuration
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
interface JWTRouteConfig {
|
|
359
|
+
jwt?:
|
|
360
|
+
| boolean // true = require JWT, false = bypass JWT
|
|
361
|
+
| {
|
|
362
|
+
required?: boolean // Default: true if global enabled
|
|
363
|
+
authorize?: JWTAuthorizationHandler // Custom authorization logic
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
type JWTAuthorizationHandler = (
|
|
368
|
+
request: FastifyRequest,
|
|
369
|
+
reply: FastifyReply,
|
|
370
|
+
user: any
|
|
371
|
+
) => Promise<boolean> | boolean
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Usage Patterns
|
|
375
|
+
|
|
376
|
+
### Pattern 1: Global JWT with Selective Public Routes
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const app = await createFastify({
|
|
380
|
+
jwt: {
|
|
381
|
+
secret: 'your-secret',
|
|
382
|
+
global: true, // All routes require JWT by default
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Public routes explicitly bypass JWT
|
|
387
|
+
app.route({
|
|
388
|
+
method: 'POST',
|
|
389
|
+
url: '/auth/login',
|
|
390
|
+
config: { jwt: false },
|
|
391
|
+
handler: async () => { /* ... */ },
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
// Protected routes don't need config
|
|
395
|
+
app.route({
|
|
396
|
+
method: 'GET',
|
|
397
|
+
url: '/data',
|
|
398
|
+
handler: async () => { /* ... */ },
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Pattern 2: Opt-In JWT Protection
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
const app = await createFastify({
|
|
406
|
+
jwt: {
|
|
407
|
+
secret: 'your-secret',
|
|
408
|
+
global: false, // JWT not required by default
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
// Unprotected by default
|
|
413
|
+
app.route({
|
|
414
|
+
method: 'GET',
|
|
415
|
+
url: '/public',
|
|
416
|
+
handler: async () => { /* ... */ },
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Explicitly require JWT
|
|
420
|
+
app.route({
|
|
421
|
+
method: 'GET',
|
|
422
|
+
url: '/protected',
|
|
423
|
+
config: { jwt: true },
|
|
424
|
+
handler: async () => { /* ... */ },
|
|
425
|
+
})
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Pattern 3: Multi-Tier Authorization
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
const app = await createFastify({
|
|
432
|
+
jwt: {
|
|
433
|
+
secret: 'your-secret',
|
|
434
|
+
global: true,
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// Tier 1: Any authenticated user
|
|
439
|
+
app.route({
|
|
440
|
+
method: 'GET',
|
|
441
|
+
url: '/user/profile',
|
|
442
|
+
handler: async (request) => {
|
|
443
|
+
return request.user
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// Tier 2: Admin users only
|
|
448
|
+
app.route({
|
|
449
|
+
method: 'GET',
|
|
450
|
+
url: '/admin/users',
|
|
451
|
+
config: {
|
|
452
|
+
jwt: {
|
|
453
|
+
authorize: async (_req, _reply, user) => user?.role === 'admin',
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
handler: async () => { /* ... */ },
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// Tier 3: Super admin with specific permission
|
|
460
|
+
app.route({
|
|
461
|
+
method: 'DELETE',
|
|
462
|
+
url: '/admin/system',
|
|
463
|
+
config: {
|
|
464
|
+
jwt: {
|
|
465
|
+
authorize: async (_req, _reply, user) =>
|
|
466
|
+
user?.role === 'admin' && user?.permissions?.includes('system:delete'),
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
handler: async () => { /* ... */ },
|
|
470
|
+
})
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Testing Your API
|
|
474
|
+
|
|
475
|
+
1. Start your server
|
|
476
|
+
2. Access Swagger documentation at `http://localhost:3000/docs`
|
|
477
|
+
3. Test authentication:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Login
|
|
481
|
+
curl -X POST http://localhost:3000/auth/login \
|
|
482
|
+
-H "Content-Type: application/json" \
|
|
483
|
+
-d '{"username":"admin","password":"secret"}'
|
|
484
|
+
|
|
485
|
+
# Use token
|
|
486
|
+
curl http://localhost:3000/protected \
|
|
487
|
+
-H "Authorization: Bearer YOUR_TOKEN"
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## License
|
|
491
|
+
|
|
492
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as fastify from 'fastify';
|
|
2
|
+
import { FastifyRequest, FastifyReply, FastifyServerOptions, FastifyInstance, RawServerDefault, FastifyBaseLogger, FastifyContextConfig, FastifyPluginCallback } from 'fastify';
|
|
3
|
+
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
|
|
4
|
+
import { RateLimitPluginOptions } from '@fastify/rate-limit';
|
|
5
|
+
import { FastifyJWTOptions } from '@fastify/jwt';
|
|
6
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
|
+
|
|
8
|
+
type JWTAuthorizationHandler = <TUser = unknown>(request: FastifyRequest, reply: FastifyReply, user: TUser) => Promise<boolean> | boolean;
|
|
9
|
+
interface JWTRouteConfig {
|
|
10
|
+
jwt?: boolean | {
|
|
11
|
+
required?: boolean;
|
|
12
|
+
authorize?: JWTAuthorizationHandler;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
declare module 'fastify' {
|
|
16
|
+
interface FastifyContextConfig {
|
|
17
|
+
jwt?: boolean | {
|
|
18
|
+
required?: boolean;
|
|
19
|
+
authorize?: JWTAuthorizationHandler;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
interface CreateFastifyOptions {
|
|
24
|
+
logger?: FastifyServerOptions['loggerInstance'];
|
|
25
|
+
rateLimit?: {
|
|
26
|
+
global?: RateLimitPluginOptions;
|
|
27
|
+
};
|
|
28
|
+
jwt?: {
|
|
29
|
+
secret: string;
|
|
30
|
+
sign?: FastifyJWTOptions['sign'];
|
|
31
|
+
verify?: FastifyJWTOptions['verify'];
|
|
32
|
+
global?: boolean;
|
|
33
|
+
};
|
|
34
|
+
swagger?: {
|
|
35
|
+
title: string;
|
|
36
|
+
version: string;
|
|
37
|
+
description: string;
|
|
38
|
+
routePrefix?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
type FastifyServer = FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse, FastifyBaseLogger, TypeBoxTypeProvider> & FastifyContextConfig;
|
|
42
|
+
declare function createFastify(options?: CreateFastifyOptions): Promise<FastifyServer>;
|
|
43
|
+
declare function runFastify(fastify: FastifyServer, host: string, port: number): Promise<void>;
|
|
44
|
+
|
|
45
|
+
declare function createFastifyPlugin(cb: FastifyPluginCallback): FastifyPluginCallback;
|
|
46
|
+
|
|
47
|
+
declare const healthPlugin: fastify.FastifyPluginCallback;
|
|
48
|
+
|
|
49
|
+
export { type CreateFastifyOptions, type FastifyServer, type JWTAuthorizationHandler, type JWTRouteConfig, createFastify, createFastifyPlugin, healthPlugin, runFastify };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// lib/fastify.ts
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import fastifyRateLimit from "@fastify/rate-limit";
|
|
4
|
+
import fastifyJwt from "@fastify/jwt";
|
|
5
|
+
import fastifySwagger from "@fastify/swagger";
|
|
6
|
+
import fastifySwaggerUI from "@fastify/swagger-ui";
|
|
7
|
+
async function createFastify(options) {
|
|
8
|
+
const fastifyOptions = {};
|
|
9
|
+
if (options?.logger) {
|
|
10
|
+
fastifyOptions.loggerInstance = options.logger;
|
|
11
|
+
}
|
|
12
|
+
const fastify = Fastify(fastifyOptions).withTypeProvider();
|
|
13
|
+
if (options?.swagger) {
|
|
14
|
+
await fastify.register(fastifySwagger, {
|
|
15
|
+
openapi: {
|
|
16
|
+
info: {
|
|
17
|
+
title: options.swagger.title,
|
|
18
|
+
version: options.swagger.version,
|
|
19
|
+
description: options.swagger.description
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
let routePrefix = options.swagger.routePrefix || "/docs/";
|
|
24
|
+
if (!routePrefix.startsWith("/")) {
|
|
25
|
+
routePrefix = "/" + routePrefix;
|
|
26
|
+
}
|
|
27
|
+
if (!routePrefix.endsWith("/")) {
|
|
28
|
+
routePrefix = routePrefix + "/";
|
|
29
|
+
}
|
|
30
|
+
await fastify.register(fastifySwaggerUI, { routePrefix });
|
|
31
|
+
}
|
|
32
|
+
if (options?.jwt) {
|
|
33
|
+
const jwtOptions = {
|
|
34
|
+
secret: options.jwt.secret
|
|
35
|
+
};
|
|
36
|
+
if (options.jwt.sign !== void 0) {
|
|
37
|
+
jwtOptions.sign = options.jwt.sign;
|
|
38
|
+
}
|
|
39
|
+
if (options.jwt.verify !== void 0) {
|
|
40
|
+
jwtOptions.verify = options.jwt.verify;
|
|
41
|
+
}
|
|
42
|
+
await fastify.register(fastifyJwt, jwtOptions);
|
|
43
|
+
if (options.jwt.global) {
|
|
44
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
45
|
+
const routeConfig = request.routeOptions.config || {};
|
|
46
|
+
const jwtConfig = routeConfig.jwt;
|
|
47
|
+
if (jwtConfig === false) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const shouldVerify = jwtConfig === true || typeof jwtConfig === "object" && jwtConfig.required !== false || jwtConfig === void 0 && options?.jwt?.global === true;
|
|
51
|
+
if (shouldVerify) {
|
|
52
|
+
try {
|
|
53
|
+
await request.jwtVerify();
|
|
54
|
+
if (typeof jwtConfig === "object" && jwtConfig.authorize) {
|
|
55
|
+
const authorized = await jwtConfig.authorize(request, reply, request.user);
|
|
56
|
+
if (!authorized) {
|
|
57
|
+
reply.status(403).send({
|
|
58
|
+
error: "Forbidden",
|
|
59
|
+
message: "Authorization failed"
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
reply.status(401).send({
|
|
66
|
+
error: "Unauthorized",
|
|
67
|
+
message: "Invalid or missing JWT token"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (options?.rateLimit?.global) {
|
|
75
|
+
await fastify.register(fastifyRateLimit, {
|
|
76
|
+
global: true,
|
|
77
|
+
...options.rateLimit.global
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return fastify;
|
|
81
|
+
}
|
|
82
|
+
async function runFastify(fastify, host, port) {
|
|
83
|
+
try {
|
|
84
|
+
await fastify.listen({ host, port });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
fastify.log.error(err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// lib/plugin.ts
|
|
92
|
+
function createFastifyPlugin(cb) {
|
|
93
|
+
return cb;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// lib/plugins/healthPlugin.ts
|
|
97
|
+
var healthPlugin = createFastifyPlugin((app) => {
|
|
98
|
+
app.get("/v1/health", {
|
|
99
|
+
schema: {
|
|
100
|
+
tags: ["health"]
|
|
101
|
+
},
|
|
102
|
+
handler: async () => {
|
|
103
|
+
return {
|
|
104
|
+
status: 200,
|
|
105
|
+
message: "ok"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
export {
|
|
111
|
+
createFastify,
|
|
112
|
+
createFastifyPlugin,
|
|
113
|
+
healthPlugin,
|
|
114
|
+
runFastify
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../lib/fastify.ts","../lib/plugin.ts","../lib/plugins/healthPlugin.ts"],"sourcesContent":["import Fastify, {\n FastifyBaseLogger,\n FastifyContextConfig,\n FastifyInstance,\n FastifyReply,\n FastifyServerOptions,\n RawServerDefault,\n} from 'fastify'\nimport type { FastifyRequest } from 'fastify'\nimport { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'\nimport fastifyRateLimit, { RateLimitPluginOptions } from '@fastify/rate-limit'\nimport fastifyJwt, { FastifyJWTOptions } from '@fastify/jwt'\nimport fastifySwagger, { SwaggerOptions } from '@fastify/swagger'\nimport fastifySwaggerUI, { FastifySwaggerUiOptions } from '@fastify/swagger-ui'\nimport { IncomingMessage, ServerResponse } from 'node:http'\n\nexport type JWTAuthorizationHandler = <TUser = unknown>(\n request: FastifyRequest,\n reply: FastifyReply,\n user: TUser\n) => Promise<boolean> | boolean\n\nexport interface JWTRouteConfig {\n jwt?:\n | boolean // true = require JWT, false = bypass JWT\n | {\n required?: boolean // Default: true if global enabled\n authorize?: JWTAuthorizationHandler // Check JWT info before route processing\n }\n}\n\n// Extend Fastify types to include custom config\ndeclare module 'fastify' {\n interface FastifyContextConfig {\n jwt?:\n | boolean\n | {\n required?: boolean\n authorize?: JWTAuthorizationHandler\n }\n }\n}\n\nexport interface CreateFastifyOptions {\n logger?: FastifyServerOptions['loggerInstance'],\n rateLimit?: {\n global?: RateLimitPluginOptions\n }\n jwt?: {\n secret: string\n sign?: FastifyJWTOptions['sign']\n verify?: FastifyJWTOptions['verify']\n global?: boolean // Enable JWT check globally for all routes\n }\n swagger?: {\n title: string\n version: string\n description: string\n routePrefix?: string\n }\n}\n\nexport type FastifyServer = FastifyInstance<\n RawServerDefault,\n IncomingMessage,\n ServerResponse,\n FastifyBaseLogger,\n TypeBoxTypeProvider\n> & FastifyContextConfig\n\nexport async function createFastify(options?: CreateFastifyOptions): Promise<FastifyServer> {\n const fastifyOptions: FastifyServerOptions = {}\n\n if (options?.logger) {\n fastifyOptions.loggerInstance = options.logger\n }\n\n const fastify = Fastify(fastifyOptions).withTypeProvider<TypeBoxTypeProvider>()\n\n // Register Swagger first to capture all routes with TypeBox schemas\n if (options?.swagger) {\n await fastify.register(fastifySwagger, {\n openapi: {\n info: {\n title: options.swagger.title,\n version: options.swagger.version,\n description: options.swagger.description,\n },\n },\n } as SwaggerOptions)\n\n let routePrefix = options.swagger.routePrefix || '/docs/'\n\n if (!routePrefix.startsWith('/')) {\n routePrefix = '/' + routePrefix\n }\n\n if (!routePrefix.endsWith('/')) {\n routePrefix = routePrefix + '/'\n }\n\n await fastify.register(fastifySwaggerUI, { routePrefix } as FastifySwaggerUiOptions)\n }\n\n // Register JWT authentication\n if (options?.jwt) {\n const jwtOptions: FastifyJWTOptions = {\n secret: options.jwt.secret,\n }\n if (options.jwt.sign !== undefined) {\n jwtOptions.sign = options.jwt.sign\n }\n if (options.jwt.verify !== undefined) {\n jwtOptions.verify = options.jwt.verify\n }\n await fastify.register(fastifyJwt, jwtOptions)\n\n // Global JWT checking hook\n if (options.jwt.global) {\n fastify.addHook('onRequest', async (request, reply) => {\n const routeConfig = (request.routeOptions.config as JWTRouteConfig) || {}\n const jwtConfig = routeConfig.jwt\n\n // Check if route explicitly bypasses JWT\n if (jwtConfig === false) {\n return\n }\n\n // Check if route explicitly requires JWT or uses global setting\n const shouldVerify =\n jwtConfig === true ||\n (typeof jwtConfig === 'object' && jwtConfig.required !== false) ||\n (jwtConfig === undefined && options?.jwt?.global === true)\n\n if (shouldVerify) {\n try {\n await request.jwtVerify()\n\n // Custom authorization - check JWT info before route processing\n if (typeof jwtConfig === 'object' && jwtConfig.authorize) {\n const authorized = await jwtConfig.authorize(request, reply, request.user)\n if (!authorized) {\n reply.status(403).send({\n error: 'Forbidden',\n message: 'Authorization failed',\n })\n return\n }\n }\n } catch (err) {\n reply.status(401).send({\n error: 'Unauthorized',\n message: 'Invalid or missing JWT token',\n })\n }\n }\n })\n }\n }\n\n // Register Rate Limiting\n if (options?.rateLimit?.global) {\n await fastify.register(fastifyRateLimit, {\n global: true,\n ...options.rateLimit.global,\n })\n }\n\n return fastify\n}\n\nexport async function runFastify(fastify: FastifyServer, host: string, port: number) {\n try {\n await fastify.listen({ host, port })\n } catch (err) {\n fastify.log.error(err)\n process.exit(1)\n }\n}\n","import { FastifyPluginCallback } from 'fastify'\n\nexport function createFastifyPlugin(cb: FastifyPluginCallback) {\n return cb;\n}\n","import { createFastifyPlugin } from '../plugin.ts'\n\nexport const healthPlugin = createFastifyPlugin((app) => {\n app.get('/v1/health', {\n schema: {\n tags: ['health'],\n },\n handler: async () => {\n return {\n status: 200,\n message: 'ok',\n }\n },\n })\n})\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a_jackie_z/fastify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A collection of Fastify plugins and utilities for building robust web applications.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sang Lu <connect.with.sang@gmail.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@fastify/jwt": "^10.0.0",
|
|
24
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
25
|
+
"@fastify/swagger": "^9.6.1",
|
|
26
|
+
"@fastify/swagger-ui": "^5.2.4",
|
|
27
|
+
"@fastify/type-provider-typebox": "^6.1.0",
|
|
28
|
+
"@sinclair/typebox": "^0.34.47",
|
|
29
|
+
"fastify": "^5.7.1",
|
|
30
|
+
"typebox": "^1.0.79"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.0.9",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"tsx": "^4.7.0",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|