@simplysm/service-server 13.0.100 → 14.0.1
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/dist/auth/auth-token-payload.js +2 -1
- package/dist/auth/auth-token-payload.js.map +1 -6
- package/dist/auth/jwt-manager.js +21 -21
- package/dist/auth/jwt-manager.js.map +1 -6
- package/dist/core/define-service.d.ts +12 -12
- package/dist/core/define-service.d.ts.map +1 -1
- package/dist/core/define-service.js +77 -63
- package/dist/core/define-service.js.map +1 -6
- package/dist/core/service-executor.d.ts.map +1 -1
- package/dist/core/service-executor.js +42 -32
- package/dist/core/service-executor.js.map +1 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -6
- package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
- package/dist/legacy/v1-auto-update-handler.js +42 -35
- package/dist/legacy/v1-auto-update-handler.js.map +1 -6
- package/dist/protocol/protocol-wrapper.d.ts +9 -9
- package/dist/protocol/protocol-wrapper.js +64 -46
- package/dist/protocol/protocol-wrapper.js.map +1 -6
- package/dist/service-server.d.ts +2 -0
- package/dist/service-server.d.ts.map +1 -1
- package/dist/service-server.js +187 -165
- package/dist/service-server.js.map +1 -6
- package/dist/services/auto-update-service.js +35 -34
- package/dist/services/auto-update-service.js.map +1 -6
- package/dist/services/orm-service.js +114 -120
- package/dist/services/orm-service.js.map +1 -6
- package/dist/transport/http/http-request-handler.d.ts.map +1 -1
- package/dist/transport/http/http-request-handler.js +58 -46
- package/dist/transport/http/http-request-handler.js.map +1 -6
- package/dist/transport/http/static-file-handler.js +42 -39
- package/dist/transport/http/static-file-handler.js.map +1 -6
- package/dist/transport/http/upload-handler.d.ts.map +1 -1
- package/dist/transport/http/upload-handler.js +60 -55
- package/dist/transport/http/upload-handler.js.map +1 -6
- package/dist/transport/socket/service-socket.d.ts +13 -13
- package/dist/transport/socket/service-socket.js +132 -108
- package/dist/transport/socket/service-socket.js.map +1 -6
- package/dist/transport/socket/websocket-handler.d.ts +10 -10
- package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
- package/dist/transport/socket/websocket-handler.js +154 -139
- package/dist/transport/socket/websocket-handler.js.map +1 -6
- package/dist/types/server-options.d.ts +1 -1
- package/dist/types/server-options.d.ts.map +1 -1
- package/dist/types/server-options.js +2 -1
- package/dist/types/server-options.js.map +1 -6
- package/dist/utils/config-manager.js +48 -46
- package/dist/utils/config-manager.js.map +1 -6
- package/dist/workers/service-protocol.worker.js +8 -11
- package/dist/workers/service-protocol.worker.js.map +1 -6
- package/package.json +12 -14
- package/src/auth/jwt-manager.ts +2 -2
- package/src/core/define-service.ts +19 -19
- package/src/core/service-executor.ts +23 -17
- package/src/index.ts +10 -12
- package/src/legacy/v1-auto-update-handler.ts +10 -10
- package/src/protocol/protocol-wrapper.ts +16 -16
- package/src/service-server.ts +52 -39
- package/src/services/auto-update-service.ts +1 -1
- package/src/services/orm-service.ts +7 -7
- package/src/transport/http/http-request-handler.ts +16 -10
- package/src/transport/http/static-file-handler.ts +8 -8
- package/src/transport/http/upload-handler.ts +16 -9
- package/src/transport/socket/service-socket.ts +22 -22
- package/src/transport/socket/websocket-handler.ts +59 -60
- package/src/types/server-options.ts +1 -1
- package/src/utils/config-manager.ts +11 -11
- package/README.md +0 -163
- package/dist/services/smtp-client-service.d.ts +0 -8
- package/dist/services/smtp-client-service.d.ts.map +0 -1
- package/dist/services/smtp-client-service.js +0 -46
- package/dist/services/smtp-client-service.js.map +0 -6
- package/docs/auth.md +0 -59
- package/docs/core.md +0 -133
- package/docs/server.md +0 -126
- package/docs/services.md +0 -58
- package/docs/transport.md +0 -164
- package/src/services/smtp-client-service.ts +0 -59
- package/tests/define-service.spec.ts +0 -66
- package/tests/orm-service.spec.ts +0 -83
- package/tests/service-executor.spec.ts +0 -114
|
@@ -5,7 +5,7 @@ import { obj } from "@simplysm/core-common";
|
|
|
5
5
|
import { getConfig } from "../utils/config-manager";
|
|
6
6
|
import path from "path";
|
|
7
7
|
|
|
8
|
-
// ──
|
|
8
|
+
// ── 컨텍스트 ──
|
|
9
9
|
|
|
10
10
|
export interface ServiceContext<TAuthInfo = unknown> {
|
|
11
11
|
server: ServiceServer<TAuthInfo>;
|
|
@@ -15,7 +15,7 @@ export interface ServiceContext<TAuthInfo = unknown> {
|
|
|
15
15
|
authTokenPayload?: AuthTokenPayload<TAuthInfo>;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
/** V1
|
|
18
|
+
/** V1 레거시 컨텍스트 (자동 업데이트 전용) */
|
|
19
19
|
legacy?: {
|
|
20
20
|
clientName?: string;
|
|
21
21
|
};
|
|
@@ -49,7 +49,7 @@ export function createServiceContext<TAuthInfo = unknown>(
|
|
|
49
49
|
if (name == null) return undefined;
|
|
50
50
|
|
|
51
51
|
if (name === "" || name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
52
|
-
throw new Error(
|
|
52
|
+
throw new Error(`유효하지 않은 클라이언트 이름: ${name}`);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return name;
|
|
@@ -79,28 +79,28 @@ export function createServiceContext<TAuthInfo = unknown>(
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const config = configParent[section];
|
|
82
|
-
if (config == null) throw new Error(
|
|
82
|
+
if (config == null) throw new Error(`설정 섹션을 찾을 수 없습니다: ${section}`);
|
|
83
83
|
return config;
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// ──
|
|
88
|
+
// ── 인증 ──
|
|
89
89
|
|
|
90
90
|
const AUTH_PERMISSIONS = Symbol("authPermissions");
|
|
91
91
|
|
|
92
|
-
/**
|
|
92
|
+
/** auth()로 래핑된 함수에서 인증 권한을 읽는다. 래핑되지 않은 경우 undefined를 반환한다. */
|
|
93
93
|
export function getServiceAuthPermissions(fn: Function): string[] | undefined {
|
|
94
94
|
return (fn as unknown as Record<symbol, unknown>)[AUTH_PERMISSIONS] as string[] | undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* 서비스 팩토리 및 메서드용 인증 래퍼.
|
|
99
99
|
*
|
|
100
|
-
* -
|
|
101
|
-
* -
|
|
102
|
-
* -
|
|
103
|
-
* -
|
|
100
|
+
* - 서비스 수준: `auth((ctx) => ({ ... }))` — 모든 메서드에 로그인 필요
|
|
101
|
+
* - 서비스 수준 (역할 지정): `auth(["admin"], (ctx) => ({ ... }))`
|
|
102
|
+
* - 메서드 수준: `auth(() => result)` — 해당 메서드에 로그인 필요
|
|
103
|
+
* - 메서드 수준 (역할 지정): `auth(["admin"], () => result)`
|
|
104
104
|
*/
|
|
105
105
|
export function auth<TFunction extends (...args: any[]) => any>(fn: TFunction): TFunction;
|
|
106
106
|
export function auth<TFunction extends (...args: any[]) => any>(
|
|
@@ -111,14 +111,14 @@ export function auth(permissionsOrFn: string[] | Function, maybeFn?: Function):
|
|
|
111
111
|
const permissions = Array.isArray(permissionsOrFn) ? permissionsOrFn : [];
|
|
112
112
|
const fn = Array.isArray(permissionsOrFn) ? maybeFn! : permissionsOrFn;
|
|
113
113
|
|
|
114
|
-
//
|
|
114
|
+
// 호출 동작을 유지하는 래퍼 생성
|
|
115
115
|
const wrapper = (...args: unknown[]) => fn(...args);
|
|
116
116
|
(wrapper as unknown as Record<symbol, unknown>)[AUTH_PERMISSIONS] = permissions;
|
|
117
117
|
|
|
118
118
|
return wrapper;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// ──
|
|
121
|
+
// ── 서비스 정의 ──
|
|
122
122
|
|
|
123
123
|
export interface ServiceDefinition<TMethods = Record<string, (...args: any[]) => any>> {
|
|
124
124
|
name: string;
|
|
@@ -127,15 +127,15 @@ export interface ServiceDefinition<TMethods = Record<string, (...args: any[]) =>
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
130
|
+
* 이름과 팩토리 함수로 서비스를 정의한다.
|
|
131
131
|
*
|
|
132
132
|
* @example
|
|
133
|
-
* //
|
|
133
|
+
* // 기본 서비스
|
|
134
134
|
* const HealthService = defineService("Health", (ctx) => ({
|
|
135
135
|
* check: () => ({ status: "ok" }),
|
|
136
136
|
* }));
|
|
137
137
|
*
|
|
138
|
-
* //
|
|
138
|
+
* // 인증이 필요한 서비스
|
|
139
139
|
* const UserService = defineService("User", auth((ctx) => ({
|
|
140
140
|
* getProfile: () => ctx.authInfo,
|
|
141
141
|
* adminOnly: auth(["admin"], () => "admin"),
|
|
@@ -152,14 +152,14 @@ export function defineService<TMethods extends Record<string, (...args: any[]) =
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// ──
|
|
155
|
+
// ── 타입 유틸리티 ──
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
158
|
+
* 클라이언트 측 타입 공유를 위해 ServiceDefinition에서 메서드 시그니처를 추출한다.
|
|
159
159
|
*
|
|
160
160
|
* @example
|
|
161
161
|
* export type UserServiceType = ServiceMethods<typeof UserService>;
|
|
162
|
-
* //
|
|
162
|
+
* // 클라이언트: client.getService<UserServiceType>("User");
|
|
163
163
|
*/
|
|
164
164
|
export type ServiceMethods<TDefinition> =
|
|
165
165
|
TDefinition extends ServiceDefinition<infer M> ? M : never;
|
|
@@ -13,55 +13,61 @@ export async function executeServiceMethod(
|
|
|
13
13
|
http?: { clientName: string; authTokenPayload?: AuthTokenPayload };
|
|
14
14
|
},
|
|
15
15
|
): Promise<unknown> {
|
|
16
|
-
//
|
|
16
|
+
// 서비스 정의 검색
|
|
17
17
|
const serviceDef = server.options.services.find((item) => item.name === def.serviceName);
|
|
18
18
|
|
|
19
19
|
if (serviceDef == null) {
|
|
20
|
-
throw new Error(
|
|
20
|
+
throw new Error(`서비스 [${def.serviceName}]를 찾을 수 없습니다.`);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// 요청 유효성 검증 (게이트키퍼)
|
|
24
24
|
const clientName = def.socket?.clientName ?? def.http?.clientName;
|
|
25
25
|
if (clientName != null) {
|
|
26
26
|
if (clientName.includes("..") || clientName.includes("/") || clientName.includes("\\")) {
|
|
27
|
-
throw new Error(`[
|
|
27
|
+
throw new Error(`[보안] 유효하지 않은 클라이언트 이름: ${clientName}`);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// 컨텍스트 생성
|
|
32
32
|
const ctx = createServiceContext(server, def.socket, def.http);
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// 팩토리를 호출하여 메서드 객체 생성
|
|
35
35
|
const methods = serviceDef.factory(ctx);
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// 메서드 검색
|
|
38
38
|
const method = (methods as Record<string, unknown>)[def.methodName];
|
|
39
39
|
if (typeof method !== "function") {
|
|
40
|
-
throw new Error(
|
|
40
|
+
throw new Error(`메서드 [${def.serviceName}.${def.methodName}]를 찾을 수 없습니다.`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const methodPerms = getServiceAuthPermissions(method);
|
|
47
|
-
const requiredPerms = methodPerms ?? serviceDef.authPermissions;
|
|
43
|
+
// 인증 확인
|
|
44
|
+
const methodPerms = getServiceAuthPermissions(method);
|
|
45
|
+
const requiredPerms = methodPerms ?? serviceDef.authPermissions;
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
if (requiredPerms != null) {
|
|
48
|
+
if (server.options.auth === undefined) {
|
|
49
|
+
// auth 설정 누락 — 설정 오류
|
|
50
|
+
throw new Error("auth 설정이 필요합니다. auth 서비스를 사용하려면 서버 옵션에 auth를 설정하세요.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (server.options.auth !== false) {
|
|
54
|
+
// auth가 설정되어 있으면 인증 검사 수행
|
|
50
55
|
const authTokenPayload = def.socket?.authTokenPayload ?? def.http?.authTokenPayload;
|
|
51
56
|
|
|
52
57
|
if (authTokenPayload == null) {
|
|
53
|
-
throw new Error("
|
|
58
|
+
throw new Error("로그인이 필요합니다.");
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
if (requiredPerms.length > 0) {
|
|
57
62
|
const hasPerm = requiredPerms.some((perm) => authTokenPayload.roles.includes(perm));
|
|
58
63
|
if (!hasPerm) {
|
|
59
|
-
throw new Error("
|
|
64
|
+
throw new Error("권한이 부족합니다.");
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
}
|
|
68
|
+
// auth === false → 의도적 비활성화, 인증 스킵
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
//
|
|
71
|
+
// 실행
|
|
66
72
|
return await method(...def.params);
|
|
67
73
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 타입
|
|
2
2
|
export * from "./types/server-options";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// 인증
|
|
5
5
|
export * from "./auth/auth-token-payload";
|
|
6
6
|
export * from "./auth/jwt-manager";
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// 코어
|
|
9
9
|
export * from "./core/define-service";
|
|
10
10
|
export * from "./core/service-executor";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// 전송 계층 - Socket
|
|
13
13
|
export * from "./transport/socket/websocket-handler";
|
|
14
14
|
export * from "./transport/socket/service-socket";
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// 전송 계층 - HTTP
|
|
17
17
|
export * from "./transport/http/http-request-handler";
|
|
18
18
|
export * from "./transport/http/upload-handler";
|
|
19
19
|
export * from "./transport/http/static-file-handler";
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// 프로토콜
|
|
22
22
|
export * from "./protocol/protocol-wrapper";
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// 서비스
|
|
25
25
|
export * from "./services/orm-service";
|
|
26
26
|
export * from "./services/auto-update-service";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Utils
|
|
27
|
+
// 유틸리티
|
|
30
28
|
export * from "./utils/config-manager";
|
|
31
29
|
|
|
32
|
-
//
|
|
30
|
+
// 레거시
|
|
33
31
|
export * from "./legacy/v1-auto-update-handler";
|
|
34
32
|
|
|
35
|
-
//
|
|
33
|
+
// 메인
|
|
36
34
|
export * from "./service-server";
|
|
@@ -18,27 +18,27 @@ interface IV1Response {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* V1
|
|
22
|
-
*
|
|
21
|
+
* V1 레거시 클라이언트 핸들러 (자동 업데이트만 지원).
|
|
22
|
+
* 그 외 모든 요청은 업그레이드 필요 에러를 반환한다.
|
|
23
23
|
*/
|
|
24
24
|
export function handleV1Connection(
|
|
25
25
|
socket: WebSocket,
|
|
26
26
|
autoUpdateMethods: { getLastVersion: (platform: string) => Promise<any> },
|
|
27
27
|
clientNameSetter?: (clientName: string | undefined) => void,
|
|
28
28
|
) {
|
|
29
|
-
//
|
|
29
|
+
// 연결 성립 알림
|
|
30
30
|
socket.send(JSON.stringify({ name: "connected" }));
|
|
31
31
|
|
|
32
|
-
socket.on("message", (data) => {
|
|
32
|
+
socket.on("message", async (data) => {
|
|
33
33
|
try {
|
|
34
34
|
const msg = JSON.parse(data.toString()) as IV1Request;
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// SdAutoUpdateService.getLastVersion만 허용
|
|
37
37
|
if (msg.command === "SdAutoUpdateService.getLastVersion") {
|
|
38
|
-
//
|
|
38
|
+
// 레거시 컨텍스트 설정
|
|
39
39
|
clientNameSetter?.(msg.clientName);
|
|
40
40
|
|
|
41
|
-
const result = autoUpdateMethods.getLastVersion(msg.params[0] as string);
|
|
41
|
+
const result = await autoUpdateMethods.getLastVersion(msg.params[0] as string);
|
|
42
42
|
|
|
43
43
|
const response: IV1Response = {
|
|
44
44
|
name: "response",
|
|
@@ -48,20 +48,20 @@ export function handleV1Connection(
|
|
|
48
48
|
};
|
|
49
49
|
socket.send(JSON.stringify(response));
|
|
50
50
|
} else {
|
|
51
|
-
//
|
|
51
|
+
// 그 외 모든 요청은 업그레이드 요구
|
|
52
52
|
const response: IV1Response = {
|
|
53
53
|
name: "response",
|
|
54
54
|
reqUuid: msg.uuid,
|
|
55
55
|
state: "error",
|
|
56
56
|
body: {
|
|
57
|
-
message: "
|
|
57
|
+
message: "앱 업그레이드가 필요합니다.",
|
|
58
58
|
code: "UPGRADE_REQUIRED",
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
socket.send(JSON.stringify(response));
|
|
62
62
|
}
|
|
63
63
|
} catch (err) {
|
|
64
|
-
logger.warn("V1
|
|
64
|
+
logger.warn("V1 메시지 처리 에러", err);
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -5,29 +5,29 @@ import { createServiceProtocol } from "@simplysm/service-common";
|
|
|
5
5
|
import type * as ServiceProtocolWorkerModule from "../workers/service-protocol.worker";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* 프로토콜 래퍼 인터페이스
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
|
|
11
|
+
* 가벼운 작업은 메인 스레드에서 처리한다.
|
|
12
12
|
*/
|
|
13
13
|
export interface ServerProtocolWrapper {
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* 메시지를 인코딩한다 (worker 자동 위임)
|
|
16
16
|
*/
|
|
17
17
|
encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 메시지를 디코딩한다 (worker 자동 위임)
|
|
21
21
|
*/
|
|
22
22
|
decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 프로토콜 리소스를 해제한다
|
|
26
26
|
*/
|
|
27
27
|
dispose(): void;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// 공유 worker 인스턴스 (지연 싱글턴)
|
|
31
31
|
let sharedWorker: WorkerProxy<typeof ServiceProtocolWorkerModule> | undefined;
|
|
32
32
|
|
|
33
33
|
function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
|
|
@@ -43,37 +43,37 @@ function getWorker(): WorkerProxy<typeof ServiceProtocolWorkerModule> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* 프로토콜 래퍼 인스턴스를 생성한다
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
|
|
49
|
+
* 가벼운 작업은 메인 스레드에서 처리한다.
|
|
50
50
|
*/
|
|
51
51
|
export function createServerProtocolWrapper(): ServerProtocolWrapper {
|
|
52
52
|
// -------------------------------------------------------------------
|
|
53
|
-
//
|
|
53
|
+
// 상태
|
|
54
54
|
// -------------------------------------------------------------------
|
|
55
55
|
|
|
56
56
|
const protocol = createServiceProtocol();
|
|
57
57
|
const SIZE_THRESHOLD = 30 * 1024; // 30KB
|
|
58
58
|
|
|
59
59
|
// -------------------------------------------------------------------
|
|
60
|
-
//
|
|
60
|
+
// 헬퍼
|
|
61
61
|
// -------------------------------------------------------------------
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* 메시지 인코딩에 worker를 사용해야 하는지 확인한다
|
|
65
65
|
*/
|
|
66
66
|
function shouldUseWorkerForEncode(msg: ServiceMessage): boolean {
|
|
67
67
|
if (!("body" in msg)) return false;
|
|
68
68
|
|
|
69
69
|
const body = msg.body;
|
|
70
70
|
|
|
71
|
-
// Uint8Array:
|
|
71
|
+
// Uint8Array: 항상 worker 사용
|
|
72
72
|
if (body instanceof Uint8Array) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// 배열: Uint8Array 요소 존재 여부 확인 (ORM 결과 등)
|
|
77
77
|
if (Array.isArray(body)) {
|
|
78
78
|
return body.length > 0 && body.some((item) => item instanceof Uint8Array);
|
|
79
79
|
}
|
|
@@ -82,7 +82,7 @@ export function createServerProtocolWrapper(): ServerProtocolWrapper {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// -------------------------------------------------------------------
|
|
85
|
-
//
|
|
85
|
+
// 공개 API
|
|
86
86
|
// -------------------------------------------------------------------
|
|
87
87
|
|
|
88
88
|
return {
|
package/src/service-server.ts
CHANGED
|
@@ -31,14 +31,18 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
31
31
|
isOpen = false;
|
|
32
32
|
|
|
33
33
|
private readonly _wsHandler: ReturnType<typeof createWebSocketHandler>;
|
|
34
|
+
private readonly _jwtSecret: string | undefined;
|
|
35
|
+
private _shutdownRegistered = false;
|
|
34
36
|
|
|
35
37
|
readonly fastify: FastifyInstance;
|
|
36
38
|
|
|
37
39
|
constructor(readonly options: ServiceServerOptions) {
|
|
38
40
|
super();
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
this._jwtSecret = options.auth != null && options.auth !== false ? options.auth.jwtSecret : undefined;
|
|
43
|
+
|
|
44
|
+
// SSL 설정 (동기)
|
|
45
|
+
// 참고: Fastify HTTPS는 Buffer 타입이 필요함 (Uint8Array를 직접 사용할 수 없음)
|
|
42
46
|
const httpsConf = options.ssl
|
|
43
47
|
? { pfx: Buffer.from(options.ssl.pfxBytes), passphrase: options.ssl.passphrase }
|
|
44
48
|
: null;
|
|
@@ -47,17 +51,27 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
47
51
|
|
|
48
52
|
this._wsHandler = createWebSocketHandler(
|
|
49
53
|
(def) => executeServiceMethod(this, def),
|
|
50
|
-
|
|
54
|
+
this._jwtSecret,
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
async listen(): Promise<void> {
|
|
55
|
-
logger.info(
|
|
59
|
+
logger.info(`서버 시작 중... ${env.VER ?? ""}`);
|
|
60
|
+
|
|
61
|
+
// auth 설정 검증: auth 미설정(undefined)인데 auth 요구 서비스가 있으면 에러
|
|
62
|
+
if (this.options.auth === undefined) {
|
|
63
|
+
const authRequiredService = this.options.services.find((s) => s.authPermissions != null);
|
|
64
|
+
if (authRequiredService != null) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`auth 설정이 필요합니다: 서비스 [${authRequiredService.name}]에 auth가 설정되어 있습니다.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
56
70
|
|
|
57
|
-
// WebSocket
|
|
71
|
+
// WebSocket 플러그인
|
|
58
72
|
await this.fastify.register(fastifyWebsocket);
|
|
59
73
|
|
|
60
|
-
//
|
|
74
|
+
// 보안 플러그인
|
|
61
75
|
await this.fastify.register(fastifyHelmet, {
|
|
62
76
|
global: true,
|
|
63
77
|
contentSecurityPolicy: {
|
|
@@ -78,17 +92,17 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
78
92
|
originAgentCluster: false,
|
|
79
93
|
});
|
|
80
94
|
|
|
81
|
-
//
|
|
95
|
+
// 업로드 플러그인
|
|
82
96
|
await this.fastify.register(fastifyMultipart);
|
|
83
97
|
|
|
84
|
-
//
|
|
98
|
+
// @fastify/static 등록
|
|
85
99
|
await this.fastify.register(fastifyStatic, {
|
|
86
100
|
root: path.resolve(this.options.rootPath, "www"),
|
|
87
101
|
wildcard: false,
|
|
88
102
|
serve: false,
|
|
89
103
|
});
|
|
90
104
|
|
|
91
|
-
// CORS
|
|
105
|
+
// CORS 설정
|
|
92
106
|
await this.fastify.register(fastifyCors, {
|
|
93
107
|
origin: (_origin, cb) => {
|
|
94
108
|
cb(null, true);
|
|
@@ -97,7 +111,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
97
111
|
exposedHeaders: ["Content-Disposition", "Content-Length"],
|
|
98
112
|
});
|
|
99
113
|
|
|
100
|
-
// JSON
|
|
114
|
+
// JSON 파서
|
|
101
115
|
this.fastify.addContentTypeParser(
|
|
102
116
|
"application/json",
|
|
103
117
|
{ parseAs: "string" },
|
|
@@ -113,22 +127,22 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
113
127
|
},
|
|
114
128
|
);
|
|
115
129
|
|
|
116
|
-
// JSON
|
|
130
|
+
// JSON 직렬화기
|
|
117
131
|
this.fastify.setSerializerCompiler(() => (data) => json.stringify(data));
|
|
118
132
|
|
|
119
|
-
// API
|
|
133
|
+
// API 라우트
|
|
120
134
|
this.fastify.all("/api/:service/:method", async (req, reply) => {
|
|
121
|
-
await handleHttpRequest(req, reply, this.
|
|
135
|
+
await handleHttpRequest(req, reply, this._jwtSecret, (def) =>
|
|
122
136
|
executeServiceMethod(this, def),
|
|
123
137
|
);
|
|
124
138
|
});
|
|
125
139
|
|
|
126
|
-
//
|
|
140
|
+
// 업로드 라우트
|
|
127
141
|
this.fastify.all("/upload", async (req, reply) => {
|
|
128
|
-
await handleUpload(req, reply, this.options.rootPath, this.
|
|
142
|
+
await handleUpload(req, reply, this.options.rootPath, this._jwtSecret);
|
|
129
143
|
});
|
|
130
144
|
|
|
131
|
-
// WebSocket
|
|
145
|
+
// WebSocket 라우트
|
|
132
146
|
const onWebSocketConnected = (socket: WebSocket, req: FastifyRequest) => {
|
|
133
147
|
const { ver, clientId, clientName } = req.query as {
|
|
134
148
|
ver: string | undefined;
|
|
@@ -138,15 +152,15 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
138
152
|
|
|
139
153
|
if (ver === "2") {
|
|
140
154
|
if (clientId == null || clientName == null) {
|
|
141
|
-
socket.close(1008, "
|
|
155
|
+
socket.close(1008, "클라이언트 ID/NAME이 누락되었습니다");
|
|
142
156
|
return;
|
|
143
157
|
}
|
|
144
158
|
this._wsHandler.addSocket(socket, clientId, clientName, req);
|
|
145
159
|
} else {
|
|
146
|
-
// V1
|
|
160
|
+
// V1 레거시 지원 (자동 업데이트 전용)
|
|
147
161
|
const autoUpdateDef = this.options.services.find((s) => s.name === "AutoUpdate");
|
|
148
162
|
if (autoUpdateDef == null) {
|
|
149
|
-
socket.close(1008, "AutoUpdate
|
|
163
|
+
socket.close(1008, "AutoUpdate 서비스가 설정되지 않았습니다");
|
|
150
164
|
return;
|
|
151
165
|
}
|
|
152
166
|
|
|
@@ -163,7 +177,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
163
177
|
this.fastify.get("/", { websocket: true }, onWebSocketConnected.bind(this));
|
|
164
178
|
this.fastify.get("/ws", { websocket: true }, onWebSocketConnected.bind(this));
|
|
165
179
|
|
|
166
|
-
//
|
|
180
|
+
// 정적 파일 와일드카드 핸들러
|
|
167
181
|
this.fastify.route({
|
|
168
182
|
method: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"],
|
|
169
183
|
url: "/*",
|
|
@@ -175,19 +189,19 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
175
189
|
},
|
|
176
190
|
});
|
|
177
191
|
|
|
178
|
-
// HTTP
|
|
192
|
+
// HTTP 서버 수준 에러 핸들링
|
|
179
193
|
this.fastify.server.on("error", (err) => {
|
|
180
|
-
logger.error("HTTP
|
|
194
|
+
logger.error("HTTP 서버 에러", err);
|
|
181
195
|
});
|
|
182
196
|
|
|
183
|
-
//
|
|
197
|
+
// 리슨
|
|
184
198
|
await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
|
|
185
199
|
|
|
186
|
-
//
|
|
200
|
+
// 정상 종료 핸들러 등록
|
|
187
201
|
this._registerGracefulShutdown();
|
|
188
202
|
|
|
189
203
|
this.isOpen = true;
|
|
190
|
-
logger.info(
|
|
204
|
+
logger.info(`서버 시작됨 (포트: ${this.options.port})`);
|
|
191
205
|
this.emit("ready");
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -196,12 +210,12 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
196
210
|
await this.fastify.close();
|
|
197
211
|
|
|
198
212
|
this.isOpen = false;
|
|
199
|
-
logger.debug("
|
|
213
|
+
logger.debug("서버 종료됨");
|
|
200
214
|
this.emit("close");
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
async broadcastReload(clientName: string | undefined, changedFileSet: Set<string>) {
|
|
204
|
-
logger.debug("
|
|
218
|
+
logger.debug("모든 서버 클라이언트에 RELOAD 브로드캐스팅");
|
|
205
219
|
await this._wsHandler.broadcastReload(clientName, changedFileSet);
|
|
206
220
|
}
|
|
207
221
|
|
|
@@ -214,25 +228,24 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
214
228
|
}
|
|
215
229
|
|
|
216
230
|
async signAuthToken(payload: AuthTokenPayload<TAuthInfo>) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return signJwt(jwtSecret, payload);
|
|
231
|
+
if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
232
|
+
return signJwt(this._jwtSecret, payload);
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
async verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>> {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return verifyJwt(jwtSecret, token);
|
|
236
|
+
if (this._jwtSecret == null) throw new Error("JWT Secret이 정의되지 않았습니다.");
|
|
237
|
+
return verifyJwt(this._jwtSecret, token);
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
private _registerGracefulShutdown() {
|
|
241
|
+
if (this._shutdownRegistered) return;
|
|
242
|
+
this._shutdownRegistered = true;
|
|
243
|
+
|
|
231
244
|
const shutdownHandler = async (signal: string) => {
|
|
232
|
-
logger.info(`${signal}
|
|
245
|
+
logger.info(`${signal} 시그널 수신됨. 서버 종료를 시작합니다...`);
|
|
233
246
|
|
|
234
247
|
const forceExitTimer = setTimeout(() => {
|
|
235
|
-
logger.error("
|
|
248
|
+
logger.error("서버 종료 시간 초과 (10초). 강제 종료합니다.");
|
|
236
249
|
process.exit(1);
|
|
237
250
|
}, 10000);
|
|
238
251
|
|
|
@@ -240,11 +253,11 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
|
|
|
240
253
|
if (this.isOpen) {
|
|
241
254
|
await this.close();
|
|
242
255
|
}
|
|
243
|
-
logger.info("
|
|
256
|
+
logger.info("서버가 정상적으로 종료되었습니다.");
|
|
244
257
|
clearTimeout(forceExitTimer);
|
|
245
258
|
process.exit(0);
|
|
246
259
|
} catch (err) {
|
|
247
|
-
logger.error("
|
|
260
|
+
logger.error("서버 종료 중 에러 발생", err);
|
|
248
261
|
process.exit(1);
|
|
249
262
|
}
|
|
250
263
|
};
|
|
@@ -12,7 +12,7 @@ export const AutoUpdateService = defineService("AutoUpdate", (ctx) => ({
|
|
|
12
12
|
| undefined
|
|
13
13
|
> {
|
|
14
14
|
const clientPath = ctx.clientPath;
|
|
15
|
-
if (clientPath == null) throw new Error("
|
|
15
|
+
if (clientPath == null) throw new Error("클라이언트 경로를 찾을 수 없습니다.");
|
|
16
16
|
|
|
17
17
|
if (!(await fsx.exists(path.resolve(clientPath, platform, "updates")))) return undefined;
|
|
18
18
|
|