@simplysm/service-server 13.0.99 → 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.
Files changed (83) hide show
  1. package/dist/auth/auth-token-payload.js +2 -1
  2. package/dist/auth/auth-token-payload.js.map +1 -6
  3. package/dist/auth/jwt-manager.js +21 -21
  4. package/dist/auth/jwt-manager.js.map +1 -6
  5. package/dist/core/define-service.d.ts +12 -12
  6. package/dist/core/define-service.d.ts.map +1 -1
  7. package/dist/core/define-service.js +77 -63
  8. package/dist/core/define-service.js.map +1 -6
  9. package/dist/core/service-executor.d.ts.map +1 -1
  10. package/dist/core/service-executor.js +42 -32
  11. package/dist/core/service-executor.js.map +1 -6
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +11 -2
  15. package/dist/index.js.map +1 -6
  16. package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
  17. package/dist/legacy/v1-auto-update-handler.js +42 -35
  18. package/dist/legacy/v1-auto-update-handler.js.map +1 -6
  19. package/dist/protocol/protocol-wrapper.d.ts +9 -9
  20. package/dist/protocol/protocol-wrapper.js +64 -46
  21. package/dist/protocol/protocol-wrapper.js.map +1 -6
  22. package/dist/service-server.d.ts +2 -0
  23. package/dist/service-server.d.ts.map +1 -1
  24. package/dist/service-server.js +187 -165
  25. package/dist/service-server.js.map +1 -6
  26. package/dist/services/auto-update-service.js +35 -34
  27. package/dist/services/auto-update-service.js.map +1 -6
  28. package/dist/services/orm-service.js +114 -120
  29. package/dist/services/orm-service.js.map +1 -6
  30. package/dist/transport/http/http-request-handler.d.ts.map +1 -1
  31. package/dist/transport/http/http-request-handler.js +58 -46
  32. package/dist/transport/http/http-request-handler.js.map +1 -6
  33. package/dist/transport/http/static-file-handler.js +42 -39
  34. package/dist/transport/http/static-file-handler.js.map +1 -6
  35. package/dist/transport/http/upload-handler.d.ts.map +1 -1
  36. package/dist/transport/http/upload-handler.js +60 -55
  37. package/dist/transport/http/upload-handler.js.map +1 -6
  38. package/dist/transport/socket/service-socket.d.ts +13 -13
  39. package/dist/transport/socket/service-socket.js +132 -108
  40. package/dist/transport/socket/service-socket.js.map +1 -6
  41. package/dist/transport/socket/websocket-handler.d.ts +10 -10
  42. package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
  43. package/dist/transport/socket/websocket-handler.js +154 -139
  44. package/dist/transport/socket/websocket-handler.js.map +1 -6
  45. package/dist/types/server-options.d.ts +1 -1
  46. package/dist/types/server-options.d.ts.map +1 -1
  47. package/dist/types/server-options.js +2 -1
  48. package/dist/types/server-options.js.map +1 -6
  49. package/dist/utils/config-manager.js +48 -46
  50. package/dist/utils/config-manager.js.map +1 -6
  51. package/dist/workers/service-protocol.worker.js +8 -11
  52. package/dist/workers/service-protocol.worker.js.map +1 -6
  53. package/package.json +12 -14
  54. package/src/auth/jwt-manager.ts +2 -2
  55. package/src/core/define-service.ts +19 -19
  56. package/src/core/service-executor.ts +23 -17
  57. package/src/index.ts +10 -12
  58. package/src/legacy/v1-auto-update-handler.ts +10 -10
  59. package/src/protocol/protocol-wrapper.ts +16 -16
  60. package/src/service-server.ts +52 -39
  61. package/src/services/auto-update-service.ts +1 -1
  62. package/src/services/orm-service.ts +7 -7
  63. package/src/transport/http/http-request-handler.ts +16 -10
  64. package/src/transport/http/static-file-handler.ts +8 -8
  65. package/src/transport/http/upload-handler.ts +16 -9
  66. package/src/transport/socket/service-socket.ts +22 -22
  67. package/src/transport/socket/websocket-handler.ts +59 -60
  68. package/src/types/server-options.ts +1 -1
  69. package/src/utils/config-manager.ts +11 -11
  70. package/README.md +0 -163
  71. package/dist/services/smtp-client-service.d.ts +0 -8
  72. package/dist/services/smtp-client-service.d.ts.map +0 -1
  73. package/dist/services/smtp-client-service.js +0 -46
  74. package/dist/services/smtp-client-service.js.map +0 -6
  75. package/docs/auth.md +0 -59
  76. package/docs/core.md +0 -133
  77. package/docs/server.md +0 -126
  78. package/docs/services.md +0 -58
  79. package/docs/transport.md +0 -164
  80. package/src/services/smtp-client-service.ts +0 -59
  81. package/tests/define-service.spec.ts +0 -66
  82. package/tests/orm-service.spec.ts +0 -83
  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
