@simplysm/service-server 13.0.85 → 13.0.88

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 CHANGED
@@ -1,40 +1,294 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify-based service server framework with WebSocket support, JWT authentication, service routing, file uploads, and built-in ORM/SMTP/auto-update services.
3
+ Fastify 기반 서비스 서버. WebSocket RPC, HTTP API, JWT 인증, 이벤트 브로드캐스트, 파일 업로드, 정적 파일 서빙을 제공한다.
4
4
 
5
- ## Installation
5
+ ## 설치
6
6
 
7
7
  ```bash
8
8
  npm install @simplysm/service-server
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ **주요 의존성:** `fastify`, `@fastify/cors`, `@fastify/websocket`, `@fastify/static`, `@fastify/multipart`, `@fastify/helmet`, `jose`, `nodemailer`, `ws`
12
12
 
13
- ```typescript
14
- import { createServiceServer, defineService } from "@simplysm/service-server";
13
+ **내부 의존성:** `@simplysm/core-common`, `@simplysm/core-node`, `@simplysm/orm-common`, `@simplysm/orm-node`, `@simplysm/service-common`
15
14
 
16
- const HealthService = defineService("Health", (ctx) => ({
17
- check: () => ({ status: "ok" }),
18
- }));
15
+ ## 서버 생성 실행
16
+
17
+ ```typescript
18
+ import { createServiceServer, defineService, auth } from "@simplysm/service-server";
19
19
 
20
20
  const server = createServiceServer({
21
- rootPath: process.cwd(),
21
+ rootPath: "/app",
22
22
  port: 3000,
23
- services: [HealthService],
23
+ ssl: { pfxBytes, passphrase: "cert-pass" }, // 선택
24
+ auth: { jwtSecret: "my-secret" }, // 선택
25
+ services: [UserService, OrmService],
24
26
  });
25
27
 
26
28
  await server.listen();
29
+ // ... 종료 시
30
+ await server.close();
31
+ ```
32
+
33
+ ### ServiceServerOptions
34
+
35
+ ```typescript
36
+ interface ServiceServerOptions {
37
+ rootPath: string; // 루트 경로 (www/, .config.json 기준)
38
+ port: number; // 리스닝 포트
39
+ ssl?: { // HTTPS 설정 (선택)
40
+ pfxBytes: Uint8Array;
41
+ passphrase: string;
42
+ };
43
+ auth?: { // JWT 인증 설정 (선택)
44
+ jwtSecret: string;
45
+ };
46
+ services: ServiceDefinition[]; // 등록할 서비스 목록
47
+ }
48
+ ```
49
+
50
+ ## 서비스 정의
51
+
52
+ `defineService`로 서비스를 정의하고, `auth`로 인증을 요구한다.
53
+
54
+ ```typescript
55
+ import { defineService, auth } from "@simplysm/service-server";
56
+ import type { ServiceContext, ServiceMethods } from "@simplysm/service-server";
57
+
58
+ // 기본 서비스 (인증 불필요)
59
+ const UserService = defineService("User", (ctx: ServiceContext) => ({
60
+ async findAll() {
61
+ return [{ id: 1, name: "Alice" }];
62
+ },
63
+ async findById(id: number) {
64
+ return { id, name: "Alice" };
65
+ },
66
+ }));
67
+
68
+ // 서비스 전체에 인증 필수
69
+ const AdminService = defineService("Admin", auth((ctx) => ({
70
+ async getStats() {
71
+ const authInfo = ctx.authInfo; // 인증 정보 접근
72
+ return { users: 100 };
73
+ },
74
+ })));
75
+
76
+ // 서비스 전체에 역할 기반 인증
77
+ const SecureService = defineService("Secure", auth(["admin", "manager"], (ctx) => ({
78
+ async deleteUser(id: number) { /* ... */ },
79
+ })));
80
+
81
+ // 메서드 단위 인증 (서비스 레벨은 공개, 특정 메서드만 인증)
82
+ const MixedService = defineService("Mixed", (ctx) => ({
83
+ async publicMethod() { /* 누구나 호출 가능 */ },
84
+ adminOnly: auth(["admin"], async () => { /* admin만 호출 가능 */ }),
85
+ }));
86
+
87
+ // 클라이언트 타입 공유용
88
+ export type UserServiceType = ServiceMethods<typeof UserService>;
89
+ ```
90
+
91
+ ### auth 래퍼
92
+
93
+ `auth` 함수는 서비스 팩토리 또는 개별 메서드에 적용할 수 있다.
94
+
95
+ ```typescript
96
+ // 서비스 레벨: 모든 메서드에 로그인 필수
97
+ auth((ctx) => ({ ... }))
98
+
99
+ // 서비스 레벨: 특정 역할 필수
100
+ auth(["admin"], (ctx) => ({ ... }))
101
+
102
+ // 메서드 레벨: 해당 메서드만 로그인 필수
103
+ auth(() => result)
104
+
105
+ // 메서드 레벨: 해당 메서드만 특정 역할 필수
106
+ auth(["admin"], () => result)
107
+ ```
108
+
109
+ 인증 검사 우선순위: 메서드 레벨 > 서비스 레벨. 메서드에 `auth`가 있으면 서비스 레벨 설정을 무시한다.
110
+
111
+ ## ServiceContext
112
+
113
+ 서비스 메서드에 주입되는 컨텍스트 객체.
114
+
115
+ ```typescript
116
+ interface ServiceContext<TAuthInfo = unknown> {
117
+ server: ServiceServer<TAuthInfo>; // 서버 인스턴스
118
+ socket?: ServiceSocket; // WebSocket 연결 (소켓 호출 시)
119
+ http?: { // HTTP 요청 (HTTP 호출 시)
120
+ clientName: string;
121
+ authTokenPayload?: AuthTokenPayload<TAuthInfo>;
122
+ };
123
+
124
+ get authInfo(): TAuthInfo | undefined; // 인증 데이터 (socket 또는 http에서 추출)
125
+ get clientName(): string | undefined; // 클라이언트 이름
126
+ get clientPath(): string | undefined; // rootPath/www/{clientName} 경로
127
+ getConfig<T>(section: string): Promise<T>; // .config.json에서 설정 읽기
128
+ }
129
+ ```
130
+
131
+ `getConfig`는 `rootPath/.config.json`을 기본으로 읽고, `clientPath/.config.json`이 있으면 머지한다.
132
+
133
+ ## JWT 인증
134
+
135
+ `jose` 라이브러리 기반. HS256 알고리즘, 토큰 유효기간 12시간.
136
+
137
+ ```typescript
138
+ // 토큰 발행
139
+ const token = await server.signAuthToken({
140
+ roles: ["admin"],
141
+ data: { userId: 1, name: "Alice" },
142
+ });
143
+
144
+ // 토큰 검증
145
+ const payload = await server.verifyAuthToken(token);
146
+ // { roles: ["admin"], data: { userId: 1, name: "Alice" } }
147
+ ```
148
+
149
+ ### AuthTokenPayload
150
+
151
+ ```typescript
152
+ interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
153
+ roles: string[]; // 역할 목록 (권한 검사용)
154
+ data: TAuthInfo; // 사용자 정의 인증 데이터
155
+ }
156
+ ```
157
+
158
+ ### JWT 유틸리티 함수
159
+
160
+ 서버 인스턴스 없이 직접 사용할 수 있는 저수준 함수.
161
+
162
+ ```typescript
163
+ import { signJwt, verifyJwt, decodeJwt } from "@simplysm/service-server";
164
+
165
+ const token = await signJwt("secret", { roles: ["user"], data: { id: 1 } });
166
+ const payload = await verifyJwt("secret", token); // 검증 + 디코드 (만료 시 에러)
167
+ const decoded = decodeJwt(token); // 검증 없이 디코드
27
168
  ```
