@yolo-croket-dev/core 1.10.0-jun.2 → 1.10.0-jun.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +7 -0
- package/functions/redact-auth-data.function.d.ts +2 -0
- package/functions/redact-auth-data.function.js +19 -0
- package/interceptors/logging-service.interceptor.js +6 -5
- package/interfaces/exception.d.ts +2 -0
- package/interfaces/exception.js +14 -0
- package/package.json +7 -5
- package/pipes/rpc-validation.pipe.js +6 -1
- package/tests/redact-auth-data.test.cjs +17 -0
- package/tests/rpc-validation-secrets.test.cjs +17 -0
- package/functions/abort/enhanced-promise-patch.d.ts +0 -17
- package/functions/abort/enhanced-promise-patch.js +0 -115
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,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,
|
package/interfaces/exception.js
CHANGED
|
@@ -88,4 +88,18 @@ __decorate([
|
|
|
88
88
|
}),
|
|
89
89
|
__metadata("design:type", Object)
|
|
90
90
|
], ErrInfo.prototype, "errOption", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
(0, swagger_1.ApiProperty)({
|
|
93
|
+
description: '인증 코드의 남은 입력 횟수',
|
|
94
|
+
required: false,
|
|
95
|
+
}),
|
|
96
|
+
__metadata("design:type", Number)
|
|
97
|
+
], ErrInfo.prototype, "remainingAttempts", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
(0, swagger_1.ApiProperty)({
|
|
100
|
+
description: '다음 요청까지 기다릴 초',
|
|
101
|
+
required: false,
|
|
102
|
+
}),
|
|
103
|
+
__metadata("design:type", Number)
|
|
104
|
+
], ErrInfo.prototype, "retryAfterSeconds", void 0);
|
|
91
105
|
exports.ErrInfo = ErrInfo;
|
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-jun.2-beta.1",
|
|
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/
|
|
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,7 +24,12 @@ 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
|
}
|
|
@@ -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;
|