@yolo-croket-dev/core 1.10.0-jun.2 → 1.10.0

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/AGENTS.md ADDED
@@ -0,0 +1,52 @@
1
+ # CORE KNOWLEDGE BASE
2
+
3
+ ## OVERVIEW
4
+
5
+ 요청 context, 예외, interceptor, AMQP helper, Redis provider, graceful shutdown을 노출하는 최고 영향도 공용 패키지다. `AbstractParam`은 Codegraph 기준 1,313개 reference를 가진다.
6
+
7
+ ## STRUCTURE
8
+
9
+ ```text
10
+ core/
11
+ ├── index.ts
12
+ ├── interfaces/ # AbstractParam/Request/Response, exceptions
13
+ ├── exceptions/ # 공용 HTTP/MSA error contract
14
+ ├── functions/ # AMQP call/publish/handler, trace/error helper
15
+ ├── interceptors/ # request/RPC logging and active-call tracking
16
+ ├── modules/ # Redis, graceful shutdown
17
+ └── tests/ # node:test CJS integration-style tests
18
+ ```
19
+
20
+ ## WHERE TO LOOK
21
+
22
+ | 작업 | 위치 | 확인 |
23
+ |---|---|---|
24
+ | Trace/request shape | `interfaces/http.ts` | 모든 서비스 metadata 호환성 |
25
+ | Error contract | `interfaces/msa-exception.ts`, `exceptions` | AMQP ACK/NACK와 consumer 변환 |
26
+ | AMQP behavior | `functions/amqp-*` | timeout, publish/call error handling |
27
+ | Shutdown | `modules/graceful-shutdown` | active interceptor drain, health file |
28
+ | Redis injection | `modules/redis.module.ts` | `REDIS_PROVIDER` contract |
29
+
30
+ ## CONVENTIONS
31
+
32
+ - Public shape 변경 전 package 내부뿐 아니라 `amqp-access`, `amqp-rabbit-mq`, `grpc`, 실제 서비스 import를 검색한다.
33
+ - `AbstractParam.traceId`를 호출 경계에서 보존한다. 새 context 필드는 직렬화와 metadata 전달 경로를 함께 확인한다.
34
+ - Exception은 status/type/message/action/errInfo 변환을 기존 handler와 일치시킨다.
35
+ - Graceful shutdown은 active request/RPC drain과 `/tmp/healthy` lifecycle을 함께 유지한다.
36
+ - Public export는 leaf barrel, category barrel, root `index.ts`를 모두 확인한다.
37
+
38
+ ## ANTI-PATTERNS
39
+
40
+ - 한 소비자 전용 shape나 비즈니스 판단을 core에 올리지 않는다.
41
+ - Trace/error field를 silent fallback으로 숨기거나 하위 호환 근거 없이 rename하지 않는다.
42
+ - AMQP timeout/ACK behavior를 실제 caller/handler 검증 없이 변경하지 않는다.
43
+ - Generated JS/d.ts를 source로 취급하지 않는다.
44
+
45
+ ## VERIFY
46
+
47
+ ```bash
48
+ yarn workspace @yolo-croket-dev/core test
49
+ yarn workspace @yolo-croket-dev/core lint
50
+ ```
51
+
52
+ `test`가 먼저 build한 뒤 `tests/*.test.cjs`를 실행한다. 현재 lint glob은 `src/**/*.ts`지만 source는 package root에 있어 lint 결과가 변경 파일을 실제 포함하는지 별도로 확인한다.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 1.10.0
4
+
5
+ - 비회원 인증 로그 마스킹·검증 오류 보호·재시도 정보 필드를 정식 발행합니다.
6
+ - 기존 1.9.14의 AMQP timeout, ACK/NACK 옵션, RabbitMQ 연결 이름 설정을 보존합니다.
7
+ - 소비 서비스에서 사용하던 1.10.0-jun.2의 gRPC metadata·예외 처리와 포트 파싱을 유지합니다.
8
+
9
+ ## 1.10.0-jun.2-beta.1
10
+
11
+ - 현재 소비 버전 1.10.0-jun.2의 gRPC metadata 및 예외 처리 동작을 유지합니다.
12
+ - 비회원 인증 요청 로그의 민감 값을 마스킹하고 검증 오류에서 입력값을 제외합니다.
13
+ - 인증 실패의 남은 입력 횟수와 재시도 대기시간 필드를 추가합니다.
@@ -152,7 +152,7 @@ let GrpcExceptionFilter = class GrpcExceptionFilter extends microservices_1.Base
152
152
  }
153
153
  }
154
154
  };
