@simplysm/service-server 13.0.0-beta.28 → 13.0.0-beta.30
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 +53 -646
- package/docs/authentication.md +109 -0
- package/docs/built-in-services.md +172 -0
- package/docs/server.md +274 -0
- package/docs/transport.md +154 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -16,62 +16,46 @@ pnpm add @simplysm/service-server
|
|
|
16
16
|
|
|
17
17
|
### Core Classes
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| `ServiceBase` | Service base abstract class. All custom services must inherit from this |
|
|
23
|
-
| `ServiceExecutor` | Internal executor that handles service method discovery, auth checks, and execution |
|
|
19
|
+
- [`ServiceServer`](docs/server.md#serviceserver) - Main server class. Creates Fastify instance and configures routes/plugins
|
|
20
|
+
- [`ServiceBase`](docs/server.md#custom-service-definition) - Service base abstract class. All custom services must inherit from this
|
|
21
|
+
- `ServiceExecutor` - Internal executor that handles service method discovery, auth checks, and execution
|
|
24
22
|
|
|
25
23
|
### Authentication
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| `getAuthPermissions` | Queries auth permissions for a service class/method (used internally by `ServiceExecutor`) |
|
|
32
|
-
| `AuthTokenPayload` | JWT payload interface (includes `roles`, `data`) |
|
|
25
|
+
- [`Authorize`](docs/authentication.md#authorize-decorator) - Stage 3 decorator. Sets authentication permissions at class or method level
|
|
26
|
+
- [`JwtManager`](docs/authentication.md#jwtmanager) - JWT token generation/verification/decoding based on jose library (HS256, 12-hour expiration)
|
|
27
|
+
- [`getAuthPermissions`](docs/authentication.md#getauthpermissions) - Queries auth permissions for a service class/method (used internally by `ServiceExecutor`)
|
|
28
|
+
- [`AuthTokenPayload`](docs/authentication.md#authtokenpayload) - JWT payload interface (includes `roles`, `data`)
|
|
33
29
|
|
|
34
30
|
### Transport Layer - WebSocket
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| `WebSocketHandler` | Handles WebSocket connection management, message routing, and event distribution |
|
|
39
|
-
| `ServiceSocket` | Wraps individual WebSocket connections. Manages ping/pong, protocol encoding/decoding, event listener management |
|
|
32
|
+
- `WebSocketHandler` - Handles WebSocket connection management, message routing, and event distribution
|
|
33
|
+
- [`ServiceSocket`](docs/transport.md#servicesocket) - Wraps individual WebSocket connections. Manages ping/pong, protocol encoding/decoding, event listener management
|
|
40
34
|
|
|
41
35
|
### Transport Layer - HTTP
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
| `UploadHandler` | Handles multipart file upload at `/upload` route (auth required) |
|
|
47
|
-
| `StaticFileHandler` | Serves static files. Prevents path traversal and blocks hidden files |
|
|
37
|
+
- `HttpRequestHandler` - Calls service methods via HTTP at `/api/:service/:method` route
|
|
38
|
+
- [`UploadHandler`](docs/transport.md#file-upload) - Handles multipart file upload at `/upload` route (auth required)
|
|
39
|
+
- `StaticFileHandler` - Serves static files. Prevents path traversal and blocks hidden files
|
|
48
40
|
|
|
49
41
|
### Protocol
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|--------|------|
|
|
53
|
-
| `ProtocolWrapper` | Message encoding/decoding wrapper. Messages over 30KB are processed in worker threads |
|
|
43
|
+
- [`ProtocolWrapper`](docs/transport.md#protocolwrapper) - Message encoding/decoding wrapper. Messages over 30KB are processed in worker threads
|
|
54
44
|
|
|
55
45
|
### Built-in Services
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `SmtpService` | nodemailer-based email sending |
|
|
62
|
-
| `AutoUpdateService` | App auto-update (provides latest version query and download path) |
|
|
47
|
+
- [`OrmService`](docs/built-in-services.md#ormservice) - DB connection/transaction/query execution (WebSocket only, auth required)
|
|
48
|
+
- [`CryptoService`](docs/built-in-services.md#cryptoservice) - SHA256 hash and AES-256-CBC encryption/decryption
|
|
49
|
+
- [`SmtpService`](docs/built-in-services.md#smtpservice) - nodemailer-based email sending
|
|
50
|
+
- [`AutoUpdateService`](docs/built-in-services.md#autoupdateservice) - App auto-update (provides latest version query and download path)
|
|
63
51
|
|
|
64
52
|
### Utilities
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
|--------|------|
|
|
68
|
-
| `ConfigManager` | JSON config file loading/caching/real-time monitoring (auto expiration based on LazyGcMap) |
|
|
54
|
+
- [`ConfigManager`](docs/server.md#configmanager) - JSON config file loading/caching/real-time monitoring (auto expiration based on LazyGcMap)
|
|
69
55
|
|
|
70
56
|
### Legacy
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|--------|------|
|
|
74
|
-
| `handleV1Connection` | V1 protocol client compatibility handling (supports auto-update only) |
|
|
58
|
+
- [`handleV1Connection`](docs/transport.md#legacy-handlev1connection) - V1 protocol client compatibility handling (supports auto-update only)
|
|
75
59
|
|
|
76
60
|
## Usage
|
|
77
61
|
|
|
@@ -102,97 +86,13 @@ server.on("close", () => {
|
|
|
102
86
|
await server.close();
|
|
103
87
|
```
|
|
104
88
|
|
|
105
|
-
###
|
|
89
|
+
### Server Options
|
|
106
90
|
|
|
107
|
-
|
|
91
|
+
See [`ServiceServerOptions`](docs/server.md#server-options-serviceserveroptions) for detailed configuration options including SSL, authentication, and directory structure.
|
|
108
92
|
|
|
109
|
-
|
|
93
|
+
### Custom Services
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|----------|------|------|
|
|
113
|
-
| `options` | `ServiceServerOptions` | Server configuration (read-only, passed via constructor) |
|
|
114
|
-
| `isOpen` | `boolean` | Whether the server is currently listening |
|
|
115
|
-
| `fastify` | `FastifyInstance` | Underlying Fastify instance (read-only, for advanced use) |
|
|
116
|
-
|
|
117
|
-
**Methods:**
|
|
118
|
-
|
|
119
|
-
| Method | Returns | Description |
|
|
120
|
-
|--------|---------|------|
|
|
121
|
-
| `listen()` | `Promise<void>` | Register all plugins/routes and start listening |
|
|
122
|
-
| `close()` | `Promise<void>` | Close all WebSocket connections and shut down the server |
|
|
123
|
-
| `generateAuthToken(payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
|
|
124
|
-
| `verifyAuthToken(token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify and decode a JWT token |
|
|
125
|
-
| `emitEvent(eventType, infoSelector, data)` | `Promise<void>` | Publish an event to matching WebSocket clients |
|
|
126
|
-
| `broadcastReload(clientName, changedFileSet)` | `Promise<void>` | Send a reload command to all connected clients |
|
|
127
|
-
|
|
128
|
-
**Events:**
|
|
129
|
-
|
|
130
|
-
| Event | Payload | Description |
|
|
131
|
-
|-------|---------|------|
|
|
132
|
-
| `ready` | `void` | Emitted when the server starts listening |
|
|
133
|
-
| `close` | `void` | Emitted when the server is closed |
|
|
134
|
-
|
|
135
|
-
### Server Options (`ServiceServerOptions`)
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
import type { ServiceServerOptions } from "@simplysm/service-server";
|
|
139
|
-
|
|
140
|
-
interface ServiceServerOptions {
|
|
141
|
-
/** Server root path (base directory for static files and config files) */
|
|
142
|
-
rootPath: string;
|
|
143
|
-
/** Listen port */
|
|
144
|
-
port: number;
|
|
145
|
-
/** SSL/TLS config (enables HTTPS) */
|
|
146
|
-
ssl?: {
|
|
147
|
-
pfxBytes: Uint8Array;
|
|
148
|
-
passphrase: string;
|
|
149
|
-
};
|
|
150
|
-
/** JWT authentication config */
|
|
151
|
-
auth?: {
|
|
152
|
-
jwtSecret: string;
|
|
153
|
-
};
|
|
154
|
-
/** List of service classes to register */
|
|
155
|
-
services: Type<ServiceBase>[];
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
The following structure is expected under `rootPath`:
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
rootPath/
|
|
163
|
-
.config.json # Root config file
|
|
164
|
-
www/ # Static file root
|
|
165
|
-
uploads/ # Upload file storage directory
|
|
166
|
-
{clientName}/ # Per-client directory
|
|
167
|
-
.config.json # Per-client config file
|
|
168
|
-
index.html
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### SSL/HTTPS Server
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
import { ServiceServer } from "@simplysm/service-server";
|
|
175
|
-
import { fsReadFile } from "@simplysm/core-node";
|
|
176
|
-
|
|
177
|
-
const pfxBytes = await fsReadFile("/path/to/cert.pfx");
|
|
178
|
-
|
|
179
|
-
const server = new ServiceServer({
|
|
180
|
-
port: 443,
|
|
181
|
-
rootPath: "/app/data",
|
|
182
|
-
ssl: {
|
|
183
|
-
pfxBytes,
|
|
184
|
-
passphrase: "certificate-password",
|
|
185
|
-
},
|
|
186
|
-
auth: { jwtSecret: "my-secret-key" },
|
|
187
|
-
services: [],
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
await server.listen();
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Custom Service Definition
|
|
194
|
-
|
|
195
|
-
Define services by inheriting from `ServiceBase`. Service methods are called via RPC from the client.
|
|
95
|
+
Services are defined by inheriting from `ServiceBase`. Service methods are called via RPC from the client.
|
|
196
96
|
|
|
197
97
|
```typescript
|
|
198
98
|
import { ServiceBase } from "@simplysm/service-server";
|
|
@@ -208,261 +108,66 @@ class MyService extends ServiceBase {
|
|
|
208
108
|
}
|
|
209
109
|
```
|
|
210
110
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`ServiceBase<TAuthInfo>` is an abstract class. The generic `TAuthInfo` type represents the shape of the authenticated user's data stored in the JWT token.
|
|
214
|
-
|
|
215
|
-
| Property | Type | Description |
|
|
216
|
-
|----------|------|------|
|
|
217
|
-
| `this.server` | `ServiceServer<TAuthInfo>` | Server instance reference |
|
|
218
|
-
| `this.socket` | `ServiceSocket \| undefined` | WebSocket connection (`undefined` for HTTP calls) |
|
|
219
|
-
| `this.http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> } \| undefined` | HTTP request context |
|
|
220
|
-
| `this.authInfo` | `TAuthInfo \| undefined` | Authenticated user's custom data (from JWT `data` field) |
|
|
221
|
-
| `this.clientName` | `string \| undefined` | Client app name (validated against path traversal) |
|
|
222
|
-
| `this.clientPath` | `string \| undefined` | Resolved per-client directory path (`rootPath/www/{clientName}`) |
|
|
223
|
-
|
|
224
|
-
#### ServiceBase Methods
|
|
225
|
-
|
|
226
|
-
| Method | Returns | Description |
|
|
227
|
-
|--------|---------|------|
|
|
228
|
-
| `getConfig<T>(section)` | `Promise<T>` | Read a section from `.config.json` (root + client configs merged) |
|
|
229
|
-
|
|
230
|
-
### Config File Reference
|
|
231
|
-
|
|
232
|
-
Read sections from `.config.json` files using `ServiceBase.getConfig()`. Root and per-client configs are automatically merged.
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
import { ServiceBase } from "@simplysm/service-server";
|
|
236
|
-
|
|
237
|
-
class MyService extends ServiceBase {
|
|
238
|
-
async getDbHost(): Promise<string> {
|
|
239
|
-
// Read "mySection" key from rootPath/.config.json or clientPath/.config.json
|
|
240
|
-
const config = await this.getConfig<{ host: string }>("mySection");
|
|
241
|
-
return config.host;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
`.config.json` example:
|
|
247
|
-
|
|
248
|
-
```json
|
|
249
|
-
{
|
|
250
|
-
"mySection": {
|
|
251
|
-
"host": "localhost"
|
|
252
|
-
},
|
|
253
|
-
"orm": {
|
|
254
|
-
"default": {
|
|
255
|
-
"dialect": "mysql",
|
|
256
|
-
"host": "localhost",
|
|
257
|
-
"port": 3306,
|
|
258
|
-
"database": "mydb",
|
|
259
|
-
"user": "root",
|
|
260
|
-
"password": "password"
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
`ConfigManager` caches config files and automatically refreshes the cache on file changes (LazyGcMap-based, auto expires after 1 hour).
|
|
111
|
+
See [Custom Service Definition](docs/server.md#custom-service-definition) for more details on `ServiceBase` properties and methods.
|
|
267
112
|
|
|
268
|
-
### Authentication
|
|
113
|
+
### Authentication
|
|
269
114
|
|
|
270
|
-
Use
|
|
115
|
+
Use the `@Authorize()` decorator to set authentication requirements:
|
|
271
116
|
|
|
272
117
|
```typescript
|
|
273
118
|
import { ServiceBase, Authorize } from "@simplysm/service-server";
|
|
274
119
|
|
|
275
|
-
// Class level: all methods require login
|
|
276
120
|
@Authorize()
|
|
277
121
|
class UserService extends ServiceBase<{ userId: number; role: string }> {
|
|
278
|
-
// Login only required (inherits from class level)
|
|
279
122
|
async getProfile(): Promise<unknown> {
|
|
280
123
|
const userId = this.authInfo?.userId;
|
|
281
124
|
// ...
|
|
282
125
|
}
|
|
283
126
|
|
|
284
|
-
// Method level: specific role required (overrides class level)
|
|
285
127
|
@Authorize(["admin"])
|
|
286
128
|
async deleteUser(targetId: number): Promise<void> {
|
|
287
129
|
// Only users with admin role can call
|
|
288
130
|
}
|
|
289
131
|
}
|
|
290
|
-
|
|
291
|
-
// No authentication required (no decorator)
|
|
292
|
-
class PublicService extends ServiceBase {
|
|
293
|
-
async healthCheck(): Promise<string> {
|
|
294
|
-
return "OK";
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
132
|
```
|
|
298
133
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
| Target | `@Authorize()` | `@Authorize(["admin"])` |
|
|
302
|
-
|-----------|----------------|-------------------------|
|
|
303
|
-
| Class | All methods require login | All methods require admin role |
|
|
304
|
-
| Method | Method requires login | Method requires admin role |
|
|
305
|
-
| None | No auth required (Public) | - |
|
|
134
|
+
See [Authentication](docs/authentication.md) for JWT token management and permission handling.
|
|
306
135
|
|
|
307
|
-
|
|
136
|
+
### HTTP/WebSocket Communication
|
|
308
137
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
Query auth permissions for a given service class and method. Primarily used internally by `ServiceExecutor`, but exported for advanced use cases.
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
import { getAuthPermissions } from "@simplysm/service-server";
|
|
315
|
-
|
|
316
|
-
// Returns string[] if permissions are set, or undefined for public (no decorator)
|
|
317
|
-
const perms = getAuthPermissions(UserService, "deleteUser");
|
|
318
|
-
// ["admin"]
|
|
319
|
-
|
|
320
|
-
const classPerms = getAuthPermissions(UserService);
|
|
321
|
-
// [] (empty array = login required, no specific role)
|
|
322
|
-
|
|
323
|
-
const publicPerms = getAuthPermissions(PublicService, "healthCheck");
|
|
324
|
-
// undefined (no auth required)
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### JWT Token Management
|
|
328
|
-
|
|
329
|
-
#### JwtManager
|
|
330
|
-
|
|
331
|
-
`JwtManager<TAuthInfo>` handles JWT operations internally. Access its functionality through `ServiceServer` methods.
|
|
332
|
-
|
|
333
|
-
| Method | Returns | Description |
|
|
334
|
-
|--------|---------|------|
|
|
335
|
-
| `sign(payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
|
|
336
|
-
| `verify(token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify token signature and expiration, return payload |
|
|
337
|
-
| `decode(token)` | `AuthTokenPayload<TAuthInfo>` | Decode token without verification (synchronous) |
|
|
338
|
-
|
|
339
|
-
Generate and verify JWT tokens through the `ServiceServer` instance:
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
import { ServiceServer } from "@simplysm/service-server";
|
|
343
|
-
|
|
344
|
-
const server = new ServiceServer({
|
|
345
|
-
port: 8080,
|
|
346
|
-
rootPath: "/app/data",
|
|
347
|
-
auth: { jwtSecret: "my-secret-key" },
|
|
348
|
-
services: [],
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// Generate token (12-hour expiration, HS256 algorithm)
|
|
352
|
-
const token = await server.generateAuthToken({
|
|
353
|
-
roles: ["admin", "user"],
|
|
354
|
-
data: { userId: 1, name: "John" },
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Verify token
|
|
358
|
-
const payload = await server.verifyAuthToken(token);
|
|
359
|
-
// payload.roles: ["admin", "user"]
|
|
360
|
-
// payload.data: { userId: 1, name: "John" }
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
#### `AuthTokenPayload`
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
import type { AuthTokenPayload } from "@simplysm/service-server";
|
|
367
|
-
|
|
368
|
-
interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
|
|
369
|
-
/** User role list (used for permission check in Authorize decorator) */
|
|
370
|
-
roles: string[];
|
|
371
|
-
/** Custom auth info (generic type) */
|
|
372
|
-
data: TAuthInfo;
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### ServiceSocket
|
|
377
|
-
|
|
378
|
-
`ServiceSocket` extends `EventEmitter` and wraps an individual WebSocket connection. It is available in service methods as `this.socket` when the request comes via WebSocket.
|
|
379
|
-
|
|
380
|
-
**Properties:**
|
|
381
|
-
|
|
382
|
-
| Property | Type | Description |
|
|
383
|
-
|----------|------|------|
|
|
384
|
-
| `clientName` | `string` | Client app name (from WebSocket query parameter) |
|
|
385
|
-
| `connectedAtDateTime` | `DateTime` | Connection timestamp |
|
|
386
|
-
| `authTokenPayload` | `AuthTokenPayload \| undefined` | Authenticated token payload (set after `auth` message) |
|
|
387
|
-
| `connReq` | `FastifyRequest` | Original Fastify request that initiated the WebSocket upgrade |
|
|
388
|
-
|
|
389
|
-
**Methods:**
|
|
390
|
-
|
|
391
|
-
| Method | Returns | Description |
|
|
392
|
-
|--------|---------|------|
|
|
393
|
-
| `send(uuid, msg)` | `Promise<number>` | Send a message to this client. Returns total bytes sent |
|
|
394
|
-
| `close()` | `void` | Terminate the WebSocket connection |
|
|
395
|
-
| `addEventListener(key, eventName, info)` | `void` | Register an event listener for this socket |
|
|
396
|
-
| `removeEventListener(key)` | `void` | Remove an event listener by key |
|
|
397
|
-
| `getEventListeners(eventName)` | `{ key, info }[]` | Get all event listeners for a given event name |
|
|
398
|
-
|
|
399
|
-
**Events:**
|
|
400
|
-
|
|
401
|
-
| Event | Payload | Description |
|
|
402
|
-
|-------|---------|------|
|
|
403
|
-
| `error` | `Error` | WebSocket error occurred |
|
|
404
|
-
| `close` | `number` | Connection closed (payload is the close code) |
|
|
405
|
-
| `message` | `{ uuid: string; msg: ServiceClientMessage }` | Decoded message received from client |
|
|
406
|
-
|
|
407
|
-
### HTTP API Call
|
|
408
|
-
|
|
409
|
-
Service methods can also be called via HTTP through the `/api/:service/:method` path.
|
|
410
|
-
|
|
411
|
-
**GET Request:**
|
|
138
|
+
Service methods can be called via HTTP or WebSocket:
|
|
412
139
|
|
|
413
140
|
```
|
|
414
141
|
GET /api/MyService/hello?json=["World"]
|
|
415
|
-
Header: x-sd-client-name: my-app
|
|
416
|
-
Header: Authorization: Bearer <token> (optional)
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**POST Request:**
|
|
420
|
-
|
|
421
|
-
```
|
|
422
142
|
POST /api/MyService/hello
|
|
423
|
-
Header: Content-Type: application/json
|
|
424
|
-
Header: x-sd-client-name: my-app
|
|
425
|
-
Header: Authorization: Bearer <token> (optional)
|
|
426
|
-
Body: ["World"]
|
|
427
143
|
```
|
|
428
144
|
|
|
429
|
-
|
|
430
|
-
- Parameters are passed in array form (in the order of method arguments).
|
|
431
|
-
- For GET requests, pass a JSON-serialized array in the `json` query parameter.
|
|
145
|
+
See [HTTP API Call](docs/transport.md#http-api-call) and [ServiceSocket](docs/transport.md#servicesocket) for transport layer details.
|
|
432
146
|
|
|
433
147
|
### File Upload
|
|
434
148
|
|
|
435
|
-
Upload files via multipart request to the `/upload` endpoint
|
|
149
|
+
Upload files via multipart request to the `/upload` endpoint:
|
|
436
150
|
|
|
437
151
|
```typescript
|
|
438
|
-
// Client-side example
|
|
439
152
|
const formData = new FormData();
|
|
440
153
|
formData.append("file", file);
|
|
441
154
|
|
|
442
155
|
const response = await fetch("/upload", {
|
|
443
156
|
method: "POST",
|
|
444
|
-
headers: {
|
|
445
|
-
Authorization: `Bearer ${token}`,
|
|
446
|
-
},
|
|
157
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
447
158
|
body: formData,
|
|
448
159
|
});
|
|
449
|
-
|
|
450
|
-
// Response: ServiceUploadResult[]
|
|
451
|
-
const results = await response.json();
|
|
452
|
-
// [{ path: "uploads/uuid.ext", filename: "original-filename.ext", size: 12345 }]
|
|
453
160
|
```
|
|
454
161
|
|
|
455
|
-
|
|
162
|
+
See [File Upload](docs/transport.md#file-upload) for more details.
|
|
456
163
|
|
|
457
|
-
###
|
|
164
|
+
### Event Publishing
|
|
458
165
|
|
|
459
|
-
Publish events to connected clients
|
|
166
|
+
Publish real-time events to connected WebSocket clients:
|
|
460
167
|
|
|
461
168
|
```typescript
|
|
462
|
-
import { ServiceServer } from "@simplysm/service-server";
|
|
463
169
|
import { ServiceEventListener } from "@simplysm/service-common";
|
|
464
170
|
|
|
465
|
-
// Event definition (from service-common)
|
|
466
171
|
class OrderUpdatedEvent extends ServiceEventListener<
|
|
467
172
|
{ orderId: number },
|
|
468
173
|
{ status: string }
|
|
@@ -470,342 +175,44 @@ class OrderUpdatedEvent extends ServiceEventListener<
|
|
|
470
175
|
readonly eventName = "OrderUpdatedEvent";
|
|
471
176
|
}
|
|
472
177
|
|
|
473
|
-
// Publish event from server
|
|
474
178
|
await server.emitEvent(
|
|
475
179
|
OrderUpdatedEvent,
|
|
476
|
-
(info) => info.orderId === 123,
|
|
477
|
-
{ status: "completed" },
|
|
180
|
+
(info) => info.orderId === 123,
|
|
181
|
+
{ status: "completed" },
|
|
478
182
|
);
|
|
479
|
-
|
|
480
|
-
// Send reload command to all clients
|
|
481
|
-
await server.broadcastReload("my-app", new Set(["main.js"]));
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### Built-in Service: OrmService
|
|
485
|
-
|
|
486
|
-
Provides database connection/query/transaction via WebSocket. `@Authorize()` decorator is applied, requiring login.
|
|
487
|
-
|
|
488
|
-
```typescript
|
|
489
|
-
import { ServiceServer, OrmService } from "@simplysm/service-server";
|
|
490
|
-
|
|
491
|
-
const server = new ServiceServer({
|
|
492
|
-
port: 8080,
|
|
493
|
-
rootPath: "/app/data",
|
|
494
|
-
auth: { jwtSecret: "secret" },
|
|
495
|
-
services: [OrmService],
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
Define ORM config in `.config.json`:
|
|
500
|
-
|
|
501
|
-
```json
|
|
502
|
-
{
|
|
503
|
-
"orm": {
|
|
504
|
-
"default": {
|
|
505
|
-
"dialect": "mysql",
|
|
506
|
-
"host": "localhost",
|
|
507
|
-
"port": 3306,
|
|
508
|
-
"database": "mydb",
|
|
509
|
-
"user": "root",
|
|
510
|
-
"password": "password"
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Methods provided by `OrmService`:
|
|
517
|
-
|
|
518
|
-
| Method | Returns | Description |
|
|
519
|
-
|--------|---------|------|
|
|
520
|
-
| `getInfo(opt)` | `Promise<{ dialect, database?, schema? }>` | Query DB connection info |
|
|
521
|
-
| `connect(opt)` | `Promise<number>` | Create DB connection. Returns connection ID |
|
|
522
|
-
| `close(connId)` | `Promise<void>` | Close DB connection |
|
|
523
|
-
| `beginTransaction(connId, isolationLevel?)` | `Promise<void>` | Begin transaction |
|
|
524
|
-
| `commitTransaction(connId)` | `Promise<void>` | Commit transaction |
|
|
525
|
-
| `rollbackTransaction(connId)` | `Promise<void>` | Rollback transaction |
|
|
526
|
-
| `executeParametrized(connId, query, params?)` | `Promise<unknown[][]>` | Execute parameterized query |
|
|
527
|
-
| `executeDefs(connId, defs, options?)` | `Promise<unknown[][]>` | Execute QueryDef-based queries |
|
|
528
|
-
| `bulkInsert(connId, tableName, columnDefs, records)` | `Promise<void>` | Bulk INSERT |
|
|
529
|
-
|
|
530
|
-
When a WebSocket connection is closed, all DB connections opened from that socket are automatically cleaned up.
|
|
531
|
-
|
|
532
|
-
### Built-in Service: CryptoService
|
|
533
|
-
|
|
534
|
-
Provides SHA256 hash and AES-256-CBC symmetric key encryption/decryption.
|
|
535
|
-
|
|
536
|
-
```typescript
|
|
537
|
-
import { ServiceServer, CryptoService } from "@simplysm/service-server";
|
|
538
|
-
|
|
539
|
-
const server = new ServiceServer({
|
|
540
|
-
port: 8080,
|
|
541
|
-
rootPath: "/app/data",
|
|
542
|
-
services: [CryptoService],
|
|
543
|
-
});
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
`.config.json` config:
|
|
547
|
-
|
|
548
|
-
```json
|
|
549
|
-
{
|
|
550
|
-
"crypto": {
|
|
551
|
-
"key": "your-32-byte-secret-key-here!!"
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
| Method | Returns | Description |
|
|
557
|
-
|--------|---------|------|
|
|
558
|
-
| `encrypt(data)` | `Promise<string>` | Generate SHA256 HMAC hash (one-way). `data` is `string \| Uint8Array` |
|
|
559
|
-
| `encryptAes(data)` | `Promise<string>` | AES-256-CBC encryption. `data` is `Uint8Array`. Returns hex string in `iv:encrypted` format |
|
|
560
|
-
| `decryptAes(encText)` | `Promise<Uint8Array>` | AES-256-CBC decryption. Returns original binary |
|
|
561
|
-
|
|
562
|
-
### Built-in Service: SmtpService
|
|
563
|
-
|
|
564
|
-
A nodemailer-based email sending service. Can pass SMTP config directly or reference server config file.
|
|
565
|
-
|
|
566
|
-
```typescript
|
|
567
|
-
import { ServiceServer, SmtpService } from "@simplysm/service-server";
|
|
568
|
-
|
|
569
|
-
const server = new ServiceServer({
|
|
570
|
-
port: 8080,
|
|
571
|
-
rootPath: "/app/data",
|
|
572
|
-
services: [SmtpService],
|
|
573
|
-
});
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
`.config.json` config (when using config reference method):
|
|
577
|
-
|
|
578
|
-
```json
|
|
579
|
-
{
|
|
580
|
-
"smtp": {
|
|
581
|
-
"default": {
|
|
582
|
-
"host": "smtp.example.com",
|
|
583
|
-
"port": 587,
|
|
584
|
-
"secure": false,
|
|
585
|
-
"user": "user@example.com",
|
|
586
|
-
"pass": "password",
|
|
587
|
-
"senderName": "My App",
|
|
588
|
-
"senderEmail": "noreply@example.com"
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
| Method | Returns | Description |
|
|
595
|
-
|--------|---------|------|
|
|
596
|
-
| `send(options)` | `Promise<string>` | Send email by directly passing SMTP config. Returns message ID |
|
|
597
|
-
| `sendByConfig(configName, options)` | `Promise<string>` | Send email by referencing SMTP config in config file. Returns message ID |
|
|
598
|
-
|
|
599
|
-
`send()` options:
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
interface SmtpSendOption {
|
|
603
|
-
host: string;
|
|
604
|
-
port?: number;
|
|
605
|
-
secure?: boolean;
|
|
606
|
-
user?: string;
|
|
607
|
-
pass?: string;
|
|
608
|
-
from: string;
|
|
609
|
-
to: string;
|
|
610
|
-
cc?: string;
|
|
611
|
-
bcc?: string;
|
|
612
|
-
subject: string;
|
|
613
|
-
html: string;
|
|
614
|
-
attachments?: SmtpSendAttachment[];
|
|
615
|
-
}
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Built-in Service: AutoUpdateService
|
|
619
|
-
|
|
620
|
-
Supports auto-update for client apps. Searches for latest version files by platform in the client directory.
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
import { ServiceServer, AutoUpdateService } from "@simplysm/service-server";
|
|
624
|
-
|
|
625
|
-
const server = new ServiceServer({
|
|
626
|
-
port: 8080,
|
|
627
|
-
rootPath: "/app/data",
|
|
628
|
-
services: [AutoUpdateService],
|
|
629
|
-
});
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
Update file structure:
|
|
633
|
-
|
|
634
|
-
```
|
|
635
|
-
rootPath/www/{clientName}/{platform}/updates/
|
|
636
|
-
1.0.0.exe (Windows)
|
|
637
|
-
1.0.1.exe
|
|
638
|
-
1.0.0.apk (Android)
|
|
639
|
-
1.0.1.apk
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
| Method | Returns | Description |
|
|
643
|
-
|--------|---------|------|
|
|
644
|
-
| `getLastVersion(platform)` | `Promise<{ version: string; downloadPath: string } \| undefined>` | Returns latest version and download path for the platform. Returns `undefined` if no update |
|
|
645
|
-
|
|
646
|
-
Return value example:
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
{
|
|
650
|
-
version: "1.0.1",
|
|
651
|
-
downloadPath: "/my-app/android/updates/1.0.1.apk",
|
|
652
|
-
}
|
|
653
183
|
```
|
|
654
184
|
|
|
655
|
-
|
|
185
|
+
See [Real-time Event Publishing](docs/transport.md#real-time-event-publishing) for more details.
|
|
656
186
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
import { ConfigManager } from "@simplysm/service-server";
|
|
661
|
-
|
|
662
|
-
// Returns undefined if the file does not exist
|
|
663
|
-
const config = await ConfigManager.getConfig<MyConfig>("/path/to/.config.json");
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
| Method | Returns | Description |
|
|
667
|
-
|--------|---------|------|
|
|
668
|
-
| `ConfigManager.getConfig<T>(filePath)` | `Promise<T \| undefined>` | Load and cache a JSON config file. Returns `undefined` if file not found |
|
|
669
|
-
|
|
670
|
-
Behavior:
|
|
671
|
-
- Caches file in `LazyGcMap` on first load.
|
|
672
|
-
- Registers file change watch (`FsWatcher`) to auto-refresh cache on changes.
|
|
673
|
-
- Cache auto-expires after 1 hour of no access, and associated watch is released.
|
|
674
|
-
- GC runs every 10 minutes to check for expired entries.
|
|
675
|
-
|
|
676
|
-
### ProtocolWrapper
|
|
677
|
-
|
|
678
|
-
Handles encoding/decoding of WebSocket messages. Automatically branches between main thread and worker thread based on message size.
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
import { ProtocolWrapper } from "@simplysm/service-server";
|
|
682
|
-
|
|
683
|
-
const protocol = new ProtocolWrapper();
|
|
684
|
-
|
|
685
|
-
// Encode a message into chunks
|
|
686
|
-
const { chunks, totalSize } = await protocol.encode(uuid, message);
|
|
687
|
-
|
|
688
|
-
// Decode received bytes
|
|
689
|
-
const result = await protocol.decode(bytes);
|
|
690
|
-
|
|
691
|
-
// Clean up
|
|
692
|
-
protocol.dispose();
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
| Method | Returns | Description |
|
|
696
|
-
|--------|---------|------|
|
|
697
|
-
| `encode(uuid, message)` | `Promise<{ chunks: Uint8Array[]; totalSize: number }>` | Encode a message into transmittable chunks |
|
|
698
|
-
| `decode(bytes)` | `Promise<ServiceMessageDecodeResult>` | Decode received bytes into a message |
|
|
699
|
-
| `dispose()` | `void` | Clean up internal protocol resources |
|
|
700
|
-
|
|
701
|
-
Worker thread branching:
|
|
702
|
-
|
|
703
|
-
| Condition | Processing Method |
|
|
704
|
-
|------|-----------|
|
|
705
|
-
| 30KB or less | Processed directly in main thread |
|
|
706
|
-
| Over 30KB | Processed in worker thread (max 4GB memory allocation) |
|
|
707
|
-
|
|
708
|
-
Messages containing large binary data (Uint8Array) also branch to worker thread.
|
|
709
|
-
|
|
710
|
-
### Legacy: handleV1Connection
|
|
711
|
-
|
|
712
|
-
Handles V1 protocol WebSocket clients. Only supports the `SdAutoUpdateService.getLastVersion` command. All other requests return an upgrade-required error.
|
|
187
|
+
### Built-in Services
|
|
713
188
|
|
|
714
|
-
|
|
715
|
-
import { handleV1Connection, AutoUpdateService } from "@simplysm/service-server";
|
|
189
|
+
The package provides several built-in services:
|
|
716
190
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
191
|
+
- [`OrmService`](docs/built-in-services.md#ormservice) - Database operations (MySQL, MSSQL, PostgreSQL)
|
|
192
|
+
- [`CryptoService`](docs/built-in-services.md#cryptoservice) - Hashing and encryption
|
|
193
|
+
- [`SmtpService`](docs/built-in-services.md#smtpservice) - Email sending
|
|
194
|
+
- [`AutoUpdateService`](docs/built-in-services.md#autoupdateservice) - Client app auto-updates
|
|
720
195
|
|
|
721
|
-
|
|
196
|
+
Register them like any other service:
|
|
722
197
|
|
|
723
198
|
```typescript
|
|
724
|
-
import { ServiceServer, ServiceBase, Authorize, OrmService, CryptoService } from "@simplysm/service-server";
|
|
725
|
-
import { ServiceEventListener } from "@simplysm/service-common";
|
|
726
|
-
|
|
727
|
-
// Define a custom service
|
|
728
|
-
@Authorize()
|
|
729
|
-
class UserService extends ServiceBase<{ userId: number; role: string }> {
|
|
730
|
-
async getProfile(): Promise<{ name: string }> {
|
|
731
|
-
const userId = this.authInfo?.userId;
|
|
732
|
-
// Use this.getConfig(), this.socket, this.server, etc.
|
|
733
|
-
return { name: "John" };
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
@Authorize(["admin"])
|
|
737
|
-
async deleteUser(targetId: number): Promise<void> {
|
|
738
|
-
// Admin-only operation
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
class PublicService extends ServiceBase {
|
|
743
|
-
async healthCheck(): Promise<string> {
|
|
744
|
-
return "OK";
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Create and start server
|
|
749
199
|
const server = new ServiceServer({
|
|
750
200
|
port: 8080,
|
|
751
201
|
rootPath: "/app/data",
|
|
752
|
-
auth: { jwtSecret: "
|
|
753
|
-
services: [
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
server.on("ready", () => {
|
|
757
|
-
console.log("Server is ready on port 8080");
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
await server.listen();
|
|
761
|
-
|
|
762
|
-
// Generate auth token for a user
|
|
763
|
-
const token = await server.generateAuthToken({
|
|
764
|
-
roles: ["admin"],
|
|
765
|
-
data: { userId: 1, role: "admin" },
|
|
202
|
+
auth: { jwtSecret: "secret" },
|
|
203
|
+
services: [OrmService, CryptoService, SmtpService],
|
|
766
204
|
});
|
|
767
|
-
|
|
768
|
-
// Emit events to connected clients
|
|
769
|
-
class UserUpdatedEvent extends ServiceEventListener<
|
|
770
|
-
{ userId: number },
|
|
771
|
-
{ action: string }
|
|
772
|
-
> {
|
|
773
|
-
readonly eventName = "UserUpdatedEvent";
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
await server.emitEvent(
|
|
777
|
-
UserUpdatedEvent,
|
|
778
|
-
(info) => info.userId === 1,
|
|
779
|
-
{ action: "profile-updated" },
|
|
780
|
-
);
|
|
781
205
|
```
|
|
782
206
|
|
|
783
|
-
## Server Route Structure
|
|
784
|
-
|
|
785
|
-
The following routes are automatically registered when `ServiceServer.listen()` is called:
|
|
786
|
-
|
|
787
|
-
| Route | Method | Description |
|
|
788
|
-
|--------|--------|------|
|
|
789
|
-
| `/api/:service/:method` | GET, POST | Service method call via HTTP |
|
|
790
|
-
| `/upload` | POST | Multipart file upload (auth required) |
|
|
791
|
-
| `/` | WebSocket | WebSocket connection endpoint |
|
|
792
|
-
| `/ws` | WebSocket | WebSocket connection endpoint (alias) |
|
|
793
|
-
| `/*` | GET, etc. | Static file serving (based on `rootPath/www/`) |
|
|
794
|
-
|
|
795
207
|
## Security
|
|
796
208
|
|
|
797
|
-
- **Helmet**: `@fastify/helmet` plugin automatically sets security headers like CSP, HSTS
|
|
798
|
-
- **CORS**: `@fastify/cors` plugin configures CORS
|
|
799
|
-
- **Path Traversal Prevention**: Static file handler and client name validation block `..`, `/`, `\` characters
|
|
800
|
-
- **Hidden File Blocking**: Files starting with `.` return a 403 response
|
|
801
|
-
- **Graceful Shutdown**: Detects `SIGINT`/`SIGTERM` signals to safely close
|
|
802
|
-
|
|
803
|
-
## Caveats
|
|
209
|
+
- **Helmet**: `@fastify/helmet` plugin automatically sets security headers like CSP, HSTS
|
|
210
|
+
- **CORS**: `@fastify/cors` plugin configures CORS
|
|
211
|
+
- **Path Traversal Prevention**: Static file handler and client name validation block `..`, `/`, `\` characters
|
|
212
|
+
- **Hidden File Blocking**: Files starting with `.` return a 403 response
|
|
213
|
+
- **Graceful Shutdown**: Detects `SIGINT`/`SIGTERM` signals to safely close WebSocket connections and server (10-second timeout)
|
|
804
214
|
|
|
805
|
-
|
|
806
|
-
- Config files (`.config.json`) contain sensitive information (DB passwords, JWT secrets, etc.), so hidden files (starting with `.`) are automatically blocked by the static file handler.
|
|
807
|
-
- WebSocket connection requires query parameters `ver=2`, `clientId`, `clientName`. Without these parameters, it operates in V1 legacy mode.
|
|
808
|
-
- If SSL is not configured, the `upgrade-insecure-requests` CSP directive is disabled.
|
|
215
|
+
See [Security](docs/server.md#security) for more details.
|
|
809
216
|
|
|
810
217
|
## License
|
|
811
218
|
|