@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 CHANGED
@@ -16,62 +16,46 @@ pnpm add @simplysm/service-server
16
16
 
17
17
  ### Core Classes
18
18
 
19
- | Module | Description |
20
- |--------|------|
21
- | `ServiceServer` | Main server class. Creates Fastify instance and configures routes/plugins |
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
- | Module | Description |
28
- |--------|------|
29
- | `JwtManager` | JWT token generation/verification/decoding based on jose library (HS256, 12-hour expiration) |
30
- | `Authorize` | Stage 3 decorator. Sets authentication permissions at class or method level |
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
- | Module | Description |
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
- | Module | Description |
44
- |--------|------|
45
- | `HttpRequestHandler` | Calls service methods via HTTP at `/api/:service/:method` route |
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
- | Module | Description |
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
- | Module | Description |
58
- |--------|------|
59
- | `OrmService` | DB connection/transaction/query execution (WebSocket only, auth required) |
60
- | `CryptoService` | SHA256 hash and AES-256-CBC encryption/decryption |
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
- | Module | Description |
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
- | Module | Description |
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
- ### ServiceServer
89
+ ### Server Options
106
90
 
107
- `ServiceServer<TAuthInfo>` extends `EventEmitter` and is the main entry point for creating a server.
91
+ See [`ServiceServerOptions`](docs/server.md#server-options-serviceserveroptions) for detailed configuration options including SSL, authentication, and directory structure.
108
92
 
109
- **Properties:**
93
+ ### Custom Services
110
94
 
111
- | Property | Type | Description |
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
- #### ServiceBase Properties
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 (`Authorize` Decorator)
113
+ ### Authentication
269
114
 
270
- Use Stage 3 decorators to set authentication requirements on services or methods. Only works when `ServiceServerOptions.auth` is configured.
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
- Decorator behavior:
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
- Method-level decorators override class-level settings.
136
+ ### HTTP/WebSocket Communication
308
137
 
309
- #### `getAuthPermissions`
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
- - The `x-sd-client-name` header is required.
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. Auth token is required.
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
- Uploaded files are stored in the `rootPath/www/uploads/` directory with UUID-based filenames.
162
+ See [File Upload](docs/transport.md#file-upload) for more details.
456
163
 
457
- ### Real-time Event Publishing
164
+ ### Event Publishing
458
165
 
459
- Publish events to connected clients from the server.
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, // Target filter
477
- { status: "completed" }, // Data to send
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
- ### ConfigManager
185
+ See [Real-time Event Publishing](docs/transport.md#real-time-event-publishing) for more details.
656
186
 
657
- A static utility class that manages loading, caching, and real-time monitoring of JSON config files. Used internally by `ServiceBase.getConfig()`.
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
- ```typescript
715
- import { handleV1Connection, AutoUpdateService } from "@simplysm/service-server";
189
+ The package provides several built-in services:
716
190
 
717
- // Used internally by ServiceServer for WebSocket connections without ver=2 query parameter
718
- handleV1Connection(webSocket, autoUpdateService);
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
- ## Full Server Example
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: "my-secret-key" },
753
- services: [UserService, PublicService, OrmService, CryptoService],
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 open WebSocket connections and server (10-second timeout).
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
- - `OrmService` is WebSocket-only. Cannot be used via HTTP requests.
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