28
169
 
29
- ## Documentation
170
+ ## 이벤트 브로드캐스트
171
+
172
+ WebSocket으로 연결된 클라이언트에게 이벤트를 전송한다.
173
+
174
+ ```typescript
175
+ import { defineEvent } from "@simplysm/service-common";
176
+
177
+ const OrderCreated = defineEvent<{ shopId: string }, { orderId: string; amount: number }>(
178
+ "order-created"
179
+ );
180
+
181
+ // 조건에 맞는 구독자에게 이벤트 전송
182
+ await server.emitEvent(
183
+ OrderCreated,
184
+ (info) => info.shopId === "shop-1",
185
+ { orderId: "order-123", amount: 50000 }
186
+ );
187
+
188
+ // 클라이언트 리로드 알림 (개발 모드용)
189
+ await server.broadcastReload("my-app", new Set(["file1.ts", "file2.ts"]));
190
+ ```
191
+
192
+ ## HTTP API
193
+
194
+ 자동으로 등록되는 HTTP 엔드포인트:
195
+
196
+ | 메서드 | 경로 | 설명 |
197
+ |--------|------|------|
198
+ | GET | `/api/:service/:method?json=...` | 서비스 호출 (params: JSON 인코딩 배열) |
199
+ | POST | `/api/:service/:method` | 서비스 호출 (params: body 배열) |
200
+ | POST | `/upload` | 파일 업로드 (multipart, 인증 필수) |
201
+ | GET | `/...` | 정적 파일 서빙 (`rootPath/www/`) |
202
+
203
+ ### HTTP 헤더
204
+
205
+ - `x-sd-client-name` (필수): 클라이언트 이름
206
+ - `Authorization: Bearer <token>` (선택): JWT 인증 토큰
207
+
208
+ ### 파일 업로드
209
+
210
+ 인증 필수. multipart/form-data로 전송. 파일은 `rootPath/www/uploads/`에 UUID 이름으로 저장된다.
211
+
212
+ ```typescript
213
+ // 응답 타입
214
+ interface ServiceUploadResult {
215
+ path: string; // "uploads/{uuid}.ext"
216
+ filename: string; // 원본 파일명
217
+ size: number; // 바이트 크기
218
+ }
219
+ ```
220
+
221
+ ## 내장 서비스
222
+
223
+ 상세 API는 [docs/builtin-services.md](docs/builtin-services.md) 참조.
224
+
225
+ ### OrmService
226
+
227
+ 인증 필수. WebSocket 전용. 소켓별 DB 커넥션 풀을 관리한다. 소켓 종료 시 자동 정리.
228
+
229
+ ```typescript
230
+ import { OrmService } from "@simplysm/service-server";
231
+
232
+ const server = createServiceServer({
233
+ services: [OrmService],
234
+ // ...
235
+ });
236
+ ```
237
+
238
+ `.config.json`에 DB 설정 필요:
239
+
240
+ ```json
241
+ {
242
+ "orm": {
243
+ "main": {
244
+ "dialect": "mysql",
245
+ "host": "localhost",
246
+ "port": 3306,
247
+ "username": "root",
248
+ "password": "password"
249
+ }
250
+ }
251
+ }
252
+ ```
253
+
254
+ ### AutoUpdateService
255
+
256
+ `clientPath/{platform}/updates/` 디렉토리에서 최신 버전을 탐색한다. `.apk`(Android), `.exe`(Windows) 지원. V1 레거시 클라이언트 호환.
257
+
258
+ ```typescript
259
+ import { AutoUpdateService } from "@simplysm/service-server";
260
+ ```
261
+
262
+ ### SmtpClientService
263
+
264
+ nodemailer 기반 이메일 전송 서비스. 직접 설정 또는 `.config.json` 기반 전송을 지원한다.
265
+
266
+ ```typescript
267
+ import { SmtpClientService } from "@simplysm/service-server";
268
+ ```
269
+
270
+ ## 설정 파일
271
+
272
+ `rootPath/.config.json`에서 설정을 읽는다. LRU 캐시(1시간 만료, 10분 GC 주기)와 파일 감시로 자동 리로드된다.
273
+
274
+ ```typescript
275
+ const dbConfig = await ctx.getConfig<DbConfig>("orm.main");
276
+ ```
277
+
278
+ 클라이언트별 설정이 있으면 (`rootPath/www/{clientName}/.config.json`) 루트 설정에 머지된다.
279
+
280
+ ## 서버 이벤트
281
+
282
+ ```typescript
283
+ server.on("ready", () => { /* 서버 시작 완료 */ });
284
+ server.on("close", () => { /* 서버 종료 완료 */ });
285
+ ```
286
+
287
+ ## Graceful Shutdown
288
+
289
+ SIGINT/SIGTERM 시그널 수신 시 자동으로 graceful shutdown을 수행한다. 10초 타임아웃 후 강제 종료.
290
+
291
+ ## 상세 API 레퍼런스
30
292
 
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 |
293
+ - [내장 서비스 상세](docs/builtin-services.md) -- OrmService, AutoUpdateService, SmtpClientService 메서드 시그니처
294
+ - [전송 계층 및 프로토콜](docs/transport-protocol.md) -- WebSocket 핸들러, ServiceSocket, 프로토콜 래퍼
@@ -1,91 +1,192 @@
1
- # Built-in Services
1
+ # 내장 서비스 상세 API
2
2
 
