fix(responses): sync query API schemas and add runtime response validation#154
Merged
Palbahngmiyine merged 10 commits intosolapi:betafrom Apr 17, 2026
Merged
Conversation
- getBalance 응답에 lowBalanceAlert, minimumCash, rechargeTo, deposit 등 실서버에 존재하는 필드를 반영해 스키마 확장 - getStatistics, getGroups, getGroup, getBlacks, getBlockGroups, getKakaoChannel, getKakaoAlimtalkTemplate(s) 응답의 드리프트(nullable, 누락 필드, Record→Array 등) 정합화 - MessageTypeRecord에 rcs_itpl/ltpl, fax, voice, bms_* 신규 메시지 타입 추가 - 조회 응답 전용 storedMessageSchema 신설해 발송용 messageSchema와 분리 - DefaultService.requestEffect/getWithQuery에 responseSchema 주입 경로 마련하고, decodeServerResponse 헬퍼 추가로 서버 응답을 런타임 검증해 드리프트 즉시 감지 - 15개 조회 서비스 메서드에 해당 스키마 연결 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
첫 페이지 호출 시 서버가 startKey를 null 또는 미포함으로 반환하는데 getKakaoChannelsResponseSchema와 getKakaoAlimtalkTemplatesResponseSchema의 startKey가 required string으로 남아 있어 런타임 검증이 실패하는 회귀를 수정. 다른 list 응답들과 동일하게 NullishOr로 정렬. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 전용 ResponseSchemaMismatchError 도입 — 2xx에서 발생하는 스키마 불일치를 ServerError(200)으로 래핑하던 문제를 분리해 5xx 재시도/알림 분기 오염 방지 - validationErrors(ArrayFormatter) 및 responseBody 보존으로 운영 환경에서도 드리프트 경로 재현 가능 - decodeServerResponse에 onExcessProperty:'preserve' 적용 — 부분 스키마가 서버의 미선언 필드를 strip 해버려 소비자 응답에서 silent data loss가 발생하던 문제 수정 - storedMessageSchema의 autoTypeDetect/replacement/voiceReplied/ unavailableSenderNumber를 Schema.transform으로 boolean 정규화 - responseSchema 제네릭을 Schema.Schema<R, I, never>로 타이트닝 — 요구사항 채널이 오염되지 않도록 강제 - decodeServerResponse 8개 유닛 테스트 추가 (fixture 기반 회귀 방어) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- booleanOrZeroOne을 Schema.transformOrFail로 엄격화해 2/-1/NaN 같은 drift 입력을 silent true로 coerce하지 않고 ResponseSchemaMismatchError로 전파 - stringifyResponseBody가 circular/BigInt로 JSON 실패 시 undefined로 정보 소실하던 동작을 '[unserializable: reason] toStringTag' 메타데이터로 보완 - ResponseSchemaMismatchError.toString()의 production 경로에서 url과 validationErrors를 유지(responseBody만 민감 페이로드라 계속 숨김) - 엣지 케이스 유닛 테스트 추가: boolean transform 거부값, 순환참조 responseBody Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ure fields Codex 2차 리뷰 발견 사항 반영: - countForChargeSchema에 rcs_itpl/ltpl, fax, voice, bms_* 13개 필드 추가 — messageTypeRecordSchema 확장과 비대칭하여 신규 메시지 타입을 가진 그룹 조회가 ResponseSchemaMismatchError로 실패하던 문제 수정 (getGroups/getGroup 회귀 방어) - storedMessageSchema의 unavailableSenderNumber/faxPageCount/voiceDuration/ voiceReplied를 NullishOr로 완화 — non-FAX/non-VOICE 메시지 행에서 null로 내려오는 경우 전체 응답 decode 실패하던 문제 수정 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Error Codex 3차 리뷰 P2: production에서 ResponseSchemaMismatchError의 responseBody 필드에 원본 응답이 유지되어, toString() 가드와 무관하게 Sentry 등 에러 리포터가 enumerable 필드를 직렬화하면서 PII가 누출될 수 있었음. ServerError와 동일하게 creation 단계에서 production이면 undefined로 제외. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion silent-failure-hunter 4차 후속: - ParseResult.ArrayFormatter/TreeFormatter는 실패 메시지에 원본 값(전화번호 등)을 그대로 문자열로 삽입하므로, production에서는 validationErrors 엔트리를 `path: [_tag]` 형태로 축약하고 message는 issue 개수 요약으로 대체 - getMessages 등 조회 API는 to/from 등을 query string에 실으므로 production에서 url의 query 부분을 '?[redacted]'로 마스킹 - 테스트 fixture를 확장해 실제 전화번호 문자열이 message/validationErrors/url 어디에도 포함되지 않음을 명시적으로 검증 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
silent-failure-hunter 5차: - NODE_ENV 가드를 safe-by-default로 반전 — staging/unset 등 비정형 환경에서도 redact를 기본 적용하고, development/test만 verbose 모드 - redactUrlForProduction이 URL fragment(#...)를 먼저 strip 하도록 보완해 fragment 내부 쿼리 문자열 또한 redact 경로에 포함 - ResponseSchemaMismatchError.toString()에서 이중 가드 제거 — creation 시점에 이미 필드가 redact된 상태라 단순히 보유 값만 렌더링 - 테스트 시나리오를 'production' → 'staging'으로 교체해 새로운 가드 범위 검증 Codex 4차에서 지적된 BadRequestError/ClientError/ServerError/NetworkError의 동일 PII 누출 패턴은 request/transport 계층 전반의 정책 변경을 요구하므로 이 PR 범위에서 분리 — 별도 후속 작업으로 처리 권장 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
silent-failure-hunter/Codex 5차: - NODE_ENV gate를 .trim().toLowerCase()로 정규화해 Windows PowerShell 등에서 'Development' 같은 변형도 verbose 모드로 인식 - redactUrlForProduction을 URL 객체 기반으로 전환해 query string뿐 아니라 fragment/userinfo(user:password@)까지 함께 redact - ResponseSchemaMismatchError.toString()에 defense-in-depth 가드 복원 — 클래스가 public이라 외부에서 직접 생성될 경우에도 production 경로에서 responseBody가 유출되지 않도록 한다 - storedMessageSchema의 옵션 객체(kakao/rcs/naver/fax/voice)와 배열 필드 (replacements/log/queues)를 Record/Array of Unknown으로 강화해 최소한의 구조 계약(object or array)이 타입 수준에서 유지되도록 함 - 테스트: NODE_ENV를 table-driven으로 확장해 production/staging/undefined/빈 문자열 모두 redact 되고 development/test만 verbose 되는 계약, 대소문자/공백 정규화 동작, userinfo/fragment redact 동작까지 커버 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ypeRecord Codex 최종 P2: statusCode 값 타입이 generic Record<string, number>로 바뀌면서 statusCode[code].sms/lms 같은 타입-narrowed 접근이 깨지던 회귀 복원. 서버가 실제 일부 필드만 내려주는 sparse 응답임을 고려해, messageTypeRecordSchema를 Schema.partial로 감싼 partialMessageTypeRecordSchema를 신설하고 dayPeriod.statusCode value에 사용. 타입 정보는 유지하면서도 한두 필드만 있는 응답도 통과한다. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements runtime schema validation for API responses using Effect Schema, introducing a new ResponseSchemaMismatchError and a decodeServerResponse utility with PII redaction capabilities. Numerous models and services were updated to enforce these schemas, and a new storedMessageSchema was added to handle server-side type drift. Feedback was provided regarding a potential PII leak in the URL redaction fallback logic for malformed URLs containing user information.
Comment on lines
+113
to
+116
| // 파싱 불가한 상대/비정상 URL은 보수적으로 첫 구분자 이후 전부 마스킹 | ||
| const cut = url.search(/[?#;]/); | ||
| return cut === -1 ? url : `${url.slice(0, cut)}?[redacted]`; | ||
| } |
There was a problem hiding this comment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /cash/v1/balance확장 포함)를 실제 호출로 확인하고 15개 조회 API의 Effect Schema를 정합화messageSchema와 분리된 조회 응답 전용storedMessageSchema신설DefaultService.requestEffect/getWithQuery에 optionalresponseSchema주입 경로 마련,decodeServerResponse헬퍼로 스키마 불일치 시 전용ResponseSchemaMismatchError발생 (production PII redact 포함)주요 수정 (응답 스키마)
lowBalanceAlert,minimumCash,rechargeTo,rechargeTryCount,autoRecharge,accountId,deposit,balanceOnly9개 필드 추가deposit/monthlyDepositAvg, BMS·fax·voice·rcs_itpl/ltpl 메시지 타입,statusCodesparse 허용 (partialMessageTypeRecordSchema)storedMessageSchema사용,nextKey/startKeyoptional, boolean 필드 0/1 수용 + 정규화scheduledDate/dateSent/dateCompleted,sdkVersion/osPlatformnullableblackListRecord → ArraynamenullablephoneNumberoptionalaccountIdnullable, liststartKeyNullishOr런타임 검증 도입
DefaultService.requestEffect/getWithQuery에responseSchema?: Schema.Schema<R, I, never>주입 경로decodeServerResponse헬퍼:Schema.decodeUnknown에onExcessProperty: 'preserve'옵션 적용 — 서버가 내려준 미선언 필드가 strip 되어 소비자 응답에서 조용히 사라지는 silent data loss 방지ResponseSchemaMismatchError: 2xx 응답의 스키마 불일치를 5xxServerError와 분리 (재시도/알림 분기 오염 방지)validationErrors(ArrayFormatter) 및responseBody보존으로 운영 환경 재현 가능shouldRedactSensitive()safe-by-default gate:NODE_ENV를.trim().toLowerCase()정규화 후development/test외 환경(production, staging, undefined 등)에서는 PII(message/validationErrors/url/responseBody)를 redactnew URL()기반으로 query + fragment + userinfo(user:password@)까지 마스킹booleanOrZeroOnetransform: 서버의 boolean|0|1 혼재를 boolean으로 정규화, 0/1 외의 숫자(NaN, 2, -1 등)는 drift 신호로 취급해 fail-loud기존 타입이 실제 서버 응답과 달라
as R캐스팅으로 숨겨지던 타입 거짓말을 바로잡는 과정에서 public surface가 좁혀짐:GetMessagesResponse.messageList[id]/GroupMessages아이템 타입이MessageSchema→StoredMessage로 변경.kakaoOptions등 중첩 옵션은 서버 정규화 포맷(발송 요청과 다름)이어서Record<string, unknown>으로 선언. 기존 nested 필드 접근은 별도 상세 스키마가 정의되기 전까지는 캐스팅 또는 런타임 파싱이 필요.GetBlacksResponse.blackList타입이Record<string, Black>→Array<Black>로 변경 (실제 서버 응답 형태).GetKakaoChannelsFinalizeResponse.startKey/nextKey타입이string | null | undefined로 확장.KakaoChannel.phoneNumber가 optional.groupSchema.sdkVersion/osPlatformnullable,blockGroupSchema.namenullable,getBlockGroups·getGroups·getGroup의 날짜 필드 nullable.ResponseSchemaMismatchError가 새 error class로 분리 —instanceof ServerError && errorCode === 'ResponseSchemaMismatch'분기는 더 이상 매치되지 않음.Follow-up (본 PR 범위 밖)
BadRequestError, transport 측ClientError/ServerError/NetworkError/DefaultError도 동일한 PII redact 정책 적용 (요구 시 별도 PR)storedMessageSchema의kakaoOptions/rcsOptions등을 실제 서버 정규화 포맷 기반 상세 스키마로 확장 (요구 시 별도 PR)Test plan
pnpm lint(biome)pnpm test— 301 tests (신설 24건: decodeServerResponse unit test)pnpm build(tsup CJS/ESM/DTS)Related
🤖 Generated with Claude Code