- // ── Context ──
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 legacy context (auto-update only) */
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(`Invalid client name: ${name}`);
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(`Configuration section not found: ${section}`);
82
+ if (config == null) throw new Error(`설정 섹션을 찾을 없습니다: ${section}`);
83
83
  return config;
84
84
  },
85
85
  };
86
86
  }
87
87
 
88
- // ── Auth ──
88
+ // ── 인증 ──
89
89
 
90
90
  const AUTH_PERMISSIONS = Symbol("authPermissions");
91
91
 
92
- /** Read auth permissions from an auth()-wrapped function. Returns undefined if not wrapped. */
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
- * Auth wrapper for service factories and methods.
98
+ * 서비스 팩토리 메서드용 인증 래퍼.
99
99
  *
100
- * - Service-level: `auth((ctx) => ({ ... }))` — all methods require login
101
- * - Service-level with roles: `auth(["admin"], (ctx) => ({ ... }))`
102
- * - Method-level: `auth(() => result)` — this method requires login
103
- * - Method-level with roles: `auth(["admin"], () => result)`
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
- // Create wrapper that preserves call behavior
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
- // ── Service Definition ──
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
- * Define a service with a name and factory function.
130
+ * 이름과 팩토리 함수로 서비스를 정의한다.
131
131
  *
132
132
  * @example
133
- * // Basic service
133
+ * // 기본 서비스
134
134
  * const HealthService = defineService("Health", (ctx) => ({
135
135
  * check: () => ({ status: "ok" }),
136
136
  * }));
137
137
  *
138
- * // Service with auth
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
- // ── Type Utility ──
155
+ // ── 타입 유틸리티 ──
156
156
 
