@simplysm/service-server 13.0.96 → 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 CHANGED
@@ -1,364 +1,98 @@
1
1
  # @simplysm/service-server
2
2
 
3
- Fastify 기반 서비스 서버. WebSocket RPC, HTTP API, JWT 인증, 이벤트 브로드캐스트, 파일 업로드, 정적 파일 서빙을 제공한다.
3
+ Service module (server) -- Fastify-based service server with WebSocket support, JWT authentication, and built-in ORM/SMTP/auto-update services.
4
4
 
5
- ## 설치
5
+ ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install @simplysm/service-server
9
9
  ```
10
10
 
11
- **주요 의존성:** `fastify`, `@fastify/cors`, `@fastify/websocket`, `@fastify/static`, `@fastify/multipart`, `@fastify/helmet`, `jose`, `nodemailer`, `ws`
12
-
13
- **내부 의존성:** `@simplysm/core-common`, `@simplysm/core-node`, `@simplysm/orm-common`, `@simplysm/orm-node`, `@simplysm/service-common`
14
-
15
- ## 문서
16
-
17
- | 카테고리 | 설명 |
18
- |---------|------|
19
- | [내장 서비스](docs/builtin-services.md) | OrmService, AutoUpdateService, SmtpClientService 메서드 시그니처 |
20
- | [전송 계층 및 프로토콜](docs/transport-protocol.md) | WebSocket 핸들러, ServiceSocket, HTTP 핸들러, 프로토콜 래퍼 |
21
-
22
- ## 빠른 시작
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
+ }));
23
74
 
24
- ```typescript
25
- import { createServiceServer, defineService, auth } from "@simplysm/service-server";
75
+ // Define an authenticated service
76
+ const UserService = defineService("User", auth((ctx) => ({
77
+ getProfile: () => ctx.authInfo,
78
+ adminOnly: auth(["admin"], () => "admin-only data"),
79
+ })));
26
80
 
81
+ // Create and start server
27
82
  const server = createServiceServer({
28
83
  rootPath: "/app",
29
84
  port: 3000,
30
- ssl: { pfxBytes, passphrase: "cert-pass" }, // 선택
31
- auth: { jwtSecret: "my-secret" }, // 선택
32
- services: [UserService, OrmService],
85
+ auth: { jwtSecret: "my-secret" },
86
+ services: [HealthService, UserService, OrmService, AutoUpdateService],
33
87
  });
34
88
 
35
89
  await server.listen();
