exisjs 0.5.9 → 0.5.11
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/auth/index.d.ts +3 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth/core.d.ts +96 -0
- package/dist/auth/oauth/core.d.ts.map +1 -0
- package/dist/auth/oauth/core.js +203 -0
- package/dist/auth/oauth/core.js.map +1 -0
- package/dist/auth/oauth/index.d.ts +45 -0
- package/dist/auth/oauth/index.d.ts.map +1 -0
- package/dist/auth/oauth/index.js +64 -0
- package/dist/auth/oauth/index.js.map +1 -0
- package/dist/auth/oauth/middleware.d.ts +63 -0
- package/dist/auth/oauth/middleware.d.ts.map +1 -0
- package/dist/auth/oauth/middleware.js +164 -0
- package/dist/auth/oauth/middleware.js.map +1 -0
- package/dist/auth/oauth/providers/custom.d.ts +41 -0
- package/dist/auth/oauth/providers/custom.d.ts.map +1 -0
- package/dist/auth/oauth/providers/custom.js +204 -0
- package/dist/auth/oauth/providers/custom.js.map +1 -0
- package/dist/auth/oauth/providers/discord.d.ts +23 -0
- package/dist/auth/oauth/providers/discord.d.ts.map +1 -0
- package/dist/auth/oauth/providers/discord.js +130 -0
- package/dist/auth/oauth/providers/discord.js.map +1 -0
- package/dist/auth/oauth/providers/facebook.d.ts +28 -0
- package/dist/auth/oauth/providers/facebook.d.ts.map +1 -0
- package/dist/auth/oauth/providers/facebook.js +124 -0
- package/dist/auth/oauth/providers/facebook.js.map +1 -0
- package/dist/auth/oauth/providers/github.d.ts +25 -0
- package/dist/auth/oauth/providers/github.d.ts.map +1 -0
- package/dist/auth/oauth/providers/github.js +132 -0
- package/dist/auth/oauth/providers/github.js.map +1 -0
- package/dist/auth/oauth/providers/google.d.ts +23 -0
- package/dist/auth/oauth/providers/google.d.ts.map +1 -0
- package/dist/auth/oauth/providers/google.js +134 -0
- package/dist/auth/oauth/providers/google.js.map +1 -0
- package/dist/auth/oauth/providers/microsoft.d.ts +28 -0
- package/dist/auth/oauth/providers/microsoft.d.ts.map +1 -0
- package/dist/auth/oauth/providers/microsoft.js +136 -0
- package/dist/auth/oauth/providers/microsoft.js.map +1 -0
- package/dist/auth/oauth/types.d.ts +158 -0
- package/dist/auth/oauth/types.d.ts.map +1 -0
- package/dist/auth/oauth/types.js +4 -0
- package/dist/auth/oauth/types.js.map +1 -0
- package/dist/auth/session.d.ts +4 -1
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +33 -5
- package/dist/auth/session.js.map +1 -1
- package/dist/cache/store.d.ts +11 -3
- package/dist/cache/store.d.ts.map +1 -1
- package/dist/cache/store.js +87 -19
- package/dist/cache/store.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +4 -6
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/database/adapters/mongodb.d.ts +58 -0
- package/dist/database/adapters/mongodb.d.ts.map +1 -0
- package/dist/database/adapters/mongodb.js +264 -0
- package/dist/database/adapters/mongodb.js.map +1 -0
- package/dist/database/adapters/mysql.d.ts +27 -0
- package/dist/database/adapters/mysql.d.ts.map +1 -0
- package/dist/database/adapters/mysql.js +188 -0
- package/dist/database/adapters/mysql.js.map +1 -0
- package/dist/database/adapters/postgres.d.ts +28 -0
- package/dist/database/adapters/postgres.d.ts.map +1 -0
- package/dist/database/adapters/postgres.js +174 -0
- package/dist/database/adapters/postgres.js.map +1 -0
- package/dist/database/adapters/sqlite.d.ts +27 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +149 -0
- package/dist/database/adapters/sqlite.js.map +1 -0
- package/dist/database/index.d.ts +42 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +58 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/manager.d.ts +114 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +269 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/migration.d.ts +57 -0
- package/dist/database/migration.d.ts.map +1 -0
- package/dist/database/migration.js +329 -0
- package/dist/database/migration.js.map +1 -0
- package/dist/database/query-builder.d.ts +89 -0
- package/dist/database/query-builder.d.ts.map +1 -0
- package/dist/database/query-builder.js +353 -0
- package/dist/database/query-builder.js.map +1 -0
- package/dist/database/types.d.ts +184 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +4 -0
- package/dist/database/types.js.map +1 -0
- package/dist/docs/ARCHITECTURE.md +349 -0
- package/dist/docs/BENCHMARKS.md +36 -0
- package/dist/docs/README.md +34 -0
- package/dist/docs/content/README.md +44 -0
- package/dist/docs/content/adapters.mdx +199 -0
- package/dist/docs/content/cache.mdx +239 -0
- package/dist/docs/content/circuit-breaker.mdx +130 -0
- package/dist/docs/content/cli/commands.mdx +214 -0
- package/dist/docs/content/cli/overview.mdx +92 -0
- package/dist/docs/content/cli/usage.mdx +61 -0
- package/dist/docs/content/config.mdx +214 -0
- package/dist/docs/content/controllers.mdx +241 -0
- package/dist/docs/content/cron.mdx +109 -0
- package/dist/docs/content/database.mdx +209 -0
- package/dist/docs/content/dataloader.mdx +170 -0
- package/dist/docs/content/decorators.mdx +204 -0
- package/dist/docs/content/errors.mdx +344 -0
- package/dist/docs/content/gateways.mdx +376 -0
- package/dist/docs/content/installation.mdx +210 -0
- package/dist/docs/content/introduction.mdx +96 -0
- package/dist/docs/content/middleware.mdx +441 -0
- package/dist/docs/content/modules.mdx +144 -0
- package/dist/docs/content/oauth.mdx +179 -0
- package/dist/docs/content/providers.mdx +147 -0
- package/dist/docs/content/queues.mdx +177 -0
- package/dist/docs/content/requests.mdx +293 -0
- package/dist/docs/content/responses.mdx +271 -0
- package/dist/docs/content/routing.mdx +541 -0
- package/dist/docs/content/rpc.mdx +146 -0
- package/dist/docs/content/security.mdx +294 -0
- package/dist/docs/content/sse.mdx +159 -0
- package/dist/docs/content/structure.mdx +116 -0
- package/dist/docs/content/testing.mdx +139 -0
- package/dist/docs/content/validation.mdx +249 -0
- package/dist/docs/content/websockets.mdx +289 -0
- package/dist/docs/integrations/README.md +23 -0
- package/dist/docs/integrations/jwt.mdx +151 -0
- package/dist/docs/integrations/mongo.mdx +183 -0
- package/dist/docs/integrations/observability.mdx +125 -0
- package/dist/docs/integrations/openai.mdx +108 -0
- package/dist/docs/integrations/postgres.mdx +190 -0
- package/dist/docs/integrations/posthog.mdx +122 -0
- package/dist/docs/integrations/redis.mdx +141 -0
- package/dist/docs/integrations/resend.mdx +117 -0
- package/dist/docs/integrations/s3.mdx +122 -0
- package/dist/docs/integrations/swagger.mdx +113 -0
- package/dist/docs/reference/README.md +3 -0
- package/dist/docs/reference/adapters.mdx +130 -0
- package/dist/docs/reference/auth.mdx +183 -0
- package/dist/docs/reference/cache.mdx +132 -0
- package/dist/docs/reference/circuit-breaker.mdx +71 -0
- package/dist/docs/reference/config.mdx +118 -0
- package/dist/docs/reference/context.mdx +168 -0
- package/dist/docs/reference/dataloader.mdx +91 -0
- package/dist/docs/reference/decorators.mdx +293 -0
- package/dist/docs/reference/di.mdx +123 -0
- package/dist/docs/reference/error.mdx +170 -0
- package/dist/docs/reference/exis.mdx +124 -0
- package/dist/docs/reference/middleware.mdx +304 -0
- package/dist/docs/reference/plugin.mdx +113 -0
- package/dist/docs/reference/queue.mdx +155 -0
- package/dist/docs/reference/router.mdx +248 -0
- package/dist/docs/reference/sanitize.mdx +130 -0
- package/dist/docs/reference/storage.mdx +116 -0
- package/dist/docs/reference/swagger.mdx +132 -0
- package/dist/docs/reference/testing.mdx +145 -0
- package/dist/docs/reference/validator.mdx +186 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/backpressure.d.ts.map +1 -1
- package/dist/middleware/backpressure.js +21 -12
- package/dist/middleware/backpressure.js.map +1 -1
- package/dist/middleware/cache.d.ts +6 -0
- package/dist/middleware/cache.d.ts.map +1 -1
- package/dist/middleware/cache.js +84 -28
- package/dist/middleware/cache.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +4 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +29 -4
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/ip-filter.d.ts.map +1 -1
- package/dist/middleware/ip-filter.js +16 -0
- package/dist/middleware/ip-filter.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +29 -12
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/queue/drivers/MemoryDriver.d.ts +6 -3
- package/dist/queue/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/queue/drivers/MemoryDriver.js +76 -23
- package/dist/queue/drivers/MemoryDriver.js.map +1 -1
- package/dist/server/bootstrapper.d.ts +3 -0
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +56 -8
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/bun-adapter.d.ts +2 -0
- package/dist/server/bun-adapter.d.ts.map +1 -1
- package/dist/server/bun-adapter.js +23 -12
- package/dist/server/bun-adapter.js.map +1 -1
- package/dist/server/request-handler.d.ts +0 -2
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +2 -24
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +24 -11
- package/dist/server/request.js.map +1 -1
- package/dist/server/uws-adapter.d.ts +80 -0
- package/dist/server/uws-adapter.d.ts.map +1 -0
- package/dist/server/uws-adapter.js +357 -0
- package/dist/server/uws-adapter.js.map +1 -0
- package/dist/server/ws-orchestrator.d.ts +1 -0
- package/dist/server/ws-orchestrator.d.ts.map +1 -1
- package/dist/server/ws-orchestrator.js +83 -0
- package/dist/server/ws-orchestrator.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/validator/tex.d.ts +21 -16
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js.map +1 -1
- package/package.json +11 -3
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/circuit-breaker"
|
|
3
|
+
nextTitle: "Circuit Breaker"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security & Authentication
|
|
7
|
+
|
|
8
|
+
ExisJS includes native implementations for Cryptography, JWT generation, Role-Based Access Control (RBAC), and Edge Security.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Password Hashing (Scrypt)
|
|
13
|
+
|
|
14
|
+
ExisJS includes a password hashing engine powered by Node.js's built-in `crypto.scrypt`.
|
|
15
|
+
|
|
16
|
+
It automatically generates a secure salt, derives the key, and formats the output into a single safely-storable string (`scrypt:cost:blockSize:parallelization:keyLength:salt:hash`).
|
|
17
|
+
|
|
18
|
+
#### Functional Paradigm
|
|
19
|
+
|
|
20
|
+
```typescript filename="src/http/auth/route.ts"
|
|
21
|
+
import { controller, route } from 'exisjs/router'
|
|
22
|
+
import { Password } from 'exisjs/auth'
|
|
23
|
+
|
|
24
|
+
export default controller({
|
|
25
|
+
register: route.post('/register', {
|
|
26
|
+
async handle(req, res) {
|
|
27
|
+
const plaintext = req.body.password
|
|
28
|
+
|
|
29
|
+
// 1. Hash the password
|
|
30
|
+
// Uses secure defaults (cost: 16384, block: 8)
|
|
31
|
+
const hashedPassword = await Password.hashPassword(plaintext)
|
|
32
|
+
|
|
33
|
+
await db.users.create({ password: hashedPassword })
|
|
34
|
+
return { success: true }
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
login: route.post('/login', {
|
|
39
|
+
async handle(req, res) {
|
|
40
|
+
const user = await db.users.findByEmail(req.body.email)
|
|
41
|
+
|
|
42
|
+
// 2. Verify the hash (timing-safe)
|
|
43
|
+
const isValid = await Password.verifyPassword(req.body.password, user.password)
|
|
44
|
+
|
|
45
|
+
if (!isValid) throw new Error("Invalid credentials")
|
|
46
|
+
return { success: true }
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Class-Based (OOP) Paradigm
|
|
53
|
+
|
|
54
|
+
```typescript filename="src/http/auth/route.ts"
|
|
55
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
56
|
+
import { Password } from 'exisjs/auth'
|
|
57
|
+
|
|
58
|
+
@Controller()
|
|
59
|
+
export default class AuthController {
|
|
60
|
+
@Post('/register')
|
|
61
|
+
async register(@Body() body: any) {
|
|
62
|
+
const plaintext = body.password
|
|
63
|
+
|
|
64
|
+
// 1. Hash the password
|
|
65
|
+
// Uses secure defaults (cost: 16384, block: 8)
|
|
66
|
+
const hashedPassword = await Password.hashPassword(plaintext)
|
|
67
|
+
|
|
68
|
+
await db.users.create({ password: hashedPassword })
|
|
69
|
+
return { success: true }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Post('/login')
|
|
73
|
+
async login(@Body() body: any) {
|
|
74
|
+
const user = await db.users.findByEmail(body.email)
|
|
75
|
+
|
|
76
|
+
// 2. Verify the hash (timing-safe)
|
|
77
|
+
const isValid = await Password.verifyPassword(body.password, user.password)
|
|
78
|
+
|
|
79
|
+
if (!isValid) throw new Error("Invalid credentials")
|
|
80
|
+
return { success: true }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. JSON Web Tokens (JWT)
|
|
88
|
+
|
|
89
|
+
ExisJS provides a JWT engine using native `createHmac('sha256')`.
|
|
90
|
+
|
|
91
|
+
```typescript filename="src/http/auth/route.ts"
|
|
92
|
+
import { JWT } from 'exisjs/auth'
|
|
93
|
+
|
|
94
|
+
// Generate a Token (Expires in 24 hours)
|
|
95
|
+
const token = JWT.signJWT({ userId: 123, role: 'admin' }, process.env.JWT_SECRET, {
|
|
96
|
+
expiresIn: 86400
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Verify a Token
|
|
100
|
+
// Automatically throws 401 Unauthorized or TokenExpiredError if invalid
|
|
101
|
+
const payload = JWT.verifyJWT<{ userId: number, role: string }>(token, process.env.JWT_SECRET)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Creating an Auth Middleware
|
|
105
|
+
|
|
106
|
+
You can wrap this in a gateway middleware to protect entire folders:
|
|
107
|
+
|
|
108
|
+
#### Functional Paradigm
|
|
109
|
+
|
|
110
|
+
```typescript filename="src/http/admin/gateway.ts"
|
|
111
|
+
import { defineGateway } from 'exisjs/router'
|
|
112
|
+
import { JWT } from 'exisjs/auth'
|
|
113
|
+
|
|
114
|
+
export default defineGateway({
|
|
115
|
+
middlewares: [
|
|
116
|
+
(req, res, next) => {
|
|
117
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
118
|
+
// Verifies the token and attaches the payload to the request
|
|
119
|
+
req.user = JWT.verifyJWT(token, process.env.JWT_SECRET)
|
|
120
|
+
next()
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Class-Based (OOP) Paradigm
|
|
127
|
+
|
|
128
|
+
```typescript filename="src/http/admin/gateway.ts"
|
|
129
|
+
import { Gateway } from 'exisjs/decorators'
|
|
130
|
+
import { JWT } from 'exisjs/auth'
|
|
131
|
+
|
|
132
|
+
@Gateway({
|
|
133
|
+
middlewares: [
|
|
134
|
+
(req, res, next) => {
|
|
135
|
+
const token = req.headers.authorization?.split(' ')[1]
|
|
136
|
+
req.user = JWT.verifyJWT(token, process.env.JWT_SECRET)
|
|
137
|
+
next()
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
export default class AdminGateway {}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 3. Role-Based Access Control (RBAC)
|
|
147
|
+
|
|
148
|
+
Once your user is authenticated and their payload is attached to `req.user`, ExisJS provides native Role-Based Access Control right inside the route configuration. You don't need any custom middlewares for this.
|
|
149
|
+
|
|
150
|
+
#### Functional Paradigm
|
|
151
|
+
|
|
152
|
+
```typescript filename="src/http/admin/route.ts"
|
|
153
|
+
import { controller, route } from 'exisjs/router'
|
|
154
|
+
import { parseUser } from '../middlewares/auth.js'
|
|
155
|
+
|
|
156
|
+
export default controller({
|
|
157
|
+
// Intercepts the request and checks req.user.permissions (or req.user.role)
|
|
158
|
+
// before the handler even executes. Throws 403 Forbidden if missing.
|
|
159
|
+
deleteDatabase: route.delete('/db', {
|
|
160
|
+
permissions: ['superadmin'],
|
|
161
|
+
middleware: [parseUser()],
|
|
162
|
+
handle() {
|
|
163
|
+
return { success: true }
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
// Accepts an array of allowed permissions
|
|
168
|
+
viewDashboard: route.get('/dashboard', {
|
|
169
|
+
permissions: ['admin', 'superadmin', 'manager'],
|
|
170
|
+
middleware: [parseUser()],
|
|
171
|
+
handle() {
|
|
172
|
+
return { metrics: [] }
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Class-Based (OOP) Paradigm
|
|
179
|
+
|
|
180
|
+
```typescript filename="src/http/admin/route.ts"
|
|
181
|
+
import { Controller, Delete, Get, Use, Permissions } from 'exisjs/decorators'
|
|
182
|
+
import { parseUser } from '../middlewares/auth.js'
|
|
183
|
+
|
|
184
|
+
@Use(parseUser())
|
|
185
|
+
@Controller()
|
|
186
|
+
export default class AdminController {
|
|
187
|
+
// Only users with superadmin permission can access this
|
|
188
|
+
@Permissions('superadmin')
|
|
189
|
+
@Delete('/db')
|
|
190
|
+
async deleteDatabase() {
|
|
191
|
+
return { success: true }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Accepts an array of allowed permissions
|
|
195
|
+
@Permissions(['admin', 'superadmin', 'manager'])
|
|
196
|
+
@Get('/dashboard')
|
|
197
|
+
async viewDashboard() {
|
|
198
|
+
return { metrics: [] }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 4. Stateful Sessions
|
|
206
|
+
|
|
207
|
+
If you prefer stateful authentication over JWTs, ExisJS ships with a secure session middleware. It securely signs the session cookie (`exis_sid`) and automatically persists changes to `req.session` back to the active store.
|
|
208
|
+
|
|
209
|
+
By default, it uses a `MemorySessionStore`, but you can plug in any distributed store (like Redis) by implementing the `SessionStore` interface.
|
|
210
|
+
|
|
211
|
+
#### Functional Paradigm
|
|
212
|
+
|
|
213
|
+
```typescript filename="src/http/server.ts"
|
|
214
|
+
import { exis } from 'exisjs'
|
|
215
|
+
import { session } from 'exisjs/auth'
|
|
216
|
+
|
|
217
|
+
export default exis({
|
|
218
|
+
async onStart(app) {
|
|
219
|
+
// 1. Mount the session globally
|
|
220
|
+
app.use(session({
|
|
221
|
+
secret: process.env.SESSION_SECRET_KEY, // Must be 32+ chars
|
|
222
|
+
ttl: 86400 * 1000 // 1 day
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Class-Based (OOP) Paradigm
|
|
229
|
+
|
|
230
|
+
```typescript filename="src/http/server.ts"
|
|
231
|
+
import { Server } from 'exisjs/decorators'
|
|
232
|
+
import { session } from 'exisjs/auth'
|
|
233
|
+
import type { App } from 'exisjs'
|
|
234
|
+
|
|
235
|
+
@Server()
|
|
236
|
+
export default class RootServer {
|
|
237
|
+
async onStart(app: App) {
|
|
238
|
+
// 1. Mount the session globally
|
|
239
|
+
app.use(session({
|
|
240
|
+
secret: process.env.SESSION_SECRET_KEY, // Must be 32+ chars
|
|
241
|
+
ttl: 86400 * 1000 // 1 day
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### Functional Paradigm
|
|
248
|
+
|
|
249
|
+
```typescript filename="src/http/cart/route.ts"
|
|
250
|
+
import { controller, route } from 'exisjs/router'
|
|
251
|
+
|
|
252
|
+
export default controller({
|
|
253
|
+
addItem: route.post('/', {
|
|
254
|
+
handle(req, res) {
|
|
255
|
+
// 2. Read and Write natively to req.session
|
|
256
|
+
// ExisJS automatically saves this back to the Store when the request ends
|
|
257
|
+
if (!req.session.cart) req.session.cart = []
|
|
258
|
+
|
|
259
|
+
req.session.cart.push(req.body.itemId)
|
|
260
|
+
return { cart: req.session.cart }
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Class-Based (OOP) Paradigm
|
|
267
|
+
|
|
268
|
+
```typescript filename="src/http/cart/route.ts"
|
|
269
|
+
import { Controller, Post, Req, Body } from 'exisjs/decorators'
|
|
270
|
+
|
|
271
|
+
@Controller()
|
|
272
|
+
export default class CartController {
|
|
273
|
+
@Post('/')
|
|
274
|
+
async addItem(@Req() req: any, @Body() body: any) {
|
|
275
|
+
// 2. Read and Write natively to req.session
|
|
276
|
+
// ExisJS automatically saves this back to the Store when the request ends
|
|
277
|
+
if (!req.session.cart) req.session.cart = []
|
|
278
|
+
|
|
279
|
+
req.session.cart.push(body.itemId)
|
|
280
|
+
return { cart: req.session.cart }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 5. Edge Security Middleware
|
|
288
|
+
|
|
289
|
+
As mentioned in the [Middleware Guide](/docs/middleware), you should strongly consider registering the following global security middleware in your `server.ts`:
|
|
290
|
+
|
|
291
|
+
- `helmet()`: Enforces Strict-Transport-Security (HSTS), X-Content-Type-Options (nosniff), and prevents Clickjacking (`X-Frame-Options: DENY`).
|
|
292
|
+
- `dbSanitize({ mongo: true })`: A universal scraper that strips NoSQL operators (`$where`) to provide defense-in-depth for MongoDB databases.
|
|
293
|
+
- `csrf({ secret })`: Enforces the Signed Double Submit Cookie pattern to protect state-changing requests (POST, PUT, DELETE) from Cookie Tossing attacks.
|
|
294
|
+
- `hpp()`: HTTP Parameter Pollution protection (removes duplicate array keys).
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/websockets"
|
|
3
|
+
nextTitle: "WebSockets"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Server-Sent Events (SSE)
|
|
7
|
+
|
|
8
|
+
Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection. Unlike WebSockets, SSE is unidirectional (server-to-client) and works over standard HTTP without requiring complex handshake protocols or specialized load balancer configurations.
|
|
9
|
+
|
|
10
|
+
SSE is the industry standard for Large Language Model (LLM) streaming (e.g., OpenAI, Anthropic) and real-time dashboard updates. ExisJS provides native SSE support.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Creating an SSE Stream
|
|
15
|
+
|
|
16
|
+
When you declare a route as an SSE endpoint using `route.sse()` or `@Sse()`, ExisJS automatically configures the correct HTTP headers (`Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`) and provides an `ExisSSE` stream object.
|
|
17
|
+
|
|
18
|
+
### Functional Paradigm
|
|
19
|
+
|
|
20
|
+
```typescript filename="src/http/chat/route.ts"
|
|
21
|
+
import { controller, route } from 'exisjs/router'
|
|
22
|
+
import Groq from 'groq-sdk'
|
|
23
|
+
|
|
24
|
+
export default controller({
|
|
25
|
+
chat: route.sse('/api/chat', {
|
|
26
|
+
async handle({ stream: sse, req }) {
|
|
27
|
+
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
|
|
28
|
+
const message = req.query.message as string
|
|
29
|
+
|
|
30
|
+
if (!message) {
|
|
31
|
+
sse.send({ error: 'Message is required' }, 'error')
|
|
32
|
+
sse.close()
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const aiStream = await groq.chat.completions.create({
|
|
38
|
+
messages: [{ role: 'user', content: message }],
|
|
39
|
+
model: 'llama-3.1-8b-instant',
|
|
40
|
+
stream: true,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Iterate over the async generator provided by the AI SDK
|
|
44
|
+
for await (const chunk of aiStream) {
|
|
45
|
+
// Prevent writing to a closed socket if the client disconnects early
|
|
46
|
+
if (!sse.connected) break
|
|
47
|
+
|
|
48
|
+
const content = chunk.choices[0]?.delta?.content || ''
|
|
49
|
+
if (content) {
|
|
50
|
+
sse.send({ text: content }, 'chunk')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
sse.send({ text: '' }, 'done')
|
|
55
|
+
} finally {
|
|
56
|
+
// Ensure the connection is always closed when processing completes
|
|
57
|
+
sse.close()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Class-Based (OOP) Paradigm
|
|
65
|
+
|
|
66
|
+
```typescript filename="src/http/chat/route.ts"
|
|
67
|
+
import { Controller, Sse, Stream, Req } from 'exisjs/decorators'
|
|
68
|
+
import type { ExisSSE } from 'exisjs/router'
|
|
69
|
+
import Groq from 'groq-sdk'
|
|
70
|
+
|
|
71
|
+
@Controller()
|
|
72
|
+
export default class AIChatController {
|
|
73
|
+
@Sse('/api/chat')
|
|
74
|
+
async handleChat(@Stream() sse: ExisSSE, @Req() req: any) {
|
|
75
|
+
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY })
|
|
76
|
+
const message = req.query.message as string
|
|
77
|
+
|
|
78
|
+
if (!message) {
|
|
79
|
+
sse.send({ error: 'Message is required' }, 'error')
|
|
80
|
+
sse.close()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const aiStream = await groq.chat.completions.create({
|
|
86
|
+
messages: [{ role: 'user', content: message }],
|
|
87
|
+
model: 'llama-3.1-8b-instant',
|
|
88
|
+
stream: true,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
for await (const chunk of aiStream) {
|
|
92
|
+
if (!sse.connected) break
|
|
93
|
+
|
|
94
|
+
const content = chunk.choices[0]?.delta?.content || ''
|
|
95
|
+
if (content) {
|
|
96
|
+
sse.send({ text: content }, 'chunk')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
sse.send({ text: '' }, 'done')
|
|
100
|
+
} finally {
|
|
101
|
+
sse.close()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 2. The `ExisSSE` API
|
|
110
|
+
|
|
111
|
+
The injected `sse` object gives you full control over the streaming connection:
|
|
112
|
+
|
|
113
|
+
- **`sse.send(data: string | Record<string, any>, eventName?: string)`**: Serializes the data (if it is an object) to JSON and sends it down the wire. You can optionally tag it with a specific `eventName`.
|
|
114
|
+
- **`sse.connected`**: A boolean that evaluates to `true` while the connection is alive. You should always check this inside long-running `for await` or `while` loops to prevent throwing "Socket Closed" errors when the client disconnects unexpectedly.
|
|
115
|
+
- **`sse.close()`**: Safely closes the stream and terminates the HTTP connection.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Sending a plain text message
|
|
119
|
+
sse.send("Processing your request...")
|
|
120
|
+
|
|
121
|
+
// Sending a JSON object tagged with a specific event type (e.g., 'metrics')
|
|
122
|
+
sse.send({ cpu_usage: 95, mem_usage: 60 }, 'metrics')
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 3. Client-Side Consumption
|
|
128
|
+
|
|
129
|
+
To consume the SSE stream from the frontend (e.g., in React, Vue, or Vanilla JS), use the native browser `EventSource` API.
|
|
130
|
+
|
|
131
|
+
```javascript filename="frontend/app.js"
|
|
132
|
+
// Connect to the ExisJS SSE endpoint
|
|
133
|
+
const eventSource = new EventSource('/api/chat?message=Hello')
|
|
134
|
+
|
|
135
|
+
// Listen for the default 'message' event (when no eventName is specified in sse.send)
|
|
136
|
+
eventSource.onmessage = (event) => {
|
|
137
|
+
const data = JSON.parse(event.data)
|
|
138
|
+
console.log("Standard message:", data)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Listen for a custom event type, such as 'chunk'
|
|
142
|
+
eventSource.addEventListener('chunk', (event) => {
|
|
143
|
+
const data = JSON.parse(event.data)
|
|
144
|
+
// Append the streamed text to the DOM
|
|
145
|
+
document.getElementById('output').textContent += data.text
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Listen for the 'done' event to close the connection from the client side
|
|
149
|
+
eventSource.addEventListener('done', () => {
|
|
150
|
+
console.log("Stream finished.")
|
|
151
|
+
eventSource.close()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Handle connection errors
|
|
155
|
+
eventSource.onerror = (error) => {
|
|
156
|
+
console.error("SSE Connection Error:", error)
|
|
157
|
+
eventSource.close()
|
|
158
|
+
}
|
|
159
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextUrl: "/docs/config"
|
|
3
|
+
nextTitle: "Configuration"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project Structure
|
|
7
|
+
|
|
8
|
+
When you bootstrap a new ExisJS project using `@exisjs/create`, it generates a clean, opinionated folder structure designed for scalability and maintainability.
|
|
9
|
+
|
|
10
|
+
Unlike frameworks that force you into deeply nested architectural layers, ExisJS relies heavily on native File-System Routing and centralized configurations to keep your project lean.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The Default Structure
|
|
15
|
+
|
|
16
|
+
Here is a look at a standard ExisJS project structure:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
my-backend/
|
|
20
|
+
├── .exis/ # Internal framework cache and build artifacts
|
|
21
|
+
├── src/
|
|
22
|
+
│ └── http/
|
|
23
|
+
│ ├── server.ts # The main backend entry point
|
|
24
|
+
│ ├── route.ts # The root API routes (e.g. `GET /`)
|
|
25
|
+
│ └── health/
|
|
26
|
+
│ └── route.ts # Healthcheck endpoints (e.g. `GET /health`)
|
|
27
|
+
├── .env # Local environment variables
|
|
28
|
+
├── env.ts # Type-safe environment variable schema
|
|
29
|
+
├── exis.config.ts # The ExisJS server configuration file
|
|
30
|
+
├── package.json
|
|
31
|
+
└── tsconfig.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Key Files & Directories
|
|
37
|
+
|
|
38
|
+
### 1. `exis.config.ts`
|
|
39
|
+
This configures the global application infrastructure. Here, you define network ports, SSL certificates, global CORS policies, Helmet security, and register native framework Plugins (like GraphQL or Drizzle).
|
|
40
|
+
|
|
41
|
+
[Read the full Configuration Guide](/docs/config)
|
|
42
|
+
|
|
43
|
+
### 2. `env.ts`
|
|
44
|
+
ExisJS strongly encourages type-safe environment variables. By defining an `env.ts` file using the built-in validator (`v.env()`), your application will gracefully crash at boot if a required environment variable (like `DATABASE_URL`) is missing.
|
|
45
|
+
|
|
46
|
+
```typescript filename="env.ts"
|
|
47
|
+
import { tex } from 'exisjs/validator'
|
|
48
|
+
|
|
49
|
+
export const env = tex.object({
|
|
50
|
+
PORT: tex.number({ coerce: true, optional: true }),
|
|
51
|
+
NODE_ENV: tex.enum(['development', 'production', 'test']),
|
|
52
|
+
DATABASE_URL: tex.string()
|
|
53
|
+
}).parse(process.env)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. `src/http/server.ts`
|
|
57
|
+
This is your application's bootstrap file. Instead of manually invoking `app.listen()`, you export your server configuration. The ExisJS CLI automatically reads this file, connects your plugins, mounts the file-system router, and starts the server.
|
|
58
|
+
|
|
59
|
+
### Functional Server Setup
|
|
60
|
+
|
|
61
|
+
```typescript filename="src/http/server.ts"
|
|
62
|
+
import { exis } from 'exisjs'
|
|
63
|
+
|
|
64
|
+
export default exis({
|
|
65
|
+
async onStart(app) {
|
|
66
|
+
// Connect to database, provide global DI services, etc.
|
|
67
|
+
},
|
|
68
|
+
async onClose(app) {
|
|
69
|
+
// Gracefully close connections
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Class-Based (OOP) Server Setup
|
|
75
|
+
|
|
76
|
+
```typescript filename="src/http/server.ts"
|
|
77
|
+
import { Server } from 'exisjs/decorators'
|
|
78
|
+
import type { App } from 'exisjs'
|
|
79
|
+
|
|
80
|
+
@Server()
|
|
81
|
+
export default class RootServer {
|
|
82
|
+
async onStart(app: App) {
|
|
83
|
+
// Connect to database, provide global DI services, etc.
|
|
84
|
+
}
|
|
85
|
+
async onClose(app: App) {
|
|
86
|
+
// Gracefully close connections
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 4. `src/http/**/route.ts`
|
|
92
|
+
ExisJS uses **File-System Routing** out of the box! Any file named `route.ts` inside the `src/http/` directory is automatically mapped to a URL based on its folder path. You can define routes functionally using `controller({...})` or with OOP classes using `@Controller()`.
|
|
93
|
+
- `src/http/route.ts` maps to `/`
|
|
94
|
+
- `src/http/health/route.ts` maps to `/health`
|
|
95
|
+
- `src/http/users/settings/route.ts` maps to `/users/settings`
|
|
96
|
+
|
|
97
|
+
### 5. `src/http/**/gateway.ts`
|
|
98
|
+
Gateways allow you to apply configurations to entire folders. If you place a `gateway.ts` file inside `src/http/admin/`, the Guards, Middlewares, and Providers defined in that Gateway will automatically apply to every single route inside `/admin/*`. Like servers, these can be built using `defineGateway({...})` or the `@Gateway()` decorator.
|
|
99
|
+
|
|
100
|
+
### 6. `.exis/`
|
|
101
|
+
This is an internal, hidden directory generated by the ExisJS CLI. It holds compiled router mappings and runtime cache to skip filesystem scanning in production. You should add `.exis/` to your `.gitignore`.
|
|
102
|
+
|
|
103
|
+
### 7. Custom Directories (e.g., `src/services/` or `src/models/`)
|
|
104
|
+
ExisJS only enforces structure inside `src/http/` (for routing) and `src/jobs/` (for background queues). For the rest of your application logic, you are free to organize it however you see fit. A common pattern is to abstract database schemas into `src/models/` and reusable business logic into `src/services/`, and then inject them into your route controllers using the Dependency Injection container.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## The ExisJS CLI
|
|
109
|
+
|
|
110
|
+
Your project is driven entirely by the `exis` CLI tool, which is typically mapped in your `package.json` scripts:
|
|
111
|
+
|
|
112
|
+
- `exis dev`: Starts the development server with Hot-Module Replacement (HMR).
|
|
113
|
+
- `exis build`: Compiles your TypeScript code to JavaScript for production.
|
|
114
|
+
- `exis start`: Starts the production server using the compiled output.
|
|
115
|
+
- `exis routes`: Prints a formatted table of every compiled API endpoint and WebSocket in your application.
|
|
116
|
+
- `exis generate <type>`: Scaffolds new routes, plugins, or middleware boilerplate.
|