@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 +98 -0
- package/docs/auth.md +48 -0
- package/docs/core.md +161 -0
- package/docs/server.md +206 -0
- package/docs/services.md +176 -0
- package/docs/transport.md +152 -0
- package/package.json +6 -6
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
|
+
```
|
package/docs/services.md
ADDED
|
@@ -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.
|
|
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-
|
|
40
|
-
"@simplysm/core-
|
|
41
|
-
"@simplysm/orm-
|
|
42
|
-
"@simplysm/
|
|
43
|
-
"@simplysm/
|
|
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",
|