3
- Pre-built service definitions ready to include in `ServiceServerOptions.services`.
3
+ ## OrmService
4
4
 
5
- ## `OrmService`
5
+ 서비스 이름: `"Orm"`. 인증 필수 (`auth` 래핑). WebSocket 전용 -- HTTP로는 사용 불가.
6
6
 
7
- Database operations service using `@simplysm/orm-node`. Requires authentication (service-level `auth` wrapper). **WebSocket only** -- cannot be used over HTTP.
7
+ 소켓별로 DB 커넥션을 `WeakMap<ServiceSocket, Map<number, DbConn>>`으로 관리한다. 소켓 `close` 이벤트 해당 소켓의 모든 커넥션을 자동 정리한다.
8
8
 
9
- Manages database connections per WebSocket socket. Connections are automatically cleaned up when the socket closes.
9
+ ### 메서드
10
10
 
11
- ### Methods
11
+ ```typescript
12
+ // DB 정보 조회 (연결 없이)
13
+ async getInfo(opt: DbConnOptions & { configName: string }): Promise<{
14
+ dialect: Dialect; // "mysql" | "postgresql" | "mssql" (mssql-azure는 mssql로 반환)
15
+ database?: string;
16
+ schema?: string;
17
+ }>
18
+
19
+ // DB 연결 생성 (connId 반환)
20
+ async connect(opt: DbConnOptions & { configName: string }): Promise<number>
21
+
22
+ // DB 연결 종료
23
+ async close(connId: number): Promise<void>
24
+
25
+ // 트랜잭션 제어
26
+ async beginTransaction(connId: number, isolationLevel?: IsolationLevel): Promise<void>
27
+ async commitTransaction(connId: number): Promise<void>
28
+ async rollbackTransaction(connId: number): Promise<void>
29
+
30
+ // 파라미터화된 쿼리 실행
31
+ async executeParametrized(
32
+ connId: number,
33
+ query: string,
34
+ params?: unknown[],
35
+ ): Promise<unknown[][]>
36
+
37
+ // QueryDef 기반 쿼리 실행 (쿼리 빌더 사용)
38
+ async executeDefs(
39
+ connId: number,
40
+ defs: QueryDef[],
41
+ options?: (ResultMeta | undefined)[],
42
+ ): Promise<unknown[][]>
43
+
44
+ // 벌크 인서트
45
+ async bulkInsert(
46
+ connId: number,
47
+ tableName: string,
48
+ columnDefs: Record<string, ColumnMeta>,
49
+ records: Record<string, unknown>[],
50
+ ): Promise<void>
51
+ ```
52
+
53
+ ### DbConnOptions
12
54
 
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 |
55
+ `@simplysm/service-common`에서 가져온다.
24
56
 
