@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 +40 -0
- package/docs/authentication.md +70 -0
- package/docs/builtin-services.md +130 -0
- package/docs/legacy.md +45 -0
- package/docs/protocol.md +34 -0
- package/docs/server.md +102 -0
- package/docs/service-definition.md +134 -0
- package/docs/transport.md +97 -0
- package/docs/utilities.md +31 -0
- package/package.json +6 -6
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
|
+
```
|
package/docs/protocol.md
ADDED
|
@@ -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.
|
|
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.
|
|
40
|
-
"@simplysm/
|
|
41
|
-
"@simplysm/orm-common": "13.0.
|
|
42
|
-
"@simplysm/
|
|
43
|
-
"@simplysm/service-common": "13.0.
|
|
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",
|