157
157
  /**
158
- * Extract method signatures from a ServiceDefinition for client-side type sharing.
158
+ * 클라이언트 타입 공유를 위해 ServiceDefinition에서 메서드 시그니처를 추출한다.
159
159
  *
160
160
  * @example
161
161
  * export type UserServiceType = ServiceMethods<typeof UserService>;
162
- * // Client: client.getService<UserServiceType>("User");
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
- // Find service definition
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(`Service [${def.serviceName}] not found.`);
20
+ throw new Error(`서비스 [${def.serviceName}] 찾을 수 없습니다.`);
21
21
  }
22
22
 
23
- // Request validation (gatekeeper)
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(`[Security] Invalid client name: ${clientName}`);
27
+ throw new Error(`[보안] 유효하지 않은 클라이언트 이름: ${clientName}`);
28
28
  }
29
29
  }
30
30
 
31
- // Create context
31
+ // 컨텍스트 생성
32
32
  const ctx = createServiceContext(server, def.socket, def.http);
33
33
 
34
- // Invoke factory to create method object
34
+ // 팩토리를 호출하여 메서드 객체 생성
35
35
  const methods = serviceDef.factory(ctx);
36
36
 
37
- // Find method
37
+ // 메서드 검색
38
38
  const method = (methods as Record<string, unknown>)[def.methodName];
39
39
  if (typeof method !== "function") {
40
- throw new Error(`Method [${def.serviceName}.${def.methodName}] not found.`);
40
+ throw new Error(`메서드 [${def.serviceName}.${def.methodName}] 찾을 수 없습니다.`);
41
41
  }
42
42
 
43
- // Auth check
44
- if (server.options.auth != null) {
45
- // Check method-level auth first, fallback to service-level
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
- if (requiredPerms != null) {
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("Login is required.");
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("Insufficient permissions.");
64
+ throw new Error("권한이 부족합니다.");
60
65
  }
61
66
  }
62
67
  }
68
+ // auth === false → 의도적 비활성화, 인증 스킵
63
69
  }
64
70
 
65
- // Execute
71
+ // 실행
66
72
  return await method(...def.params);
67
73
  }
package/src/index.ts CHANGED
@@ -1,36 +1,34 @@
1
- // Types
1
+ // 타입
2
2
  export * from "./types/server-options";
3
3
 
4
- // Auth
4
+ // 인증
5
5
  export * from "./auth/auth-token-payload";
6
6
  export * from "./auth/jwt-manager";
7
7
 
8
- // Core
8
+ // 코어
9
9
  export * from "./core/define-service";
10
10
  export * from "./core/service-executor";
11
11
 
12
- // Transport - Socket
12
+ // 전송 계층 - Socket
13
13
  export * from "./transport/socket/websocket-handler";
14
14
  export * from "./transport/socket/service-socket";
15
15
 
16
- // Transport - HTTP
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
- // Protocol
21
+ // 프로토콜
22
22
  export * from "./protocol/protocol-wrapper";
23
23
 
24
- // Services
24
+ // 서비스
25
25
  export * from "./services/orm-service";
26
26
  export * from "./services/auto-update-service";
27
- export * from "./services/smtp-client-service";
28
-
29
- // Utils
27
+ // 유틸리티
30
28
  export * from "./utils/config-manager";
31
29
 
32
- // Legacy
30
+ // 레거시
33
31
  export * from "./legacy/v1-auto-update-handler";
34
32
 
35
- // Main
33
+ // 메인
36
34
  export * from "./service-server";
@@ -18,27 +18,27 @@ interface IV1Response {
18
18
  }
19
19
 
20
20
  /**
21
- * V1 legacy client handler (only auto-update supported).
22
- * All other requests return an upgrade-required error.
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
- // Notify connection established
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
- // Only allow SdAutoUpdateService.getLastVersion
36
+ // SdAutoUpdateService.getLastVersion만 허용
37
37
  if (msg.command === "SdAutoUpdateService.getLastVersion") {
38
- // Set legacy context
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
- // All other requests prompt for upgrade
51
+ // 모든 요청은 업그레이드 요구
52
52
  const response: IV1Response = {
53
53
  name: "response",
54
54
  reqUuid: msg.uuid,
55
55
  state: "error",
56
56
  body: {
57
- message: "App upgrade is required.",
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 message processing error", err);
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
- * Protocol wrapper interface
8
+ * 프로토콜 래퍼 인터페이스
9
9
  *
10
- * Automatically offloads heavy message encoding/decoding to a worker thread
11
- * while using main thread for lightweight operations.
10
+ * 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
11
+ * 가벼운 작업은 메인 스레드에서 처리한다.
12
12
  */
13
13
  export interface ServerProtocolWrapper {
14
14
  /**
15
- * Encode message (auto worker delegation)
15
+ * 메시지를 인코딩한다 (worker 자동 위임)
16
16
  */
17
17
  encode(uuid: string, message: ServiceMessage): Promise<{ chunks: Bytes[]; totalSize: number }>;
18
18
 
19
19
  /**
20
- * Decode message (auto worker delegation)
20
+ * 메시지를 디코딩한다 (worker 자동 위임)
21
21
  */
22
22
  decode(bytes: Bytes): Promise<ServiceMessageDecodeResult<ServiceMessage>>;
23
23
 
24
24
  /**
25
- * Dispose protocol resources
25
+ * 프로토콜 리소스를 해제한다
26
26
  */
27
27
  dispose(): void;
28
28
  }
29
29
 
30
- // Shared worker instance (lazy singleton)
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
- * Create a protocol wrapper instance
46
+ * 프로토콜 래퍼 인스턴스를 생성한다
47
47
  *
48
- * Automatically offloads heavy message encoding/decoding to a worker thread
49
- * while using main thread for lightweight operations.
48
+ * 무거운 메시지 인코딩/디코딩을 worker 스레드에 자동으로 위임하고,
49
+ * 가벼운 작업은 메인 스레드에서 처리한다.
50
50
  */
