@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 +79 -345
- package/docs/auth.md +48 -0
- package/docs/core.md +161 -0
- package/docs/server.md +206 -0
- package/docs/services.md +176 -0
- package/docs/transport.md +152 -0
- package/package.json +8 -8
- package/docs/builtin-services.md +0 -249
- package/docs/transport-protocol.md +0 -200
package/README.md
CHANGED
|
@@ -1,364 +1,98 @@
|
|
|
1
1
|
# @simplysm/service-server
|
|
2
2
|
|
|
3
|
-
Fastify
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
##
|
|
92
|
+
## Documentation
|
|
363
93
|
|
|
364
|
-
|
|
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
|