@simplysm/service-server 13.0.82 → 13.0.83

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,40 @@
1
+ # @simplysm/service-server
2
+
3
+ Fastify-based service server framework with WebSocket support, JWT authentication, service routing, file uploads, and built-in ORM/SMTP/auto-update services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @simplysm/service-server
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createServiceServer, defineService } from "@simplysm/service-server";
15
+
16
+ const HealthService = defineService("Health", (ctx) => ({
17
+ check: () => ({ status: "ok" }),
18
+ }));
19
+
20
+ const server = createServiceServer({
21
+ rootPath: process.cwd(),
22
+ port: 3000,
23
+ services: [HealthService],
24
+ });
25
+
26
+ await server.listen();
27
+ ```
28
+
29
+ ## Documentation
30
+
31
+ | Category | File | Description |
32
+ |----------|------|-------------|
33
+ | Server | [docs/server.md](docs/server.md) | `ServiceServer` class and `createServiceServer` factory |
34
+ | Service Definition | [docs/service-definition.md](docs/service-definition.md) | `defineService`, `auth`, `ServiceContext`, `ServiceMethods` |
35
+ | Authentication | [docs/authentication.md](docs/authentication.md) | JWT management and `AuthTokenPayload` |
36
+ | Transport | [docs/transport.md](docs/transport.md) | WebSocket handler, HTTP request handler, upload handler, static file handler |
37
+ | Protocol | [docs/protocol.md](docs/protocol.md) | `ServerProtocolWrapper` with worker thread offloading |
38
+ | Built-in Services | [docs/builtin-services.md](docs/builtin-services.md) | `OrmService`, `AutoUpdateService`, `SmtpClientService` |
39
+ | Utilities | [docs/utilities.md](docs/utilities.md) | `getConfig` with file watching and caching |
40
+ | Legacy | [docs/legacy.md](docs/legacy.md) | V1 auto-update handler for backward compatibility |
@@ -0,0 +1,70 @@
1
+ # Authentication
2
+
3
+ JWT-based authentication using the `jose` library with HS256 algorithm.
4
+
5
+ ## `AuthTokenPayload<TAuthInfo>`
6
+
7
+ ```typescript
8
+ interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
9
+ roles: string[]; // Role strings for permission checks
10
+ data: TAuthInfo; // Custom application-specific auth data
11
+ }
12
+ ```
13
+
14
+ ---
15
+
16
+ ## `signJwt<TAuthInfo>(jwtSecret, payload): Promise<string>`
17
+
18
+ Signs a JWT token with HS256 algorithm. Tokens are issued with the current timestamp and expire after 12 hours.
19
+
20
+ ```typescript
21
+ import { signJwt } from "@simplysm/service-server";
22
+
23
+ const token = await signJwt("my-secret", {
24
+ roles: ["admin"],
25
+ data: { userId: 123, name: "John" },
26
+ });
27
+ ```
28
+
29
+ ---
30
+
31
+ ## `verifyJwt<TAuthInfo>(jwtSecret, token): Promise<AuthTokenPayload<TAuthInfo>>`
32
+
33
+ Verifies a JWT token and returns the decoded payload. Throws a descriptive error for expired or invalid tokens.
34
+
35
+ ```typescript
36
+ import { verifyJwt } from "@simplysm/service-server";
37
+
38
+ try {
39
+ const payload = await verifyJwt("my-secret", token);
40
+ console.log(payload.roles, payload.data);
41
+ } catch (err) {
42
+ // "Token has expired." or "Invalid token."
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## `decodeJwt<TAuthInfo>(token): AuthTokenPayload<TAuthInfo>`
49
+
50
+ Decodes a JWT token **without** verifying its signature. Useful for reading token contents when verification is not needed.
51
+
52
+ ```typescript
53
+ import { decodeJwt } from "@simplysm/service-server";
54
+
55
+ const payload = decodeJwt(token);
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Server-level Auth Helpers
61
+
62
+ `ServiceServer` provides convenience methods that delegate to the JWT functions using the configured `auth.jwtSecret`:
63
+
64
+ ```typescript
65
+ // Sign a token
66
+ const token = await server.signAuthToken({ roles: ["user"], data: myAuthInfo });
67
+
68
+ // Verify a token
69
+ const payload = await server.verifyAuthToken(token);
70
+ ```
@@ -0,0 +1,130 @@
1
+ # Built-in Services
2
+
3
+ Pre-built service definitions ready to include in `ServiceServerOptions.services`.
4
+
5
+ ## `OrmService`
6
+
7
+ Database operations service using `@simplysm/orm-node`. Requires authentication (service-level `auth` wrapper). **WebSocket only** -- cannot be used over HTTP.
8
+
9
+ Manages database connections per WebSocket socket. Connections are automatically cleaned up when the socket closes.
10
+
11
+ ### Methods
12
+
13
+ | Method | Parameters | Returns | Description |
14
+ |--------|-----------|---------|-------------|
15
+ | `getInfo` | `opt: DbConnOptions & { configName }` | `{ dialect, database?, schema? }` | Get database connection info from config |
16
+ | `connect` | `opt: DbConnOptions & { configName }` | `number` (connId) | Open a database connection |
17
+ | `close` | `connId: number` | `void` | Close a database connection |
18
+ | `beginTransaction` | `connId, isolationLevel?` | `void` | Begin a transaction |
19
+ | `commitTransaction` | `connId` | `void` | Commit the current transaction |
20
+ | `rollbackTransaction` | `connId` | `void` | Rollback the current transaction |
21
+ | `executeParametrized` | `connId, query, params?` | `unknown[][]` | Execute a parameterized query |
22
+ | `executeDefs` | `connId, defs, options?` | `unknown[][]` | Execute query definitions with optional result parsing |
23
+ | `bulkInsert` | `connId, tableName, columnDefs, records` | `void` | Perform bulk insert |
24
+
25
+ ### Configuration
26
+
27
+ Reads from the `"orm"` config section via `ctx.getConfig("orm")`. Config file (`.config.json`) example:
28
+
29
+ ```json
30
+ {
31
+ "orm": {
32
+ "default": {
33
+ "dialect": "mysql",
34
+ "host": "localhost",
35
+ "port": 3306,
36
+ "username": "root",
37
+ "password": "password",
38
+ "database": "mydb"
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Type export
45
+
46
+ ```typescript
47
+ export type OrmServiceType = ServiceMethods<typeof OrmService>;
48
+ ```
49
+
50
+ ---
51
+
52
+ ## `AutoUpdateService`
53
+
54
+ Provides app auto-update version checking. Scans `{clientPath}/{platform}/updates/` for versioned files.
55
+
56
+ ### Methods
57
+
58
+ | Method | Parameters | Returns | Description |
59
+ |--------|-----------|---------|-------------|
60
+ | `getLastVersion` | `platform: string` | `{ version, downloadPath } \| undefined` | Get the latest version for a platform |
61
+
62
+ - **Android**: Looks for `.apk` files
63
+ - **Other platforms**: Looks for `.exe` files
64
+ - Version is extracted from the filename (e.g., `1.2.3.apk`)
65
+ - Uses `semver.maxSatisfying` to find the highest version
66
+
67
+ ### Type export
68
+
69
+ ```typescript
70
+ export type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>;
71
+ ```
72
+
73
+ ---
74
+
75
+ ## `SmtpClientService`
76
+
77
+ Email sending service using `nodemailer`.
78
+
79
+ ### Methods
80
+
81
+ | Method | Parameters | Returns | Description |
82
+ |--------|-----------|---------|-------------|
83
+ | `send` | `options: SmtpClientSendOption` | `string` (messageId) | Send an email with explicit SMTP settings |
84
+ | `sendByConfig` | `configName, options: SmtpClientSendByDefaultOption` | `string` (messageId) | Send using a named SMTP configuration |
85
+
86
+ ### Configuration
87
+
88
+ `sendByConfig` reads from the `"smtp"` config section. Config file (`.config.json`) example:
89
+
90
+ ```json
91
+ {
92
+ "smtp": {
93
+ "default": {
94
+ "host": "smtp.example.com",
95
+ "port": 587,
96
+ "secure": false,
97
+ "user": "user@example.com",
98
+ "pass": "password",
99
+ "senderName": "My App",
100
+ "senderEmail": "noreply@example.com"
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Type export
107
+
108
+ ```typescript
109
+ export type SmtpClientServiceType = ServiceMethods<typeof SmtpClientService>;
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Registering Built-in Services
115
+
116
+ ```typescript
117
+ import {
118
+ createServiceServer,
119
+ OrmService,
120
+ AutoUpdateService,
121
+ SmtpClientService,
122
+ } from "@simplysm/service-server";
123
+
124
+ const server = createServiceServer({
125
+ rootPath: process.cwd(),
126
+ port: 3000,
127
+ auth: { jwtSecret: "my-secret" },
128
+ services: [OrmService, AutoUpdateService, SmtpClientService],
129
+ });
130
+ ```
package/docs/legacy.md ADDED
@@ -0,0 +1,45 @@
1
+ # Legacy
2
+
3
+ ## `handleV1Connection(socket, autoUpdateMethods, clientNameSetter?): void`
4
+
5
+ Handles V1 legacy WebSocket client connections. Only the `SdAutoUpdateService.getLastVersion` command is supported; all other requests receive an `UPGRADE_REQUIRED` error response.
6
+
7
+ ### Parameters
8
+
9
+ | Parameter | Type | Description |
10
+ |-----------|------|-------------|
11
+ | `socket` | `WebSocket` | The raw WebSocket connection |
12
+ | `autoUpdateMethods` | `{ getLastVersion: (platform: string) => Promise<any> }` | Auto-update method implementations |
13
+ | `clientNameSetter` | `(clientName: string \| undefined) => void` | Optional callback to set the legacy client name on the context |
14
+
15
+ ### V1 Protocol
16
+
17
+ **Request format:**
18
+ ```json
19
+ {
20
+ "uuid": "request-id",
21
+ "command": "SdAutoUpdateService.getLastVersion",
22
+ "params": ["android"],
23
+ "clientName": "my-app"
24
+ }
25
+ ```
26
+
27
+ **Success response:**
28
+ ```json
29
+ {
30
+ "name": "response",
31
+ "reqUuid": "request-id",
32
+ "state": "success",
33
+ "body": { "version": "1.2.3", "downloadPath": "/my-app/android/updates/1.2.3.apk" }
34
+ }
35
+ ```
36
+
37
+ **Upgrade-required response (for unsupported commands):**
38
+ ```json
39
+ {
40
+ "name": "response",
41
+ "reqUuid": "request-id",
42
+ "state": "error",
43
+ "body": { "message": "App upgrade is required.", "code": "UPGRADE_REQUIRED" }
44
+ }
45
+ ```
@@ -0,0 +1,34 @@
1
+ # Protocol
2
+
3
+ Message encoding/decoding with automatic worker thread offloading for heavy payloads.
4
+
5
+ ## `ServerProtocolWrapper`
6
+
7
+ ```typescript
8
+ interface ServerProtocolWrapper {
9
+ encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
10
+ decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
11
+ dispose(): void;
12
+ }
13
+ ```
14
+
15
+ ## `createServerProtocolWrapper(): ServerProtocolWrapper`
16
+
17
+ Creates a protocol wrapper instance that automatically delegates heavy operations to a shared worker thread.
18
+
19
+ ### Worker delegation strategy
20
+
21
+ **Encoding** is offloaded to the worker when:
22
+ - The message body is a `Uint8Array`
23
+ - The message body is an array containing `Uint8Array` elements
24
+
25
+ **Decoding** is offloaded to the worker when:
26
+ - The incoming bytes exceed 30 KB
27
+
28
+ Lightweight operations stay on the main thread for lower latency.
29
+
30
+ ### Worker details
31
+
32
+ - Uses a lazy singleton worker thread shared across all protocol wrappers
33
+ - Worker has a 4 GB memory limit (`maxOldGenerationSizeMb: 4096`)
34
+ - Built on `@simplysm/core-node` `Worker` / `WorkerProxy`
package/docs/server.md ADDED
@@ -0,0 +1,102 @@
1
+ # Server
2
+
3
+ The main server class built on Fastify with WebSocket support, CORS, Helmet security, static file serving, and graceful shutdown.
4
+
5
+ ## `ServiceServer<TAuthInfo>`
6
+
7
+ **Extends:** `EventEmitter<{ ready: void; close: void }>`
8
+
9
+ ### Constructor
10
+
11
+ ```typescript
12
+ new ServiceServer<TAuthInfo>(options: ServiceServerOptions)
13
+ ```
14
+
15
+ ### Properties
16
+
17
+ | Property | Type | Description |
18
+ |----------|------|-------------|
19
+ | `isOpen` | `boolean` | Whether the server is currently listening |
20
+ | `fastify` | `FastifyInstance` | The underlying Fastify instance |
21
+ | `options` | `ServiceServerOptions` | The server configuration |
22
+
23
+ ### Methods
24
+
25
+ #### `listen(): Promise<void>`
26
+
27
+ Starts the server. Registers all Fastify plugins (WebSocket, Helmet, Multipart, Static, CORS), sets up routes (`/api/:service/:method`, `/upload`, `/ws`, `/*`), and begins listening on the configured port.
28
+
29
+ Emits the `"ready"` event once listening.
30
+
31
+ #### `close(): Promise<void>`
32
+
33
+ Closes all WebSocket connections and shuts down the Fastify server. Emits the `"close"` event.
34
+
35
+ #### `broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>`
36
+
37
+ Broadcasts a reload message to all connected WebSocket clients.
38
+
39
+ #### `emitEvent<TInfo, TData>(eventDef, infoSelector, data): Promise<void>`
40
+
41
+ Emits a typed event to connected clients matching the `infoSelector` filter.
42
+
43
+ ```typescript
44
+ await server.emitEvent(
45
+ myEventDef,
46
+ (info) => info.userId === targetUserId,
47
+ { message: "hello" },
48
+ );
49
+ ```
50
+
51
+ #### `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>`
52
+
53
+ Signs a JWT token with the configured secret. Throws if `auth.jwtSecret` is not configured.
54
+
55
+ #### `verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>`
56
+
57
+ Verifies and decodes a JWT token. Throws if expired or invalid.
58
+
59
+ ### Events
60
+
61
+ | Event | Description |
62
+ |-------|-------------|
63
+ | `ready` | Emitted after the server starts listening |
64
+ | `close` | Emitted after the server is closed |
65
+
66
+ ### Graceful Shutdown
67
+
68
+ The server automatically registers `SIGINT` and `SIGTERM` handlers. On signal, it closes all connections and exits. If shutdown exceeds 10 seconds, the process is force-exited.
69
+
70
+ ---
71
+
72
+ ## `createServiceServer<TAuthInfo>(options): ServiceServer<TAuthInfo>`
73
+
74
+ Factory function that creates a new `ServiceServer` instance.
75
+
76
+ ---
77
+
78
+ ## `ServiceServerOptions`
79
+
80
+ ```typescript
81
+ interface ServiceServerOptions {
82
+ rootPath: string; // Root directory for www/ static files and configs
83
+ port: number; // Port to listen on
84
+ ssl?: {
85
+ pfxBytes: Uint8Array; // PFX certificate bytes
86
+ passphrase: string; // PFX passphrase
87
+ };
88
+ auth?: {
89
+ jwtSecret: string; // Secret for JWT signing/verification
90
+ };
91
+ services: ServiceDefinition[]; // Array of service definitions
92
+ }
93
+ ```
94
+
95
+ ## Routes
96
+
97
+ | Route | Method | Description |
98
+ |-------|--------|-------------|
99
+ | `/api/:service/:method` | GET, POST | HTTP service method invocation |
100
+ | `/upload` | POST | Multipart file upload (requires auth) |
101
+ | `/` or `/ws` | WebSocket | WebSocket connection endpoint |
102
+ | `/*` | ALL | Static file serving from `{rootPath}/www/` |
@@ -0,0 +1,134 @@
1
+ # Service Definition
2
+
3
+ APIs for defining services, applying authentication, and sharing types with clients.
4
+
5
+ ## `defineService<TMethods>(name, factory): ServiceDefinition<TMethods>`
6
+
7
+ Creates a service definition with a name and a factory function that receives a `ServiceContext` and returns an object of methods.
8
+
9
+ ```typescript
10
+ import { defineService } from "@simplysm/service-server";
11
+
12
+ const HealthService = defineService("Health", (ctx) => ({
13
+ check: () => ({ status: "ok" }),
14
+ getClientName: () => ctx.clientName,
15
+ }));
16
+ ```
17
+
18
+ ---
19
+
20
+ ## `auth(fn): Function`
21
+
22
+ Wraps a service factory or individual method to require authentication. Accepts an optional array of role permissions.
23
+
24
+ ### Overloads
25
+
26
+ ```typescript
27
+ // Require login (any authenticated user)
28
+ auth(fn)
29
+
30
+ // Require specific roles
31
+ auth(["admin", "editor"], fn)
32
+ ```
33
+
34
+ ### Service-level auth
35
+
36
+ All methods in the service require authentication:
37
+
38
+ ```typescript
39
+ const UserService = defineService("User", auth((ctx) => ({
40
+ getProfile: () => ctx.authInfo,
41
+ updateProfile: (data: any) => { /* ... */ },
42
+ })));
43
+ ```
44
+
45
+ ### Method-level auth
46
+
47
+ Only specific methods require authentication or specific roles:
48
+
49
+ ```typescript
50
+ const MixedService = defineService("Mixed", (ctx) => ({
51
+ publicMethod: () => "anyone can call this",
52
+ protectedMethod: auth(() => "login required"),
53
+ adminMethod: auth(["admin"], () => "admin only"),
54
+ }));
55
+ ```
56
+
57
+ ### Permission resolution
58
+
59
+ - Method-level auth takes precedence over service-level auth.
60
+ - If `requiredPerms` is an empty array (`auth(fn)`), any authenticated user can access.
61
+ - If `requiredPerms` contains roles (`auth(["admin"], fn)`), the user must have at least one matching role.
62
+
63
+ ---
64
+
65
+ ## `ServiceContext<TAuthInfo>`
66
+
67
+ The context object passed to service factory functions.
68
+
69
+ ### Properties
70
+
71
+ | Property | Type | Description |
72
+ |----------|------|-------------|
73
+ | `server` | `ServiceServer<TAuthInfo>` | The server instance |
74
+ | `socket` | `ServiceSocket \| undefined` | WebSocket connection (if called via WebSocket) |
75
+ | `http` | `{ clientName: string; authTokenPayload?: AuthTokenPayload } \| undefined` | HTTP request info (if called via HTTP) |
76
+ | `legacy` | `{ clientName?: string } \| undefined` | V1 legacy context (auto-update only) |
77
+
78
+ ### Computed Properties
79
+
80
+ | Property | Type | Description |
81
+ |----------|------|-------------|
82
+ | `authInfo` | `TAuthInfo \| undefined` | The authenticated user's data from the JWT payload |
83
+ | `clientName` | `string \| undefined` | Client application name (validated for path safety) |
84
+ | `clientPath` | `string \| undefined` | Resolved path: `{rootPath}/www/{clientName}` |
85
+
86
+ ### Methods
87
+
88
+ #### `getConfig<T>(section: string): Promise<T>`
89
+
90
+ Reads a configuration section from `.config.json` files. Merges root-level config (`{rootPath}/.config.json`) with client-level config (`{clientPath}/.config.json`), where client values override root values.
91
+
92
+ ```typescript
93
+ const dbConfig = await ctx.getConfig<DbSettings>("database");
94
+ ```
95
+
96
+ ---
97
+
98
+ ## `ServiceDefinition<TMethods>`
99
+
100
+ ```typescript
101
+ interface ServiceDefinition<TMethods> {
102
+ name: string;
103
+ factory: (ctx: ServiceContext) => TMethods;
104
+ authPermissions?: string[];
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## `ServiceMethods<TDefinition>`
111
+
112
+ Type utility that extracts method signatures from a `ServiceDefinition`. Useful for sharing types with the client.
113
+
114
+ ```typescript
115
+ const UserService = defineService("User", auth((ctx) => ({
116
+ getProfile: () => ctx.authInfo,
117
+ })));
118
+
119
+ // Export for client-side usage
120
+ export type UserServiceType = ServiceMethods<typeof UserService>;
121
+
122
+ // Client side:
123
+ // client.getService<UserServiceType>("User");
124
+ ```
125
+
126
+ ---
127
+
128
+ ## `executeServiceMethod(server, def): Promise<unknown>`
129
+
130
+ Internal function that locates and invokes a service method. Performs service lookup, client name validation, context creation, auth checking, and method execution.
131
+
132
+ ## `getServiceAuthPermissions(fn): string[] | undefined`
133
+
134
+ Reads auth permissions metadata from an `auth()`-wrapped function. Returns `undefined` if the function is not wrapped.
@@ -0,0 +1,97 @@
1
+ # Transport
2
+
3
+ Handles communication between clients and the server over WebSocket and HTTP.
4
+
5
+ ## WebSocket
6
+
7
+ ### `WebSocketHandler`
8
+
9
+ Manages multiple WebSocket connections, routes messages to services, and handles event broadcasting.
10
+
11
+ ```typescript
12
+ interface WebSocketHandler {
13
+ addSocket(socket: WebSocket, clientId: string, clientName: string, connReq: FastifyRequest): void;
14
+ closeAll(): void;
15
+ broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
16
+ emit<TInfo, TData>(eventDef: ServiceEventDef<TInfo, TData>, infoSelector: (item: TInfo) => boolean, data: TData): Promise<void>;
17
+ }
18
+ ```
19
+
20
+ ### `createWebSocketHandler(runMethod, jwtSecret): WebSocketHandler`
21
+
22
+ Creates a WebSocket handler instance. The `runMethod` callback is invoked to execute service methods.
23
+
24
+ **Message routing:**
25
+
26
+ | Message Pattern | Action |
27
+ |----------------|--------|
28
+ | `"ServiceName.methodName"` | Invoke service method |
29
+ | `"evt:add"` | Register event listener |
30
+ | `"evt:remove"` | Remove event listener |
31
+ | `"evt:gets"` | Get all listeners for an event |
32
+ | `"evt:emit"` | Emit event to matching clients |
33
+ | `"auth"` | Authenticate WebSocket connection via JWT |
34
+
35
+ ---
36
+
37
+ ### `ServiceSocket`
38
+
39
+ Manages a single WebSocket connection with protocol encoding/decoding, ping/pong keep-alive, and event listener tracking.
40
+
41
+ ```typescript
42
+ interface ServiceSocket {
43
+ readonly connectedAtDateTime: DateTime;
44
+ readonly clientName: string;
45
+ readonly connReq: FastifyRequest;
46
+ authTokenPayload?: AuthTokenPayload;
47
+
48
+ close(): void;
49
+ send(uuid: string, msg: ServiceServerMessage): Promise<number>;
50
+ addListener(key: string, eventName: string, info: unknown): void;
51
+ removeListener(key: string): void;
52
+ getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
53
+ filterEventTargetKeys(targetKeys: string[]): string[];
54
+ on(event: "error" | "close" | "message", handler: Function): void;
55
+ }
56
+ ```
57
+
58
+ ### `createServiceSocket(socket, clientId, clientName, connReq): ServiceSocket`
59
+
60
+ Creates a service socket instance. Features:
61
+
62
+ - **Protocol encoding/decoding** via `ServerProtocolWrapper` (with worker thread offloading)
63
+ - **Ping/pong keep-alive** every 5 seconds; terminates unresponsive connections
64
+ - **Event listener tracking** for pub/sub messaging
65
+ - **Progress reporting** for chunked message transfers
66
+
67
+ ---
68
+
69
+ ## HTTP
70
+
71
+ ### `handleHttpRequest<TAuthInfo>(req, reply, jwtSecret, runMethod): Promise<void>`
72
+
73
+ Handles HTTP API requests on `/api/:service/:method`.
74
+
75
+ - **GET**: Parameters parsed from `?json=` query parameter
76
+ - **POST**: Parameters parsed from JSON request body (must be an array)
77
+ - **Auth**: Reads `Authorization: Bearer <token>` header; returns 401 on failure
78
+ - **Client name**: Required via `x-sd-client-name` header
79
+
80
+ ### `handleUpload(req, reply, rootPath, jwtSecret): Promise<void>`
81
+
82
+ Handles multipart file uploads on `/upload`.
83
+
84
+ - Requires authentication (JWT in `Authorization` header)
85
+ - Files saved to `{rootPath}/www/uploads/` with UUID-based filenames
86
+ - Returns `ServiceUploadResult[]` with path, original filename, and size
87
+ - Cleans up incomplete files on failure
88
+
89
+ ### `handleStaticFile(req, reply, rootPath, urlPath): Promise<void>`
90
+
91
+ Serves static files from `{rootPath}/www/`.
92
+
93
+ - Path traversal protection
94
+ - Auto-redirects directories to include trailing slash
95
+ - Serves `index.html` for directory requests
96
+ - Blocks access to hidden files (dotfiles) with 403
97
+ - Returns appropriate HTML error pages for 403, 404, 500
@@ -0,0 +1,31 @@
1
+ # Utilities
2
+
3
+ ## `getConfig<TConfig>(filePath): Promise<TConfig | undefined>`
4
+
5
+ Reads and caches a JSON configuration file with automatic live-reloading via file system watcher.
6
+
7
+ ### Features
8
+
9
+ - **Caching**: Configurations are cached in a `LazyGcMap` with auto-renewal on access
10
+ - **Live-reload**: File changes are detected and the cache is updated automatically (100ms debounce)
11
+ - **Garbage collection**: Cache entries expire after 1 hour of inactivity; GC runs every 10 minutes
12
+ - **Watcher cleanup**: File watchers are released when cache entries expire or files are deleted
13
+
14
+ ### Usage
15
+
16
+ ```typescript
17
+ import { getConfig } from "@simplysm/service-server";
18
+
19
+ const config = await getConfig<{ key: string }>("/path/to/.config.json");
20
+ ```
21
+
22
+ This function is used internally by `ServiceContext.getConfig()` to load root and client-level configuration files.
23
+
24
+ ### Behavior
25
+
26
+ 1. Returns cached value if available (resets expiry timer)
27
+ 2. If file does not exist, returns `undefined`
28
+ 3. Reads and parses the JSON file, stores in cache
29
+ 4. Registers a file watcher for live-reload
30
+ 5. On file change: re-reads and updates cache
31
+ 6. On file deletion: removes cache entry and closes watcher
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "13.0.82",
3
+ "version": "13.0.83",
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.82",
40
- "@simplysm/orm-node": "13.0.82",
41
- "@simplysm/orm-common": "13.0.82",
42
- "@simplysm/core-node": "13.0.82",
43
- "@simplysm/service-common": "13.0.82"
39
+ "@simplysm/core-common": "13.0.83",
40
+ "@simplysm/core-node": "13.0.83",
41
+ "@simplysm/orm-common": "13.0.83",
42
+ "@simplysm/orm-node": "13.0.83",
43
+ "@simplysm/service-common": "13.0.83"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/nodemailer": "^6.4.23",