25
- ### Configuration
57
+ ```typescript
58
+ type DbConnOptions = {
59
+ configName?: string;
60
+ config?: Record<string, unknown>; // .config.json 설정 오버라이드
61
+ };
62
+ ```
26
63
 
27
- Reads from the `"orm"` config section via `ctx.getConfig("orm")`. Config file (`.config.json`) example:
64
+ ### 설정 예시
65
+
66
+ `.config.json`:
28
67
 
29
68
  ```json
30
69
  {
31
70
  "orm": {
32
- "default": {
71
+ "main": {
33
72
  "dialect": "mysql",
34
73
  "host": "localhost",
35
74
  "port": 3306,
36
75
  "username": "root",
37
76
  "password": "password",
38
77
  "database": "mydb"
78
+ },
79
+ "secondary": {
80
+ "dialect": "postgresql",
81
+ "host": "pg-host",
82
+ "port": 5432,
83
+ "username": "admin",
84
+ "password": "password",
85
+ "database": "mydb",
86
+ "schema": "public"
39
87
  }
40
88
  }
41
89
  }
42
90
  ```
43
91
 
44
- ### Type export
92
+ ### 타입 내보내기
45
93
 
46
94
  ```typescript
47
- export type OrmServiceType = ServiceMethods<typeof OrmService>;
95
+ import type { OrmServiceType } from "@simplysm/service-server";
96
+ // 클라이언트에서 타입 공유: client.getService<OrmServiceType>("Orm")
48
97
  ```
