@simplysm/service-server 13.0.97 → 13.0.98

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 ADDED
@@ -0,0 +1,98 @@
1
+ # @simplysm/service-server
2
+
3
+ Service module (server) -- Fastify-based service server with WebSocket support, JWT authentication, and built-in ORM/SMTP/auto-update services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @simplysm/service-server
9
+ ```
10
+
11
+ ## Exports
12
+
13
+ ```typescript
14
+ import {
15
+ // Main
16
+ ServiceServer,
17
+ createServiceServer,
18
+ type ServiceServerOptions,
19
+ // Auth
20
+ type AuthTokenPayload,
21
+ signJwt,
22
+ verifyJwt,
23
+ decodeJwt,
24
+ // Core
25
+ type ServiceContext,
26
+ createServiceContext,
27
+ getServiceAuthPermissions,
28
+ auth,
29
+ type ServiceDefinition,
30
+ defineService,
31
+ type ServiceMethods,
32
+ executeServiceMethod,
33
+ // Transport - Socket
34
+ type WebSocketHandler,
35
+ createWebSocketHandler,
36
+ type ServiceSocket,
37
+ createServiceSocket,
38
+ // Transport - HTTP
39
+ handleHttpRequest,
40
+ handleUpload,
41
+ handleStaticFile,
42
+ // Protocol
43
+ type ServerProtocolWrapper,
44
+ createServerProtocolWrapper,
45
+ // Services
46
+ OrmService,
47
+ type OrmServiceType,
48
+ AutoUpdateService,
49
+ type AutoUpdateServiceType,
50
+ SmtpClientService,
51
+ type SmtpClientServiceType,
52
+ // Utils
53
+ getConfig,
54
+ // Legacy
55
+ handleV1Connection,
56
+ } from "@simplysm/service-server";
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```typescript
62
+ import {
63
+ createServiceServer,
64
+ defineService,
65
+ auth,
66
+ OrmService,
67
+ AutoUpdateService,
68
+ } from "@simplysm/service-server";
69
+
70
+ // Define a custom service
71
+ const HealthService = defineService("Health", (ctx) => ({
72
+ check: () => ({ status: "ok" }),
73
+ }));
74
+
75
+ // Define an authenticated service
76
+ const UserService = defineService("User", auth((ctx) => ({
77
+ getProfile: () => ctx.authInfo,
78
+ adminOnly: auth(["admin"], () => "admin-only data"),
79
+ })));
80
+
81
+ // Create and start server
82
+ const server = createServiceServer({
83
+ rootPath: "/app",
84
+ port: 3000,
85
+ auth: { jwtSecret: "my-secret" },
86
+ services: [HealthService, UserService, OrmService, AutoUpdateService],
87
+ });
88
+
89
+ await server.listen();
90
+ ```
91
+
92
+ ## Documentation
93
+
94
+ - [Auth](docs/auth.md)
95
+ - [Core](docs/core.md)
96
+ - [Transport](docs/transport.md)
97
+ - [Built-in Services](docs/services.md)
98
+ - [Server](docs/server.md)
package/docs/auth.md ADDED
@@ -0,0 +1,48 @@
1
+ # Auth
2
+
3
+ JWT-based authentication utilities using the `jose` library (HS256 algorithm).
4
+
5
+ ## `AuthTokenPayload`
6
+
7
+ JWT token payload structure. Extends `JWTPayload` from `jose`.
8
+
9
+ ```typescript
10
+ interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
11
+ roles: string[];
12
+ data: TAuthInfo;
13
+ }
14
+ ```
15
+
16
+ ## `signJwt`
17
+
18
+ Sign a JWT token. Tokens expire after 12 hours.
19
+
20
+ ```typescript
21
+ async function signJwt<TAuthInfo = unknown>(
22
+ jwtSecret: string,
23
+ payload: AuthTokenPayload<TAuthInfo>,
24
+ ): Promise<string>;
25
+ ```
26
+
27
+ ## `verifyJwt`
28
+
29
+ Verify a JWT token and return the payload.
30
+
31
+ ```typescript
32
+ async function verifyJwt<TAuthInfo = unknown>(
33
+ jwtSecret: string,
34
+ token: string,
35
+ ): Promise<AuthTokenPayload<TAuthInfo>>;
36
+ ```
37
+
38
+ Throws:
39
+ - `"Token has expired."` if the token is expired
40
+ - `"Invalid token."` for all other verification failures
41
+
42
+ ## `decodeJwt`
43
+
44
+ Decode a JWT token without verification.
45
+
46
+ ```typescript
47
+ function decodeJwt<TAuthInfo = unknown>(token: string): AuthTokenPayload<TAuthInfo>;
48
+ ```
package/docs/core.md ADDED
@@ -0,0 +1,161 @@
1
+ # Core
2
+
3
+ Service definition, context, authentication, and method execution.
4
+
5
+ ## ServiceContext
6
+
7
+ Context object passed to service factory functions.
8
+
9
+ ```typescript
10
+ interface ServiceContext<TAuthInfo = unknown> {
11
+ server: ServiceServer<TAuthInfo>;
12
+ socket?: ServiceSocket;
13
+ http?: {
14
+ clientName: string;
15
+ authTokenPayload?: AuthTokenPayload<TAuthInfo>;
16
+ };
17
+ legacy?: { clientName?: string };
18
+
19
+ get authInfo(): TAuthInfo | undefined;
20
+ get clientName(): string | undefined;
21
+ get clientPath(): string | undefined;
22
+ getConfig<T>(section: string): Promise<T>;
23
+ }
24
+ ```
25
+
26
+ **Properties:**
27
+ - `authInfo` -- Authenticated user data (from socket or HTTP auth token)
28
+ - `clientName` -- Client application name (validated for path traversal)
29
+ - `clientPath` -- Resolved client directory path (`{rootPath}/www/{clientName}`)
30
+ - `getConfig(section)` -- Reads config from `.config.json` files (root + client-specific, merged)
31
+
32
+ ### `createServiceContext`
33
+
34
+ ```typescript
35
+ function createServiceContext<TAuthInfo = unknown>(
36
+ server: ServiceServer<TAuthInfo>,
37
+ socket?: ServiceSocket,
38
+ http?: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> },
39
+ legacy?: { clientName?: string },
40
+ ): ServiceContext<TAuthInfo>;
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Auth Helpers
46
+
47
+ ### `getServiceAuthPermissions`
48
+
49
+ Read auth permissions from an `auth()`-wrapped function. Returns `undefined` if not wrapped.
50
+
51
+ ```typescript
52
+ function getServiceAuthPermissions(fn: Function): string[] | undefined;
53
+ ```
54
+
55
+ ### `auth`
56
+
57
+ Auth wrapper for service factories and methods.
58
+
59
+ ```typescript
60
+ // Login required (no specific roles)
61
+ function auth<TFunction extends (...args: any[]) => any>(fn: TFunction): TFunction;
62
+
63
+ // Login required with specific roles
64
+ function auth<TFunction extends (...args: any[]) => any>(
65
+ permissions: string[],
66
+ fn: TFunction,
67
+ ): TFunction;
68
+ ```
69
+
70
+ **Usage levels:**
71
+ - Service-level: `auth((ctx) => ({ ... }))` -- all methods require login
72
+ - Service-level with roles: `auth(["admin"], (ctx) => ({ ... }))`
73
+ - Method-level: `auth(() => result)` -- this method requires login
74
+ - Method-level with roles: `auth(["admin"], () => result)`
75
+
76
+ ---
77
+
78
+ ## Service Definition
79
+
80
+ ### `ServiceDefinition`
81
+
82
+ ```typescript
83
+ interface ServiceDefinition<TMethods = Record<string, (...args: any[]) => any>> {
84
+ name: string;
85
+ factory: (ctx: ServiceContext) => TMethods;
86
+ authPermissions?: string[];
87
+ }
88
+ ```
89
+
90
+ ### `defineService`
91
+
92
+ Define a service with a name and factory function.
93
+
94
+ ```typescript
95
+ function defineService<TMethods extends Record<string, (...args: any[]) => any>>(
96
+ name: string,
97
+ factory: (ctx: ServiceContext) => TMethods,
98
+ ): ServiceDefinition<TMethods>;
99
+ ```
100
+
101
+ **Example:**
102
+ ```typescript
103
+ const HealthService = defineService("Health", (ctx) => ({
104
+ check: () => ({ status: "ok" }),
105
+ }));
106
+
107
+ const UserService = defineService("User", auth((ctx) => ({
108
+ getProfile: () => ctx.authInfo,
109
+ adminOnly: auth(["admin"], () => "admin"),
110
+ })));
111
+ ```
112
+
113
+ ### `ServiceMethods`
114
+
115
+ Extract method signatures from a `ServiceDefinition` for client-side type sharing.
116
+
117
+ ```typescript
118
+ type ServiceMethods<TDefinition> =
119
+ TDefinition extends ServiceDefinition<infer M> ? M : never;
120
+ ```
121
+
122
+ **Example:**
123
+ ```typescript
124
+ export type UserServiceType = ServiceMethods<typeof UserService>;
125
+ // Client: client.getService<UserServiceType>("User");
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Service Execution
131
+
132
+ ### `executeServiceMethod`
133
+
134
+ Execute a service method with auth checking.
135
+
136
+ ```typescript
137
+ async function executeServiceMethod(
138
+ server: ServiceServer,
139
+ def: {
140
+ serviceName: string;
141
+ methodName: string;
142
+ params: unknown[];
143
+ socket?: ServiceSocket;
144
+ http?: { clientName: string; authTokenPayload?: AuthTokenPayload };
145
+ },
146
+ ): Promise<unknown>;
147
+ ```
148
+
149
+ **Behavior:**
150
+ 1. Finds the service definition by name
151
+ 2. Validates the client name (path traversal guard)
152
+ 3. Creates a `ServiceContext`
153
+ 4. Invokes the factory to create the method object
154
+ 5. Checks auth permissions (method-level first, then service-level fallback)
155
+ 6. Executes the method with provided params
156
+
157
+ Throws:
158
+ - `"Service [name] not found."` if service is not registered
159
+ - `"Method [service.method] not found."` if method does not exist
160
+ - `"Login is required."` if auth is required but no token is present
161
+ - `"Insufficient permissions."` if the user lacks required roles
package/docs/server.md ADDED
@@ -0,0 +1,206 @@
1
+ # Server
2
+
3
+ ## ServiceServerOptions
4
+
5
+ Server configuration options.
6
+
7
+ ```typescript
8
+ interface ServiceServerOptions {
9
+ rootPath: string;
10
+ port: number;
11
+ ssl?: {
12
+ pfxBytes: Uint8Array;
13
+ passphrase: string;
14
+ };
15
+ auth?: {
16
+ jwtSecret: string;
17
+ };
18
+ services: ServiceDefinition[];
19
+ }
20
+ ```
21
+
22
+ ## ServerProtocolWrapper
23
+
24
+ Server-side protocol wrapper. Automatically offloads heavy message encoding/decoding to a worker thread while using main thread for lightweight operations.
25
+
26
+ ```typescript
27
+ interface ServerProtocolWrapper {
28
+ encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
29
+ decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
30
+ dispose(): void;
31
+ }
32
+ ```
33
+
34
+ ### `createServerProtocolWrapper`
35
+
36
+ ```typescript
37
+ function createServerProtocolWrapper(): ServerProtocolWrapper;
38
+ ```
39
+
40
+ **Behavior:**
41
+ - Messages with `Uint8Array` body or arrays containing `Uint8Array` are encoded via worker thread
42
+ - Messages larger than 30KB are decoded via worker thread
43
+ - Worker is a lazy singleton shared across all protocol wrappers (4GB memory limit)
44
+ - Small messages are processed on the main thread using `createServiceProtocol()`
45
+
46
+ ---
47
+
48
+ ## Config Utilities
49
+
50
+ ### `getConfig`
51
+
52
+ Read and cache a JSON config file with automatic live-reload via file watcher.
53
+
54
+ ```typescript
55
+ async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
56
+ ```
57
+
58
+ **Behavior:**
59
+ - Returns `undefined` if file does not exist
60
+ - Caches loaded config in a `LazyGcMap` (expires after 1 hour, GC runs every 10 minutes)
61
+ - Registers a file watcher that live-reloads config on changes
62
+ - Watcher is cleaned up when the cache entry expires
63
+
64
+ ---
65
+
66
+ ## Legacy
67
+
68
+ ### `handleV1Connection`
69
+
70
+ V1 legacy client handler. Only auto-update is supported; all other requests return an upgrade-required error.
71
+
72
+ ```typescript
73
+ function handleV1Connection(
74
+ socket: WebSocket,
75
+ autoUpdateMethods: { getLastVersion: (platform: string) => Promise<any> },
76
+ clientNameSetter?: (clientName: string | undefined) => void,
77
+ ): void;
78
+ ```
79
+
80
+ ---
81
+
82
+ ## ServiceServer
83
+
84
+ Main server class. Extends `EventEmitter<{ ready: void; close: void }>`.
85
+
86
+ ```typescript
87
+ class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{ ready: void; close: void }> {
88
+ isOpen: boolean;
89
+ readonly fastify: FastifyInstance;
90
+ readonly options: ServiceServerOptions;
91
+
92
+ constructor(options: ServiceServerOptions);
93
+
94
+ async listen(): Promise<void>;
95
+ async close(): Promise<void>;
96
+ async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
97
+ async emitEvent<TInfo, TData>(
98
+ eventDef: ServiceEventDef<TInfo, TData>,
99
+ infoSelector: (item: TInfo) => boolean,
100
+ data: TData,
101
+ ): Promise<void>;
102
+ async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
103
+ async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
104
+ }
105
+ ```
106
+
107
+ ### `listen`
108
+
109
+ Start the server. Registers all Fastify plugins and routes:
110
+
111
+ - `@fastify/websocket` -- WebSocket support
112
+ - `@fastify/helmet` -- Security headers
113
+ - `@fastify/multipart` -- File upload support
114
+ - `@fastify/static` -- Static file serving
115
+ - `@fastify/cors` -- Cross-origin resource sharing
116
+
117
+ **Routes:**
118
+ - `GET/POST /api/:service/:method` -- HTTP API endpoint
119
+ - `POST /upload` -- File upload endpoint
120
+ - `GET /ws` or `GET /` (WebSocket) -- WebSocket connection (V2 protocol with V1 legacy fallback)
121
+ - `GET/POST/PUT/DELETE/PATCH/HEAD /*` -- Static file wildcard handler
122
+
123
+ Registers graceful shutdown handlers for `SIGINT` and `SIGTERM` (10s timeout before force exit).
124
+
125
+ ### `close`
126
+
127
+ Close all WebSocket connections and shut down the Fastify server.
128
+
129
+ ### `broadcastReload`
130
+
131
+ Broadcast a reload message to all connected WebSocket clients.
132
+
133
+ ```typescript
134
+ async broadcastReload(
135
+ clientName: string | undefined,
136
+ changedFileSet: Set<string>,
137
+ ): Promise<void>;
138
+ ```
139
+
140
+ ### `emitEvent`
141
+
142
+ Emit an event to matching WebSocket clients.
143
+
144
+ ```typescript
145
+ async emitEvent<TInfo, TData>(
146
+ eventDef: ServiceEventDef<TInfo, TData>,
147
+ infoSelector: (item: TInfo) => boolean,
148
+ data: TData,
149
+ ): Promise<void>;
150
+ ```
151
+
152
+ ### `signAuthToken`
153
+
154
+ Sign a JWT auth token.
155
+
156
+ ```typescript
157
+ async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
158
+ ```
159
+
160
+ ### `verifyAuthToken`
161
+
162
+ Verify a JWT auth token.
163
+
164
+ ```typescript
165
+ async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
166
+ ```
167
+
168
+ ## `createServiceServer`
169
+
170
+ Factory function.
171
+
172
+ ```typescript
173
+ function createServiceServer<TAuthInfo = unknown>(
174
+ options: ServiceServerOptions,
175
+ ): ServiceServer<TAuthInfo>;
176
+ ```
177
+
178
+ ## Example
179
+
180
+ ```typescript
181
+ import {
182
+ createServiceServer,
183
+ defineService,
184
+ auth,
185
+ OrmService,
186
+ AutoUpdateService,
187
+ SmtpClientService,
188
+ } from "@simplysm/service-server";
189
+
190
+ const MyService = defineService("My", auth((ctx) => ({
191
+ hello: (name: string) => `Hello, ${name}!`,
192
+ })));
193
+
194
+ const server = createServiceServer({
195
+ rootPath: "/app",
196
+ port: 3000,
197
+ auth: { jwtSecret: process.env.JWT_SECRET! },
198
+ services: [MyService, OrmService, AutoUpdateService, SmtpClientService],
199
+ });
200
+
201
+ server.on("ready", () => {
202
+ console.log("Server is ready");
203
+ });
204
+
205
+ await server.listen();
206
+ ```
@@ -0,0 +1,176 @@
1
+ # Built-in Services
2
+
3
+ Pre-defined service definitions ready to use with `ServiceServer`.
4
+
5
+ ## OrmService
6
+
7
+ Database ORM service. Requires authentication (wrapped with `auth()`). Requires WebSocket connection (cannot be used over HTTP).
8
+
9
+ ```typescript
10
+ import { OrmService, type OrmServiceType } from "@simplysm/service-server";
11
+ ```
12
+
13
+ ### Definition
14
+
15
+ ```typescript
16
+ const OrmService: ServiceDefinition;
17
+ ```
18
+
19
+ Service name: `"Orm"`
20
+
21
+ ### Methods
22
+
23
+ ```typescript
24
+ interface OrmServiceType {
25
+ getInfo(opt: DbConnOptions & { configName: string }): Promise<{
26
+ dialect: Dialect;
27
+ database?: string;
28
+ schema?: string;
29
+ }>;
30
+ connect(opt: DbConnOptions & { configName: string }): Promise<number>;
31
+ close(connId: number): Promise<void>;
32
+ beginTransaction(connId: number, isolationLevel?: IsolationLevel): Promise<void>;
33
+ commitTransaction(connId: number): Promise<void>;
34
+ rollbackTransaction(connId: number): Promise<void>;
35
+ executeParametrized(connId: number, query: string, params?: unknown[]): Promise<unknown[][]>;
36
+ executeDefs(
37
+ connId: number,
38
+ defs: QueryDef[],
39
+ options?: (ResultMeta | undefined)[],
40
+ ): Promise<unknown[][]>;
41
+ bulkInsert(
42
+ connId: number,
43
+ tableName: string,
44
+ columnDefs: Record<string, ColumnMeta>,
45
+ records: Record<string, unknown>[],
46
+ ): Promise<void>;
47
+ }
48
+ ```
49
+
50
+ **Behavior:**
51
+ - Database connections are tracked per WebSocket socket using a `WeakMap`
52
+ - Connections are automatically cleaned up when the socket closes
53
+ - Configuration is loaded from `.config.json` under the `"orm"` section
54
+ - Supports `mssql-azure` dialect (mapped to `mssql` for query building)
55
+
56
+ **Configuration example** (`.config.json`):
57
+ ```json
58
+ {
59
+ "orm": {
60
+ "myDb": {
61
+ "dialect": "mysql",
62
+ "host": "localhost",
63
+ "port": 3306,
64
+ "database": "mydb",
65
+ "user": "root",
66
+ "password": "secret"
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ---
73
+
74
+ ## AutoUpdateService
75
+
76
+ Auto-update service for client applications. No authentication required.
77
+
78
+ ```typescript
79
+ import { AutoUpdateService, type AutoUpdateServiceType } from "@simplysm/service-server";
80
+ ```
81
+
82
+ ### Definition
83
+
84
+ ```typescript
85
+ const AutoUpdateService: ServiceDefinition;
86
+ ```
87
+
88
+ Service name: `"AutoUpdate"`
89
+
90
+ ### Methods
91
+
92
+ ```typescript
93
+ interface AutoUpdateServiceType {
94
+ getLastVersion(platform: string): Promise<
95
+ | { version: string; downloadPath: string }
96
+ | undefined
97
+ >;
98
+ }
99
+ ```
100
+
101
+ **Behavior:**
102
+ - Scans `{clientPath}/{platform}/updates/` for version files
103
+ - Android: looks for `.apk` files
104
+ - Other platforms: looks for `.exe` files
105
+ - Returns the highest semver version found
106
+ - Returns `undefined` if no updates directory or no valid versions exist
107
+
108
+ ---
109
+
110
+ ## SmtpClientService
111
+
112
+ SMTP email sending service. No authentication required.
113
+
114
+ ```typescript
115
+ import { SmtpClientService, type SmtpClientServiceType } from "@simplysm/service-server";
116
+ ```
117
+
118
+ ### Definition
119
+
120
+ ```typescript
121
+ const SmtpClientService: ServiceDefinition;
122
+ ```
123
+
124
+ Service name: `"SmtpClient"`
125
+
126
+ ### Methods
127
+
128
+ ```typescript
129
+ interface SmtpClientServiceType {
130
+ send(options: SmtpClientSendOption): Promise<string>;
131
+ sendByConfig(configName: string, options: SmtpClientSendByDefaultOption): Promise<string>;
132
+ }
133
+ ```
134
+
135
+ **`send`** -- Send email with explicit SMTP configuration. Returns the message ID.
136
+
137
+ **`sendByConfig`** -- Send email using server-side SMTP configuration. Configuration is loaded from `.config.json` under the `"smtp"` section.
138
+
139
+ **Configuration example** (`.config.json`):
140
+ ```json
141
+ {
142
+ "smtp": {
143
+ "default": {
144
+ "senderName": "My App",
145
+ "senderEmail": "noreply@example.com",
146
+ "user": "smtp-user",
147
+ "pass": "smtp-pass",
148
+ "host": "smtp.example.com",
149
+ "port": 587,
150
+ "secure": false
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Usage
159
+
160
+ Register built-in services when creating the server:
161
+
162
+ ```typescript
163
+ import {
164
+ createServiceServer,
165
+ OrmService,
166
+ AutoUpdateService,
167
+ SmtpClientService,
168
+ } from "@simplysm/service-server";
169
+
170
+ const server = createServiceServer({
171
+ rootPath: "/app",
172
+ port: 3000,
173
+ auth: { jwtSecret: "secret" },
174
+ services: [OrmService, AutoUpdateService, SmtpClientService],
175
+ });
176
+ ```
@@ -0,0 +1,152 @@
1
+ # Transport
2
+
3
+ ## WebSocket Transport
4
+
5
+ ### `WebSocketHandler`
6
+
7
+ Manages multiple WebSocket connections, routes messages to services, and handles event broadcasting.
8
+
9
+ ```typescript
10
+ interface WebSocketHandler {
11
+ addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
12
+ closeAll(): void;
13
+ broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
14
+ emit<TInfo, TData>(
15
+ eventDef: ServiceEventDef<TInfo, TData>,
16
+ infoSelector: (item: TInfo) => boolean,
17
+ data: TData,
18
+ ): Promise<void>;
19
+ }
20
+ ```
21
+
22
+ ### `createWebSocketHandler`
23
+
24
+ ```typescript
25
+ function createWebSocketHandler(
26
+ runMethod: (def: {
27
+ serviceName: string;
28
+ methodName: string;
29
+ params: unknown[];
30
+ socket?: ServiceSocket;
31
+ }) => Promise<unknown>,
32
+ jwtSecret: string | undefined,
33
+ ): WebSocketHandler;
34
+ ```
35
+
36
+ **Behavior:**
37
+ - Routes incoming messages to service methods, auth, and event operations
38
+ - Manages a map of connected `ServiceSocket` instances by client ID
39
+ - Replaces existing connections for the same client ID
40
+ - Handles `auth`, `evt:add`, `evt:remove`, `evt:gets`, `evt:emit`, and service method requests
41
+
42
+ ---
43
+
44
+ ### `ServiceSocket`
45
+
46
+ Manages a single WebSocket connection with protocol encoding/decoding, ping/pong keep-alive, and event listener tracking.
47
+
48
+ ```typescript
49
+ interface ServiceSocket {
50
+ readonly connectedAtDateTime: DateTime;
51
+ readonly clientName: string;
52
+ readonly connReq: FastifyRequest;
53
+ authTokenPayload?: AuthTokenPayload;
54
+
55
+ close(): void;
56
+ send(uuid: string, msg: ServiceServerMessage): Promise<number>;
57
+ addListener(key: string, eventName: string, info: unknown): void;
58
+ removeListener(key: string): void;
59
+ getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
60
+ filterEventTargetKeys(targetKeys: string[]): string[];
61
+
62
+ on(event: "error", handler: (err: Error) => void): void;
63
+ on(event: "close", handler: (code: number) => void): void;
64
+ on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
65
+ }
66
+ ```
67
+
68
+ ### `createServiceSocket`
69
+
70
+ ```typescript
71
+ function createServiceSocket(
72
+ socket: WebSocket,
73
+ clientId: string,
74
+ clientName: string,
75
+ connReq: FastifyRequest,
76
+ ): ServiceSocket;
77
+ ```
78
+
79
+ **Behavior:**
80
+ - Wraps raw WebSocket with protocol encoding/decoding via `ServerProtocolWrapper`
81
+ - Sends ping every 5s; terminates if pong not received
82
+ - Handles raw ping/pong packets (`0x01` ping, `0x02` pong)
83
+ - Tracks event listeners per socket for event broadcasting
84
+ - Sends progress notifications for chunked message reception
85
+
86
+ ---
87
+
88
+ ## HTTP Transport
89
+
90
+ ### `handleHttpRequest`
91
+
92
+ Handle HTTP API requests (GET/POST) to `/api/:service/:method`.
93
+
94
+ ```typescript
95
+ async function handleHttpRequest<TAuthInfo = unknown>(
96
+ req: FastifyRequest,
97
+ reply: FastifyReply,
98
+ jwtSecret: string | undefined,
99
+ runMethod: (def: {
100
+ serviceName: string;
101
+ methodName: string;
102
+ params: unknown[];
103
+ http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
104
+ }) => Promise<unknown>,
105
+ ): Promise<void>;
106
+ ```
107
+
108
+ **Behavior:**
109
+ - Requires `x-sd-client-name` header
110
+ - GET: reads params from `?json=...` query parameter
111
+ - POST: reads params from request body (must be an array)
112
+ - Parses `Authorization: Bearer <token>` header if present
113
+
114
+ ### `handleUpload`
115
+
116
+ Handle multipart file upload to `/upload`.
117
+
118
+ ```typescript
119
+ async function handleUpload(
120
+ req: FastifyRequest,
121
+ reply: FastifyReply,
122
+ rootPath: string,
123
+ jwtSecret: string | undefined,
124
+ ): Promise<void>;
125
+ ```
126
+
127
+ **Behavior:**
128
+ - Requires authentication (JWT token in Authorization header)
129
+ - Saves files to `{rootPath}/www/uploads/` with UUID filenames
130
+ - Returns `ServiceUploadResult[]` with path, filename, and size
131
+ - Cleans up incomplete files on error
132
+
133
+ ### `handleStaticFile`
134
+
135
+ Handle static file serving.
136
+
137
+ ```typescript
138
+ async function handleStaticFile(
139
+ req: FastifyRequest,
140
+ reply: FastifyReply,
141
+ rootPath: string,
142
+ urlPath: string,
143
+ ): Promise<void>;
144
+ ```
145
+
146
+ **Behavior:**
147
+ - Serves files from `{rootPath}/www/`
148
+ - Path traversal protection (rejects paths outside allowed root)
149
+ - Redirects directories to trailing-slash URLs
150
+ - Returns `index.html` for directory requests
151
+ - Returns 403 for hidden files (starting with `.`)
152
+ - Returns 404 for missing files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "13.0.97",
3
+ "version": "13.0.98",
4
4
  "description": "Simplysm package - service module (server)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -36,11 +36,11 @@
36
36
  "semver": "^7.7.4",
37
37
  "utf-8-validate": "^6.0.6",
38
38
  "ws": "^8.19.0",
39
- "@simplysm/core-common": "13.0.97",
40
- "@simplysm/core-node": "13.0.97",
41
- "@simplysm/orm-node": "13.0.97",
42
- "@simplysm/orm-common": "13.0.97",
43
- "@simplysm/service-common": "13.0.97"
39
+ "@simplysm/core-node": "13.0.98",
40
+ "@simplysm/core-common": "13.0.98",
41
+ "@simplysm/orm-common": "13.0.98",
42
+ "@simplysm/service-common": "13.0.98",
43
+ "@simplysm/orm-node": "13.0.98"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/nodemailer": "^7.0.11",