155
- GrpcExceptionFilter = __decorate([
155
+ exports.GrpcExceptionFilter = GrpcExceptionFilter;
156
+ exports.GrpcExceptionFilter = GrpcExceptionFilter = __decorate([
156
157
  (0, common_1.Catch)()
157
158
  ], GrpcExceptionFilter);
158
- exports.GrpcExceptionFilter = GrpcExceptionFilter;
@@ -17,11 +17,18 @@ class HttpExceptionCustom extends common_1.HttpException {
17
17
  }
18
18
  }
19
19
  exports.HttpExceptionCustom = HttpExceptionCustom;
20
+ function resolveAmqpTimeout(timeoutMs) {
21
+ if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
22
+ return DEFAULT_TIMEOUT;
23
+ }
24
+ return Math.min(timeoutMs, DEFAULT_TIMEOUT);
25
+ }
20
26
  async function AmqpHandler(client, messagePattern, payload) {
21
- var _a, _b, _c, _d;
27
+ var _a, _b, _c, _d, _e;
22
28
  let data;
29
+ const timeoutMs = resolveAmqpTimeout((_a = payload === null || payload === void 0 ? void 0 : payload.param) === null || _a === void 0 ? void 0 : _a.timeout);
23
30
  try {
24
- data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(DEFAULT_TIMEOUT)));
31
+ data = await (0, rxjs_1.lastValueFrom)(client.send(messagePattern, payload).pipe((0, operators_1.timeout)(timeoutMs)));
25
32
  if (data.isError) {
26
33
  const status = data.status || 500;
27
34
  const subMessage = data.subMessage;
@@ -29,7 +36,7 @@ async function AmqpHandler(client, messagePattern, payload) {
29
36
  * 파일 객체 처리
30
37
  * 다른 서비스에서 이미 처리가 될 수 있음
31
38
  * */
32
- (0, _1.reWriteFileValue)((_a = data === null || data === void 0 ? void 0 : data.arg) === null || _a === void 0 ? void 0 : _a.file);
39
+ (0, _1.reWriteFileValue)((_b = data === null || data === void 0 ? void 0 : data.arg) === null || _b === void 0 ? void 0 : _b.file);
33
40
  const errInfo = data.errInfo ? data.errInfo : {
34
41
  message: data.errMessage,
35
42
  type: data.errType,
@@ -50,7 +57,7 @@ async function AmqpHandler(client, messagePattern, payload) {
50
57
  catch (err) {
51
58
  if (typeof err === 'string' && err.includes('There is no matching message handler defined in the remote service.')) {
52
59
  msaLogger.error('NOT_EXISTS_MESSAGE_PATTERN', {
53
- traceId: ((_b = payload === null || payload === void 0 ? void 0 : payload.param) === null || _b === void 0 ? void 0 : _b.traceId) || '-',
60
+ traceId: ((_c = payload === null || payload === void 0 ? void 0 : payload.param) === null || _c === void 0 ? void 0 : _c.traceId) || '-',
54
61
  errInfo: {
55
62
  errType: 'NOT_EXISTS_MESSAGE_PATTERN',
56
63
  message: `${messagePattern} 해당 메시지 패턴은 존재하지 않습니다.`,
@@ -64,7 +71,7 @@ async function AmqpHandler(client, messagePattern, payload) {
64
71
  }
65
72
  else if (err instanceof rxjs_1.TimeoutError || err.message === DEFAULT_TIMEOUT_MESSAGE) {
66
73
  msaLogger.error('TIMEOUT_ERROR', {
67
- traceId: ((_c = payload === null || payload === void 0 ? void 0 : payload.param) === null || _c === void 0 ? void 0 : _c.traceId) || '-',
74
+ traceId: ((_d = payload === null || payload === void 0 ? void 0 : payload.param) === null || _d === void 0 ? void 0 : _d.traceId) || '-',
68
75
  errInfo: {
69
76
  errType: 'TIMEOUT_ERROR',
70
77
  message: DEFAULT_TIMEOUT_MESSAGE,
@@ -77,7 +84,7 @@ async function AmqpHandler(client, messagePattern, payload) {
77
84
  });
78
85
  }
79
86
  else {
80
- msaLogger.warn('AMQP_HANDLER_ERROR', { traceId: ((_d = payload === null || payload === void 0 ? void 0 : payload.param) === null || _d === void 0 ? void 0 : _d.traceId) || '-' }, { err });
87
+ msaLogger.warn('AMQP_HANDLER_ERROR', { traceId: ((_e = payload === null || payload === void 0 ? void 0 : payload.param) === null || _e === void 0 ? void 0 : _e.traceId) || '-' }, { err });
81
88
  throw err;
82
89
  }
83
90
  }
@@ -1,7 +1,7 @@
1
1
  import { Metadata } from '@grpc/grpc-js';
2
2
  import { AbstractParam } from '../interfaces';
3
3
  export declare const GRPC_PARAM_METADATA_KEY = "param";
4
- export declare type GrpcParamMetadata = AbstractParam;
4
+ export type GrpcParamMetadata = AbstractParam;
5
5
  /**
6
6
  * @author jun
7
7
  * @description gRPC metadata에 AMQP param 호환 컨텍스트를 저장합니다.
@@ -0,0 +1,2 @@
1
+ /** @author jun @description 인증 정보 로그 마스킹 @process 원본을 변경하지 않고 중첩 객체의 민감 값을 가린다. */
2
+ export declare function redactAuthData(value: unknown, seen?: WeakSet<object>): unknown;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactAuthData = void 0;
4
+ const SENSITIVE_KEY = /password|secret|token|authorization|cookie|phone|verificationcode|verificationid|api.?key/i;
5
+ /** @author jun @description 인증 정보 로그 마스킹 @process 원본을 변경하지 않고 중첩 객체의 민감 값을 가린다. */
6
+ function redactAuthData(value, seen = new WeakSet()) {
7
+ if (!value || typeof value !== 'object')
8
+ return value;
9
+ if (seen.has(value))
10
+ return '[Circular]';
11
+ seen.add(value);
12
+ if (Array.isArray(value))
13
+ return value.map(item => redactAuthData(item, seen));
14
+ return Object.fromEntries(Object.entries(value).map(([key, item,]) => [
15
+ key,
16
+ SENSITIVE_KEY.test(key) ? '***MASKED***' : redactAuthData(item, seen),
17
+ ]));
18
+ }
19
+ exports.redactAuthData = redactAuthData;
@@ -39,6 +39,7 @@ const common_1 = require("@nestjs/common");
39
39
  const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
40
40
  const operators_1 = require("rxjs/operators");
41
41
  const dd_trace_1 = __importDefault(require("dd-trace"));
42
+ const redact_auth_data_function_1 = require("../functions/redact-auth-data.function");
42
43
  const grpc_param_metadata_function_1 = require("../functions/grpc-param-metadata.function");
43
44
  const { SERVER_NAME } = process.env;
44
45
  const msaLogger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE);
@@ -130,9 +131,9 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
130
131
  traceId,
131
132
  messageBrokerInfo: {
132
133
  messagePatten: pattern,
133
- payload: JSON.stringify(this.keyCutter(JSON.stringify(payload))),
134
+ payload: JSON.stringify(this.keyCutter(JSON.stringify((0, redact_auth_data_function_1.redactAuthData)(payload)))),
134
135
  },
135
- paramInfo: pastParam,
136
+ paramInfo: (0, redact_auth_data_function_1.redactAuthData)(pastParam),
136
137
  traceInfo: {
137
138
  from: pastFrom,
138
139
  froms: pastFroms,
@@ -159,7 +160,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
159
160
  },
160
161
  messageBrokerInfo: {
161
162
  messagePatten: pattern,
162
- payload: JSON.stringify(this.keyCutter(JSON.stringify(payload))),
163
+ payload: JSON.stringify(this.keyCutter(JSON.stringify((0, redact_auth_data_function_1.redactAuthData)(payload)))),
163
164
  },
164
165
  traceInfo: {
165
166
  from: pastFrom,
@@ -189,7 +190,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
189
190
  },
190
191
  messageBrokerInfo: {
191
192
  messagePatten: pattern,
192
- payload: JSON.stringify(this.keyCutter(JSON.stringify(payload))),
193
+ payload: JSON.stringify(this.keyCutter(JSON.stringify((0, redact_auth_data_function_1.redactAuthData)(payload)))),
193
194
  },
194
195
  traceInfo: {
195
196
  from: pastFrom,
@@ -205,7 +206,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
205
206
  },
206
207
  messageBrokerInfo: {
207
208
  messagePatten: pattern,
208
- payload: JSON.stringify(this.keyCutter(JSON.stringify(payload))),
209
+ payload: JSON.stringify(this.keyCutter(JSON.stringify((0, redact_auth_data_function_1.redactAuthData)(payload)))),
209
210
  },
210
211
  traceInfo: {
211
212
  from: pastFrom,
@@ -219,7 +220,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
219
220
  }));
220
221
  }
221
222
  };
222
- LoggingServiceInterceptor = __decorate([
223
+ exports.LoggingServiceInterceptor = LoggingServiceInterceptor;
224
+ exports.LoggingServiceInterceptor = LoggingServiceInterceptor = __decorate([
223
225
  (0, common_1.Injectable)()
224
226
  ], LoggingServiceInterceptor);
225
- exports.LoggingServiceInterceptor = LoggingServiceInterceptor;
@@ -370,9 +370,9 @@ let RPCInterceptor = class RPCInterceptor {
370
370
  })));
371
371
  }
372
372
  };
373
- RPCInterceptor = __decorate([
373
+ exports.RPCInterceptor = RPCInterceptor;
374
+ exports.RPCInterceptor = RPCInterceptor = __decorate([
374
375
  (0, common_1.Injectable)(),
375
376
  __metadata("design:paramtypes", [core_1.Reflector,
376
377
  graceful_shutdown_service_1.GracefulShutdownService])
377
378
  ], RPCInterceptor);
378
- exports.RPCInterceptor = RPCInterceptor;
@@ -18,4 +18,6 @@ export declare class ErrInfo {
18
18
  message?: string;
19
19
  action?: ErrAction;
20
20
  errOption?: MsaExceptionOption;
21
+ remainingAttempts?: number;
22
+ retryAfterSeconds?: number;
21
23
  }
@@ -18,9 +18,10 @@ var ErrActionTypeEnum;
18
18
  ErrActionTypeEnum["LEGACY_SHOW_TOAST"] = "LEGACY_SHOW_TOAST";
19
19
  ErrActionTypeEnum["FORCE_UPDATE"] = "FORCE_UPDATE";
20
20
  ErrActionTypeEnum["UPDATE_ACCESS_TOKEN"] = "UPDATE_ACCESS_TOKEN";
21
- })(ErrActionTypeEnum = exports.ErrActionTypeEnum || (exports.ErrActionTypeEnum = {}));
21
+ })(ErrActionTypeEnum || (exports.ErrActionTypeEnum = ErrActionTypeEnum = {}));
22
22
  class ErrAction {
23
23
  }