49
98
 
50
99
  ---
51
100
 
52
- ## `AutoUpdateService`
101
+ ## AutoUpdateService
102
+
103
+ 서비스 이름: `"AutoUpdate"`. 인증 불필요. `clientPath/{platform}/updates/` 디렉토리에서 semver 기준 최신 버전 파일을 탐색한다.
53
104
 
54
- Provides app auto-update version checking. Scans `{clientPath}/{platform}/updates/` for versioned files.
105
+ ### 메서드
55
106
 
56
- ### Methods
107
+ ```typescript
108
+ async getLastVersion(platform: string): Promise<{
109
+ version: string; // 최신 버전 문자열 (예: "1.2.3")
110
+ downloadPath: string; // 다운로드 경로 (예: "/my-app/android/updates/1.2.3.apk")
111
+ } | undefined>
112
+ ```
113
+
114
+ ### 지원 플랫폼
115
+
116
+ | platform | 파일 확장자 | 디렉토리 경로 |
117
+ |----------|------------|---------------|
118
+ | `"android"` | `.apk` | `www/{clientName}/android/updates/` |
119
+ | 기타 | `.exe` | `www/{clientName}/{platform}/updates/` |
120
+
121
+ 파일명은 버전 번호여야 한다 (예: `1.2.3.apk`, `2.0.0.exe`). `semver.maxSatisfying`으로 최신 버전을 결정한다.
57
122
 
58
- | Method | Parameters | Returns | Description |
59
- |--------|-----------|---------|-------------|
60
- | `getLastVersion` | `platform: string` | `{ version, downloadPath } \| undefined` | Get the latest version for a platform |
123
+ ### V1 레거시 호환
61
124
 
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
125
+ V1 프로토콜 클라이언트(WebSocket `ver` 쿼리 파라미터 없음)도 `SdAutoUpdateService.getLastVersion` 명령으로 자동 업데이트를 요청할 수 있다. V1 클라이언트의 다른 요청은 `UPGRADE_REQUIRED` 에러를 반환한다.
66
126
 
67
- ### Type export
127
+ ### 타입 내보내기
68
128
 
69
129
  ```typescript
70
- export type AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>;
130
+ import type { AutoUpdateServiceType } from "@simplysm/service-server";
71
131
  ```
72
132
 
73
133
  ---
74
134
 
75
- ## `SmtpClientService`
135
+ ## SmtpClientService
76
136
 
77
- Email sending service using `nodemailer`.
137
+ 서비스 이름: `"SmtpClient"`. 인증 불필요. nodemailer 기반 이메일 전송.
78
138
 
79
- ### Methods
139
+ ### 메서드
80
140
 
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 |
141
+ ```typescript
142
+ // 직접 SMTP 설정으로 전송
143
+ async send(options: SmtpClientSendOption): Promise<string>
144
+ // 반환값: messageId
145
+
146
+ // .config.json의 smtp 설정으로 전송
147
+ async sendByConfig(configName: string, options: SmtpClientSendByDefaultOption): Promise<string>
148
+ // 반환값: messageId
149
+ ```
85
150
 
86
- ### Configuration
151
+ ### SmtpClientSendOption
87
152
 