51
51
  export function createServerProtocolWrapper(): ServerProtocolWrapper {
52
52
  // -------------------------------------------------------------------
53
- // State
53
+ // 상태
54
54
  // -------------------------------------------------------------------
55
55
 
56
56
  const protocol = createServiceProtocol();
57
57
  const SIZE_THRESHOLD = 30 * 1024; // 30KB
58
58
 
59
59
  // -------------------------------------------------------------------
60
- // Helpers
60
+ // 헬퍼
61
61
  // -------------------------------------------------------------------
62
62
 
63
63
  /**
64
- * Check if message should use worker for encoding
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: always use worker
71
+ // Uint8Array: 항상 worker 사용
72
72
  if (body instanceof Uint8Array) {
73
73
  return true;
74
74
  }
75
75
 
76
- // Array: check for Uint8Array elements (ORM results, etc.)
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
- // Public API
85
+ // 공개 API
86
86
  // -------------------------------------------------------------------
87
87
 
88
88
  return {
@@ -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
- // SSL configuration (synchronous)
41
- // Note: Fastify HTTPS requires Buffer type (Uint8Array not directly usable)
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
- options.auth?.jwtSecret,
54
+ this._jwtSecret,
51
55
  );
52
56
  }
53
57
 
54
58
  async listen(): Promise<void> {
55
- logger.info(`Server starting... ${env.VER ?? ""}`);
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 plugin
71
+ // WebSocket 플러그인
58
72
  await this.fastify.register(fastifyWebsocket);
59
73
 
60
- // Security plugin
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
- // Upload plugin
95
+ // 업로드 플러그인
82
96
  await this.fastify.register(fastifyMultipart);
83
97
 
84
- // Register @fastify/static
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 configuration
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 parser
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 serializer
130
+ // JSON 직렬화기
117
131
  this.fastify.setSerializerCompiler(() => (data) => json.stringify(data));
118
132
 
119
- // API routes
133
+ // API 라우트
120
134
  this.fastify.all("/api/:service/:method", async (req, reply) => {
121
- await handleHttpRequest(req, reply, this.options.auth?.jwtSecret, (def) =>
135
+ await handleHttpRequest(req, reply, this._jwtSecret, (def) =>
122
136
  executeServiceMethod(this, def),
123
137
  );
124
138
  });
125
139
 
126
- // Upload route
140
+ // 업로드 라우트
127
141
  this.fastify.all("/upload", async (req, reply) => {
128
- await handleUpload(req, reply, this.options.rootPath, this.options.auth?.jwtSecret);
142
+ await handleUpload(req, reply, this.options.rootPath, this._jwtSecret);
129
143
  });
130
144
 
131
- // WebSocket route
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, "Missing Client ID/NAME");
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 legacy support (auto-update only)
160
+ // V1 레거시 지원 (자동 업데이트 전용)
147
161
  const autoUpdateDef = this.options.services.find((s) => s.name === "AutoUpdate");
148
162
  if (autoUpdateDef == null) {
149
- socket.close(1008, "AutoUpdate service not configured");
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
- // Static file wildcard handler
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 server-level error handling
192
+ // HTTP 서버 수준 에러 핸들링
179
193
  this.fastify.server.on("error", (err) => {
180
- logger.error("HTTP server error", err);
194
+ logger.error("HTTP 서버 에러", err);
181
195
  });
182
196
 
183
- // Listen
197
+ // 리슨
184
198
  await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
185
199
 
186
- // Register graceful shutdown handler
200
+ // 정상 종료 핸들러 등록
187
201
  this._registerGracefulShutdown();
188
202
 
189
203
  this.isOpen = true;
190
- logger.info(`Server started (port: ${this.options.port})`);
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("Server closed");
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("Broadcasting RELOAD to all server clients");
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
- const jwtSecret = this.options.auth?.jwtSecret;
218
- if (jwtSecret == null) throw new Error("JWT Secret is not defined.");
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
- const jwtSecret = this.options.auth?.jwtSecret;
225
- if (jwtSecret == null) throw new Error("JWT Secret is not defined.");
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} signal received. Starting server shutdown...`);
245
+ logger.info(`${signal} 시그널 수신됨. 서버 종료를 시작합니다...`);
233
246
 
234
247
  const forceExitTimer = setTimeout(() => {
235
- logger.error("Server shutdown timed out (10s). Forcing exit.");
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("Server shut down gracefully.");
256
+ logger.info("서버가 정상적으로 종료되었습니다.");
244
257
  clearTimeout(forceExitTimer);
245
258
  process.exit(0);
246
259
  } catch (err) {
247
- logger.error("Error during server shutdown", err);
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("Client path not found.");
15
+ if (clientPath == null) throw new Error("클라이언트 경로를 찾을 수 없습니다.");
16
16
 
17
17
  if (!(await fsx.exists(path.resolve(clientPath, platform, "updates")))) return undefined;
18
18