24
+ exports.ErrAction = ErrAction;
24
25
  __decorate([
25
26
  (0, swagger_1.ApiProperty)({
26
27
  description: '액션 타입',
@@ -57,9 +58,9 @@ __decorate([
57
58
  }),
58
59
  __metadata("design:type", String)
59
60
  ], ErrAction.prototype, "webViewUrl", void 0);
60
- exports.ErrAction = ErrAction;
61
61
  class ErrInfo {
62
62
  }
63
+ exports.ErrInfo = ErrInfo;
63
64
  __decorate([
64
65
  (0, swagger_1.ApiProperty)({
65
66
  description: '에러 종류 식별자',
@@ -88,4 +89,17 @@ __decorate([
88
89
  }),
89
90
  __metadata("design:type", Object)
90
91
  ], ErrInfo.prototype, "errOption", void 0);
91
- exports.ErrInfo = ErrInfo;
92
+ __decorate([
93
+ (0, swagger_1.ApiProperty)({
94
+ description: '인증 코드의 남은 입력 횟수',
95
+ required: false,
96
+ }),
97
+ __metadata("design:type", Number)
98
+ ], ErrInfo.prototype, "remainingAttempts", void 0);
99
+ __decorate([
100
+ (0, swagger_1.ApiProperty)({
101
+ description: '다음 요청까지 기다릴 초',
102
+ required: false,
103
+ }),
104
+ __metadata("design:type", Number)
105
+ ], ErrInfo.prototype, "retryAfterSeconds", void 0);
@@ -21,4 +21,4 @@ var LogLevelEnum;
21
21
  LogLevelEnum["DEBUG"] = "debug";
22
22
  LogLevelEnum["WARN"] = "warn";
23
23
  LogLevelEnum["ERROR"] = "error";
24
- })(LogLevelEnum = exports.LogLevelEnum || (exports.LogLevelEnum = {}));
24
+ })(LogLevelEnum || (exports.LogLevelEnum = LogLevelEnum = {}));
@@ -1,6 +1,10 @@
1
1
  import { HttpException } from '@nestjs/common';