88
- `sendByConfig` reads from the `"smtp"` config section. Config file (`.config.json`) example:
153
+ `@simplysm/service-common`에서 가져온다.
154
+
155
+ ```typescript
156
+ interface SmtpClientSendOption {
157
+ host: string;
158
+ port: number;
159
+ secure?: boolean;
160
+ user?: string;
161
+ pass?: string;
162
+ from: string;
163
+ to: string | string[];
164
+ cc?: string | string[];
165
+ bcc?: string | string[];
166
+ subject: string;
167
+ html?: string;
168
+ text?: string;
169
+ attachments?: Array<{ filename: string; content: Uint8Array }>;
170
+ }
171
+ ```
172
+
173
+ ### SmtpClientSendByDefaultOption
174
+
175
+ ```typescript
176
+ interface SmtpClientSendByDefaultOption {
177
+ to: string | string[];
178
+ cc?: string | string[];
179
+ bcc?: string | string[];
180
+ subject: string;
181
+ html?: string;
182
+ text?: string;
183
+ attachments?: Array<{ filename: string; content: Uint8Array }>;
184
+ }
185
+ ```
186
+
187
+ ### 설정 예시
188
+
189
+ `.config.json`:
89
190
 
90
191
  ```json
91
192
  {
@@ -94,7 +195,7 @@ Email sending service using `nodemailer`.
94
195
  "host": "smtp.example.com",
95
196
  "port": 587,
96
197
  "secure": false,
97
- "user": "user@example.com",
198
+ "user": "noreply@example.com",
98
199
  "pass": "password",
99
200
  "senderName": "My App",
100
201
  "senderEmail": "noreply@example.com"
@@ -103,28 +204,37 @@ Email sending service using `nodemailer`.
103
204
  }
104
205
  ```
105
206
 
106
- ### Type export
207
+ `senderEmail`이 없으면 `user`가 발신자 이메일로 사용된다.
208
+
209
+ ### 사용 예시
107
210
 
108
211
  ```typescript
109
- export type SmtpClientServiceType = ServiceMethods<typeof SmtpClientService>;
110
- ```
212
+ // 서버에서 직접 사용
213
+ const ctx = createServiceContext(server);
214
+ const methods = SmtpClientService.factory(ctx);
215
+
216
+ // 직접 설정
217
+ await methods.send({
218
+ host: "smtp.example.com",
219
+ port: 587,
220
+ user: "user@example.com",
221
+ pass: "pass",
222
+ from: '"My App" <noreply@example.com>',
223
+ to: "recipient@example.com",
224
+ subject: "테스트",
225
+ html: "<p>Hello</p>",
226
+ });
111
227
 
112
- ---
228
+ // .config.json 기반
229
+ await methods.sendByConfig("default", {
230
+ to: "recipient@example.com",
231
+ subject: "테스트",
232
+ html: "<p>Hello</p>",
233
+ });
234
+ ```
113
235
 
114
- ## Registering Built-in Services
236
+ ### 타입 내보내기
115
237
 
116
238
  ```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
