@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.
@@ -0,0 +1,109 @@
1
+ # Authentication
2
+
3
+ ## Authorize Decorator
4
+
5
+ Use Stage 3 decorators to set authentication requirements on services or methods. Only works when `ServiceServerOptions.auth` is configured.
6
+
7
+ ```typescript
8
+ import { ServiceBase, Authorize } from "@simplysm/service-server";
9
+
10
+ // Class level: all methods require login
11
+ @Authorize()
12
+ class UserService extends ServiceBase<{ userId: number; role: string }> {
13
+ // Login only required (inherits from class level)
14
+ async getProfile(): Promise<unknown> {
15
+ const userId = this.authInfo?.userId;
16
+ // ...
17
+ }
18
+
19
+ // Method level: specific role required (overrides class level)
20
+ @Authorize(["admin"])
21
+ async deleteUser(targetId: number): Promise<void> {
22
+ // Only users with admin role can call
23
+ }
24
+ }
25
+
26
+ // No authentication required (no decorator)
27
+ class PublicService extends ServiceBase {
28
+ async healthCheck(): Promise<string> {
29
+ return "OK";
30
+ }
31
+ }
32
+ ```
33
+
34
+ Decorator behavior:
35
+
36
+ | Target | `@Authorize()` | `@Authorize(["admin"])` |
37
+ |-----------|----------------|-------------------------|
38
+ | Class | All methods require login | All methods require admin role |
39
+ | Method | Method requires login | Method requires admin role |
40
+ | None | No auth required (Public) | - |
41
+
42
+ Method-level decorators override class-level settings.
43
+
44
+ ## getAuthPermissions
45
+
46
+ Query auth permissions for a given service class and method. Primarily used internally by `ServiceExecutor`, but exported for advanced use cases.
47
+
48
+ ```typescript
49
+ import { getAuthPermissions } from "@simplysm/service-server";
50
+
51
+ // Returns string[] if permissions are set, or undefined for public (no decorator)
52
+ const perms = getAuthPermissions(UserService, "deleteUser");
53
+ // ["admin"]
54
+
55
+ const classPerms = getAuthPermissions(UserService);
56
+ // [] (empty array = login required, no specific role)
57
+
58
+ const publicPerms = getAuthPermissions(PublicService, "healthCheck");
59
+ // undefined (no auth required)
60
+ ```
61
+
62
+ ## JWT Token Management
63
+
64
+ ### JwtManager
65
+
66
+ `JwtManager<TAuthInfo>` handles JWT operations internally. Access its functionality through `ServiceServer` methods.
67
+
68
+ | Method | Returns | Description |
69
+ |--------|---------|------|
70
+ | `sign(payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
71
+ | `verify(token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify token signature and expiration, return payload |
72
+ | `decode(token)` | `AuthTokenPayload<TAuthInfo>` | Decode token without verification (synchronous) |
73
+
74
+ Generate and verify JWT tokens through the `ServiceServer` instance:
75
+
76
+ ```typescript
77
+ import { ServiceServer } from "@simplysm/service-server";
78
+
79
+ const server = new ServiceServer({
80
+ port: 8080,
81
+ rootPath: "/app/data",
82
+ auth: { jwtSecret: "my-secret-key" },
83
+ services: [],
84
+ });
85
+
86
+ // Generate token (12-hour expiration, HS256 algorithm)
87
+ const token = await server.generateAuthToken({
88
+ roles: ["admin", "user"],
89
+ data: { userId: 1, name: "John" },
90
+ });
91
+
92
+ // Verify token
93
+ const payload = await server.verifyAuthToken(token);
94
+ // payload.roles: ["admin", "user"]
95
+ // payload.data: { userId: 1, name: "John" }
96
+ ```
97
+
98
+ ### AuthTokenPayload
99
+
100
+ ```typescript
101
+ import type { AuthTokenPayload } from "@simplysm/service-server";
102
+
103
+ interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
104
+ /** User role list (used for permission check in Authorize decorator) */
105
+ roles: string[];
106
+ /** Custom auth info (generic type) */
107
+ data: TAuthInfo;
108
+ }
109
+ ```
@@ -0,0 +1,172 @@
1
+ # Built-in Services
2
+
3
+ ## OrmService
4
+
5
+ Provides database connection/query/transaction via WebSocket. `@Authorize()` decorator is applied, requiring login.
6
+
7
+ ```typescript
8
+ import { ServiceServer, OrmService } from "@simplysm/service-server";
9
+
10
+ const server = new ServiceServer({
11
+ port: 8080,
12
+ rootPath: "/app/data",
13
+ auth: { jwtSecret: "secret" },
14
+ services: [OrmService],
15
+ });
16
+ ```
17
+
18
+ Define ORM config in `.config.json`:
19
+
20
+ ```json
21
+ {
22
+ "orm": {
23
+ "default": {
24
+ "dialect": "mysql",
25
+ "host": "localhost",
26
+ "port": 3306,
27
+ "database": "mydb",
28
+ "user": "root",
29
+ "password": "password"
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Methods provided by `OrmService`:
36
+
37
+ | Method | Returns | Description |
38
+ |--------|---------|------|
39
+ | `getInfo(opt)` | `Promise<{ dialect, database?, schema? }>` | Query DB connection info |
40
+ | `connect(opt)` | `Promise<number>` | Create DB connection. Returns connection ID |
41
+ | `close(connId)` | `Promise<void>` | Close DB connection |
42
+ | `beginTransaction(connId, isolationLevel?)` | `Promise<void>` | Begin transaction |
43
+ | `commitTransaction(connId)` | `Promise<void>` | Commit transaction |
44
+ | `rollbackTransaction(connId)` | `Promise<void>` | Rollback transaction |
45
+ | `executeParametrized(connId, query, params?)` | `Promise<unknown[][]>` | Execute parameterized query |
46
+ | `executeDefs(connId, defs, options?)` | `Promise<unknown[][]>` | Execute QueryDef-based queries |
47
+ | `bulkInsert(connId, tableName, columnDefs, records)` | `Promise<void>` | Bulk INSERT |
48
+
49
+ When a WebSocket connection is closed, all DB connections opened from that socket are automatically cleaned up.
50
+
51
+ ## CryptoService
52
+
53
+ Provides SHA256 hash and AES-256-CBC symmetric key encryption/decryption.
54
+
55
+ ```typescript
56
+ import { ServiceServer, CryptoService } from "@simplysm/service-server";
57
+
58
+ const server = new ServiceServer({
59
+ port: 8080,
60
+ rootPath: "/app/data",
61
+ services: [CryptoService],
62
+ });
63
+ ```
64
+
65
+ `.config.json` config:
66
+
67
+ ```json
68
+ {
69
+ "crypto": {
70
+ "key": "your-32-byte-secret-key-here!!"
71
+ }
72
+ }
73
+ ```
74
+
75
+ | Method | Returns | Description |
76
+ |--------|---------|------|
77
+ | `encrypt(data)` | `Promise<string>` | Generate SHA256 HMAC hash (one-way). `data` is `string \| Uint8Array` |
78
+ | `encryptAes(data)` | `Promise<string>` | AES-256-CBC encryption. `data` is `Uint8Array`. Returns hex string in `iv:encrypted` format |
79
+ | `decryptAes(encText)` | `Promise<Uint8Array>` | AES-256-CBC decryption. Returns original binary |
80
+
81
+ ## SmtpService
82
+
83
+ A nodemailer-based email sending service. Can pass SMTP config directly or reference server config file.
84
+
85
+ ```typescript
86
+ import { ServiceServer, SmtpService } from "@simplysm/service-server";
87
+
88
+ const server = new ServiceServer({
89
+ port: 8080,
90
+ rootPath: "/app/data",
91
+ services: [SmtpService],
92
+ });
93
+ ```
94
+
95
+ `.config.json` config (when using config reference method):
96
+
97
+ ```json
98
+ {
99
+ "smtp": {
100
+ "default": {
101
+ "host": "smtp.example.com",
102
+ "port": 587,
103
+ "secure": false,
104
+ "user": "user@example.com",
105
+ "pass": "password",
106
+ "senderName": "My App",
107
+ "senderEmail": "noreply@example.com"
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ | Method | Returns | Description |
114
+ |--------|---------|------|
115
+ | `send(options)` | `Promise<string>` | Send email by directly passing SMTP config. Returns message ID |
116
+ | `sendByConfig(configName, options)` | `Promise<string>` | Send email by referencing SMTP config in config file. Returns message ID |
117
+
118
+ `send()` options:
119
+
120
+ ```typescript
121
+ interface SmtpSendOption {
122
+ host: string;
123
+ port?: number;
124
+ secure?: boolean;
125
+ user?: string;
126
+ pass?: string;
127
+ from: string;
128
+ to: string;
129
+ cc?: string;
130
+ bcc?: string;
131
+ subject: string;
132
+ html: string;
133
+ attachments?: SmtpSendAttachment[];
134
+ }
135
+ ```
136
+
137
+ ## AutoUpdateService
138
+
139
+ Supports auto-update for client apps. Searches for latest version files by platform in the client directory.
140
+
141
+ ```typescript
142
+ import { ServiceServer, AutoUpdateService } from "@simplysm/service-server";
143
+
144
+ const server = new ServiceServer({
145
+ port: 8080,
146
+ rootPath: "/app/data",
147
+ services: [AutoUpdateService],
148
+ });
149
+ ```
150
+
151
+ Update file structure:
152
+
153
+ ```
154
+ rootPath/www/{clientName}/{platform}/updates/
155
+ 1.0.0.exe (Windows)
156
+ 1.0.1.exe
157
+ 1.0.0.apk (Android)
158
+ 1.0.1.apk
159
+ ```
160
+
161
+ | Method | Returns | Description |
162
+ |--------|---------|------|
163
+ | `getLastVersion(platform)` | `Promise<{ version: string; downloadPath: string } \| undefined>` | Returns latest version and download path for the platform. Returns `undefined` if no update |
164
+
165
+ Return value example:
166
+
167
+ ```typescript
168
+ {
169
+ version: "1.0.1",
170
+ downloadPath: "/my-app/android/updates/1.0.1.apk",
171
+ }
172
+ ```
package/docs/server.md ADDED
@@ -0,0 +1,274 @@
1
+ # ServiceServer
2
+
3
+ ## ServiceServer
4
+
5
+ `ServiceServer<TAuthInfo>` extends `EventEmitter` and is the main entry point for creating a server.
6
+
7
+ **Properties:**
8
+
9
+ | Property | Type | Description |
10
+ |----------|------|------|
11
+ | `options` | `ServiceServerOptions` | Server configuration (read-only, passed via constructor) |
12
+ | `isOpen` | `boolean` | Whether the server is currently listening |
13
+ | `fastify` | `FastifyInstance` | Underlying Fastify instance (read-only, for advanced use) |
14
+
15
+ **Methods:**
16
+
17
+ | Method | Returns | Description |
18
+ |--------|---------|------|
19
+ | `listen()` | `Promise<void>` | Register all plugins/routes and start listening |
20
+ | `close()` | `Promise<void>` | Close all WebSocket connections and shut down the server |
21
+ | `generateAuthToken(payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
22
+ | `verifyAuthToken(token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify and decode a JWT token |
23
+ | `emitEvent(eventType, infoSelector, data)` | `Promise<void>` | Publish an event to matching WebSocket clients |
24
+ | `broadcastReload(clientName, changedFileSet)` | `Promise<void>` | Send a reload command to all connected clients |
25
+
26
+ **Events:**
27
+
28
+ | Event | Payload | Description |
29
+ |-------|---------|------|
30
+ | `ready` | `void` | Emitted when the server starts listening |
31
+ | `close` | `void` | Emitted when the server is closed |
32
+
33
+ ## Server Options (`ServiceServerOptions`)
34
+
35
+ ```typescript
36
+ import type { ServiceServerOptions } from "@simplysm/service-server";
37
+
38
+ interface ServiceServerOptions {
39
+ /** Server root path (base directory for static files and config files) */
40
+ rootPath: string;
41
+ /** Listen port */
42
+ port: number;
43
+ /** SSL/TLS config (enables HTTPS) */
44
+ ssl?: {
45
+ pfxBytes: Uint8Array;
46
+ passphrase: string;
47
+ };
48
+ /** JWT authentication config */
49
+ auth?: {
50
+ jwtSecret: string;
51
+ };
52
+ /** List of service classes to register */
53
+ services: Type<ServiceBase>[];
54
+ }
55
+ ```
56
+
57
+ The following structure is expected under `rootPath`:
58
+
59
+ ```
60
+ rootPath/
61
+ .config.json # Root config file
62
+ www/ # Static file root
63
+ uploads/ # Upload file storage directory
64
+ {clientName}/ # Per-client directory
65
+ .config.json # Per-client config file
66
+ index.html
67
+ ```
68
+
69
+ ## SSL/HTTPS Server
70
+
71
+ ```typescript
72
+ import { ServiceServer } from "@simplysm/service-server";
73
+ import { fsReadFile } from "@simplysm/core-node";
74
+
75
+ const pfxBytes = await fsReadFile("/path/to/cert.pfx");
76
+
77
+ const server = new ServiceServer({
78
+ port: 443,
79
+ rootPath: "/app/data",
80
+ ssl: {
81
+ pfxBytes,
82
+ passphrase: "certificate-password",
83
+ },
84
+ auth: { jwtSecret: "my-secret-key" },
85
+ services: [],
86
+ });
87
+
88
+ await server.listen();
89
+ ```
90
+
91
+ ## Custom Service Definition
92
+
93
+ Define services by inheriting from `ServiceBase`. Service methods are called via RPC from the client.
94
+
95
+ ```typescript
96
+ import { ServiceBase } from "@simplysm/service-server";
97
+
98
+ class MyService extends ServiceBase {
99
+ async hello(name: string): Promise<string> {
100
+ return `Hello, ${name}!`;
101
+ }
102
+
103
+ async getServerTime(): Promise<Date> {
104
+ return new Date();
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### ServiceBase Properties
110
+
111
+ `ServiceBase<TAuthInfo>` is an abstract class. The generic `TAuthInfo` type represents the shape of the authenticated user's data stored in the JWT token.
112
+
113
+ | Property | Type | Description |
114
+ |----------|------|------|
115
+ | `this.server` | `ServiceServer<TAuthInfo>` | Server instance reference |
116
+ | `this.socket` | `ServiceSocket \| undefined` | WebSocket connection (`undefined` for HTTP calls) |
117
+ | `this.http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> } \| undefined` | HTTP request context |
118
+ | `this.authInfo` | `TAuthInfo \| undefined` | Authenticated user's custom data (from JWT `data` field) |
119
+ | `this.clientName` | `string \| undefined` | Client app name (validated against path traversal) |
120
+ | `this.clientPath` | `string \| undefined` | Resolved per-client directory path (`rootPath/www/{clientName}`) |
121
+
122
+ ### ServiceBase Methods
123
+
124
+ | Method | Returns | Description |
125
+ |--------|---------|------|
126
+ | `getConfig<T>(section)` | `Promise<T>` | Read a section from `.config.json` (root + client configs merged) |
127
+
128
+ ## Config File Reference
129
+
130
+ Read sections from `.config.json` files using `ServiceBase.getConfig()`. Root and per-client configs are automatically merged.
131
+
132
+ ```typescript
133
+ import { ServiceBase } from "@simplysm/service-server";
134
+
135
+ class MyService extends ServiceBase {
136
+ async getDbHost(): Promise<string> {
137
+ // Read "mySection" key from rootPath/.config.json or clientPath/.config.json
138
+ const config = await this.getConfig<{ host: string }>("mySection");
139
+ return config.host;
140
+ }
141
+ }
142
+ ```
143
+
144
+ `.config.json` example:
145
+
146
+ ```json
147
+ {
148
+ "mySection": {
149
+ "host": "localhost"
150
+ },
151
+ "orm": {
152
+ "default": {
153
+ "dialect": "mysql",
154
+ "host": "localhost",
155
+ "port": 3306,
156
+ "database": "mydb",
157
+ "user": "root",
158
+ "password": "password"
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ `ConfigManager` caches config files and automatically refreshes the cache on file changes (LazyGcMap-based, auto expires after 1 hour).
165
+
166
+ ## ConfigManager
167
+
168
+ A static utility class that manages loading, caching, and real-time monitoring of JSON config files. Used internally by `ServiceBase.getConfig()`.
169
+
170
+ ```typescript
171
+ import { ConfigManager } from "@simplysm/service-server";
172
+
173
+ // Returns undefined if the file does not exist
174
+ const config = await ConfigManager.getConfig<MyConfig>("/path/to/.config.json");
175
+ ```
176
+
177
+ | Method | Returns | Description |
178
+ |--------|---------|------|
179
+ | `ConfigManager.getConfig<T>(filePath)` | `Promise<T \| undefined>` | Load and cache a JSON config file. Returns `undefined` if file not found |
180
+
181
+ Behavior:
182
+ - Caches file in `LazyGcMap` on first load.
183
+ - Registers file change watch (`FsWatcher`) to auto-refresh cache on changes.
184
+ - Cache auto-expires after 1 hour of no access, and associated watch is released.
185
+ - GC runs every 10 minutes to check for expired entries.
186
+
187
+ ## Server Route Structure
188
+
189
+ The following routes are automatically registered when `ServiceServer.listen()` is called:
190
+
191
+ | Route | Method | Description |
192
+ |--------|--------|------|
193
+ | `/api/:service/:method` | GET, POST | Service method call via HTTP |
194
+ | `/upload` | POST | Multipart file upload (auth required) |
195
+ | `/` | WebSocket | WebSocket connection endpoint |
196
+ | `/ws` | WebSocket | WebSocket connection endpoint (alias) |
197
+ | `/*` | GET, etc. | Static file serving (based on `rootPath/www/`) |
198
+
199
+ ## Full Server Example
200
+
201
+ ```typescript
202
+ import { ServiceServer, ServiceBase, Authorize, OrmService, CryptoService } from "@simplysm/service-server";
203
+ import { ServiceEventListener } from "@simplysm/service-common";
204
+
205
+ // Define a custom service
206
+ @Authorize()
207
+ class UserService extends ServiceBase<{ userId: number; role: string }> {
208
+ async getProfile(): Promise<{ name: string }> {
209
+ const userId = this.authInfo?.userId;
210
+ // Use this.getConfig(), this.socket, this.server, etc.
211
+ return { name: "John" };
212
+ }
213
+
214
+ @Authorize(["admin"])
215
+ async deleteUser(targetId: number): Promise<void> {
216
+ // Admin-only operation
217
+ }
218
+ }
219
+
220
+ class PublicService extends ServiceBase {
221
+ async healthCheck(): Promise<string> {
222
+ return "OK";
223
+ }
224
+ }
225
+
226
+ // Create and start server
227
+ const server = new ServiceServer({
228
+ port: 8080,
229
+ rootPath: "/app/data",
230
+ auth: { jwtSecret: "my-secret-key" },
231
+ services: [UserService, PublicService, OrmService, CryptoService],
232
+ });
233
+
234
+ server.on("ready", () => {
235
+ console.log("Server is ready on port 8080");
236
+ });
237
+
238
+ await server.listen();
239
+
240
+ // Generate auth token for a user
241
+ const token = await server.generateAuthToken({
242
+ roles: ["admin"],
243
+ data: { userId: 1, role: "admin" },
244
+ });
245
+
246
+ // Emit events to connected clients
247
+ class UserUpdatedEvent extends ServiceEventListener<
248
+ { userId: number },
249
+ { action: string }
250
+ > {
251
+ readonly eventName = "UserUpdatedEvent";
252
+ }
253
+
254
+ await server.emitEvent(
255
+ UserUpdatedEvent,
256
+ (info) => info.userId === 1,
257
+ { action: "profile-updated" },
258
+ );
259
+ ```
260
+
261
+ ## Security
262
+
263
+ - **Helmet**: `@fastify/helmet` plugin automatically sets security headers like CSP, HSTS.
264
+ - **CORS**: `@fastify/cors` plugin configures CORS.
265
+ - **Path Traversal Prevention**: Static file handler and client name validation block `..`, `/`, `\` characters.
266
+ - **Hidden File Blocking**: Files starting with `.` return a 403 response.
267
+ - **Graceful Shutdown**: Detects `SIGINT`/`SIGTERM` signals to safely close open WebSocket connections and server (10-second timeout).
268
+
269
+ ## Caveats
270
+
271
+ - `OrmService` is WebSocket-only. Cannot be used via HTTP requests.
272
+ - 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.
273
+ - WebSocket connection requires query parameters `ver=2`, `clientId`, `clientName`. Without these parameters, it operates in V1 legacy mode.
274
+ - If SSL is not configured, the `upgrade-insecure-requests` CSP directive is disabled.
@@ -0,0 +1,154 @@
1
+ # Transport Layer
2
+
3
+ ## ServiceSocket
4
+
5
+ `ServiceSocket` extends `EventEmitter` and wraps an individual WebSocket connection. It is available in service methods as `this.socket` when the request comes via WebSocket.
6
+
7
+ **Properties:**
8
+
9
+ | Property | Type | Description |
10
+ |----------|------|------|
11
+ | `clientName` | `string` | Client app name (from WebSocket query parameter) |
12
+ | `connectedAtDateTime` | `DateTime` | Connection timestamp |
13
+ | `authTokenPayload` | `AuthTokenPayload \| undefined` | Authenticated token payload (set after `auth` message) |
14
+ | `connReq` | `FastifyRequest` | Original Fastify request that initiated the WebSocket upgrade |
15
+
16
+ **Methods:**
17
+
18
+ | Method | Returns | Description |
19
+ |--------|---------|------|
20
+ | `send(uuid, msg)` | `Promise<number>` | Send a message to this client. Returns total bytes sent |
21
+ | `close()` | `void` | Terminate the WebSocket connection |
22
+ | `addEventListener(key, eventName, info)` | `void` | Register an event listener for this socket |
23
+ | `removeEventListener(key)` | `void` | Remove an event listener by key |
24
+ | `getEventListeners(eventName)` | `{ key, info }[]` | Get all event listeners for a given event name |
25
+
26
+ **Events:**
27
+
28
+ | Event | Payload | Description |
29
+ |-------|---------|------|
30
+ | `error` | `Error` | WebSocket error occurred |
31
+ | `close` | `number` | Connection closed (payload is the close code) |
32
+ | `message` | `{ uuid: string; msg: ServiceClientMessage }` | Decoded message received from client |
33
+
34
+ ## HTTP API Call
35
+
36
+ Service methods can also be called via HTTP through the `/api/:service/:method` path.
37
+
38
+ **GET Request:**
39
+
40
+ ```
41
+ GET /api/MyService/hello?json=["World"]
42
+ Header: x-sd-client-name: my-app
43
+ Header: Authorization: Bearer <token> (optional)
44
+ ```
45
+
46
+ **POST Request:**
47
+
48
+ ```
49
+ POST /api/MyService/hello
50
+ Header: Content-Type: application/json
51
+ Header: x-sd-client-name: my-app
52
+ Header: Authorization: Bearer <token> (optional)
53
+ Body: ["World"]
54
+ ```
55
+
56
+ - The `x-sd-client-name` header is required.
57
+ - Parameters are passed in array form (in the order of method arguments).
58
+ - For GET requests, pass a JSON-serialized array in the `json` query parameter.
59
+
60
+ ## File Upload
61
+
62
+ Upload files via multipart request to the `/upload` endpoint. Auth token is required.
63
+
64
+ ```typescript
65
+ // Client-side example
66
+ const formData = new FormData();
67
+ formData.append("file", file);
68
+
69
+ const response = await fetch("/upload", {
70
+ method: "POST",
71
+ headers: {
72
+ Authorization: `Bearer ${token}`,
73
+ },
74
+ body: formData,
75
+ });
76
+
77
+ // Response: ServiceUploadResult[]
78
+ const results = await response.json();
79
+ // [{ path: "uploads/uuid.ext", filename: "original-filename.ext", size: 12345 }]
80
+ ```
81
+
82
+ Uploaded files are stored in the `rootPath/www/uploads/` directory with UUID-based filenames.
83
+
84
+ ## Real-time Event Publishing
85
+
86
+ Publish events to connected clients from the server.
87
+
88
+ ```typescript
89
+ import { ServiceServer } from "@simplysm/service-server";
90
+ import { ServiceEventListener } from "@simplysm/service-common";
91
+
92
+ // Event definition (from service-common)
93
+ class OrderUpdatedEvent extends ServiceEventListener<
94
+ { orderId: number },
95
+ { status: string }
96
+ > {
97
+ readonly eventName = "OrderUpdatedEvent";
98
+ }
99
+
100
+ // Publish event from server
101
+ await server.emitEvent(
102
+ OrderUpdatedEvent,
103
+ (info) => info.orderId === 123, // Target filter
104
+ { status: "completed" }, // Data to send
105
+ );
106
+
107
+ // Send reload command to all clients
108
+ await server.broadcastReload("my-app", new Set(["main.js"]));
109
+ ```
110
+
111
+ ## ProtocolWrapper
112
+
113
+ Handles encoding/decoding of WebSocket messages. Automatically branches between main thread and worker thread based on message size.
114
+
115
+ ```typescript
116
+ import { ProtocolWrapper } from "@simplysm/service-server";
117
+
118
+ const protocol = new ProtocolWrapper();
119
+
120
+ // Encode a message into chunks
121
+ const { chunks, totalSize } = await protocol.encode(uuid, message);
122
+
123
+ // Decode received bytes
124
+ const result = await protocol.decode(bytes);
125
+
126
+ // Clean up
127
+ protocol.dispose();
128
+ ```
129
+
130
+ | Method | Returns | Description |
131
+ |--------|---------|------|
132
+ | `encode(uuid, message)` | `Promise<{ chunks: Uint8Array[]; totalSize: number }>` | Encode a message into transmittable chunks |
133
+ | `decode(bytes)` | `Promise<ServiceMessageDecodeResult>` | Decode received bytes into a message |
134
+ | `dispose()` | `void` | Clean up internal protocol resources |
135
+
136
+ Worker thread branching:
137
+
138
+ | Condition | Processing Method |
139
+ |------|-----------|
140
+ | 30KB or less | Processed directly in main thread |
141
+ | Over 30KB | Processed in worker thread (max 4GB memory allocation) |
142
+
143
+ Messages containing large binary data (Uint8Array) also branch to worker thread.
144
+
145
+ ## Legacy: handleV1Connection
146
+
147
+ Handles V1 protocol WebSocket clients. Only supports the `SdAutoUpdateService.getLastVersion` command. All other requests return an upgrade-required error.
148
+
149
+ ```typescript
150
+ import { handleV1Connection, AutoUpdateService } from "@simplysm/service-server";
151
+
152
+ // Used internally by ServiceServer for WebSocket connections without ver=2 query parameter
153
+ handleV1Connection(webSocket, autoUpdateService);
154
+ ```