2
2
  import { LogLevelEnum } from './log-level.enum';
3
3
  import type { ErrAction } from './exception';
4
+ export declare enum MsaExceptionMessageAckBehaviorEnum {
5
+ ACK = "ACK",
6
+ NACK = "NACK"
7
+ }
4
8
  export interface MsaExceptionOption {
5
9
  /**
6
10
  * 로그 레벨
@@ -8,6 +12,12 @@ export interface MsaExceptionOption {
8
12
  * - 기본값은 ERROR 입니다.
9
13
  * */
10
14
  logErrorLevel?: LogLevelEnum;
15
+ /**
16
+ * 메시지 consumer 실패 시 ACK/NACK override 옵션
17
+ * - 현재는 amqp-rabbit-mq subscribe consumer에서만 해석합니다.
18
+ * - option이 없으면 consumer handler의 기본 errorBehavior를 따릅니다.
19
+ */
20
+ messageAckBehavior?: MsaExceptionMessageAckBehaviorEnum;
11
21
  }
12
22
  /**
13
23
  * @author jun
@@ -27,7 +37,7 @@ export interface MsaException {
27
37
  errOption?: MsaExceptionOption;
28
38
  }
29
39
  /** HttpException과 MsaException을 결합한 타입 */
30
- export declare type MsaHttpException = MsaException & HttpException;
40
+ export type MsaHttpException = MsaException & HttpException;
31
41
  /**
32
42
  * @description MsaException 타입 가드
33
43
  * @param error 검사할 에러 객체
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isMsaException = void 0;
3
+ exports.isMsaException = exports.MsaExceptionMessageAckBehaviorEnum = void 0;
4
4
  const common_1 = require("@nestjs/common");
5
+ var MsaExceptionMessageAckBehaviorEnum;
6
+ (function (MsaExceptionMessageAckBehaviorEnum) {
7
+ MsaExceptionMessageAckBehaviorEnum["ACK"] = "ACK";
8
+ MsaExceptionMessageAckBehaviorEnum["NACK"] = "NACK";
9
+ })(MsaExceptionMessageAckBehaviorEnum || (exports.MsaExceptionMessageAckBehaviorEnum = MsaExceptionMessageAckBehaviorEnum = {}));
5
10
  /**
6
11
  * @description MsaException 타입 가드
7
12
  * @param error 검사할 에러 객체
@@ -48,7 +48,8 @@ let GracefulShutdownModule = class GracefulShutdownModule {
48
48
  msaLogger.info('GracefulShutdownService.onModuleDestroy: all interceptors finished, shutting down', { traceId: 'ON_MODULE_DESTROY' });
49
49
  }
50
50
  };
51
- GracefulShutdownModule = __decorate([
51
+ exports.GracefulShutdownModule = GracefulShutdownModule;
52
+ exports.GracefulShutdownModule = GracefulShutdownModule = __decorate([
52
53
  (0, common_1.Global)(),
53
54
  (0, common_1.Module)({
54
55
  providers: [graceful_shutdown_service_1.GracefulShutdownService],
@@ -56,4 +57,3 @@ GracefulShutdownModule = __decorate([
56
57
  }),
57
58
  __metadata("design:paramtypes", [graceful_shutdown_service_1.GracefulShutdownService])
58
59
  ], GracefulShutdownModule);
59
- exports.GracefulShutdownModule = GracefulShutdownModule;
@@ -1,10 +1,16 @@
1
- import { RmqOptions, ServerRMQ } from '@nestjs/microservices';
2
- import { Channel } from 'amqp-connection-manager';
3
- import { ConsumeMessage } from 'amqplib';
4
- export declare type GracefulRmqOptions = RmqOptions['options'] & {
1
+ import { ServerRMQ } from '@nestjs/microservices';
2
+ import type { RmqOptions } from '@nestjs/microservices';
3
+ import type { Channel } from 'amqp-connection-manager';
4
+ import type { ConsumeMessage } from 'amqplib';
5
+ export type GracefulRmqOptions = RmqOptions['options'] & {
5
6
  waitingEndingHandlersTimeoutMs?: number;
6
7
  waitingEndingHandlersIntervalMs?: number;
7
8
  };
9
+ /**
10
+ * @author jun
11
+ * @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
12
+ * @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
13
+ */
8
14
  export declare class GracefulServerRMQ extends ServerRMQ {
9
15
  protected readonly options: GracefulRmqOptions;
10
16
  protected runningMessages: number;
@@ -1,18 +1,67 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.GracefulServerRMQ = void 0;
4
7
  const microservices_1 = require("@nestjs/microservices");
8
+ const os_1 = __importDefault(require("os"));
5
9
  const node_util_1 = require("node:util");
6
10
  const sleep = (0, node_util_1.promisify)(setTimeout);
7
11
  const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS = 5000;
8
12
  const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS = 500;
13
+ const RMQ_UNKNOWN_CONNECTION_IDENTITY = 'unknown';
14
+ function createRmqConnectionName(queue) {
15
+ const environment = process.env.ENV || process.env.NODE_ENV || RMQ_UNKNOWN_CONNECTION_IDENTITY;
16
+ const owner = process.env.RMQ_OWNER || process.env.USER || process.env.USERNAME || RMQ_UNKNOWN_CONNECTION_IDENTITY;
17
+ return [
18
+ queue || RMQ_UNKNOWN_CONNECTION_IDENTITY,
19
+ environment,
20
+ owner,
21
+ os_1.default.hostname(),
22
+ `pid-${process.pid}`,
23
+ ].join('/');
24
+ }
25
+ function addRmqConnectionName(options) {
26
+ var _a;
27
+ const connectionOptions = (_a = options.socketOptions) === null || _a === void 0 ? void 0 : _a.connectionOptions;
28
+ const configuredClientProperties = connectionOptions === null || connectionOptions === void 0 ? void 0 : connectionOptions.clientProperties;
29
+ const clientProperties = configuredClientProperties
30
+ && typeof configuredClientProperties === 'object'
31
+ && !Array.isArray(configuredClientProperties)
32
+ ? configuredClientProperties
33
+ : {};
34
+ const configuredConnectionName = clientProperties.connection_name;
35
+ const connectionName = typeof configuredConnectionName === 'string' && configuredConnectionName.trim()
36
+ ? configuredConnectionName
37
+ : createRmqConnectionName(options.queue);
38
+ return {
39
+ ...options,
40
+ socketOptions: {
41
+ ...options.socketOptions,
42
+ connectionOptions: {
43
+ ...connectionOptions,
44
+ clientProperties: {
45
+ ...clientProperties,
46
+ connection_name: connectionName,
47
+ },
48
+ },
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * @author jun
54
+ * @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
55
+ * @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
56
+ */
9
57
  class GracefulServerRMQ extends microservices_1.ServerRMQ {
10
58
  constructor(options) {
11
- super(options);
12
- this.options = options;
59
+ const optionsWithConnectionName = addRmqConnectionName(options);
60
+ super(optionsWithConnectionName);
13
61
  this.runningMessages = 0;
14
62
  this.closing = false;
15
63
  this.consumerTag = null;
64
+ this.options = optionsWithConnectionName;
16
65
  this.waitingEndingHandlersTimeoutMs = this.getOptionsProp(this.options, 'waitingEndingHandlersTimeoutMs')
17
66
  || RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS;
18
67
  this.waitingEndingHandlersIntervalMs = this.getOptionsProp(this.options, 'waitingEndingHandlersIntervalMs')
@@ -133,7 +133,7 @@ let GracefulShutdownService = class GracefulShutdownService {
133
133
  });
134
134
  }
135
135
  };
136
- GracefulShutdownService = __decorate([
136
+ exports.GracefulShutdownService = GracefulShutdownService;
137
+ exports.GracefulShutdownService = GracefulShutdownService = __decorate([
137
138
  (0, common_1.Injectable)()
138
139
  ], GracefulShutdownService);
139
- exports.GracefulShutdownService = GracefulShutdownService;
@@ -15,10 +15,10 @@ const redisProvider = {
15
15
  };
16
16
  let RedisModule = class RedisModule {
17
17
  };
18
- RedisModule = __decorate([
18
+ exports.RedisModule = RedisModule;
19
+ exports.RedisModule = RedisModule = __decorate([
19
20
  (0, common_1.Module)({
20
21
  providers: [redisProvider],
21
22
  exports: [redisProvider],
22
23
  })
23
24
  ], RedisModule);
24
- exports.RedisModule = RedisModule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolo-croket-dev/core",
3
- "version": "1.10.0-jun.2",
3
+ "version": "1.10.0",
4
4
  "author": "Yolo Co., Ltd.",
5
5
  "description": "Yolo commonization module",
6
6
  "main": "index.js",
@@ -10,14 +10,15 @@
10
10
  "fix": "eslint --fix src/**/*.ts",
11
11
  "clean": "tsc --build --clean",
12
12
  "prepublishOnly": "npm run build",
13
- "postpublish": "npm run clean"
13
+ "postpublish": "npm run clean",
14
+ "test": "yarn build && node --test tests/*.test.cjs"
14
15
  },
15
16
  "license": "UNLICENSED",
16
17
  "peerDependencies": {
18
+ "@grpc/grpc-js": "^1.14.3",
17
19
  "@nestjs/common": "^11.0.0",
18
20
  "@nestjs/microservices": "^11.0.0",
19
21
  "@nestjs/swagger": "^11.0.0",
20
- "@grpc/grpc-js": "^1.14.3",
21
22
  "class-transformer": "^0.5.1",
22
23
  "class-validator": "^0.14.0",
23
24
  "dd-trace": "^5.0.0"
@@ -32,10 +33,11 @@
32
33
  "uuid": "^8.3.2"
33
34
  },
34
35
  "devDependencies": {
36
+ "@grpc/grpc-js": "^1.14.3",
35
37
  "@nestjs/common": "^11.1.8",
36
- "@nestjs/microservices": "^11.1.8",
38
+ "@nestjs/core": "^11.1.8",
39
+ "@nestjs/microservices": "11.0.13",
37
40
  "@nestjs/swagger": "^11.2.1",
38
- "@grpc/grpc-js": "^1.14.3",
39
41
  "@types/node": "^24.9.1",
40
42
  "@types/randomstring": "^1.1.8",
41
43
  "@types/uuid": "^8.3.4"
@@ -24,15 +24,20 @@ let ValidationPipe = class ValidationPipe {
24
24
  return value;
25
25
  }
26
26
  const object = (0, class_transformer_1.plainToInstance)(this.dto, value.payload);
27
- const errors = await (0, class_validator_1.validate)(object);
27
+ const errors = await (0, class_validator_1.validate)(object, {
28
+ validationError: {
29
+ target: false,
30
+ value: false,
31
+ },
32
+ });
28
33
  if (errors.length > 0) {
29
34
  throw new exceptions_1.BadRequest('VALIDATION_ERROR', JSON.stringify(errors));
30
35
  }
31
36
  return value;
32
37
  }
33
38
  };
34
- ValidationPipe = __decorate([
39
+ exports.ValidationPipe = ValidationPipe;
40
+ exports.ValidationPipe = ValidationPipe = __decorate([
35
41
  (0, common_1.Injectable)(),
36
42
  __metadata("design:paramtypes", [Object])
37
43
  ], ValidationPipe);
38
- exports.ValidationPipe = ValidationPipe;
@@ -0,0 +1,47 @@
1
+ const assert = require('node:assert/strict');
2
+ const test = require('node:test');
3
+ const { Observable } = require('rxjs');
4
+
5
+ const { AmqpHandler, HttpExceptionCustom } = require('../functions/amqp-handler.function.js');
6
+
7
+ test('param.timeout이 지나면 AMQP Observable 구독을 해제한다', async () => {
8
+ let isUnsubscribed = false;
9
+ const client = {
10
+ send() {
11
+ return new Observable(() => () => {
12
+ isUnsubscribed = true;
13
+ });
14
+ },
15
+ };
16
+
17
+ await assert.rejects(
18
+ AmqpHandler(client, 'test.timeout', {
19
+ payload: {},
20
+ param: {
21
+ traceId: 'trace-id',
22
+ timeout: 5,
23
+ },
24
+ }),
25
+ (error) => {
26
+ assert.equal(error instanceof HttpExceptionCustom, true);
27
+ assert.equal(error.errInfo.type, 'TIMEOUT_ERROR');
28
+ return true;
29
+ },
30
+ );
31
+
32
+ assert.equal(isUnsubscribed, true);
33
+ });
34
+
35
+ test('응답이 먼저 완료되면 기존 AMQP 응답을 반환한다', async () => {
36
+ const client = {
37
+ send() {
38
+ return new Observable((subscriber) => {
39
+ subscriber.next({ isError: false, data: { ok: true } });
40
+ subscriber.complete();
41
+ });
42
+ },
43
+ };
44
+
45
+ const result = await AmqpHandler(client, 'test.success', { payload: {}, param: { traceId: 'trace-id', timeout: 50 } });
46
+ assert.deepEqual(result, { data: { ok: true } });
47
+ });
@@ -0,0 +1,102 @@
1
+ const assert = require('node:assert/strict');
2
+ const os = require('node:os');
3
+ const test = require('node:test');
4
+
5
+ const {
6
+ GracefulServerRMQ,
7
+ } = require('../modules/graceful-shutdown/graceful-shutdown.rmq.js');
8
+
9
+ const CONNECTION_NAME_ENV_KEYS = [
10
+ 'ENV',
11
+ 'NODE_ENV',
12
+ 'RMQ_OWNER',
13
+ 'USER',
14
+ 'USERNAME',
15
+ ];
16
+
17
+ function getClientProperties(options) {
18
+ const server = new GracefulServerRMQ(options);
19
+ return server.options.socketOptions.connectionOptions.clientProperties;
20
+ }
21
+
22
+ function runWithEnvironment(environment, callback) {
23
+ const originalEnvironment = Object.fromEntries(
24
+ CONNECTION_NAME_ENV_KEYS.map((key) => [key, process.env[key]]),
25
+ );
26
+ try {
27
+ for (const key of CONNECTION_NAME_ENV_KEYS) {
28
+ delete process.env[key];
29
+ }
30
+ Object.assign(process.env, environment);
31
+ callback();
32
+ } finally {
33
+ for (const key of CONNECTION_NAME_ENV_KEYS) {
34
+ const originalValue = originalEnvironment[key];
35
+ if (originalValue === undefined) {
36
+ delete process.env[key];
37
+ } else {
38
+ process.env[key] = originalValue;
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ test('자동 connection_name에 큐와 실행 주체 정보를 포함한다', () => {
45
+ runWithEnvironment({ ENV: 'release', RMQ_OWNER: 'jun' }, () => {
46
+ const actualClientProperties = getClientProperties({ queue: 'STORE_ITEM_SERVER' });
47
+ const expectedConnectionName = `STORE_ITEM_SERVER/release/jun/${os.hostname()}/pid-${process.pid}`;
48
+ assert.equal(actualClientProperties.connection_name, expectedConnectionName);
49
+ });
50
+ });
51
+
52
+ test('기존 connectionOptions와 clientProperties를 보존한다', () => {
53
+ runWithEnvironment({ NODE_ENV: 'production', USER: 'deploy' }, () => {
54
+ const inputOptions = {
55
+ queue: 'USER_SERVICE',
56
+ socketOptions: {
57
+ noDelay: true,
58
+ connectionOptions: {
59
+ locale: 'ko_KR',
60
+ clientProperties: {
61
+ product: 'croket',
62
+ },
63
+ },
64
+ },
65
+ };
66
+ const server = new GracefulServerRMQ(inputOptions);
67
+ assert.equal(server.options.socketOptions.noDelay, true);
68
+ assert.equal(server.options.socketOptions.connectionOptions.locale, 'ko_KR');
69
+ assert.equal(server.options.socketOptions.connectionOptions.clientProperties.product, 'croket');
70
+ });
71
+ });
72
+
73
+ test('명시적으로 설정한 문자열 connection_name을 보존한다', () => {
74
+ const actualClientProperties = getClientProperties({
75
+ queue: 'USER_SERVICE',
76
+ socketOptions: {
77
+ connectionOptions: {
78
+ clientProperties: {
79
+ connection_name: 'custom-connection-name',
80
+ },
81
+ },
82
+ },
83
+ });
84
+ assert.equal(actualClientProperties.connection_name, 'custom-connection-name');
85
+ });
86
+
87
+ test('문자열이 아닌 connection_name은 자동 생성값으로 대체한다', () => {
88
+ runWithEnvironment({ ENV: 'release', RMQ_OWNER: 'jun' }, () => {
89
+ const actualClientProperties = getClientProperties({
90
+ queue: 'STORE_ITEM_SERVER',
91
+ socketOptions: {
92
+ connectionOptions: {
93
+ clientProperties: {
94
+ connection_name: 123,
95
+ },
96
+ },
97
+ },
98
+ });
99
+ const expectedConnectionName = `STORE_ITEM_SERVER/release/jun/${os.hostname()}/pid-${process.pid}`;
100
+ assert.equal(actualClientProperties.connection_name, expectedConnectionName);
101
+ });
102
+ });
@@ -0,0 +1,17 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { redactAuthData } = require('../functions/redact-auth-data.function');
4
+
5
+ test('인증 요청·응답과 중첩 메타데이터를 원본 변경 없이 마스킹한다', () => {
6
+ const input = { payload: { verificationCode: '123456', phoneNumber: '01012345678', recaptchaToken: 'captcha' },
7
+ result: [{ access_token: 'jwt', refresh_token: 'refresh' }], param: { authorization: 'Bearer jwt', traceId: 'trace' } };
8
+ const output = redactAuthData(input);
9
+ assert.equal(output.payload.verificationCode, '***MASKED***');
10
+ assert.equal(output.payload.phoneNumber, '***MASKED***');
11
+ assert.equal(output.payload.recaptchaToken, '***MASKED***');
12
+ assert.equal(output.result[0].access_token, '***MASKED***');
13
+ assert.equal(output.result[0].refresh_token, '***MASKED***');
14
+ assert.equal(output.param.authorization, '***MASKED***');
15
+ assert.equal(output.param.traceId, 'trace');
16
+ assert.equal(input.payload.verificationCode, '123456');
17
+ });
@@ -0,0 +1,17 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { IsString } = require('class-validator');
4
+ const { ValidationPipe } = require('../pipes/rpc-validation.pipe');
5
+
6
+ test('검증 실패 예외는 다른 필드의 OTP·CAPTCHA와 target/value를 포함하지 않는다', async () => {
7
+ class Payload {}
8
+ IsString()(Payload.prototype, 'verificationId');
9
+ const pipe = new ValidationPipe(Payload);
10
+ await assert.rejects(pipe.transform({ payload: { verificationId: 123, verificationCode: '123456', recaptchaToken: 'captcha-secret' } }, { metatype: Object }), e => {
11
+ assert.ok(!e.subMessage.includes('123456'));
12
+ assert.ok(!e.subMessage.includes('captcha-secret'));
13
+ assert.ok(!e.subMessage.includes('target'));
14
+ assert.ok(!e.subMessage.includes('value'));
15
+ return true;
16
+ });
17
+ });
@@ -1,17 +0,0 @@
1
- declare global {
2
- var Promise: PromiseConstructor;
3
- }
4
- export declare class EnhancedPromisePatch {
5
- private static isPatched;
6
- private static originalPromise;
7
- private static originalSetTimeout;
8
- static patch(): void;
9
- private static patchPromise;
10
- private static patchSetTimeout;
11
- static unpatch(): void;
12
- /**
13
- * 현재 패치 상태 확인
14
- * @returns 패치 여부
15
- */
16
- static getPatchStatus(): boolean;
17
- }
@@ -1,115 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EnhancedPromisePatch = void 0;
4
- const abort_1 = require("./abort");
5
- class EnhancedPromisePatch {
6
- static patch() {
7
- if (this.isPatched) {
8
- return;
9
- }
10
- this.patchPromise();
11
- this.patchSetTimeout();
12
- this.isPatched = true;
13
- }
14
- static patchPromise() {
15
- this.originalPromise = global.Promise;
16
- // Promise 생성자 패치
17
- global.Promise = new Proxy(global.Promise, {
18
- construct(target, args) {
19
- const abortSignal = (0, abort_1.getCurrentAbortSignal)();
20
- if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
21
- return Promise.reject(new Error('REQUEST_ABORTED: Promise가 중단되었습니다'));
22
- }
23
- // target은 Promise 생성자이므로, new 연산자와 함께 사용해야 하며, args는 [executor] 형태의 튜플임을 명시합니다.
24
- const promise = Reflect.construct(target, args);
25
- if (!abortSignal) {
26
- return promise;
27
- }
28
- // Promise 체인에 중단 확인 추가
29
- return promise.then((result) => {
30
- if (abortSignal.aborted) {
31
- throw new Error('REQUEST_ABORTED: Promise 실행 중 중단되었습니다');
32
- }
33
- return result;
34
- }, (error) => {
35
- if (abortSignal.aborted) {
36
- throw new Error('REQUEST_ABORTED: Promise 실행 중 중단되었습니다');
37
- }
38
- throw error;
39
- });
40
- },
41
- });
42
- // Promise.resolve 패치
43
- const originalResolve = global.Promise.resolve;
44
- global.Promise.resolve = (value) => {
45
- const abortSignal = (0, abort_1.getCurrentAbortSignal)();
46
- if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
47
- return Promise.reject(new Error('REQUEST_ABORTED: Promise.resolve가 중단되었습니다'));
48
- }
49
- const promise = originalResolve.call(global.Promise, value);
50
- if (!abortSignal) {
51
- return promise;
52
- }
53
- return promise.then((result) => {
54
- if (abortSignal.aborted) {
55
- throw new Error('REQUEST_ABORTED: Promise.resolve 실행 중 중단되었습니다');
56
- }
57
- return result;
58
- }, (error) => {
59
- if (abortSignal.aborted) {
60
- throw new Error('REQUEST_ABORTED: Promise.resolve 실행 중 중단되었습니다');
61
- }
62
- throw error;
63
- });
64
- };
65
- // Promise.reject 패치
66
- const originalReject = global.Promise.reject;
67
- global.Promise.reject = (reason) => {
68
- const abortSignal = (0, abort_1.getCurrentAbortSignal)();
69
- if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
70
- return Promise.reject(new Error('REQUEST_ABORTED: Promise.reject가 중단되었습니다'));
71
- }
72
- return originalReject.call(global.Promise, reason);
73
- };
74
- }
75
- static patchSetTimeout() {
76
- this.originalSetTimeout = global.setTimeout;
77
- // eslint-disable-next-line @typescript-eslint/ban-types
78
- global.setTimeout = (callback, delay, ...args) => {
79
- const abortSignal = (0, abort_1.getCurrentAbortSignal)();
80
- if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
81
- throw new Error('REQUEST_ABORTED: setTimeout이 중단되었습니다');
82
- }
83
- const timerId = this.originalSetTimeout(() => {
84
- if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
85
- throw new Error('REQUEST_ABORTED: setTimeout 콜백이 중단되었습니다');
86
- }
87
- callback(...args);
88
- }, delay);
89
- if (abortSignal) {
90
- const abortHandler = () => {
91
- clearTimeout(timerId);
92
- };
93
- abortSignal.addEventListener('abort', abortHandler, { once: true });
94
- }
95
- return timerId;
96
- };
97
- }
98
- static unpatch() {
99
- if (!this.isPatched) {
100
- return;
101
- }
102
- global.Promise = this.originalPromise;
103
- global.setTimeout = this.originalSetTimeout;
104
- this.isPatched = false;
105
- }
106
- /**
107
- * 현재 패치 상태 확인
108
- * @returns 패치 여부
109
- */
110
- static getPatchStatus() {
111
- return this.isPatched;
112
- }
113
- }
114
- exports.EnhancedPromisePatch = EnhancedPromisePatch;
115
- EnhancedPromisePatch.isPatched = false;