- });
239
+ import type { SmtpClientServiceType } from "@simplysm/service-server";
130
240
  ```
@@ -0,0 +1,200 @@
1
+ # 전송 계층 및 프로토콜
2
+
3
+ ## WebSocket 전송
4
+
5
+ ### WebSocketHandler
6
+
7
+ 다수의 WebSocket 연결을 관리하고, 메시지를 서비스로 라우팅하며, 이벤트 브로드캐스트를 처리한다.
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
+ function createWebSocketHandler(
22
+ runMethod: (def: {
23
+ serviceName: string;
24
+ methodName: string;
25
+ params: unknown[];
26
+ socket?: ServiceSocket;
27
+ }) => Promise<unknown>,
28
+ jwtSecret: string | undefined,
29
+ ): WebSocketHandler;
30
+ ```
31
+
32
+ ### WebSocket 연결 흐름
33
+
34
+ 1. 클라이언트가 `/` 또는 `/ws`로 WebSocket 연결
35
+ 2. 쿼리 파라미터로 프로토콜 버전 구분:
36
+ - `ver=2&clientId=...&clientName=...` -- V2 프로토콜 (현재)
37
+ - 쿼리 없음 -- V1 레거시 (AutoUpdate만 지원)
38
+ 3. V2: `createServiceSocket`으로 소켓 래핑, 메시지 라우팅 시작
39
+ 4. 같은 `clientId`로 재연결 시 이전 소켓을 종료하고 새 소켓으로 교체
40
+
41
+ ### WebSocket 메시지 프로토콜
42
+
43
+ V2 클라이언트 메시지 포맷 (`ServiceClientMessage`):
44
+
45
+ | `name` | `body` | 설명 |
46
+ |--------|--------|------|
47
+ | `"{Service}.{Method}"` | `unknown[]` (파라미터 배열) | 서비스 메서드 호출 |
48
+ | `"auth"` | `string` (JWT 토큰) | 인증 토큰 설정 |
49
+ | `"evt:add"` | `{ key, name, info }` | 이벤트 리스너 등록 |
50
+ | `"evt:remove"` | `{ key }` | 이벤트 리스너 제거 |
51
+ | `"evt:gets"` | `{ name }` | 이벤트 리스너 목록 조회 |
52
+ | `"evt:emit"` | `{ keys, data }` | 대상 키에 이벤트 전송 |
53
+
54
+ V2 서버 응답 (`ServiceServerMessage`):
55
+
56
+ | `name` | `body` | 설명 |
57
+ |--------|--------|------|
58
+ | `"response"` | `unknown` (결과) | 성공 응답 |
59
+ | `"error"` | `{ name, message, code, stack }` | 에러 응답 |
60
+ | `"progress"` | `{ totalSize, completedSize }` | 전송 진행률 |
61
+ | `"evt:on"` | `{ keys, data }` | 이벤트 수신 |
62
+ | `"reload"` | `{ clientName, changedFileSet }` | 리로드 알림 |
63
+
64
+ ---
65
+
66
+ ### ServiceSocket
67
+
68
+ 단일 WebSocket 연결을 관리하는 인터페이스. 프로토콜 인코딩/디코딩, ping/pong keep-alive(5초 간격), 이벤트 리스너 추적을 담당한다.
69
+
70
+ ```typescript
71
+ interface ServiceSocket {
72
+ readonly connectedAtDateTime: DateTime;
73
+ readonly clientName: string;
74
+ readonly connReq: FastifyRequest;
75
+ authTokenPayload?: AuthTokenPayload;
76
+
77
+ close(): void;
78
+ send(uuid: string, msg: ServiceServerMessage): Promise<number>;
79
+
80
+ // 이벤트 리스너 관리
81
+ addListener(key: string, eventName: string, info: unknown): void;
82
+ removeListener(key: string): void;
83
+ getEventListeners(eventName: string): Array<{ key: string; info: unknown }>;
84
+ filterEventTargetKeys(targetKeys: string[]): string[];
85
+
86
+ // 이벤트 핸들러 등록
87
+ on(event: "error", handler: (err: Error) => void): void;
88
+ on(event: "close", handler: (code: number) => void): void;
89
+ on(event: "message", handler: (data: { uuid: string; msg: ServiceClientMessage }) => void): void;
90
+ }
91
+
92
+ function createServiceSocket(
93
+ socket: WebSocket,
94
+ clientId: string,
95
+ clientName: string,
96
+ connReq: FastifyRequest,
97
+ ): ServiceSocket;
98
+ ```
99
+
100
+ ---
101
+
102
+ ## HTTP 전송
103
+
104
+ ### handleHttpRequest
105
+
106
+ HTTP API 요청 처리 함수. 서버 내부에서 `/api/:service/:method` 라우트에 등록된다.
107
+
108
+ ```typescript
109
+ async function handleHttpRequest<TAuthInfo>(
110
+ req: FastifyRequest,
111
+ reply: FastifyReply,
112
+ jwtSecret: string | undefined,
113
+ runMethod: (def: {
114
+ serviceName: string;
115
+ methodName: string;
116
+ params: unknown[];
117
+ http: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> };
118
+ }) => Promise<unknown>,
119
+ ): Promise<void>;
120
+ ```
121
+
122
+ - GET: `?json=` 쿼리 파라미터에서 파라미터 배열을 JSON 파싱
123
+ - POST: request body를 배열로 파싱
124
+ - `x-sd-client-name` 헤더 필수
125
+ - `Authorization: Bearer <token>` 헤더가 있으면 JWT 검증 후 `authTokenPayload`로 전달
126
+
127
+ ### handleUpload
128
+
129
+ 파일 업로드 처리 함수. `/upload` 라우트에 등록된다. 인증 필수.
130
+
131
+ ```typescript
132
+ async function handleUpload(
133
+ req: FastifyRequest,
134
+ reply: FastifyReply,
135
+ rootPath: string,
136
+ jwtSecret: string | undefined,
137
+ ): Promise<void>;
138
+ ```
139
+
140
+ - multipart/form-data 필수
141
+ - 파일을 `rootPath/www/uploads/{UUID}.{ext}`로 저장
142
+ - 업로드 실패 시 불완전한 파일 자동 삭제
143
+ - 응답: `ServiceUploadResult[]`
144
+
145
+ ### handleStaticFile
146
+
147
+ 정적 파일 서빙 함수. 와일드카드 라우트 `/*`에 등록된다.
148
+
149
+ ```typescript
150
+ async function handleStaticFile(
151
+ req: FastifyRequest,
152
+ reply: FastifyReply,
153
+ rootPath: string,
154
+ urlPath: string,
155
+ ): Promise<void>;
156
+ ```
157
+
158
+ - `rootPath/www/` 디렉토리 기준
159
+ - 디렉토리 접근 시 trailing slash 리다이렉트 + `index.html` 서빙
160
+ - `.`으로 시작하는 파일은 403 거부
161
+ - path traversal 방지 (보안 가드)
162
+
163
+ ---
164
+
165
+ ## 프로토콜 래퍼
166
+
167
+ 메시지 인코딩/디코딩을 자동으로 워커 스레드에 오프로드한다. 30KB 이상의 메시지 또는 `Uint8Array` 포함 메시지는 워커에서 처리하여 메인 스레드 블로킹을 방지한다.
168
+
169
+ ```typescript
170
+ interface ServerProtocolWrapper {
171
+ encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
172
+ decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
173
+ dispose(): void;
174
+ }
175
+
176
+ function createServerProtocolWrapper(): ServerProtocolWrapper;
177
+ ```
178
+
179
+ ### 워커 오프로드 조건
180
+
181
+ - **인코딩**: 메시지 body가 `Uint8Array`이거나, 배열 내에 `Uint8Array` 요소가 있으면 워커 사용
182
+ - **디코딩**: 메시지 크기가 30KB 초과이면 워커 사용
183
+ - 워커는 lazy singleton으로 관리 (최대 4GB 메모리 제한)
184
+
185
+ ---
186
+
187
+ ## 설정 관리
188
+
189
+ ### getConfig
190
+
191
+ `.config.json` 파일을 읽고 캐시하는 함수.
192
+
193
+ ```typescript
194
+ async function getConfig<TConfig>(filePath: string): Promise<TConfig | undefined>;
195
+ ```
196
+
197
+ - LRU 캐시: 1시간 만료, 10분마다 GC
198
+ - 파일 감시(FsWatcher)로 변경 시 자동 리로드
199
+ - 파일 삭제 시 캐시에서 제거 및 감시 해제
200
+ - 캐시 만료 시 감시도 함께 해제
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "13.0.85",
3
+ "version": "13.0.88",
4
4
  "description": "Simplysm package - service module (server)",
