@simplysm/service-server 13.0.85 → 13.0.86
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 +275 -21
- package/docs/builtin-services.md +172 -62
- package/docs/transport-protocol.md +200 -0
- package/package.json +7 -7
- package/docs/authentication.md +0 -70
- package/docs/legacy.md +0 -45
- package/docs/protocol.md +0 -34
- package/docs/server.md +0 -102
- package/docs/service-definition.md +0 -134
- package/docs/transport.md +0 -97
- package/docs/utilities.md +0 -31
package/README.md
CHANGED
|
@@ -1,40 +1,294 @@
|
|
|
1
1
|
# @simplysm/service-server
|
|
2
2
|
|
|
3
|
-
Fastify
|
|
3
|
+
Fastify 기반 서비스 서버. WebSocket RPC, HTTP API, JWT 인증, 이벤트 브로드캐스트, 파일 업로드, 정적 파일 서빙을 제공한다.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 설치
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @simplysm/service-server
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**주요 의존성:** `fastify`, `@fastify/cors`, `@fastify/websocket`, `@fastify/static`, `@fastify/multipart`, `@fastify/helmet`, `jose`, `nodemailer`, `ws`
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
## 서버 생성 및 실행
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createServiceServer, defineService, auth } from "@simplysm/service-server";
|
|
19
19
|
|
|
20
20
|
const server = createServiceServer({
|
|
21
|
-
rootPath:
|
|
21
|
+
rootPath: "/app",
|
|
22
22
|
port: 3000,
|
|
23
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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, 프로토콜 래퍼
|
package/docs/builtin-services.md
CHANGED
|
@@ -1,91 +1,192 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 내장 서비스 상세 API
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## OrmService
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
서비스 이름: `"Orm"`. 인증 필수 (`auth` 래핑). WebSocket 전용 -- HTTP로는 사용 불가.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
소켓별로 DB 커넥션을 `WeakMap<ServiceSocket, Map<number, DbConn>>`으로 관리한다. 소켓 `close` 이벤트 시 해당 소켓의 모든 커넥션을 자동 정리한다.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### 메서드
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
```typescript
|
|
58
|
+
type DbConnOptions = {
|
|
59
|
+
configName?: string;
|
|
60
|
+
config?: Record<string, unknown>; // .config.json 설정 오버라이드
|
|
61
|
+
};
|
|
62
|
+
```
|
|
26
63
|
|
|
27
|
-
|
|
64
|
+
### 설정 예시
|
|
65
|
+
|
|
66
|
+
`.config.json`:
|
|
28
67
|
|
|
29
68
|
```json
|
|
30
69
|
{
|
|
31
70
|
"orm": {
|
|
32
|
-
"
|
|
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
|
-
###
|
|
92
|
+
### 타입 내보내기
|
|
45
93
|
|
|
46
94
|
```typescript
|
|
47
|
-
|
|
95
|
+
import type { OrmServiceType } from "@simplysm/service-server";
|
|
96
|
+
// 클라이언트에서 타입 공유: client.getService<OrmServiceType>("Orm")
|
|
48
97
|
```
|
|
49
98
|
|
|
50
99
|
---
|
|
51
100
|
|
|
52
|
-
##
|
|
101
|
+
## AutoUpdateService
|
|
102
|
+
|
|
103
|
+
서비스 이름: `"AutoUpdate"`. 인증 불필요. `clientPath/{platform}/updates/` 디렉토리에서 semver 기준 최신 버전 파일을 탐색한다.
|
|
53
104
|
|
|
54
|
-
|
|
105
|
+
### 메서드
|
|
55
106
|
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|--------|-----------|---------|-------------|
|
|
60
|
-
| `getLastVersion` | `platform: string` | `{ version, downloadPath } \| undefined` | Get the latest version for a platform |
|
|
123
|
+
### V1 레거시 호환
|
|
61
124
|
|
|
62
|
-
|
|
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
|
-
###
|
|
127
|
+
### 타입 내보내기
|
|
68
128
|
|
|
69
129
|
```typescript
|
|
70
|
-
|
|
130
|
+
import type { AutoUpdateServiceType } from "@simplysm/service-server";
|
|
71
131
|
```
|
|
72
132
|
|
|
73
133
|
---
|
|
74
134
|
|
|
75
|
-
##
|
|
135
|
+
## SmtpClientService
|
|
76
136
|
|
|
77
|
-
|
|
137
|
+
서비스 이름: `"SmtpClient"`. 인증 불필요. nodemailer 기반 이메일 전송.
|
|
78
138
|
|
|
79
|
-
###
|
|
139
|
+
### 메서드
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
###
|
|
151
|
+
### SmtpClientSendOption
|
|
87
152
|
|
|
88
|
-
|
|
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": "
|
|
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
|
-
|
|
207
|
+
`senderEmail`이 없으면 `user`가 발신자 이메일로 사용된다.
|
|
208
|
+
|
|
209
|
+
### 사용 예시
|
|
107
210
|
|
|
108
211
|
```typescript
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "13.0.86",
|
|
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/
|
|
40
|
-
"@simplysm/
|
|
41
|
-
"@simplysm/
|
|
42
|
-
"@simplysm/
|
|
43
|
-
"@simplysm/
|
|
39
|
+
"@simplysm/core-common": "13.0.86",
|
|
40
|
+
"@simplysm/orm-common": "13.0.86",
|
|
41
|
+
"@simplysm/service-common": "13.0.86",
|
|
42
|
+
"@simplysm/core-node": "13.0.86",
|
|
43
|
+
"@simplysm/orm-node": "13.0.86"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/nodemailer": "^
|
|
46
|
+
"@types/nodemailer": "^7.0.11",
|
|
47
47
|
"@types/semver": "^7.7.1",
|
|
48
48
|
"@types/ws": "^8.18.1"
|
|
49
49
|
}
|
package/docs/authentication.md
DELETED
|
@@ -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
|