36
- // ... 종료 시
37
- await server.close();
38
- ```
39
-
40
- ## 서버 설정
41
-
42
- ### ServiceServerOptions
43
-
44
- ```typescript
45
- interface ServiceServerOptions {
46
- rootPath: string; // 루트 경로 (www/, .config.json 기준)
47
- port: number; // 리스닝 포트
48
- ssl?: { // HTTPS 설정 (선택)
49
- pfxBytes: Uint8Array;
50
- passphrase: string;
51
- };
52
- auth?: { // JWT 인증 설정 (선택)
53
- jwtSecret: string;
54
- };
55
- services: ServiceDefinition[]; // 등록할 서비스 목록
56
- }
57
- ```
58
-
59
- ### ServiceServer
60
-
61
- ```typescript
62
- class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
63
- ready: void;
64
- close: void;
65
- }> {
66
- isOpen: boolean;
67
- readonly fastify: FastifyInstance;
68
- readonly options: ServiceServerOptions;
69
-
70
- constructor(options: ServiceServerOptions);
71
- async listen(): Promise<void>;
72
- async close(): Promise<void>;
73
-
74
- // 이벤트 브로드캐스트
75
- async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>): Promise<void>;
76
- async emitEvent<TInfo, TData>(
77
- eventDef: ServiceEventDef<TInfo, TData>,
78
- infoSelector: (item: TInfo) => boolean,
79
- data: TData,
80
- ): Promise<void>;
81
-
82
- // JWT 인증
83
- async signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>;
84
- async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>;
85
- }
86
-
87
- // 팩토리 함수
88
- function createServiceServer<TAuthInfo = unknown>(
89
- options: ServiceServerOptions,
90
- ): ServiceServer<TAuthInfo>;
91
- ```
92
-
93
- ## 서비스 정의
94
-
95
- `defineService`로 서비스를 정의하고, `auth`로 인증을 요구한다.
96
-
97
- ```typescript
98
- import { defineService, auth } from "@simplysm/service-server";
99
- import type { ServiceContext, ServiceMethods } from "@simplysm/service-server";
100
-
101
- // 기본 서비스 (인증 불필요)
102
- const UserService = defineService("User", (ctx: ServiceContext) => ({
103
- async findAll() {
104
- return [{ id: 1, name: "Alice" }];
105
- },
106
- async findById(id: number) {
107
- return { id, name: "Alice" };
108
- },
109
- }));
110
-
111
- // 서비스 전체에 인증 필수
112
- const AdminService = defineService("Admin", auth((ctx) => ({
113
- async getStats() {
114
- const authInfo = ctx.authInfo; // 인증 정보 접근
115
- return { users: 100 };
116
- },
117
- })));
118
-
119
- // 서비스 전체에 역할 기반 인증
120
- const SecureService = defineService("Secure", auth(["admin", "manager"], (ctx) => ({
121
- async deleteUser(id: number) { /* ... */ },
122
- })));
123
-
124
- // 메서드 단위 인증 (서비스 레벨은 공개, 특정 메서드만 인증)
125
- const MixedService = defineService("Mixed", (ctx) => ({
126
- async publicMethod() { /* 누구나 호출 가능 */ },
127
- adminOnly: auth(["admin"], async () => { /* admin만 호출 가능 */ }),
128
- }));
129
-
130
- // 클라이언트 타입 공유용
131
- export type UserServiceType = ServiceMethods<typeof UserService>;
132
- ```
133
-
134
- ### defineService
135
-
136
- ```typescript
137
- function defineService<TMethods extends Record<string, (...args: any[]) => any>>(
138
- name: string,
139
- factory: (ctx: ServiceContext) => TMethods,
140
- ): ServiceDefinition<TMethods>;
141
- ```
142
-
143
- ### ServiceDefinition
144
-
145
- ```typescript
146
- interface ServiceDefinition<TMethods = Record<string, (...args: any[]) => any>> {
147
- name: string;
148
- factory: (ctx: ServiceContext) => TMethods;
149
- authPermissions?: string[];
150
- }
151
- ```
152
-
153
- ### ServiceMethods (타입 유틸리티)
154
-
155
- `ServiceDefinition`에서 메서드 시그니처를 추출하는 타입. 클라이언트-서버 간 타입 공유에 사용한다.
156
-
157
- ```typescript
158
- type ServiceMethods<TDefinition> =
159
- TDefinition extends ServiceDefinition<infer M> ? M : never;
160
-
161
- // 사용 예시
162
- export type UserServiceType = ServiceMethods<typeof UserService>;
163
- // 클라이언트: client.getService<UserServiceType>("User");
164
- ```
165
-
166
- ### auth 래퍼
167
-
168
- `auth` 함수는 서비스 팩토리 또는 개별 메서드에 적용할 수 있다.
169
-
170
- ```typescript
171
- // 서비스 레벨: 모든 메서드에 로그인 필수
172
- auth((ctx) => ({ ... }))
173
-
174
- // 서비스 레벨: 특정 역할 필수
175
- auth(["admin"], (ctx) => ({ ... }))
176
-
177
- // 메서드 레벨: 해당 메서드만 로그인 필수
178
- auth(() => result)
179
-
180
- // 메서드 레벨: 해당 메서드만 특정 역할 필수
181
- auth(["admin"], () => result)
182
- ```
183
-
184
- 인증 검사 우선순위: 메서드 레벨 > 서비스 레벨. 메서드에 `auth`가 있으면 서비스 레벨 설정을 무시한다.
185
-
186
- ## ServiceContext
187
-
188
- 서비스 메서드에 주입되는 컨텍스트 객체.
189
-
190
- ```typescript
191
- interface ServiceContext<TAuthInfo = unknown> {
192
- server: ServiceServer<TAuthInfo>; // 서버 인스턴스
193
- socket?: ServiceSocket; // WebSocket 연결 (소켓 호출 시)
194
- http?: { // HTTP 요청 (HTTP 호출 시)
195
- clientName: string;
196
- authTokenPayload?: AuthTokenPayload<TAuthInfo>;
197
- };
198
-
199
- get authInfo(): TAuthInfo | undefined; // 인증 데이터 (socket 또는 http에서 추출)
200
- get clientName(): string | undefined; // 클라이언트 이름
201
- get clientPath(): string | undefined; // rootPath/www/{clientName} 경로
202
- getConfig<T>(section: string): Promise<T>; // .config.json에서 설정 읽기
203
- }
204
- ```
205
-
206
- `getConfig`는 `rootPath/.config.json`을 기본으로 읽고, `clientPath/.config.json`이 있으면 머지한다.
207
-
208
- ## JWT 인증
209
-
210
- `jose` 라이브러리 기반. HS256 알고리즘, 토큰 유효기간 12시간.
211
-
212
- ```typescript
213
- // 토큰 발행
214
- const token = await server.signAuthToken({
215
- roles: ["admin"],
216
- data: { userId: 1, name: "Alice" },
217
- });
218
-
219
- // 토큰 검증
220
- const payload = await server.verifyAuthToken(token);
221
- // { roles: ["admin"], data: { userId: 1, name: "Alice" } }
222
- ```
223
-
224
- ### AuthTokenPayload
225
-
226
- ```typescript
227
- interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
228
- roles: string[]; // 역할 목록 (권한 검사용)
229
- data: TAuthInfo; // 사용자 정의 인증 데이터
230
- }
231
- ```
232
-
233
- ### JWT 유틸리티 함수
234
-
235
- 서버 인스턴스 없이 직접 사용할 수 있는 저수준 함수.
236
-
237
- ```typescript
238
- import { signJwt, verifyJwt, decodeJwt } from "@simplysm/service-server";
239
-
240
- const token = await signJwt("secret", { roles: ["user"], data: { id: 1 } });
241
- const payload = await verifyJwt("secret", token); // 검증 + 디코드 (만료 시 에러)
242
- const decoded = decodeJwt(token); // 검증 없이 디코드
243
- ```
244
-
245
- ## 이벤트 브로드캐스트
246
-
247
- WebSocket으로 연결된 클라이언트에게 이벤트를 전송한다.
248
-
249
- ```typescript
250
- import { defineEvent } from "@simplysm/service-common";
251
-
252
- const OrderCreated = defineEvent<{ shopId: string }, { orderId: string; amount: number }>(
253
- "order-created"
254
- );
255
-
256
- // 조건에 맞는 구독자에게 이벤트 전송
257
- await server.emitEvent(
258
- OrderCreated,
259
- (info) => info.shopId === "shop-1",
260
- { orderId: "order-123", amount: 50000 }
261
- );
262
-
263
- // 클라이언트 리로드 알림 (개발 모드용)
264
- await server.broadcastReload("my-app", new Set(["file1.ts", "file2.ts"]));
265
- ```
266
-
267
- ## HTTP API
268
-
269
- 자동으로 등록되는 HTTP 엔드포인트:
270
-
271
- | 메서드 | 경로 | 설명 |
272
- |--------|------|------|
273
- | GET | `/api/:service/:method?json=...` | 서비스 호출 (params: JSON 인코딩 배열) |
274
- | POST | `/api/:service/:method` | 서비스 호출 (params: body 배열) |
275
- | POST | `/upload` | 파일 업로드 (multipart, 인증 필수) |
276
- | GET | `/...` | 정적 파일 서빙 (`rootPath/www/`) |
277
-
278
- ### HTTP 헤더
279
-
280
- - `x-sd-client-name` (필수): 클라이언트 이름
281
- - `Authorization: Bearer <token>` (선택): JWT 인증 토큰
282
-
283
- ### 파일 업로드
284
-
285
- 인증 필수. multipart/form-data로 전송. 파일은 `rootPath/www/uploads/`에 UUID 이름으로 저장된다.
286
-
287
- ```typescript
288
- // 응답 타입
289
- interface ServiceUploadResult {
290
- path: string; // "uploads/{uuid}.ext"
291
- filename: string; // 원본 파일명
292
- size: number; // 바이트 크기
293
- }
294
- ```
295
-
296
- ## 내장 서비스
297
-
298
- 상세 API는 [docs/builtin-services.md](docs/builtin-services.md) 참조.
299
-
300
- ### OrmService
301
-
302
- 인증 필수. WebSocket 전용. 소켓별 DB 커넥션 풀을 관리한다. 소켓 종료 시 자동 정리.
303
-
304
- ```typescript
305
- import { OrmService } from "@simplysm/service-server";
306
-
307
- const server = createServiceServer({
308
- services: [OrmService],
309
- // ...
310
- });
311
- ```
312
-
313
- `.config.json`에 DB 설정 필요:
314
-
315
- ```json
316
- {
317
- "orm": {
318
- "main": {
319
- "dialect": "mysql",
320
- "host": "localhost",
321
- "port": 3306,
322
- "username": "root",
323
- "password": "password"
324
- }
325
- }
326
- }
327
- ```
328
-
329
- ### AutoUpdateService
330
-
331
- `clientPath/{platform}/updates/` 디렉토리에서 최신 버전을 탐색한다. `.apk`(Android), `.exe`(Windows) 지원. V1 레거시 클라이언트 호환.
332
-
333
- ```typescript
334
- import { AutoUpdateService } from "@simplysm/service-server";
335
- ```
336
-
337
- ### SmtpClientService
338
-
339
- nodemailer 기반 이메일 전송 서비스. 직접 설정 또는 `.config.json` 기반 전송을 지원한다.
340
-
341
- ```typescript
342
- import { SmtpClientService } from "@simplysm/service-server";
343
- ```
344
-
345
- ## 설정 파일
346
-
347
- `rootPath/.config.json`에서 설정을 읽는다. LRU 캐시(1시간 만료, 10분 GC 주기)와 파일 감시로 자동 리로드된다.
348
-
349
- ```typescript
350
- const dbConfig = await ctx.getConfig<DbConfig>("orm");
351
- ```
352
-
353
- 클라이언트별 설정이 있으면 (`rootPath/www/{clientName}/.config.json`) 루트 설정에 머지된다.
354
-
355
- ## 서버 이벤트
356
-
357
- ```typescript
358
- server.on("ready", () => { /* 서버 시작 완료 */ });
359
- server.on("close", () => { /* 서버 종료 완료 */ });
360
90
  ```
361
91
 
362
- ## Graceful Shutdown
92
+ ## Documentation
363
93
 
364
- SIGINT/SIGTERM 시그널 수신 시 자동으로 graceful shutdown을 수행한다. 10초 타임아웃 후 강제 종료.
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