@simplysm/service-server 14.0.97 → 14.0.99

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.
@@ -15,6 +15,7 @@ export declare class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
15
15
  private readonly _wsHandler;
16
16
  private readonly _jwtSecret;
17
17
  private _shutdownRegistered;
18
+ private readonly _acmeManager;
18
19
  readonly fastify: FastifyInstance;
19
20
  constructor(options: ServiceServerOptions);
20
21
  listen(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"service-server.d.ts","sourceRoot":"","sources":["../src/service-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE,OAAO,EAAQ,YAAY,EAAO,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAa/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAOnE,MAAM,WAAW,gBAAgB,CAAC,SAAS,SAAS,eAAe;IACjE,IAAI,CACF,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,EACnD,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,qBAAa,aAAa,CAAC,SAAS,GAAG,OAAO,CAAE,SAAQ,YAAY,CAAC;IACnE,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;IASY,QAAQ,CAAC,OAAO,EAAE,oBAAoB;IARlD,MAAM,UAAS;IAEf,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4C;IACvE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,mBAAmB,CAAS;IAEpC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;gBAEb,OAAO,EAAE,oBAAoB;IAoB5C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAyJvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,QAAQ,CAAC,SAAS,SAAS,eAAe,EAAE,QAAQ,EAAE,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC;IAMvF,SAAS,CAAC,SAAS,SAAS,eAAe,EAC/C,QAAQ,EAAE,SAAS,EACnB,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,EACnD,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC;IAKpB,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC;IAKlD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAK1E,OAAO,CAAC,yBAAyB;CA4BlC;AAED,wBAAgB,mBAAmB,CAAC,SAAS,GAAG,OAAO,EACrD,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAAC,SAAS,CAAC,CAE1B"}
1
+ {"version":3,"file":"service-server.d.ts","sourceRoot":"","sources":["../src/service-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE,OAAO,EAAQ,YAAY,EAAO,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAgB/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAOnE,MAAM,WAAW,gBAAgB,CAAC,SAAS,SAAS,eAAe;IACjE,IAAI,CACF,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,EACnD,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,qBAAa,aAAa,CAAC,SAAS,GAAG,OAAO,CAAE,SAAQ,YAAY,CAAC;IACnE,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,IAAI,CAAC;CACb,CAAC;IAUY,QAAQ,CAAC,OAAO,EAAE,oBAAoB;IATlD,MAAM,UAAS;IAEf,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4C;IACvE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IAEvD,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;gBAEb,OAAO,EAAE,oBAAoB;IAyD5C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAyKvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAU5B,QAAQ,CAAC,SAAS,SAAS,eAAe,EAAE,QAAQ,EAAE,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC;IAMvF,SAAS,CAAC,SAAS,SAAS,eAAe,EAC/C,QAAQ,EAAE,SAAS,EACnB,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,EACnD,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC;IAKpB,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC;IAKlD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAK1E,OAAO,CAAC,yBAAyB;CA4BlC;AAED,wBAAgB,mBAAmB,CAAC,SAAS,GAAG,OAAO,EACrD,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAAC,SAAS,CAAC,CAE1B"}
@@ -10,6 +10,7 @@ import fastifyHelmet from "@fastify/helmet";
10
10
  import fastifyCors from "@fastify/cors";
11
11
  import path from "path";
12
12
  import { Buffer } from "node:buffer";
13
+ import { AcmeManager } from "./ssl/acme-manager.js";
13
14
  import { handleUpload } from "./transport/http/upload-handler.js";
14
15
  import { createWebSocketHandler } from "./transport/socket/websocket-handler.js";
15
16
  import { signJwt, verifyJwt } from "./auth/jwt-manager.js";
@@ -23,22 +24,65 @@ export class ServiceServer extends EventEmitter {
23
24
  _wsHandler;
24
25
  _jwtSecret;
25
26
  _shutdownRegistered = false;
27
+ _acmeManager;
26
28
  fastify;
27
29
  constructor(options) {
28
30
  super();
29
31
  this.options = options;
30
32
  this._jwtSecret =
31
33
  options.auth != null && options.auth !== false ? options.auth.jwtSecret : undefined;
32
- // SSL 설정 (동기)
34
+ // SSL 설정
33
35
  // 참고: Fastify HTTPS는 Buffer 타입이 필요함 (Uint8Array를 직접 사용할 수 없음)
34
- const httpsConf = options.ssl
35
- ? { pfx: Buffer.from(options.ssl.pfxBytes), passphrase: options.ssl.passphrase }
36
- : null;
36
+ let httpsConf = null;
37
+ if (options.ssl != null) {
38
+ if ("letsencrypt" in options.ssl) {
39
+ const acmeManager = new AcmeManager({
40
+ rootPath: options.rootPath,
41
+ domains: options.ssl.letsencrypt.domains,
42
+ email: options.ssl.letsencrypt.email,
43
+ staging: options.ssl.letsencrypt.staging,
44
+ });
45
+ this._acmeManager = acmeManager;
46
+ // 인증서 없이 ALPNCallback 만으로 HTTPS 서버 생성.
47
+ // 발급 전엔 acme-tls/1 챌린지에만 응답하고, 실인증서는 listen() 에서 setSecureContext 로 주입한다.
48
+ httpsConf = {
49
+ ALPNCallback: function ({ protocols }) {
50
+ if (protocols.includes("acme-tls/1")) {
51
+ const ctx = acmeManager.getChallengeContext();
52
+ if (ctx != null) {
53
+ this.setKeyCert(ctx);
54
+ return "acme-tls/1";
55
+ }
56
+ return undefined;
57
+ }
58
+ return protocols.includes("http/1.1") ? "http/1.1" : undefined;
59
+ },
60
+ };
61
+ }
62
+ else if ("pfxBytes" in options.ssl) {
63
+ httpsConf = {
64
+ pfx: Buffer.from(options.ssl.pfxBytes),
65
+ ...(options.ssl.passphrase != null ? { passphrase: options.ssl.passphrase } : {}),
66
+ };
67
+ }
68
+ else {
69
+ httpsConf = {
70
+ key: Buffer.from(options.ssl.pemKeyBytes),
71
+ cert: Buffer.from(options.ssl.certBytes),
72
+ ...(options.ssl.caBytes != null ? { ca: Buffer.from(options.ssl.caBytes) } : {}),
73
+ ...(options.ssl.passphrase != null ? { passphrase: options.ssl.passphrase } : {}),
74
+ };
75
+ }
76
+ }
37
77
  this.fastify = fastify({ https: httpsConf });
38
78
  this._wsHandler = createWebSocketHandler((def) => executeServiceMethod(this, def), this._jwtSecret);
39
79
  }
40
80
  async listen() {
41
81
  logger.info(`서버 시작 중... ${env("VER") ?? ""}`);
82
+ // letsencrypt 는 핸드셰이크 중 인증서를 주입하는 TLSSocket.setKeyCert 가 필요 (Node 20.18.0+/22.9.0+)
83
+ if (this._acmeManager != null) {
84
+ assertAcmeNodeSupport();
85
+ }
42
86
  // auth 설정 검증: auth 미설정(undefined)인데 auth 요구 서비스가 있으면 에러
43
87
  if (this.options.auth == null) {
44
88
  const authRequiredService = this.options.services.find((s) => s.authPermissions != null);
@@ -151,6 +195,16 @@ export class ServiceServer extends EventEmitter {
151
195
  });
152
196
  // 리슨
153
197
  await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
198
+ // Let's Encrypt: 리슨(443 바인딩) 후 인증서를 확보해 적용 (하이브리드 기동)
199
+ if (this._acmeManager != null) {
200
+ const server = this.fastify.server;
201
+ const material = await this._acmeManager.ensureCertificate();
202
+ server.setSecureContext({ key: material.key, cert: material.cert });
203
+ this._acmeManager.onRenew((m) => {
204
+ server.setSecureContext({ key: m.key, cert: m.cert });
205
+ logger.info("갱신된 인증서가 적용되었습니다.");
206
+ });
207
+ }
154
208
  // 정상 종료 핸들러 등록
155
209
  this._registerGracefulShutdown();
156
210
  this.isOpen = true;
@@ -158,6 +212,7 @@ export class ServiceServer extends EventEmitter {
158
212
  this.emit("ready");
159
213
  }
160
214
  async close() {
215
+ this._acmeManager?.stop();
161
216
  this._wsHandler.closeAll();
162
217
  await this.fastify.close();
163
218
  this.isOpen = false;
@@ -221,4 +276,15 @@ function createV1AutoUpdateMethods(methods) {
221
276
  getLastVersion: (platform) => getLastVersion(platform),
222
277
  };
223
278
  }
279
+ /**
280
+ * Let's Encrypt(TLS-ALPN-01) 는 핸드셰이크 중 인증서를 주입하는 `TLSSocket.setKeyCert` 를 쓴다.
281
+ * 이 API 는 Node 20.18.0+ / 22.9.0+ 에서만 제공되므로 미만 버전이면 throw.
282
+ */
283
+ function assertAcmeNodeSupport() {
284
+ const [major = 0, minor = 0] = process.versions.node.split(".").map((v) => Number(v));
285
+ const supported = (major === 20 && minor >= 18) || (major === 22 && minor >= 9) || major >= 23;
286
+ if (!supported) {
287
+ throw new Error(`Let's Encrypt(letsencrypt) SSL 은 Node 20.18.0+ 또는 22.9.0+ 가 필요합니다. 현재 버전: ${process.versions.node}`);
288
+ }
289
+ }
224
290
  //# sourceMappingURL=service-server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service-server.js","sourceRoot":"","sources":["../src/service-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service-server.js","sourceRoot":"","sources":["../src/service-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import tls from "node:tls";
2
+ export interface AcmeManagerOptions {
3
+ rootPath: string;
4
+ domains: string[];
5
+ email: string;
6
+ staging?: boolean;
7
+ }
8
+ export interface AcmeCertMaterial {
9
+ /** 개인키 PEM */
10
+ key: string;
11
+ /** 인증서 체인 PEM */
12
+ cert: string;
13
+ }
14
+ /**
15
+ * Let's Encrypt(ACME) 인증서 자동 발급·갱신 매니저.
16
+ *
17
+ * TLS-ALPN-01 챌린지로 발급한다. 챌린지 응답 인증서는 {@link getChallengeContext} 로 노출하여
18
+ * 서버의 ALPNCallback 이 `acme-tls/1` 핸드셰이크에 주입한다.
19
+ */
20
+ export declare class AcmeManager {
21
+ private readonly _options;
22
+ private readonly _dir;
23
+ private _challengeContext;
24
+ private _renewTimer;
25
+ private _onRenew;
26
+ constructor(_options: AcmeManagerOptions);
27
+ private get _accountKeyPath();
28
+ private get _certPath();
29
+ private get _certKeyPath();
30
+ /** ALPNCallback 에서 사용할 현재 챌린지 컨텍스트 (챌린지 진행 중에만 존재) */
31
+ getChallengeContext(): tls.SecureContext | undefined;
32
+ /** 갱신 성공 시 호출될 핸들러 등록 (무중단 핫스왑용) */
33
+ onRenew(handler: (material: AcmeCertMaterial) => void): void;
34
+ /**
35
+ * 적용할 인증서를 확보한다.
36
+ * 캐시된 유효 인증서가 있으면 로드, 없으면(또는 만료 임박) 신규 발급.
37
+ * 발급 실패 시 throw (하이브리드 기동: 최초 발급 실패는 기동 실패).
38
+ */
39
+ ensureCertificate(): Promise<AcmeCertMaterial>;
40
+ /** 갱신 타이머 정리 */
41
+ stop(): void;
42
+ private _loadValidCachedCert;
43
+ private _directoryUrl;
44
+ private _getAccountKey;
45
+ private _issue;
46
+ private _scheduleRenewal;
47
+ /** 긴 지연(>24.8일)은 setTimeout 오버플로를 피해 분할 무장 */
48
+ private _armTimer;
49
+ private _renew;
50
+ }
51
+ //# sourceMappingURL=acme-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acme-manager.d.ts","sourceRoot":"","sources":["../../src/ssl/acme-manager.ts"],"names":[],"mappings":"AACA,OAAO,GAAG,MAAM,UAAU,CAAC;AAe3B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IAMV,OAAO,CAAC,QAAQ,CAAC,QAAQ;IALrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,QAAQ,CAAqD;gBAExC,QAAQ,EAAE,kBAAkB;IAIzD,OAAO,KAAK,eAAe,GAE1B;IACD,OAAO,KAAK,SAAS,GAEpB;IACD,OAAO,KAAK,YAAY,GAEvB;IAED,sDAAsD;IACtD,mBAAmB,IAAI,GAAG,CAAC,aAAa,GAAG,SAAS;IAIpD,oCAAoC;IACpC,OAAO,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAI5D;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAYpD,gBAAgB;IAChB,IAAI,IAAI,IAAI;YAOE,oBAAoB;IAiClC,OAAO,CAAC,aAAa;YAUP,cAAc;YAUd,MAAM;IA0CpB,OAAO,CAAC,gBAAgB;IAKxB,8CAA8C;IAC9C,OAAO,CAAC,SAAS;YASH,MAAM;CAarB"}
@@ -0,0 +1,176 @@
1
+ import * as acme from "acme-client";
2
+ import tls from "node:tls";
3
+ import { chmod } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fsx } from "@simplysm/core-node";
6
+ import { createLogger, env } from "@simplysm/core-common";
7
+ const logger = createLogger("service-server:AcmeManager");
8
+ /** 만료 이 시점 전이면 갱신 (30일) */
9
+ const RENEW_BEFORE_MS = 30 * 24 * 60 * 60 * 1000;
10
+ /** 갱신 실패 시 재시도 간격 (1시간) */
11
+ const RETRY_DELAY_MS = 60 * 60 * 1000;
12
+ /** setTimeout 최대 지연 (약 24.8일). 초과 시 분할하여 재무장 */
13
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
14
+ /**
15
+ * Let's Encrypt(ACME) 인증서 자동 발급·갱신 매니저.
16
+ *
17
+ * TLS-ALPN-01 챌린지로 발급한다. 챌린지 응답 인증서는 {@link getChallengeContext} 로 노출하여
18
+ * 서버의 ALPNCallback 이 `acme-tls/1` 핸드셰이크에 주입한다.
19
+ */
20
+ export class AcmeManager {
21
+ _options;
22
+ _dir;
23
+ _challengeContext;
24
+ _renewTimer;
25
+ _onRenew;
26
+ constructor(_options) {
27
+ this._options = _options;
28
+ this._dir = path.resolve(_options.rootPath, ".acme");
29
+ }
30
+ get _accountKeyPath() {
31
+ return path.resolve(this._dir, "account.key");
32
+ }
33
+ get _certPath() {
34
+ return path.resolve(this._dir, "cert.pem");
35
+ }
36
+ get _certKeyPath() {
37
+ return path.resolve(this._dir, "cert.key");
38
+ }
39
+ /** ALPNCallback 에서 사용할 현재 챌린지 컨텍스트 (챌린지 진행 중에만 존재) */
40
+ getChallengeContext() {
41
+ return this._challengeContext;
42
+ }
43
+ /** 갱신 성공 시 호출될 핸들러 등록 (무중단 핫스왑용) */
44
+ onRenew(handler) {
45
+ this._onRenew = handler;
46
+ }
47
+ /**
48
+ * 적용할 인증서를 확보한다.
49
+ * 캐시된 유효 인증서가 있으면 로드, 없으면(또는 만료 임박) 신규 발급.
50
+ * 발급 실패 시 throw (하이브리드 기동: 최초 발급 실패는 기동 실패).
51
+ */
52
+ async ensureCertificate() {
53
+ const cached = await this._loadValidCachedCert();
54
+ if (cached != null) {
55
+ logger.info(`캐시된 인증서 사용 (만료: ${cached.notAfter.toISOString()})`);
56
+ this._scheduleRenewal(cached.notAfter);
57
+ return { key: cached.key, cert: cached.cert };
58
+ }
59
+ logger.info("유효한 캐시 인증서 없음. 신규 발급을 시작합니다.");
60
+ return this._issue();
61
+ }
62
+ /** 갱신 타이머 정리 */
63
+ stop() {
64
+ if (this._renewTimer != null) {
65
+ clearTimeout(this._renewTimer);
66
+ this._renewTimer = undefined;
67
+ }
68
+ }
69
+ async _loadValidCachedCert() {
70
+ if (!(await fsx.exists(this._certPath)) || !(await fsx.exists(this._certKeyPath))) {
71
+ return undefined;
72
+ }
73
+ const cert = await fsx.read(this._certPath);
74
+ const key = await fsx.read(this._certKeyPath);
75
+ let info;
76
+ try {
77
+ info = acme.crypto.readCertificateInfo(cert);
78
+ }
79
+ catch (err) {
80
+ logger.warn("캐시 인증서 파싱 실패. 재발급을 진행합니다.", err);
81
+ return undefined;
82
+ }
83
+ // 만료 30일 이내면 캐시 무효 처리 (즉시 갱신 대상)
84
+ if (info.notAfter.getTime() - Date.now() <= RENEW_BEFORE_MS) {
85
+ return undefined;
86
+ }
87
+ // 요청 도메인을 모두 포함하는지 확인
88
+ const certDomains = new Set([info.domains.commonName, ...info.domains.altNames]);
89
+ if (!this._options.domains.every((d) => certDomains.has(d))) {
90
+ logger.info("캐시 인증서가 요청 도메인을 모두 포함하지 않음. 재발급을 진행합니다.");
91
+ return undefined;
92
+ }
93
+ return { key, cert, notAfter: info.notAfter };
94
+ }
95
+ _directoryUrl() {
96
+ // 운영 환경 변수로 ACME 디렉토리 URL 재정의 가능 (사설 CA·테스트용 pebble 등)
97
+ const override = env("SD_ACME_DIRECTORY_URL");
98
+ if (override != null && override !== "")
99
+ return override;
100
+ return this._options.staging === true
101
+ ? acme.directory.letsencrypt.staging
102
+ : acme.directory.letsencrypt.production;
103
+ }
104
+ async _getAccountKey() {
105
+ if (await fsx.exists(this._accountKeyPath)) {
106
+ return fsx.read(this._accountKeyPath);
107
+ }
108
+ const accountKey = (await acme.crypto.createPrivateRsaKey()).toString();
109
+ await fsx.write(this._accountKeyPath, accountKey);
110
+ await chmod(this._accountKeyPath, 0o600).catch(() => { });
111
+ return accountKey;
112
+ }
113
+ async _issue() {
114
+ const accountKey = await this._getAccountKey();
115
+ const client = new acme.Client({ directoryUrl: this._directoryUrl(), accountKey });
116
+ const [certKey, csr] = await acme.crypto.createCsr({
117
+ commonName: this._options.domains[0],
118
+ altNames: this._options.domains,
119
+ });
120
+ const cert = await client.auto({
121
+ csr,
122
+ email: this._options.email,
123
+ termsOfServiceAgreed: true,
124
+ challengePriority: ["tls-alpn-01"],
125
+ // 로컬 사전검증(발급 호스트가 자기 공개 도메인:443 으로 hairpin 접속) 생략.
126
+ // 망 구성에 따라 hairpin 이 막힐 수 있고, 실검증은 CA(LE) 가 직접 수행한다.
127
+ skipChallengeVerification: true,
128
+ // challengePriority 를 tls-alpn-01 로 고정했으므로 이 콜백은 항상 tls-alpn-01 챌린지로 호출된다.
129
+ challengeCreateFn: async (authz, _challenge, keyAuthorization) => {
130
+ const [alpnKey, alpnCert] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
131
+ this._challengeContext = tls.createSecureContext({ key: alpnKey, cert: alpnCert });
132
+ },
133
+ challengeRemoveFn: () => {
134
+ this._challengeContext = undefined;
135
+ return Promise.resolve();
136
+ },
137
+ });
138
+ const keyPem = certKey.toString();
139
+ await fsx.write(this._certPath, cert);
140
+ await fsx.write(this._certKeyPath, keyPem);
141
+ await chmod(this._certKeyPath, 0o600).catch(() => { });
142
+ const info = acme.crypto.readCertificateInfo(cert);
143
+ logger.info(`인증서 발급 완료 (도메인: ${this._options.domains.join(", ")}, 만료: ${info.notAfter.toISOString()})`);
144
+ this._scheduleRenewal(info.notAfter);
145
+ return { key: keyPem, cert };
146
+ }
147
+ _scheduleRenewal(notAfter) {
148
+ this.stop();
149
+ this._armTimer(notAfter.getTime() - RENEW_BEFORE_MS);
150
+ }
151
+ /** 긴 지연(>24.8일)은 setTimeout 오버플로를 피해 분할 무장 */
152
+ _armTimer(targetTime) {
153
+ const remaining = Math.max(0, targetTime - Date.now());
154
+ if (remaining > MAX_TIMER_DELAY_MS) {
155
+ this._renewTimer = setTimeout(() => this._armTimer(targetTime), MAX_TIMER_DELAY_MS);
156
+ }
157
+ else {
158
+ this._renewTimer = setTimeout(() => void this._renew(), remaining);
159
+ }
160
+ }
161
+ async _renew() {
162
+ try {
163
+ logger.info("인증서 갱신을 시작합니다.");
164
+ const material = await this._issue();
165
+ this._onRenew?.(material);
166
+ logger.info("인증서 갱신 및 적용이 완료되었습니다.");
167
+ }
168
+ catch (err) {
169
+ // 갱신 실패는 문제 발생 사실이므로 error. 기존 유효 인증서로 계속 서비스하며 재시도.
170
+ logger.error("인증서 갱신 실패. 기존 인증서를 유지하고 재시도를 예약합니다.", err);
171
+ this.stop();
172
+ this._renewTimer = setTimeout(() => void this._renew(), RETRY_DELAY_MS);
173
+ }
174
+ }
175
+ }
176
+ //# sourceMappingURL=acme-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acme-manager.js","sourceRoot":"","sources":["../../src/ssl/acme-manager.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,18 @@ export interface ServiceServerOptions {
5
5
  port: number;
6
6
  ssl?: {
7
7
  pfxBytes: Uint8Array;
8
- passphrase: string;
8
+ passphrase?: string;
9
+ } | {
10
+ pemKeyBytes: Uint8Array;
11
+ certBytes: Uint8Array;
12
+ caBytes?: Uint8Array;
13
+ passphrase?: string;
14
+ } | {
15
+ letsencrypt: {
16
+ domains: string[];
17
+ email: string;
18
+ staging?: boolean;
19
+ };
9
20
  };
10
21
  auth?: {
11
22
  jwtSecret: string;
@@ -1 +1 @@
1
- {"version":3,"file":"server-options.d.ts","sourceRoot":"","sources":["../../src/types/server-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE;QACJ,QAAQ,EAAE,UAAU,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,IAAI,CAAC,EACD;QACE,SAAS,EAAE,MAAM,CAAC;KACnB,GACD,KAAK,CAAC;IACV,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACvC"}
1
+ {"version":3,"file":"server-options.d.ts","sourceRoot":"","sources":["../../src/types/server-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EACA;QACE,QAAQ,EAAE,UAAU,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GACD;QACE,WAAW,EAAE,UAAU,CAAC;QACxB,SAAS,EAAE,UAAU,CAAC;QACtB,OAAO,CAAC,EAAE,UAAU,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GACD;QACE,WAAW,EAAE;YACX,OAAO,EAAE,MAAM,EAAE,CAAC;YAClB,KAAK,EAAE,MAAM,CAAC;YACd,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;KACH,CAAC;IACN,IAAI,CAAC,EACD;QACE,SAAS,EAAE,MAAM,CAAC;KACnB,GACD,KAAK,CAAC;IACV,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACvC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/service-server",
3
- "version": "14.0.97",
3
+ "version": "14.0.99",
4
4
  "description": "심플리즘 패키지 - 서비스 (server)",
5
5
  "author": "심플리즘",
6
6
  "license": "Apache-2.0",
@@ -23,17 +23,18 @@
23
23
  "@fastify/multipart": "^9.4.0",
24
24
  "@fastify/static": "^9.1.3",
25
25
  "@fastify/websocket": "^11.2.0",
26
+ "acme-client": "^5.4.0",
26
27
  "bufferutil": "^4.1.0",
27
28
  "fastify": "^5.8.5",
28
29
  "jose": "^6.2.3",
29
30
  "semver": "^7.8.4",
30
31
  "utf-8-validate": "^6.0.6",
31
32
  "ws": "^8.21.0",
32
- "@simplysm/core-common": "14.0.97",
33
- "@simplysm/orm-node": "14.0.97",
34
- "@simplysm/orm-common": "14.0.97",
35
- "@simplysm/core-node": "14.0.97",
36
- "@simplysm/service-common": "14.0.97"
33
+ "@simplysm/core-common": "14.0.99",
34
+ "@simplysm/core-node": "14.0.99",
35
+ "@simplysm/orm-node": "14.0.99",
36
+ "@simplysm/orm-common": "14.0.99",
37
+ "@simplysm/service-common": "14.0.99"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/semver": "^7.7.1",
@@ -12,6 +12,9 @@ import fastifyHelmet from "@fastify/helmet";
12
12
  import fastifyCors from "@fastify/cors";
13
13
  import path from "path";
14
14
  import { Buffer } from "node:buffer";
15
+ import type { TLSSocket } from "node:tls";
16
+ import type * as https from "node:https";
17
+ import { AcmeManager } from "./ssl/acme-manager";
15
18
  import { handleUpload } from "./transport/http/upload-handler";
16
19
  import { createWebSocketHandler } from "./transport/socket/websocket-handler";
17
20
  import type { WebSocket } from "ws";
@@ -40,6 +43,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
40
43
  private readonly _wsHandler: ReturnType<typeof createWebSocketHandler>;
41
44
  private readonly _jwtSecret: string | undefined;
42
45
  private _shutdownRegistered = false;
46
+ private readonly _acmeManager: AcmeManager | undefined;
43
47
 
44
48
  readonly fastify: FastifyInstance;
45
49
 
@@ -49,11 +53,48 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
49
53
  this._jwtSecret =
50
54
  options.auth != null && options.auth !== false ? options.auth.jwtSecret : undefined;
51
55
 
52
- // SSL 설정 (동기)
56
+ // SSL 설정
53
57
  // 참고: Fastify HTTPS는 Buffer 타입이 필요함 (Uint8Array를 직접 사용할 수 없음)
54
- const httpsConf = options.ssl
55
- ? { pfx: Buffer.from(options.ssl.pfxBytes), passphrase: options.ssl.passphrase }
56
- : null;
58
+ let httpsConf: https.ServerOptions | null = null;
59
+ if (options.ssl != null) {
60
+ if ("letsencrypt" in options.ssl) {
61
+ const acmeManager = new AcmeManager({
62
+ rootPath: options.rootPath,
63
+ domains: options.ssl.letsencrypt.domains,
64
+ email: options.ssl.letsencrypt.email,
65
+ staging: options.ssl.letsencrypt.staging,
66
+ });
67
+ this._acmeManager = acmeManager;
68
+
69
+ // 인증서 없이 ALPNCallback 만으로 HTTPS 서버 생성.
70
+ // 발급 전엔 acme-tls/1 챌린지에만 응답하고, 실인증서는 listen() 에서 setSecureContext 로 주입한다.
71
+ httpsConf = {
72
+ ALPNCallback: function (this: TLSSocket, { protocols }): string | undefined {
73
+ if (protocols.includes("acme-tls/1")) {
74
+ const ctx = acmeManager.getChallengeContext();
75
+ if (ctx != null) {
76
+ this.setKeyCert(ctx);
77
+ return "acme-tls/1";
78
+ }
79
+ return undefined;
80
+ }
81
+ return protocols.includes("http/1.1") ? "http/1.1" : undefined;
82
+ },
83
+ };
84
+ } else if ("pfxBytes" in options.ssl) {
85
+ httpsConf = {
86
+ pfx: Buffer.from(options.ssl.pfxBytes),
87
+ ...(options.ssl.passphrase != null ? { passphrase: options.ssl.passphrase } : {}),
88
+ };
89
+ } else {
90
+ httpsConf = {
91
+ key: Buffer.from(options.ssl.pemKeyBytes),
92
+ cert: Buffer.from(options.ssl.certBytes),
93
+ ...(options.ssl.caBytes != null ? { ca: Buffer.from(options.ssl.caBytes) } : {}),
94
+ ...(options.ssl.passphrase != null ? { passphrase: options.ssl.passphrase } : {}),
95
+ };
96
+ }
97
+ }
57
98
 
58
99
  this.fastify = fastify({ https: httpsConf });
59
100
 
@@ -66,6 +107,11 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
66
107
  async listen(): Promise<void> {
67
108
  logger.info(`서버 시작 중... ${env("VER") ?? ""}`);
68
109
 
110
+ // letsencrypt 는 핸드셰이크 중 인증서를 주입하는 TLSSocket.setKeyCert 가 필요 (Node 20.18.0+/22.9.0+)
111
+ if (this._acmeManager != null) {
112
+ assertAcmeNodeSupport();
113
+ }
114
+
69
115
  // auth 설정 검증: auth 미설정(undefined)인데 auth 요구 서비스가 있으면 에러
70
116
  if (this.options.auth == null) {
71
117
  const authRequiredService = this.options.services.find((s) => s.authPermissions != null);
@@ -208,6 +254,17 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
208
254
  // 리슨
209
255
  await this.fastify.listen({ port: this.options.port, host: "0.0.0.0" });
210
256
 
257
+ // Let's Encrypt: 리슨(443 바인딩) 후 인증서를 확보해 적용 (하이브리드 기동)
258
+ if (this._acmeManager != null) {
259
+ const server = this.fastify.server as unknown as https.Server;
260
+ const material = await this._acmeManager.ensureCertificate();
261
+ server.setSecureContext({ key: material.key, cert: material.cert });
262
+ this._acmeManager.onRenew((m) => {
263
+ server.setSecureContext({ key: m.key, cert: m.cert });
264
+ logger.info("갱신된 인증서가 적용되었습니다.");
265
+ });
266
+ }
267
+
211
268
  // 정상 종료 핸들러 등록
212
269
  this._registerGracefulShutdown();
213
270
 
@@ -217,6 +274,7 @@ export class ServiceServer<TAuthInfo = unknown> extends EventEmitter<{
217
274
  }
218
275
 
219
276
  async close(): Promise<void> {
277
+ this._acmeManager?.stop();
220
278
  this._wsHandler.closeAll();
221
279
  await this.fastify.close();
222
280
 
@@ -297,3 +355,18 @@ function createV1AutoUpdateMethods(
297
355
  getLastVersion: (platform) => getLastVersion(platform),
298
356
  };
299
357
  }
358
+
359
+ /**
360
+ * Let's Encrypt(TLS-ALPN-01) 는 핸드셰이크 중 인증서를 주입하는 `TLSSocket.setKeyCert` 를 쓴다.
361
+ * 이 API 는 Node 20.18.0+ / 22.9.0+ 에서만 제공되므로 미만 버전이면 throw.
362
+ */
363
+ function assertAcmeNodeSupport(): void {
364
+ const [major = 0, minor = 0] = process.versions.node.split(".").map((v) => Number(v));
365
+ const supported =
366
+ (major === 20 && minor >= 18) || (major === 22 && minor >= 9) || major >= 23;
367
+ if (!supported) {
368
+ throw new Error(
369
+ `Let's Encrypt(letsencrypt) SSL 은 Node 20.18.0+ 또는 22.9.0+ 가 필요합니다. 현재 버전: ${process.versions.node}`,
370
+ );
371
+ }
372
+ }
@@ -0,0 +1,215 @@
1
+ import * as acme from "acme-client";
2
+ import tls from "node:tls";
3
+ import { chmod } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fsx } from "@simplysm/core-node";
6
+ import { createLogger, env } from "@simplysm/core-common";
7
+
8
+ const logger = createLogger("service-server:AcmeManager");
9
+
10
+ /** 만료 이 시점 전이면 갱신 (30일) */
11
+ const RENEW_BEFORE_MS = 30 * 24 * 60 * 60 * 1000;
12
+ /** 갱신 실패 시 재시도 간격 (1시간) */
13
+ const RETRY_DELAY_MS = 60 * 60 * 1000;
14
+ /** setTimeout 최대 지연 (약 24.8일). 초과 시 분할하여 재무장 */
15
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
16
+
17
+ export interface AcmeManagerOptions {
18
+ rootPath: string;
19
+ domains: string[];
20
+ email: string;
21
+ staging?: boolean;
22
+ }
23
+
24
+ export interface AcmeCertMaterial {
25
+ /** 개인키 PEM */
26
+ key: string;
27
+ /** 인증서 체인 PEM */
28
+ cert: string;
29
+ }
30
+
31
+ /**
32
+ * Let's Encrypt(ACME) 인증서 자동 발급·갱신 매니저.
33
+ *
34
+ * TLS-ALPN-01 챌린지로 발급한다. 챌린지 응답 인증서는 {@link getChallengeContext} 로 노출하여
35
+ * 서버의 ALPNCallback 이 `acme-tls/1` 핸드셰이크에 주입한다.
36
+ */
37
+ export class AcmeManager {
38
+ private readonly _dir: string;
39
+ private _challengeContext: tls.SecureContext | undefined;
40
+ private _renewTimer: NodeJS.Timeout | undefined;
41
+ private _onRenew: ((material: AcmeCertMaterial) => void) | undefined;
42
+
43
+ constructor(private readonly _options: AcmeManagerOptions) {
44
+ this._dir = path.resolve(_options.rootPath, ".acme");
45
+ }
46
+
47
+ private get _accountKeyPath(): string {
48
+ return path.resolve(this._dir, "account.key");
49
+ }
50
+ private get _certPath(): string {
51
+ return path.resolve(this._dir, "cert.pem");
52
+ }
53
+ private get _certKeyPath(): string {
54
+ return path.resolve(this._dir, "cert.key");
55
+ }
56
+
57
+ /** ALPNCallback 에서 사용할 현재 챌린지 컨텍스트 (챌린지 진행 중에만 존재) */
58
+ getChallengeContext(): tls.SecureContext | undefined {
59
+ return this._challengeContext;
60
+ }
61
+
62
+ /** 갱신 성공 시 호출될 핸들러 등록 (무중단 핫스왑용) */
63
+ onRenew(handler: (material: AcmeCertMaterial) => void): void {
64
+ this._onRenew = handler;
65
+ }
66
+
67
+ /**
68
+ * 적용할 인증서를 확보한다.
69
+ * 캐시된 유효 인증서가 있으면 로드, 없으면(또는 만료 임박) 신규 발급.
70
+ * 발급 실패 시 throw (하이브리드 기동: 최초 발급 실패는 기동 실패).
71
+ */
72
+ async ensureCertificate(): Promise<AcmeCertMaterial> {
73
+ const cached = await this._loadValidCachedCert();
74
+ if (cached != null) {
75
+ logger.info(`캐시된 인증서 사용 (만료: ${cached.notAfter.toISOString()})`);
76
+ this._scheduleRenewal(cached.notAfter);
77
+ return { key: cached.key, cert: cached.cert };
78
+ }
79
+
80
+ logger.info("유효한 캐시 인증서 없음. 신규 발급을 시작합니다.");
81
+ return this._issue();
82
+ }
83
+
84
+ /** 갱신 타이머 정리 */
85
+ stop(): void {
86
+ if (this._renewTimer != null) {
87
+ clearTimeout(this._renewTimer);
88
+ this._renewTimer = undefined;
89
+ }
90
+ }
91
+
92
+ private async _loadValidCachedCert(): Promise<
93
+ { key: string; cert: string; notAfter: Date } | undefined
94
+ > {
95
+ if (!(await fsx.exists(this._certPath)) || !(await fsx.exists(this._certKeyPath))) {
96
+ return undefined;
97
+ }
98
+
99
+ const cert = await fsx.read(this._certPath);
100
+ const key = await fsx.read(this._certKeyPath);
101
+
102
+ let info;
103
+ try {
104
+ info = acme.crypto.readCertificateInfo(cert);
105
+ } catch (err) {
106
+ logger.warn("캐시 인증서 파싱 실패. 재발급을 진행합니다.", err);
107
+ return undefined;
108
+ }
109
+
110
+ // 만료 30일 이내면 캐시 무효 처리 (즉시 갱신 대상)
111
+ if (info.notAfter.getTime() - Date.now() <= RENEW_BEFORE_MS) {
112
+ return undefined;
113
+ }
114
+
115
+ // 요청 도메인을 모두 포함하는지 확인
116
+ const certDomains = new Set([info.domains.commonName, ...info.domains.altNames]);
117
+ if (!this._options.domains.every((d) => certDomains.has(d))) {
118
+ logger.info("캐시 인증서가 요청 도메인을 모두 포함하지 않음. 재발급을 진행합니다.");
119
+ return undefined;
120
+ }
121
+
122
+ return { key, cert, notAfter: info.notAfter };
123
+ }
124
+
125
+ private _directoryUrl(): string {
126
+ // 운영 환경 변수로 ACME 디렉토리 URL 재정의 가능 (사설 CA·테스트용 pebble 등)
127
+ const override = env("SD_ACME_DIRECTORY_URL");
128
+ if (override != null && override !== "") return override;
129
+
130
+ return this._options.staging === true
131
+ ? acme.directory.letsencrypt.staging
132
+ : acme.directory.letsencrypt.production;
133
+ }
134
+
135
+ private async _getAccountKey(): Promise<string> {
136
+ if (await fsx.exists(this._accountKeyPath)) {
137
+ return fsx.read(this._accountKeyPath);
138
+ }
139
+ const accountKey = (await acme.crypto.createPrivateRsaKey()).toString();
140
+ await fsx.write(this._accountKeyPath, accountKey);
141
+ await chmod(this._accountKeyPath, 0o600).catch(() => {});
142
+ return accountKey;
143
+ }
144
+
145
+ private async _issue(): Promise<AcmeCertMaterial> {
146
+ const accountKey = await this._getAccountKey();
147
+ const client = new acme.Client({ directoryUrl: this._directoryUrl(), accountKey });
148
+
149
+ const [certKey, csr] = await acme.crypto.createCsr({
150
+ commonName: this._options.domains[0],
151
+ altNames: this._options.domains,
152
+ });
153
+
154
+ const cert = await client.auto({
155
+ csr,
156
+ email: this._options.email,
157
+ termsOfServiceAgreed: true,
158
+ challengePriority: ["tls-alpn-01"],
159
+ // 로컬 사전검증(발급 호스트가 자기 공개 도메인:443 으로 hairpin 접속) 생략.
160
+ // 망 구성에 따라 hairpin 이 막힐 수 있고, 실검증은 CA(LE) 가 직접 수행한다.
161
+ skipChallengeVerification: true,
162
+ // challengePriority 를 tls-alpn-01 로 고정했으므로 이 콜백은 항상 tls-alpn-01 챌린지로 호출된다.
163
+ challengeCreateFn: async (authz, _challenge, keyAuthorization) => {
164
+ const [alpnKey, alpnCert] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
165
+ this._challengeContext = tls.createSecureContext({ key: alpnKey, cert: alpnCert });
166
+ },
167
+ challengeRemoveFn: () => {
168
+ this._challengeContext = undefined;
169
+ return Promise.resolve();
170
+ },
171
+ });
172
+
173
+ const keyPem = certKey.toString();
174
+ await fsx.write(this._certPath, cert);
175
+ await fsx.write(this._certKeyPath, keyPem);
176
+ await chmod(this._certKeyPath, 0o600).catch(() => {});
177
+
178
+ const info = acme.crypto.readCertificateInfo(cert);
179
+ logger.info(
180
+ `인증서 발급 완료 (도메인: ${this._options.domains.join(", ")}, 만료: ${info.notAfter.toISOString()})`,
181
+ );
182
+ this._scheduleRenewal(info.notAfter);
183
+
184
+ return { key: keyPem, cert };
185
+ }
186
+
187
+ private _scheduleRenewal(notAfter: Date): void {
188
+ this.stop();
189
+ this._armTimer(notAfter.getTime() - RENEW_BEFORE_MS);
190
+ }
191
+
192
+ /** 긴 지연(>24.8일)은 setTimeout 오버플로를 피해 분할 무장 */
193
+ private _armTimer(targetTime: number): void {
194
+ const remaining = Math.max(0, targetTime - Date.now());
195
+ if (remaining > MAX_TIMER_DELAY_MS) {
196
+ this._renewTimer = setTimeout(() => this._armTimer(targetTime), MAX_TIMER_DELAY_MS);
197
+ } else {
198
+ this._renewTimer = setTimeout(() => void this._renew(), remaining);
199
+ }
200
+ }
201
+
202
+ private async _renew(): Promise<void> {
203
+ try {
204
+ logger.info("인증서 갱신을 시작합니다.");
205
+ const material = await this._issue();
206
+ this._onRenew?.(material);
207
+ logger.info("인증서 갱신 및 적용이 완료되었습니다.");
208
+ } catch (err) {
209
+ // 갱신 실패는 문제 발생 사실이므로 error. 기존 유효 인증서로 계속 서비스하며 재시도.
210
+ logger.error("인증서 갱신 실패. 기존 인증서를 유지하고 재시도를 예약합니다.", err);
211
+ this.stop();
212
+ this._renewTimer = setTimeout(() => void this._renew(), RETRY_DELAY_MS);
213
+ }
214
+ }
215
+ }
@@ -4,10 +4,24 @@ import type { V1RequestHandler } from "../legacy/v1-auto-update-handler";
4
4
  export interface ServiceServerOptions {
5
5
  rootPath: string;
6
6
  port: number;
7
- ssl?: {
8
- pfxBytes: Uint8Array;
9
- passphrase: string;
10
- };
7
+ ssl?:
8
+ | {
9
+ pfxBytes: Uint8Array;
10
+ passphrase?: string;
11
+ }
12
+ | {
13
+ pemKeyBytes: Uint8Array;
14
+ certBytes: Uint8Array;
15
+ caBytes?: Uint8Array;
16
+ passphrase?: string;
17
+ }
18
+ | {
19
+ letsencrypt: {
20
+ domains: string[];
21
+ email: string;
22
+ staging?: boolean;
23
+ };
24
+ };
11
25
  auth?:
12
26
  | {
13
27
  jwtSecret: string;