5
5
  "author": "simplysm",
6
6
  "license": "Apache-2.0",
@@ -36,14 +36,14 @@
36
36
  "semver": "^7.7.4",
37
37
  "utf-8-validate": "^6.0.6",
38
38
  "ws": "^8.19.0",
39
- "@simplysm/orm-common": "13.0.85",
40
- "@simplysm/core-common": "13.0.85",
41
- "@simplysm/orm-node": "13.0.85",
42
- "@simplysm/service-common": "13.0.85",
43
- "@simplysm/core-node": "13.0.85"
39
+ "@simplysm/orm-common": "13.0.88",
40
+ "@simplysm/core-common": "13.0.88",
41
+ "@simplysm/orm-node": "13.0.88",
42
+ "@simplysm/core-node": "13.0.88",
43
+ "@simplysm/service-common": "13.0.88"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/nodemailer": "^6.4.23",
46
+ "@types/nodemailer": "^7.0.11",
47
47
  "@types/semver": "^7.7.1",
48
48
  "@types/ws": "^8.18.1"
49
49
  }
@@ -1,70 +0,0 @@
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
- ```
package/docs/legacy.md DELETED
@@ -1,45 +0,0 @@
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 DELETED
@@ -1,34 +0,0 @@
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 DELETED
@@ -1,102 +0,0 @@
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/` |
@@ -1,134 +0,0 @@
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.
package/docs/transport.md DELETED
@@ -1,97 +0,0 @@
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
package/docs/utilities.md DELETED
@@ -1,31 +0,0 @@
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