vortez 5.0.0-dev.17 → 5.0.0-dev.19
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/.gitignore +9 -4
- package/README.md +681 -176
- package/build/Vortez.d.ts +1 -0
- package/build/Vortez.js +1 -0
- package/build/Vortez.js.map +1 -1
- package/build/beta/JwtManager/HeaderValidator.d.ts +25 -0
- package/build/beta/JwtManager/HeaderValidator.js +47 -0
- package/build/beta/JwtManager/HeaderValidator.js.map +1 -0
- package/build/beta/JwtManager/Jwt.d.ts +14 -6
- package/build/beta/JwtManager/Jwt.js +15 -7
- package/build/beta/JwtManager/Jwt.js.map +1 -1
- package/build/beta/JwtManager/JwtManager.d.ts +31 -24
- package/build/beta/JwtManager/JwtManager.js +61 -40
- package/build/beta/JwtManager/JwtManager.js.map +1 -1
- package/build/beta/JwtManager/JwtUtils.d.ts +19 -4
- package/build/beta/JwtManager/JwtUtils.js +18 -0
- package/build/beta/JwtManager/JwtUtils.js.map +1 -1
- package/build/beta/JwtManager/KeyEntry.d.ts +52 -0
- package/build/beta/JwtManager/KeyEntry.js +42 -0
- package/build/beta/JwtManager/KeyEntry.js.map +1 -0
- package/build/beta/JwtManager/KeyGenerator.d.ts +5 -0
- package/build/beta/JwtManager/KeyGenerator.js +12 -9
- package/build/beta/JwtManager/KeyGenerator.js.map +1 -1
- package/build/server/Response.d.ts +1 -1
- package/build/server/Response.js +1 -1
- package/build/server/Response.js.map +1 -1
- package/build/server/Server.d.ts +4 -4
- package/build/server/Server.js +5 -5
- package/build/server/Server.js.map +1 -1
- package/build/server/ServerDebug.d.ts +10 -1
- package/build/server/ServerDebug.js +85 -17
- package/build/server/ServerDebug.js.map +1 -1
- package/build/server/config/Config.d.ts +274 -47
- package/build/server/config/Config.js +68 -47
- package/build/server/config/Config.js.map +1 -1
- package/build/server/config/{ConfigLoader.d.ts → Loader.d.ts} +4 -5
- package/build/server/config/{ConfigLoader.js → Loader.js} +7 -10
- package/build/server/config/Loader.js.map +1 -0
- package/build/server/router/Router.d.ts +87 -30
- package/build/server/router/Router.js +110 -48
- package/build/server/router/Router.js.map +1 -1
- package/build/server/router/algorithm/Algorithm.d.ts +39 -0
- package/build/server/router/algorithm/Algorithm.js +20 -0
- package/build/server/router/algorithm/Algorithm.js.map +1 -0
- package/build/server/router/algorithm/FIFO.d.ts +15 -0
- package/build/server/router/algorithm/FIFO.js +24 -0
- package/build/server/router/algorithm/FIFO.js.map +1 -0
- package/build/server/router/algorithm/Tree.d.ts +38 -0
- package/build/server/router/algorithm/Tree.js +126 -0
- package/build/server/router/algorithm/Tree.js.map +1 -0
- package/build/server/router/middleware/WsMiddleware.js +1 -1
- package/build/server/router/middleware/WsMiddleware.js.map +1 -1
- package/build/utilities/Flatten.d.ts +56 -0
- package/build/utilities/Flatten.js +59 -0
- package/build/utilities/Flatten.js.map +1 -0
- package/build/utilities/Utilities.d.ts +7 -58
- package/build/utilities/Utilities.js +8 -33
- package/build/utilities/Utilities.js.map +1 -1
- package/build/utilities/schema/Introspection.d.ts +24 -0
- package/build/utilities/schema/Introspection.js +87 -0
- package/build/utilities/schema/Introspection.js.map +1 -0
- package/build/utilities/schema/JSONSchema.d.ts +68 -0
- package/build/utilities/schema/JSONSchema.js +13 -0
- package/build/utilities/schema/JSONSchema.js.map +1 -0
- package/build/utilities/schema/Schema.d.ts +253 -0
- package/build/utilities/schema/Schema.js +241 -0
- package/build/utilities/schema/Schema.js.map +1 -0
- package/build/utilities/schema/SchemaError.d.ts +10 -0
- package/build/utilities/schema/SchemaError.js +13 -0
- package/build/utilities/schema/SchemaError.js.map +1 -0
- package/build/utilities/schema/Validator.d.ts +94 -0
- package/build/utilities/schema/Validator.js +246 -0
- package/build/utilities/schema/Validator.js.map +1 -0
- package/package.json +1 -1
- package/tests/config/config.js +233 -0
- package/tests/jwtManager/jwtManager.js +310 -46
- package/tests/router.js +596 -0
- package/tests/schema/schema.js +368 -0
- package/tests/test.env +0 -0
- package/tests/test.js +3 -3
- package/build/server/config/ConfigLoader.js.map +0 -1
- package/build/server/config/ConfigValidator.d.ts +0 -71
- package/build/server/config/ConfigValidator.js +0 -131
- package/build/server/config/ConfigValidator.js.map +0 -1
- package/examples/in-docs.js +0 -96
package/README.md
CHANGED
|
@@ -1,52 +1,299 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vortez
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Throughout its development, I’ve gained **tons of new skills and insights** that I’m excited to share.
|
|
3
|
+
A lightweight, production-ready Node.js web framework for building APIs, websites, single-page applications (SPAs), and progressive web apps (PWAs). Built with TypeScript and designed for simplicity without sacrificing power.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
I constantly refactor the code whenever I spot areas that can be polished or optimized.
|
|
5
|
+
The package exports a default `Vortez` server class, plus named exports for `Router`, `Config`, `Template`, `Utilities`, `ServerError`, `Logger`, and `Beta`.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/vortez)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://nodejs.org/)
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
My goal is to make **Vortez** a tool that helps developers build **APIs**, **PWAs**, **websites**, and—thanks to the [Vizui module](https://github.com/NetFeez/Vizui)—**SPAs** with ease and confidence.
|
|
11
|
+
## Features
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- 🚀 **Fast and Lightweight** — Minimal overhead, maximum performance
|
|
14
|
+
- 📦 **Built with TypeScript** — Full type safety with compiled JavaScript output
|
|
15
|
+
- 🔄 **Flexible Routing** — Pattern-based routing with wildcards and dynamic parameters
|
|
16
|
+
- 🔌 **WebSocket Support** — Real-time bidirectional communication out of the box
|
|
17
|
+
- 🛡️ **HTTPS/SSL Ready** — Secure connections with easy configuration
|
|
18
|
+
- 🎯 **Middleware Pipeline** — Composable, snapshot-based middleware system
|
|
19
|
+
- 📝 **Request/Response Helpers** — Simplified APIs for JSON, files, and text responses
|
|
20
|
+
- ⚙️ **Modular Architecture** — Use what you need, extend what you want
|
|
21
|
+
- 🔐 **Beta Features** — JWT authentication and email support (development version)
|
|
22
|
+
|
|
23
|
+
## Table of Contents
|
|
24
|
+
|
|
25
|
+
- [Quick Start](#quick-start)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Getting Started](#getting-started)
|
|
28
|
+
- [Configuration Files](#configuration-files)
|
|
29
|
+
- [Examples Repository](#examples-repository)
|
|
30
|
+
- [Core Concepts](#core-concepts)
|
|
31
|
+
- [Routing Rules](#routing-rules)
|
|
32
|
+
- [Server Configuration](#server-configuration)
|
|
33
|
+
- [Use Cases](#use-cases)
|
|
34
|
+
- [Development Features](#development-features)
|
|
17
35
|
|
|
18
36
|
---
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
## Quick Start
|
|
21
39
|
|
|
22
|
-
|
|
40
|
+
### Installation
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
```console
|
|
43
|
+
npm install vortez
|
|
44
|
+
```
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
mpm install vortez
|
|
28
|
-
```
|
|
29
|
-
* **Development version**
|
|
46
|
+
**Development version** (with beta features):
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
```console
|
|
49
|
+
npm install vortez@dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Requirements
|
|
53
|
+
|
|
54
|
+
- **Node.js** ≥ 16.0.0
|
|
55
|
+
- **ES Modules** — Set `"type": "module"` in your `package.json`
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "my-app",
|
|
60
|
+
"type": "module",
|
|
61
|
+
"main": "index.js"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
34
64
|
|
|
35
65
|
> [!IMPORTANT]
|
|
36
|
-
>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
> CommonJS support will be added in a future release. For now, ES modules are required.
|
|
67
|
+
|
|
68
|
+
### Minimal Example
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import Vortez from 'vortez';
|
|
72
|
+
|
|
73
|
+
const server = new Vortez({ port: 3000 });
|
|
74
|
+
|
|
75
|
+
// Define a route
|
|
76
|
+
server.router.addAction('GET', '/hello', (request, response) => {
|
|
77
|
+
response.sendJson({ message: 'Hello World' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Start the server
|
|
81
|
+
await server.start();
|
|
82
|
+
// Server running on http://localhost:3000
|
|
83
|
+
```
|
|
46
84
|
|
|
47
85
|
---
|
|
48
86
|
|
|
49
|
-
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
Vortez is available on npm:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
npm install vortez
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For the latest development version with experimental features:
|
|
96
|
+
|
|
97
|
+
```console
|
|
98
|
+
npm install vortez@dev
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Getting Started
|
|
104
|
+
|
|
105
|
+
### Creating a Server
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import Vortez from 'vortez';
|
|
109
|
+
|
|
110
|
+
const server = new Vortez();
|
|
111
|
+
|
|
112
|
+
// Configure (optional)
|
|
113
|
+
server.config.set('port', 3000);
|
|
114
|
+
server.config.set('host', 'localhost');
|
|
115
|
+
|
|
116
|
+
// Add routes
|
|
117
|
+
server.router.addAction('GET', '/', (request, response) => {
|
|
118
|
+
response.sendJson({ status: 'ok' });
|
|
119
|
+
});
|
|
120
|
+
server.router.addFile('/favicon.ico', '/assets/icon.ico');
|
|
121
|
+
|
|
122
|
+
// Start listening
|
|
123
|
+
await server.start();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Constructor Options
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const server = new Vortez({
|
|
130
|
+
host: 'localhost', // Default: 'localhost'
|
|
131
|
+
port: 80, // Default: 80
|
|
132
|
+
ssl: null // Optional HTTPS configuration
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Configuration Files
|
|
137
|
+
|
|
138
|
+
You can also build a `Config` directly or load one from disk.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import Vortez, { Config } from 'vortez';
|
|
142
|
+
|
|
143
|
+
const config = new Config({
|
|
144
|
+
host: 'localhost',
|
|
145
|
+
port: 3000,
|
|
146
|
+
routing: {
|
|
147
|
+
algorithm: 'FIFO'
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
console.log(config.toJson());
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
import Vortez, { Config } from 'vortez';
|
|
156
|
+
|
|
157
|
+
const config = await Config.Loader.load('config.json');
|
|
158
|
+
// You can directly use of Vortez import
|
|
159
|
+
const config = await Vortez.Config.Loader.load('config.json');
|
|
160
|
+
|
|
161
|
+
const server = new Vortez(config);
|
|
162
|
+
await server.start();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This workflow is also covered in the dedicated examples repository.
|
|
166
|
+
|
|
167
|
+
### Examples Repository
|
|
168
|
+
|
|
169
|
+
The runnable examples are being moved to a separate repository to keep this package focused.
|
|
170
|
+
|
|
171
|
+
Until that repository is published, the snippets in this README remain the canonical reference.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Core Concepts
|
|
176
|
+
|
|
177
|
+
### URL Rules
|
|
178
|
+
|
|
179
|
+
URL rules define how requests are matched to routes. They use patterns with special markers:
|
|
180
|
+
|
|
181
|
+
| Pattern | Behavior | Example |
|
|
182
|
+
|---------|----------|---------|
|
|
183
|
+
| `/` | Root path | `/` |
|
|
184
|
+
| `/path` | Literal segment | `/api/users` |
|
|
185
|
+
| `/$param` | Dynamic parameter | `/$id` captures `123` as `id` |
|
|
186
|
+
| `/*` | Wildcard (matches all sub-routes) | `/files/*` matches `/files/a/b/c` |
|
|
187
|
+
| `/path/$id/sub` | Mixed patterns | `/user/$id/posts` |
|
|
188
|
+
|
|
189
|
+
**Examples:**
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// Static routes
|
|
193
|
+
server.router.addAction('GET', '/', homeHandler);
|
|
194
|
+
server.router.addAction('GET', '/api/status', statusHandler);
|
|
195
|
+
|
|
196
|
+
// Dynamic routes with parameters
|
|
197
|
+
server.router.addAction('GET', '/api/users/$id', (request, response) => {
|
|
198
|
+
const userId = request.ruleParams.id;
|
|
199
|
+
// Handle request...
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Wildcard routes
|
|
203
|
+
server.router.addAction('GET', '/api/*', catchAllHandler);
|
|
204
|
+
server.router.addFile('/docs/*', 'public/docs/index.html');
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Request Object
|
|
208
|
+
|
|
209
|
+
The `request` object contains information about the incoming HTTP request:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
server.router.addAction('GET', '/api/$action/$id', (request, response) => {
|
|
213
|
+
console.log(request.method); // 'GET', 'POST', etc.
|
|
214
|
+
console.log(request.url); // Full URL
|
|
215
|
+
console.log(request.ruleParams); // { action: '...', id: '...' }
|
|
216
|
+
console.log(request.searchParams); // Query parameters object (Record<string, string | undefined>)
|
|
217
|
+
console.log(request.headers); // HTTP headers
|
|
218
|
+
// For body, use: const body = await request.post;
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Response Object
|
|
223
|
+
|
|
224
|
+
The `response` object provides methods to send data back to the client:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
// Send JSON
|
|
228
|
+
response.sendJson({ key: 'value' });
|
|
229
|
+
|
|
230
|
+
// Send HTML/text
|
|
231
|
+
response.send('Hello World');
|
|
232
|
+
|
|
233
|
+
// Send file
|
|
234
|
+
response.sendFile('path/to/file.html');
|
|
235
|
+
|
|
236
|
+
// Send with custom status and headers
|
|
237
|
+
response.sendJson({ id: 123 }, {
|
|
238
|
+
status: 201,
|
|
239
|
+
headers: {
|
|
240
|
+
'X-Test': 'TestHeader'
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// The same options shape is supported by send, sendJson, sendFile and sendTemplate
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Middleware Execution Model
|
|
248
|
+
|
|
249
|
+
Vortez uses a **snapshot middleware composition model**:
|
|
250
|
+
|
|
251
|
+
- Global middleware registered via `router.httpMiddleware.use()` / `router.httpMiddleware.useError()`
|
|
252
|
+
and `router.wsMiddleware.use()` / `router.wsMiddleware.useError()` is captured at rule registration time
|
|
253
|
+
- Middleware is not updated retroactively for existing rules
|
|
254
|
+
- When mounting sub-routers, child middleware is appended after parent middleware
|
|
255
|
+
|
|
256
|
+
**Key principle:** Register all global middleware first, then add routes.
|
|
257
|
+
|
|
258
|
+
The `router.use()` helper merges middleware instances; individual middleware functions are added on `router.httpMiddleware` or `router.wsMiddleware`.
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
const router = server.router;
|
|
262
|
+
|
|
263
|
+
// Register middleware first
|
|
264
|
+
router.httpMiddleware.use(authMiddleware);
|
|
265
|
+
router.httpMiddleware.use(loggingMiddleware);
|
|
266
|
+
|
|
267
|
+
// Add routes (they capture current middleware)
|
|
268
|
+
router.addAction('GET', '/old', oldHandler);
|
|
269
|
+
// old → [authMiddleware, loggingMiddleware]
|
|
270
|
+
|
|
271
|
+
// Add more middleware
|
|
272
|
+
router.httpMiddleware.use(newMiddleware);
|
|
273
|
+
|
|
274
|
+
// New routes get updated middleware
|
|
275
|
+
router.addAction('GET', '/new', newHandler)
|
|
276
|
+
// Add a middleware only to a single rule
|
|
277
|
+
.httpMiddleware.use(otherMiddleware);
|
|
278
|
+
// new → [authMiddleware, loggingMiddleware, newMiddleware]
|
|
279
|
+
|
|
280
|
+
// Old routes still use original middleware!
|
|
281
|
+
// old → [authMiddleware, loggingMiddleware]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Practical example with sub-routers:**
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
const apiRouter = new Vortez.Router();
|
|
288
|
+
apiRouter.httpMiddleware.use(apiAuthMiddleware);
|
|
289
|
+
apiRouter.addAction('GET', '/users', getUsersHandler);
|
|
290
|
+
|
|
291
|
+
server.router.httpMiddleware.use(globalMiddleware);
|
|
292
|
+
server.router.mount(apiRouter, '/api');
|
|
293
|
+
// Final middleware chain: [globalMiddleware, apiAuthMiddleware]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
50
297
|
|
|
51
298
|
## Static Pages
|
|
52
299
|
|
|
@@ -63,256 +310,514 @@ const server = new Vortez();
|
|
|
63
310
|
server.router.addFolder('/source', 'source');
|
|
64
311
|
server.router.addFile('/', 'source/index.html');
|
|
65
312
|
|
|
313
|
+
// Set all other routes or an specific rule for the static page
|
|
314
|
+
server.router.addFile('/*', 'source/index.html');
|
|
315
|
+
server.router.addFile('/app/*', 'source/index.html');
|
|
316
|
+
|
|
66
317
|
// Starting the server
|
|
67
|
-
server.start();
|
|
318
|
+
await server.start();
|
|
68
319
|
```
|
|
69
320
|
|
|
70
321
|
---
|
|
71
322
|
|
|
72
|
-
## APIs
|
|
323
|
+
## REST APIs
|
|
73
324
|
|
|
74
|
-
|
|
325
|
+
Build a complete REST API:
|
|
75
326
|
|
|
76
327
|
```js
|
|
77
328
|
import Vortez from 'vortez';
|
|
78
329
|
|
|
79
|
-
const server = new Vortez();
|
|
330
|
+
const server = new Vortez({ port: 3000 });
|
|
80
331
|
|
|
81
|
-
//
|
|
82
|
-
server.router.addAction('GET', '/api/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
332
|
+
// Users resource
|
|
333
|
+
server.router.addAction('GET', '/api/users', (req, res) => {
|
|
334
|
+
res.sendJson([
|
|
335
|
+
{ id: 1, name: 'Alice' },
|
|
336
|
+
{ id: 2, name: 'Bob' }
|
|
337
|
+
]);
|
|
87
338
|
});
|
|
88
339
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
server.router.addAction('GET', '/api/params/$id', (request, response) => {
|
|
93
|
-
response.sendJson({
|
|
94
|
-
message: 'Hello World',
|
|
95
|
-
route: `[${request.method}] -> ${request.url}`,
|
|
96
|
-
params: request.ruleParams,
|
|
97
|
-
query: request.searchParams
|
|
98
|
-
});
|
|
340
|
+
server.router.addAction('GET', '/api/users/$id', (req, res) => {
|
|
341
|
+
const id = req.ruleParams.id;
|
|
342
|
+
res.sendJson({ id, name: `User ${id}` });
|
|
99
343
|
});
|
|
100
344
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
345
|
+
server.router.addAction('POST', '/api/users', async (req, res) => {
|
|
346
|
+
const user = await req.post;
|
|
347
|
+
res.sendJson({ id: 123, ...user }, { status: 201 });
|
|
104
348
|
});
|
|
105
349
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
350
|
+
server.router.addAction('PUT', '/api/users/$id', async (req, res) => {
|
|
351
|
+
const id = req.ruleParams.id;
|
|
352
|
+
const body = await req.post;
|
|
353
|
+
res.sendJson({ id, ...body });
|
|
109
354
|
});
|
|
110
355
|
|
|
111
|
-
|
|
112
|
-
|
|
356
|
+
server.router.addAction('DELETE', '/api/users/$id', (req, res) => {
|
|
357
|
+
res.send('', { status: 204 });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await server.start();
|
|
113
361
|
```
|
|
114
362
|
|
|
115
363
|
---
|
|
116
364
|
|
|
117
|
-
##
|
|
365
|
+
## Single Page Applications
|
|
118
366
|
|
|
119
|
-
|
|
367
|
+
Serve an SPA with client-side routing:
|
|
120
368
|
|
|
121
369
|
```js
|
|
122
370
|
import Vortez from 'vortez';
|
|
123
371
|
|
|
124
372
|
const server = new Vortez();
|
|
125
373
|
|
|
126
|
-
|
|
127
|
-
server.router.addFile('/app
|
|
374
|
+
// Serve the main app shell for all app routes
|
|
375
|
+
server.router.addFile('/app', 'public/app.html');
|
|
376
|
+
server.router.addFile('/app/*', 'public/app.html');
|
|
377
|
+
|
|
378
|
+
// Serve static assets
|
|
128
379
|
server.router.addFolder('/public', 'public');
|
|
129
380
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
381
|
+
// Optional: API routes
|
|
382
|
+
server.router.addAction('GET', '/api/data', (req, res) => {
|
|
383
|
+
res.sendJson({ /* ... */ });
|
|
384
|
+
});
|
|
134
385
|
|
|
135
|
-
server.start();
|
|
386
|
+
await server.start();
|
|
136
387
|
```
|
|
137
388
|
|
|
138
389
|
---
|
|
139
390
|
|
|
391
|
+
## Routing Rules
|
|
392
|
+
|
|
393
|
+
### Serving Folders
|
|
394
|
+
|
|
395
|
+
Serve an entire directory and its subdirectories:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
server.router.addFolder('/public', 'public');
|
|
399
|
+
server.router.addFolder('/docs', 'documentation');
|
|
400
|
+
|
|
401
|
+
// Absolute paths are supported
|
|
402
|
+
server.router.addFolder('/cdn', '/var/www/cdn');
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
> [!WARNING]
|
|
406
|
+
> Never expose sensitive directories:
|
|
407
|
+
> - ❌ `server.router.addFolder('/', '.')` — Exposes the entire project
|
|
408
|
+
> - ❌ `server.router.addFolder('/', 'src')` — Exposes source code
|
|
409
|
+
>
|
|
410
|
+
> Exposed contents include:
|
|
411
|
+
> - Private certificate keys
|
|
412
|
+
> - Database credentials in configuration files
|
|
413
|
+
> - API tokens and secrets
|
|
414
|
+
> - Source code and internal structure
|
|
415
|
+
|
|
416
|
+
### Serving Files
|
|
417
|
+
|
|
418
|
+
Serve a single file at a specific route:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
server.router.addFile('/', 'public/index.html');
|
|
422
|
+
server.router.addFile('/sitemap.xml', 'public/sitemap.xml');
|
|
423
|
+
|
|
424
|
+
// Useful for SPAs - serve the same file for multiple routes
|
|
425
|
+
server.router.addFile('/app/*', 'public/app.html');
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Action Routes
|
|
429
|
+
|
|
430
|
+
Execute code to handle requests dynamically:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
// Simple JSON API
|
|
434
|
+
server.router.addAction('GET', '/api/status', (request, response) => {
|
|
435
|
+
response.sendJson({
|
|
436
|
+
status: 'online',
|
|
437
|
+
timestamp: new Date().toISOString()
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// With route parameters
|
|
442
|
+
server.router.addAction('GET', '/api/users/$id', (request, response) => {
|
|
443
|
+
const userId = request.ruleParams.id;
|
|
444
|
+
response.sendJson({ id: userId, name: 'User ' + userId });
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// With query parameters
|
|
448
|
+
server.router.addAction('GET', '/api/search', (request, response) => {
|
|
449
|
+
const query = request.searchParams.q;
|
|
450
|
+
response.sendJson({ results: [], query });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// POST with body
|
|
454
|
+
server.router.addAction('POST', '/api/data', async (request, response) => {
|
|
455
|
+
const body = await request.post;
|
|
456
|
+
response.sendJson({ received: body }, { status: 201 });
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Multiple methods
|
|
460
|
+
server.router.addAction('GET', '/resource/$id', getResourceHandler);
|
|
461
|
+
server.router.addAction('PUT', '/resource/$id', updateResourceHandler);
|
|
462
|
+
server.router.addAction('DELETE', '/resource/$id', deleteResourceHandler);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### WebSocket Routes
|
|
466
|
+
|
|
467
|
+
Handle real-time WebSocket connections:
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
const connections = new Set();
|
|
471
|
+
|
|
472
|
+
server.router.addWebsocket('/chat', (request, socket) => {
|
|
473
|
+
console.log('[WS] New connection from:', request.url);
|
|
474
|
+
|
|
475
|
+
// Notify others
|
|
476
|
+
connections.forEach(conn => {
|
|
477
|
+
conn.send('A user connected');
|
|
478
|
+
});
|
|
479
|
+
connections.add(socket);
|
|
480
|
+
|
|
481
|
+
// Handle incoming messages
|
|
482
|
+
socket.on('message', (data, info) => {
|
|
483
|
+
console.log('Message:', data.toString());
|
|
484
|
+
|
|
485
|
+
// Broadcast to all connections
|
|
486
|
+
connections.forEach(conn => {
|
|
487
|
+
if (conn !== socket) {
|
|
488
|
+
conn.send(data);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Handle disconnection
|
|
494
|
+
socket.on('finish', () => {
|
|
495
|
+
connections.delete(socket);
|
|
496
|
+
connections.forEach(conn => {
|
|
497
|
+
conn.send('A user disconnected');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Handle errors
|
|
502
|
+
socket.on('error', (error) => {
|
|
503
|
+
console.error('[WS-Error]:', error);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
> [!NOTE]
|
|
509
|
+
> WebSocket routes use a separate namespace from HTTP routes, so you can have `/api` as both an HTTP action and a WebSocket route without conflicts.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
140
513
|
## Server Configuration
|
|
141
514
|
|
|
142
|
-
|
|
515
|
+
### Configuration Methods
|
|
516
|
+
|
|
517
|
+
Configure the server using `server.config`:
|
|
143
518
|
|
|
144
519
|
```js
|
|
145
520
|
import Vortez from 'vortez';
|
|
146
521
|
const server = new Vortez();
|
|
147
522
|
|
|
148
|
-
|
|
149
|
-
server.config.
|
|
150
|
-
server.config.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
server.config.
|
|
155
|
-
|
|
523
|
+
// Set individual values
|
|
524
|
+
server.config.set('port', 3000);
|
|
525
|
+
server.config.set('host', '0.0.0.0');
|
|
526
|
+
|
|
527
|
+
// Set nested values
|
|
528
|
+
server.config.set('ssl.cert', 'path/to/cert.pem');
|
|
529
|
+
server.config.set('ssl.key', 'path/to/key.pem');
|
|
530
|
+
|
|
531
|
+
// Get values
|
|
532
|
+
const port = server.config.get('port');
|
|
533
|
+
const algorithm = server.config.get('routing.algorithm');
|
|
156
534
|
```
|
|
157
535
|
|
|
158
|
-
|
|
536
|
+
### Constructor Configuration
|
|
159
537
|
|
|
160
|
-
|
|
538
|
+
Pass configuration to the constructor:
|
|
161
539
|
|
|
162
540
|
```js
|
|
163
541
|
const server = new Vortez({
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
ssl:
|
|
542
|
+
host: '0.0.0.0',
|
|
543
|
+
port: 8080,
|
|
544
|
+
ssl: {
|
|
545
|
+
cert: 'path/to/cert.pem',
|
|
546
|
+
key: 'path/to/key.pem',
|
|
547
|
+
port: 443
|
|
548
|
+
}
|
|
167
549
|
});
|
|
168
550
|
```
|
|
169
551
|
|
|
170
|
-
|
|
552
|
+
### HTTPS/SSL Configuration
|
|
553
|
+
|
|
554
|
+
Enable HTTPS with SSL certificates:
|
|
171
555
|
|
|
172
556
|
```js
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
557
|
+
server.config.set('ssl', {
|
|
558
|
+
cert: 'path/to/certificate.pem',
|
|
559
|
+
key: 'path/to/private-key.pem',
|
|
560
|
+
port: 443 // Optional: HTTPS port (default: 443)
|
|
561
|
+
});
|
|
562
|
+
```
|
|
177
563
|
|
|
178
|
-
|
|
564
|
+
The framework will automatically create an HTTPS server alongside the HTTP server.
|
|
565
|
+
|
|
566
|
+
### Available Configuration Keys
|
|
567
|
+
|
|
568
|
+
```js
|
|
569
|
+
server.config.set('host', 'localhost'); // Server hostname
|
|
570
|
+
server.config.set('port', 80); // HTTP port
|
|
571
|
+
server.config.set('ssl.cert', 'cert.pem'); // SSL certificate path
|
|
572
|
+
server.config.set('ssl.key', 'key.pem'); // SSL private key path
|
|
573
|
+
server.config.set('ssl.port', 443); // HTTPS port
|
|
574
|
+
server.config.set('templates.error', 'error.html'); // Error page template
|
|
575
|
+
server.config.set('templates.folder', 'folder.html'); // Folder listing template
|
|
576
|
+
server.config.set('routing.algorithm', 'FIFO'); // Routing algorithm ('FIFO' or 'Tree')
|
|
179
577
|
```
|
|
180
578
|
|
|
181
579
|
---
|
|
182
580
|
|
|
183
|
-
##
|
|
581
|
+
## Real-World Example (ArtFolder Style)
|
|
582
|
+
|
|
583
|
+
This is a complete composition pattern based on the real-world project structure used in ArtFolder,
|
|
584
|
+
with separated routers for UI pages, API endpoints, and WebSocket endpoints.
|
|
585
|
+
|
|
586
|
+
```js
|
|
587
|
+
import Vortez, { ServerError, Template } from 'vortez';
|
|
588
|
+
|
|
589
|
+
const config = await Vortez.Config.Loader.load('.config.json');
|
|
590
|
+
const server = new Vortez(config);
|
|
184
591
|
|
|
185
|
-
|
|
592
|
+
const clientRouter = new Vortez.Router();
|
|
593
|
+
const apiRouter = new Vortez.Router();
|
|
594
|
+
const socketRouter = new Vortez.Router();
|
|
595
|
+
|
|
596
|
+
// -------------------------
|
|
597
|
+
// Shared API middleware
|
|
598
|
+
// -------------------------
|
|
599
|
+
apiRouter.httpMiddleware.useError((error, request, response, next, state) => {
|
|
600
|
+
if (error instanceof ServerError) {
|
|
601
|
+
return response.sendJson({ error: error.message }, { status: error.status });
|
|
602
|
+
}
|
|
603
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
604
|
+
return response.sendJson({ error: message }, { status: 500 });
|
|
605
|
+
});
|
|
186
606
|
|
|
187
|
-
|
|
607
|
+
apiRouter.httpMiddleware.use(async (request, response, next, state) => {
|
|
608
|
+
const page = Number(request.searchParams.page ?? '1');
|
|
609
|
+
const limit = Number(request.searchParams.limit ?? '20');
|
|
610
|
+
if (!Number.isFinite(page) || !Number.isFinite(limit)) {
|
|
611
|
+
throw new ServerError('Invalid pagination params', 400);
|
|
612
|
+
}
|
|
613
|
+
state.page = page;
|
|
614
|
+
state.limit = limit;
|
|
615
|
+
return next();
|
|
616
|
+
});
|
|
188
617
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
618
|
+
// -------------------------
|
|
619
|
+
// Client router (SSR + static)
|
|
620
|
+
// -------------------------
|
|
621
|
+
clientRouter.addAction('GET', '/', async (request, response) => {
|
|
622
|
+
const importMap = await Template.load('assets/importmap.json', {});
|
|
623
|
+
await response.sendTemplate('assets/app.html', {
|
|
624
|
+
title: 'Art Folder',
|
|
625
|
+
style: '/client/styles/styles.css',
|
|
626
|
+
logic: '/client/logic/build/logic.js',
|
|
627
|
+
importMap
|
|
628
|
+
});
|
|
629
|
+
});
|
|
192
630
|
|
|
193
|
-
|
|
631
|
+
clientRouter.addAction('GET', '/app/*', async (request, response) => {
|
|
632
|
+
const importMap = await Template.load('assets/importmap.json', {});
|
|
633
|
+
await response.sendTemplate('assets/app.html', {
|
|
634
|
+
title: 'Art Folder',
|
|
635
|
+
style: '/client/styles/styles.css',
|
|
636
|
+
logic: '/client/logic/build/logic.js',
|
|
637
|
+
importMap
|
|
638
|
+
});
|
|
639
|
+
});
|
|
194
640
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* `/blog/$category/$postId` — Matches `/blog/tech/42`, capturing `tech` as `category` and `42` as `postId`.
|
|
641
|
+
clientRouter.addFolder('/client', 'client');
|
|
642
|
+
clientRouter.addFile('/favicon.ico', 'client/source/images/Mochis.gif');
|
|
198
643
|
|
|
199
|
-
|
|
644
|
+
// -------------------------
|
|
645
|
+
// API router
|
|
646
|
+
// -------------------------
|
|
647
|
+
apiRouter.addAction('GET', '/health', (request, response, state) => {
|
|
648
|
+
response.sendJson({ ok: true, page: state.page, limit: state.limit });
|
|
649
|
+
});
|
|
200
650
|
|
|
201
|
-
|
|
651
|
+
apiRouter.addAction('POST', '/echo', async (request, response) => {
|
|
652
|
+
const body = await request.post;
|
|
653
|
+
response.sendJson({ received: body }, { status: 201 });
|
|
654
|
+
});
|
|
202
655
|
|
|
203
|
-
|
|
656
|
+
apiRouter.addAction('GET', '/user/$uuid', (request, response) => {
|
|
657
|
+
response.sendJson({ userId: request.ruleParams.uuid });
|
|
658
|
+
});
|
|
204
659
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
| [File](#file) | Serves a single file |
|
|
209
|
-
| [Action](#action) | Lets you handle requests programmatically |
|
|
210
|
-
| [WebSocket](#websocket) | Allows managing WebSocket connections on a given route |
|
|
660
|
+
apiRouter.addAction('GET', '*', (request) => {
|
|
661
|
+
throw new ServerError(`No route found for ${request.method} -> ${request.url}`, 404);
|
|
662
|
+
});
|
|
211
663
|
|
|
212
|
-
|
|
664
|
+
// -------------------------
|
|
665
|
+
// WebSocket router
|
|
666
|
+
// -------------------------
|
|
667
|
+
socketRouter.addWebsocket('/user/$uuid', (request, socket) => {
|
|
668
|
+
socket.sendJson({
|
|
669
|
+
type: 'welcome',
|
|
670
|
+
uuid: request.ruleParams.uuid,
|
|
671
|
+
queryUuid: request.searchParams.uuid
|
|
672
|
+
});
|
|
213
673
|
|
|
214
|
-
|
|
674
|
+
socket.on('message', (data) => {
|
|
675
|
+
socket.sendJson({ type: 'echo', data: data.toString('utf8') });
|
|
676
|
+
});
|
|
677
|
+
});
|
|
215
678
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
679
|
+
// Mount everything
|
|
680
|
+
server.router.mount(clientRouter);
|
|
681
|
+
server.router.mount(apiRouter, '/api');
|
|
682
|
+
server.router.mount(socketRouter, '/rtc');
|
|
683
|
+
|
|
684
|
+
await server.start();
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
This pattern keeps each concern isolated and mirrors how larger applications organize routes in production.
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Use Cases
|
|
692
|
+
|
|
693
|
+
### Static Website
|
|
694
|
+
|
|
695
|
+
Simple static site with assets:
|
|
229
696
|
|
|
230
697
|
```js
|
|
231
|
-
|
|
232
|
-
|
|
698
|
+
import Vortez from 'vortez';
|
|
699
|
+
|
|
700
|
+
const server = new Vortez();
|
|
701
|
+
|
|
702
|
+
server.router.addFile('/', 'public/index.html');
|
|
703
|
+
server.router.addFolder('/assets', 'public');
|
|
704
|
+
|
|
705
|
+
await server.start();
|
|
233
706
|
```
|
|
234
707
|
|
|
235
|
-
###
|
|
708
|
+
### Full-Stack Application
|
|
236
709
|
|
|
237
|
-
|
|
710
|
+
Combine API endpoints with static frontend:
|
|
238
711
|
|
|
239
712
|
```js
|
|
240
|
-
|
|
241
|
-
|
|
713
|
+
import Vortez from 'vortez';
|
|
714
|
+
|
|
715
|
+
const server = new Vortez();
|
|
716
|
+
|
|
717
|
+
// API
|
|
718
|
+
server.router.addAction('GET', '/api/posts', getPosts);
|
|
719
|
+
server.router.addAction('POST', '/api/posts', createPost);
|
|
720
|
+
|
|
721
|
+
// Frontend
|
|
722
|
+
server.router.addFile('/', 'public/index.html');
|
|
723
|
+
server.router.addFolder('/assets', 'public');
|
|
724
|
+
|
|
725
|
+
await server.start();
|
|
242
726
|
```
|
|
243
727
|
|
|
244
|
-
###
|
|
728
|
+
### Single Page Application
|
|
245
729
|
|
|
246
|
-
|
|
730
|
+
Serve a client-side routed application shell:
|
|
247
731
|
|
|
248
732
|
```js
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
733
|
+
import Vortez from 'vortez';
|
|
734
|
+
|
|
735
|
+
const server = new Vortez();
|
|
736
|
+
|
|
737
|
+
server.router.addFile('/', 'public/app.html');
|
|
738
|
+
server.router.addFile('/app/*', 'public/app.html');
|
|
739
|
+
server.router.addFolder('/assets', 'public');
|
|
740
|
+
|
|
741
|
+
server.router.addAction('GET', '/api/status', (request, response) => {
|
|
742
|
+
response.sendJson({ status: 'ok' });
|
|
254
743
|
});
|
|
744
|
+
|
|
745
|
+
await server.start();
|
|
255
746
|
```
|
|
256
747
|
|
|
257
|
-
###
|
|
748
|
+
### Real-Time Chat Application
|
|
258
749
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
> [!NOTE]
|
|
262
|
-
> WebSocket URLs use a separate namespace from Files, Folders, and Actions,
|
|
263
|
-
> so they won’t conflict even if they share the same route patterns.
|
|
750
|
+
WebSocket-powered chat:
|
|
264
751
|
|
|
265
752
|
```js
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
server.addWebSocket('/Test/WS-Chat', (request, socket) => {
|
|
269
|
-
console.log('[WS] New connection');
|
|
270
|
-
connections.forEach(user => user.Send('A user has connected.'));
|
|
271
|
-
connections.add(socket);
|
|
753
|
+
import Vortez from 'vortez';
|
|
272
754
|
|
|
273
|
-
|
|
274
|
-
|
|
755
|
+
const server = new Vortez();
|
|
756
|
+
const clients = new Set();
|
|
275
757
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
} else if (info.opCode === 8) {
|
|
283
|
-
connections.forEach(user => user.Send('A user has disconnected.'));
|
|
284
|
-
}
|
|
758
|
+
server.router.addWebsocket('/chat', (request, socket) => {
|
|
759
|
+
clients.add(socket);
|
|
760
|
+
|
|
761
|
+
socket.on('message', (data) => {
|
|
762
|
+
clients.forEach(client => client.send(data));
|
|
285
763
|
});
|
|
764
|
+
|
|
765
|
+
socket.on('finish', () => clients.delete(socket));
|
|
286
766
|
});
|
|
767
|
+
|
|
768
|
+
server.router.addFile('/', 'public/index.html');
|
|
769
|
+
await server.start();
|
|
287
770
|
```
|
|
288
771
|
|
|
289
772
|
---
|
|
290
773
|
|
|
291
|
-
# Development
|
|
774
|
+
# Development Features
|
|
292
775
|
|
|
293
|
-
##
|
|
776
|
+
## Beta Features
|
|
294
777
|
|
|
295
|
-
The
|
|
778
|
+
The development version includes experimental functionality:
|
|
296
779
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
780
|
+
```console
|
|
781
|
+
npm install vortez@dev
|
|
782
|
+
```
|
|
300
783
|
|
|
301
|
-
|
|
784
|
+
Access beta features:
|
|
302
785
|
|
|
303
|
-
|
|
786
|
+
```js
|
|
787
|
+
import { Beta } from 'vortez';
|
|
788
|
+
const { Mail, JwtManager } = Beta;
|
|
304
789
|
|
|
305
|
-
|
|
306
|
-
|
|
790
|
+
// JWT token management
|
|
791
|
+
const jwt = new JwtManager({ alg: 'HS256', key: 'secret' });
|
|
792
|
+
const token = jwt.sign({ userId: 1 });
|
|
793
|
+
|
|
794
|
+
// Email functionality
|
|
795
|
+
const mailer = new Mail({
|
|
796
|
+
host: 'smtp.example.com',
|
|
797
|
+
port: 587,
|
|
798
|
+
username: 'user',
|
|
799
|
+
password: 'secret',
|
|
800
|
+
email: 'user@example.com',
|
|
801
|
+
useStartTLS: true
|
|
802
|
+
});
|
|
307
803
|
```
|
|
308
804
|
|
|
309
805
|
> [!WARNING]
|
|
310
|
-
>
|
|
311
|
-
> It includes the latest features that may not yet be fully tested.
|
|
806
|
+
> Beta features are experimental and may change significantly. Use with caution in production.
|
|
312
807
|
|
|
313
|
-
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## Contributing
|
|
811
|
+
|
|
812
|
+
Contributions are welcome! Feel free to submit issues and pull requests on the repository.
|
|
813
|
+
|
|
814
|
+
## License
|
|
815
|
+
|
|
816
|
+
Licensed under the terms specified in the [LICENSE](LICENSE) file.
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Support
|
|
821
|
+
|
|
822
|
+
For questions, issues, or feature requests, please open an issue on the GitHub repository.
|
|
314
823
|
|
|
315
|
-
```js
|
|
316
|
-
import { Beta } from 'vortez';
|
|
317
|
-
const { Mail, JwtManager } = Beta;
|
|
318
|
-
```
|