@rscc/common-core 0.2.0 → 0.4.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/README.md +166 -25
- package/dist/index.cjs +158 -10
- package/dist/index.d.cts +174 -32
- package/dist/index.d.ts +174 -32
- package/dist/index.js +152 -10
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -75,6 +75,79 @@ declare const ResultCode: {
|
|
|
75
75
|
/** ResultCode 코드 값의 유니온 타입 ("200" | "400" | ...). */
|
|
76
76
|
type ResultCode = (typeof ResultCode)[keyof typeof ResultCode];
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* CSRF double-submit 클라이언트 헬퍼 — contracts/session-auth.md §4·§5 (수동 동기화).
|
|
80
|
+
*
|
|
81
|
+
* 쿠키 운반 인증 모드(`jwt-cookie` / `session`)에서 서버는 CSRF 토큰을 HttpOnly 가 아닌 쿠키
|
|
82
|
+
* (`XSRF-TOKEN`)로 발급하고, 클라이언트는 **비안전 메서드** 요청마다 그 값을 헤더(`X-XSRF-TOKEN`)에
|
|
83
|
+
* 되돌려 보낸다. 서버는 쿠키 값과 헤더 값의 동등성만 확인한다.
|
|
84
|
+
*
|
|
85
|
+
* 부착 규칙 (골든 벡터 XC-01~08):
|
|
86
|
+
* - 비안전 메서드(GET·HEAD·OPTIONS·TRACE 외)일 때만.
|
|
87
|
+
* - CSRF 쿠키가 있고 값이 비어 있지 않을 때만 (없으면 부착 안 함 — 오류 없음).
|
|
88
|
+
* - 요청 URL 이 **같은 출처**(상대 URL 포함)이거나 명시한 `allowedOrigins` 에 속할 때만 —
|
|
89
|
+
* 교차 출처로는 보내지 않는다(토큰 유출 방지).
|
|
90
|
+
* - `document` 가 없는 환경(SSR·Node)에서는 `readCookie` 주입이 없으면 아무것도 붙이지 않는다.
|
|
91
|
+
*
|
|
92
|
+
* SSR 안전: 모듈 로드 시점에 `document`/`location` 에 접근하지 않는다 (호출 시점에만 존재 확인).
|
|
93
|
+
*/
|
|
94
|
+
/** CSRF 토큰 쿠키 기본 이름 (contracts/session-auth.md §2.1). */
|
|
95
|
+
declare const DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
|
|
96
|
+
/** CSRF 요청 헤더 기본 이름 — Spring `CookieCsrfTokenRepository` 관례 (contracts/session-auth.md §2.1). */
|
|
97
|
+
declare const DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
|
|
98
|
+
/** CSRF 헤더 자동 부착 옵션 (apiClient `csrf` / useSse `csrf`). */
|
|
99
|
+
interface CsrfOptions {
|
|
100
|
+
/** CSRF 토큰 쿠키 이름. 기본 {@link DEFAULT_CSRF_COOKIE_NAME} (`"XSRF-TOKEN"`) — 서버 설정과 일치시킬 것. */
|
|
101
|
+
cookieName?: string;
|
|
102
|
+
/** CSRF 요청 헤더 이름. 기본 {@link DEFAULT_CSRF_HEADER_NAME} (`"X-XSRF-TOKEN"`). */
|
|
103
|
+
headerName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* 같은 출처 외에 헤더를 보내도 되는 **오리진** 목록 (예: `["https://api.example.com"]`) —
|
|
106
|
+
* 같은 사이트 서브도메인 토폴로지(`app.example.com` → `api.example.com`, contracts §8)용.
|
|
107
|
+
* 항목은 `scheme://host[:port]` 로 정규화해 비교한다(경로·끝 슬래시·대소문자·기본 포트 무관).
|
|
108
|
+
* 와일드카드는 지원하지 않는다. 기본: 없음 (같은 출처만).
|
|
109
|
+
*/
|
|
110
|
+
allowedOrigins?: readonly string[];
|
|
111
|
+
/**
|
|
112
|
+
* **원시 쿠키 문자열**(`"a=1; XSRF-TOKEN=abc"` 형태) 공급자 — `document.cookie` 대신 쓴다
|
|
113
|
+
* (React Native·테스트 등 `document` 가 없는 환경). null/undefined 반환은 쿠키 없음.
|
|
114
|
+
* 기본: `document` 가 있으면 `document.cookie`, 없으면 쿠키 없음.
|
|
115
|
+
*/
|
|
116
|
+
readCookie?: () => string | null | undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 원시 쿠키 문자열에서 `name` 쿠키 값을 읽는다 (contracts/session-auth.md §2.3).
|
|
120
|
+
*
|
|
121
|
+
* - `;` 로 나누고 각 조각의 앞뒤 공백을 제거한 뒤 **첫 `=`** 기준으로 이름·값을 가른다.
|
|
122
|
+
* - 이름은 **대소문자 구분 정확 일치**, 같은 이름이 여럿이면 **첫 값**(XC-08).
|
|
123
|
+
* - 값은 디코딩하지 않는다(`decodeURIComponent` 미적용). 빈 값(`name=`)은 `""` 를 반환한다.
|
|
124
|
+
* - `cookieString` 생략 시 `document` 가 있으면 `document.cookie` 를 쓰고, 없으면(SSR·Node) null.
|
|
125
|
+
* `document.cookie` 접근 자체가 실패하는 환경(샌드박스 iframe 등)도 null.
|
|
126
|
+
*
|
|
127
|
+
* @returns 쿠키 값, 해당 쿠키가 없으면 null.
|
|
128
|
+
*/
|
|
129
|
+
declare function readCookie(name: string, cookieString?: string): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* 비안전 메서드 여부 — GET·HEAD·OPTIONS·TRACE 외 전부 true (대소문자 무관).
|
|
132
|
+
* `method` 생략(또는 빈 문자열)은 fetch 기본값 GET 으로 보아 false.
|
|
133
|
+
*/
|
|
134
|
+
declare function isUnsafeMethod(method?: string): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* 요청 하나에 붙일 CSRF 헤더를 계산한다 — 붙여야 하면 `[헤더명, 쿠키값]`, 아니면 null.
|
|
137
|
+
*
|
|
138
|
+
* 판정 순서: 비안전 메서드? → CSRF 쿠키 있음(빈 값 제외)? → 같은 출처(상대 URL, 또는 `location` 이
|
|
139
|
+
* 있을 때 그 오리진과 같은 절대 URL) 또는 `allowedOrigins` 소속? → 부착 (골든 벡터 XC-01~05·07·08).
|
|
140
|
+
* 호출자가 이미 같은 헤더를 실었는지(XC-06)는 호출 측(apiClient·useSse)이 확인한다.
|
|
141
|
+
*
|
|
142
|
+
* 오류를 던지지 않는다 — `document`·`readCookie` 가 모두 없으면(SSR) null. URL 을 해석할 수 없으면
|
|
143
|
+
* 교차 출처로 간주해 null (토큰 유출 방지 쪽으로 실패).
|
|
144
|
+
*
|
|
145
|
+
* @param url 요청 URL (상대/절대). 상대 URL 은 같은 출처로 본다.
|
|
146
|
+
* @param method HTTP 메서드. 생략 시 GET.
|
|
147
|
+
* @param options `true` 또는 생략 = 기본값, 객체 = {@link CsrfOptions}.
|
|
148
|
+
*/
|
|
149
|
+
declare function csrfHeaderFor(url: string, method: string | undefined, options?: true | CsrfOptions): [name: string, value: string] | null;
|
|
150
|
+
|
|
78
151
|
/**
|
|
79
152
|
* 아웃바운드 회복탄력성 — 지수 백오프 + full jitter 재시도. 런타임 의존성 0(native 만).
|
|
80
153
|
*
|
|
@@ -229,8 +302,34 @@ interface ApiClientConfig {
|
|
|
229
302
|
* 상대 baseUrl("/api")도 지원.
|
|
230
303
|
*/
|
|
231
304
|
baseUrl: string;
|
|
232
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* 토큰 공급자 (`bearer` 모드). 지정 시 반환값이 truthy 면 `Authorization: Bearer <token>` 부착
|
|
307
|
+
* (호출자가 Authorization 을 이미 실었으면 덮어쓰지 않음). 쿠키 모드(`jwt-cookie`/`session`)의
|
|
308
|
+
* 웹 클라이언트는 쓰지 않는다 — `credentials`·`csrf` 참조.
|
|
309
|
+
*/
|
|
233
310
|
getToken?: () => string | null | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* fetch `credentials` 기본값 — 쿠키 운반 인증 모드(`jwt-cookie`/`session`, contracts/session-auth.md §5)용.
|
|
313
|
+
* 같은 출처 API 는 `"same-origin"`(fetch 기본값과 같음), 서브도메인 등 교차 출처 API 는 `"include"`.
|
|
314
|
+
*
|
|
315
|
+
* - 미지정 = fetch 에 `credentials` 속성 자체를 넘기지 않는다 (현행 동작 — fetch 기본값 `"same-origin"`).
|
|
316
|
+
* - 호출별 `init.credentials` 가 있으면 그 값이 우선한다.
|
|
317
|
+
*/
|
|
318
|
+
credentials?: RequestCredentials;
|
|
319
|
+
/**
|
|
320
|
+
* CSRF double-submit 헤더 자동 부착 (contracts/session-auth.md §4·§5, 골든 벡터 XC-01~08).
|
|
321
|
+
* `true` = 기본값(쿠키 `XSRF-TOKEN` → 헤더 `X-XSRF-TOKEN`, 같은 출처만), 객체 = {@link CsrfOptions}
|
|
322
|
+
* (쿠키·헤더 이름, 서브도메인 API 용 `allowedOrigins`, `document` 없는 환경용 `readCookie`).
|
|
323
|
+
* 미지정/false = 부착 안 함 (현행 동작).
|
|
324
|
+
*
|
|
325
|
+
* - **비안전 메서드**(GET·HEAD·OPTIONS·TRACE 외)이고 CSRF 쿠키가 있으며 요청 URL(baseUrl 조인 후)이
|
|
326
|
+
* 같은 출처(상대 URL 포함)이거나 `allowedOrigins` 에 속할 때만 붙인다 — 교차 출처로는 보내지 않는다.
|
|
327
|
+
* - 호출자가 이미 같은 이름의 헤더를 실었으면 덮어쓰지 않는다 (호출자 우선, XC-06).
|
|
328
|
+
* - 쿠키는 **시도마다 다시 읽는다** — 재시도 사이에 서버가 토큰을 회전해도 새 값을 싣는다.
|
|
329
|
+
* - `document` 가 없는 환경(SSR·Node)에서는 `readCookie` 주입이 없으면 아무것도 붙이지 않는다(오류 없음).
|
|
330
|
+
* - `getToken`(bearer)과 독립적으로 동작한다.
|
|
331
|
+
*/
|
|
332
|
+
csrf?: boolean | CsrfOptions;
|
|
234
333
|
/** 401 수신 시 throw 직전에 호출되는 콜백 (로그아웃/리다이렉트 등 소비자 정책 주입). */
|
|
235
334
|
onUnauthorized?: (error: ApiError) => void;
|
|
236
335
|
/** fetch 구현체 주입 (테스트용). 기본 globalThis.fetch. */
|
|
@@ -315,14 +414,22 @@ interface ApiClient {
|
|
|
315
414
|
*
|
|
316
415
|
* 동작 (contracts/common-response.schema.json, trace.md):
|
|
317
416
|
* - 매 요청에 traceId 를 생성해 traceIdHeader(기본 X-Trace-Id)로 부착한다.
|
|
417
|
+
* - `Content-Type: application/json` 은 호출자가 Content-Type 을 지정하지 않았고 body 가
|
|
418
|
+
* **문자열**일 때만 기본 부착한다 (JSON.stringify 관용). body 없는 요청(GET 등)엔 붙이지
|
|
419
|
+
* 않아 교차 출처 GET 의 CORS preflight 를 유발하지 않으며, URLSearchParams/Blob/FormData/
|
|
420
|
+
* ArrayBuffer/ReadableStream 등은 fetch 기본값에 맡긴다. 문자열이지만 JSON 이 아닌 본문은
|
|
421
|
+
* Content-Type 을 직접 지정할 것.
|
|
318
422
|
* - 응답이 CommonResponse 봉투면 success 검사 후 data 를 언랩한다
|
|
319
423
|
* (data 부재 시 undefined). 비봉투 JSON 은 그대로 반환.
|
|
320
424
|
* - 실패 봉투 / HTTP 에러는 code·message·traceId 를 담은 ApiError 를 throw.
|
|
321
425
|
* - 401 은 throw 직전에 onUnauthorized 콜백을 호출한다.
|
|
426
|
+
* - 인증 운반 (contracts/session-auth.md): `bearer` = `getToken` 으로 Authorization 헤더,
|
|
427
|
+
* `jwt-cookie`/`session` = `credentials` + `csrf` (쿠키는 브라우저가 싣고 CSRF 헤더만 자동 부착).
|
|
322
428
|
* - retry 지정 시 요청을 core retry() 로 감싼다 ({@link ApiClientRetryOptions} —
|
|
323
429
|
* 멱등 메서드 기본, Retry-After 하한, 시도 간 동일 traceId).
|
|
324
430
|
*
|
|
325
|
-
* 저장소 접근·경로·이벤트명 하드코딩 없음 — 전부 config
|
|
431
|
+
* 저장소 접근·경로·이벤트명 하드코딩 없음 — 전부 config 주입 (opt-in `csrf` 의 CSRF 쿠키 읽기만 예외 —
|
|
432
|
+
* 기본 `document.cookie`, `readCookie` 로 교체 가능).
|
|
326
433
|
*/
|
|
327
434
|
declare function createApiClient(config: ApiClientConfig): ApiClient;
|
|
328
435
|
|
|
@@ -575,9 +682,13 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
|
|
|
575
682
|
* 경량 서킷 브레이커. 런타임 의존성 0 (native 만).
|
|
576
683
|
*
|
|
577
684
|
* Java `com.rscc.common.util.CircuitBreaker` / Python `rscc_common.circuit_breaker` 와
|
|
578
|
-
* 시맨틱 동일 (3언어 공유 벡터 CB-01~
|
|
685
|
+
* 시맨틱 동일 (3언어 공유 벡터 CB-01~13 — 각 언어 테스트에 수동 동기화. Python 은
|
|
579
686
|
* 시간 단위가 초 — `초 × 1000 == ms`).
|
|
580
687
|
*
|
|
688
|
+
* - **권장 사용법 — `execute(fn)`**: 게이트(allowRequest) → 실행 → 결과 보고를 한 번에
|
|
689
|
+
* 처리해 보고를 **정확히 1회** 보장한다. 차단 시 {@link CircuitOpenError} 로 reject
|
|
690
|
+
* (fn 미호출), 취소(`name === "AbortError"`)는 성공·실패 어느 쪽에도 집계하지 않는다
|
|
691
|
+
* (onIgnore — CB-12). 수동 게이트(allowRequest + 보고)는 execute 로 감쌀 수 없는 경우에만.
|
|
581
692
|
* - **연속(consecutive) 실패 카운트**: `onFailure()` 가 failureThreshold 에 도달(`>=`)하면
|
|
582
693
|
* OPEN. `onSuccess()` 는 카운터를 0 으로 리셋한다 (슬라이딩 윈도 비채택 — 단순성).
|
|
583
694
|
* - **lazy 전이**: OPEN → HALF_OPEN 전이는 `allowRequest()` 호출 시점에 판정한다
|
|
@@ -586,38 +697,51 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
|
|
|
586
697
|
* - **HALF_OPEN 프로브 1개**: 프로브가 진행 중이면 나머지 `allowRequest()` 는 false.
|
|
587
698
|
* 프로브 성공 → CLOSED(카운터 0), 프로브 실패 → OPEN 재진입(openedAt 갱신 —
|
|
588
699
|
* 전체 openDurationMs 재대기).
|
|
589
|
-
* - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
* 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만
|
|
700
|
+
* - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()`/
|
|
701
|
+
* `onIgnore()` 중 하나를 **정확히 1회** 호출한다 (수동 게이트라면 try/catch 로 보고 보장 —
|
|
702
|
+
* 성공·실패로 판정할 수 없는 취소 등은 `onIgnore()` 로 그랜트만 반납). OPEN 대기 중 늦게
|
|
703
|
+
* 도착한 보고는 상태·openedAt·카운터를 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만
|
|
704
|
+
* 소진한다.
|
|
593
705
|
* - **스테일 그랜트 격리**: CLOSED 시절 그랜트가 아직 보고되지 않은 동안에는
|
|
594
706
|
* openDurationMs 가 경과해도 HALF_OPEN 전이(프로브)가 보류된다 — 스테일 보고가 프로브
|
|
595
|
-
* 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는
|
|
707
|
+
* 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는 (아래 용서 이후의 매우 늦은
|
|
708
|
+
* 보고를 제외하면) 항상 프로브의 것이다 (CB-09).
|
|
709
|
+
* - **유실 그랜트 용서 (2× 규칙)**: 보고가 끝내 오지 않는 그랜트(보고 누락·유실)가 위 보류를
|
|
710
|
+
* 무기한 지속시켜 서킷이 OPEN 에 영구 고착되는 것을 막는다 — OPEN 진입 후
|
|
711
|
+
* `2 × openDurationMs` 경과 시점에도 미보고 그랜트가 남아 있으면 이를 유실로 간주해 0 으로
|
|
712
|
+
* 용서하고 정상 HALF_OPEN 전이 + 프로브 획득을 수행한다 (CB-10). 용서 뒤 매우 늦은 스테일
|
|
713
|
+
* 보고가 HALF_OPEN 중 도착하면 프로브 결과로 취급되지만 무해하다 — 스테일 성공은 하류가
|
|
714
|
+
* 실제로 응답했다는 증거이고, 스테일 실패는 보수적인 OPEN 재진입일 뿐이다 (스테일
|
|
715
|
+
* onIgnore 는 프로브 슬롯을 조기 해제해 프로브가 1개 더 허가될 뿐 — 최초 보고가 판정).
|
|
716
|
+
* - **유실 프로브 용서**: HALF_OPEN 프로브가 openDurationMs(`>=`) 동안 보고되지 않으면 유실로
|
|
717
|
+
* 간주해 다음 allowRequest() 에 새 프로브를 넘긴다 — 보고 누락 프로브 1개로 HALF_OPEN 에 영구
|
|
718
|
+
* 고착되는 것을 막는다 (CB-13). 옛 프로브의 늦은 보고는 새 프로브 결과로 취급되며 위와 같은
|
|
719
|
+
* 이유로 무해하다.
|
|
720
|
+
*
|
|
721
|
+
* @example 권장 — execute 로 게이트·보고를 위임한다.
|
|
722
|
+
* ```ts
|
|
723
|
+
* const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
|
|
724
|
+
* const user = await cb.execute(() => api.request<User>("/users/1")); // OPEN 이면 CircuitOpenError
|
|
725
|
+
* ```
|
|
596
726
|
*
|
|
597
|
-
* @example 서킷 + 재시도 조합 —
|
|
598
|
-
*
|
|
727
|
+
* @example 서킷 + 재시도 조합 — 시도마다 execute 로 게이트·보고. OPEN 차단
|
|
728
|
+
* ({@link CircuitOpenError})은 shouldRetry 에서 비재시도로 걸러 즉시 실패시킨다(빠른 실패 유지).
|
|
599
729
|
* ```ts
|
|
600
730
|
* const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* try {
|
|
606
|
-
* const r = await call();
|
|
607
|
-
* cb.onSuccess();
|
|
608
|
-
* return r;
|
|
609
|
-
* } catch (error) {
|
|
610
|
-
* cb.onFailure();
|
|
611
|
-
* throw error;
|
|
612
|
-
* }
|
|
613
|
-
* },
|
|
614
|
-
* {
|
|
615
|
-
* shouldRetry: (error) => error !== circuitOpen, // OPEN 차단은 즉시 실패
|
|
616
|
-
* onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
|
|
617
|
-
* }
|
|
618
|
-
* );
|
|
731
|
+
* await retry(() => cb.execute(() => call()), {
|
|
732
|
+
* shouldRetry: (error) => !(error instanceof CircuitOpenError), // OPEN 차단은 즉시 실패
|
|
733
|
+
* onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
|
|
734
|
+
* });
|
|
619
735
|
* ```
|
|
620
736
|
*/
|
|
737
|
+
/**
|
|
738
|
+
* 서킷 차단 거부 오류 — `execute()` 가 allowRequest()==false(OPEN 대기·프로브 보류·HALF_OPEN
|
|
739
|
+
* 프로브 경합)일 때 reject 하며, 이때 작업 함수는 **호출되지 않는다** (CB-12).
|
|
740
|
+
* Java `CircuitOpenException` / Python `CircuitOpenError` 패리티. 재시도 조합에서 비재시도 신호로 쓴다.
|
|
741
|
+
*/
|
|
742
|
+
declare class CircuitOpenError extends Error {
|
|
743
|
+
constructor(message?: string);
|
|
744
|
+
}
|
|
621
745
|
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
622
746
|
interface CircuitBreakerOptions {
|
|
623
747
|
/** OPEN 전이 임계 — 연속 실패가 이 값에 도달(`>=`)하면 OPEN. 필수 양의 정수 (아니면 RangeError). */
|
|
@@ -631,14 +755,32 @@ interface CircuitBreaker {
|
|
|
631
755
|
/**
|
|
632
756
|
* 요청 통과 여부 — CLOSED 는 true, OPEN 대기 중 false, openDurationMs 경과 시
|
|
633
757
|
* HALF_OPEN 전이 + 프로브 슬롯 획득(true) — 단 CLOSED 시절 미보고 그랜트가 남아 있으면
|
|
634
|
-
* 전이를 보류하고 false (CB-09)
|
|
635
|
-
*
|
|
758
|
+
* 전이를 보류하고 false (CB-09), 보류는 OPEN 진입 후 `2 × openDurationMs` 까지만 —
|
|
759
|
+
* 그 시점엔 미보고 그랜트를 유실로 용서하고 전이 + 프로브 획득(true) (CB-10).
|
|
760
|
+
* HALF_OPEN 프로브 경합 시 나머지 false — 단 프로브가 openDurationMs 경과(`>=`)까지
|
|
761
|
+
* 미보고면 유실로 간주해 이 호출에 새 프로브를 넘긴다(true) (CB-13).
|
|
762
|
+
* true 를 받은 호출만 결과를 onSuccess()/onFailure()/onIgnore() 중 하나로 보고해야 한다.
|
|
636
763
|
*/
|
|
637
764
|
allowRequest(): boolean;
|
|
638
765
|
/** 성공 보고 — CLOSED 카운터 리셋, HALF_OPEN 프로브 성공 시 CLOSED 복귀. OPEN 중엔 무시. */
|
|
639
766
|
onSuccess(): void;
|
|
640
767
|
/** 실패 보고 — CLOSED 연속 카운트 증가(임계 도달 시 OPEN), HALF_OPEN 프로브 실패 시 OPEN 재진입. OPEN 중엔 무시. */
|
|
641
768
|
onFailure(): void;
|
|
769
|
+
/**
|
|
770
|
+
* 결과 없는 그랜트 반납 — 취소(abort) 등 성공·실패로 판정할 수 없는 호출이 보고 대신
|
|
771
|
+
* 호출한다 (CB-11). CLOSED: 미보고 그랜트만 반납(연속 실패 카운터 무변화). HALF_OPEN:
|
|
772
|
+
* 프로브 슬롯 해제 — HALF_OPEN 유지, 다음 allowRequest() 가 새 프로브를 획득한다.
|
|
773
|
+
* OPEN: 미보고 그랜트 잔량만 소진(그 외 무변화).
|
|
774
|
+
*/
|
|
775
|
+
onIgnore(): void;
|
|
776
|
+
/**
|
|
777
|
+
* fn 래핑 (권장 사용법) — allowRequest() 게이트 → 실행 → 결과 보고를 **정확히 1회** 보장한다.
|
|
778
|
+
* 거부는 reject(CircuitOpenError) — 동기 throw 아님, 거부 시 fn 은 호출되지 않는다.
|
|
779
|
+
* resolve → onSuccess() 후 값 반환. throw/reject 는 `name === "AbortError"` 면 onIgnore(),
|
|
780
|
+
* 그 외(TimeoutError 포함)는 onFailure() 후 **같은 값을 래핑 없이** 다시 throw 한다 (CB-12).
|
|
781
|
+
* fn 의 동기 throw 도 동일하게 처리된다.
|
|
782
|
+
*/
|
|
783
|
+
execute<T>(fn: () => T | PromiseLike<T>): Promise<T>;
|
|
642
784
|
/** 현재 상태 조회 — lazy 전이 특성상 openDurationMs 경과 후에도 allowRequest() 전엔 OPEN 으로 보고된다. */
|
|
643
785
|
state(): CircuitState;
|
|
644
786
|
}
|
|
@@ -807,7 +949,7 @@ declare function generateIdempotencyKey(): string;
|
|
|
807
949
|
* 시맨틱(입력→판정)은 3언어 동일하며 반환 형태만 다르다."
|
|
808
950
|
*
|
|
809
951
|
* Node 전용 API(`timingSafeEqual` 등) 미사용 — 브라우저/Node 공용
|
|
810
|
-
* (`globalThis.crypto.subtle`, Node
|
|
952
|
+
* (`globalThis.crypto.subtle`, Node 20+ / 모던 브라우저).
|
|
811
953
|
*/
|
|
812
954
|
/** 웹훅 서명 헤더명 기본값 (contracts/webhook-signature.md). */
|
|
813
955
|
declare const WEBHOOK_SIGNATURE_HEADER = "X-Rscc-Signature";
|
|
@@ -1307,4 +1449,4 @@ declare function composeHangul(s: string): string;
|
|
|
1307
1449
|
*/
|
|
1308
1450
|
declare function matchesHangul(query: string, target: string): boolean;
|
|
1309
1451
|
|
|
1310
|
-
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, type CircuitState, type CommonResponse, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type JosaPair, type ListQueryOptions, type PageResponse, type PhoneType, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createTokenBucket, createTtlCache, decodeJwtPayload, decomposeHangul, formatPhoneNumber, generateIdempotencyKey, getTokenExpiry, isBulkResult, isChosungQuery, isForeignerRrn, isRetryableStatus, isTokenExpired, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseFlag, parseRetryAfterMs, parseSseFrame, parseWireDateTime, pickJosa, readSseStream, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
|
1452
|
+
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type JosaPair, type ListQueryOptions, type PageResponse, type PhoneType, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatPhoneNumber, generateIdempotencyKey, getTokenExpiry, isBulkResult, isChosungQuery, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseFlag, parseRetryAfterMs, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseStream, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,79 @@ declare const ResultCode: {
|
|
|
75
75
|
/** ResultCode 코드 값의 유니온 타입 ("200" | "400" | ...). */
|
|
76
76
|
type ResultCode = (typeof ResultCode)[keyof typeof ResultCode];
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* CSRF double-submit 클라이언트 헬퍼 — contracts/session-auth.md §4·§5 (수동 동기화).
|
|
80
|
+
*
|
|
81
|
+
* 쿠키 운반 인증 모드(`jwt-cookie` / `session`)에서 서버는 CSRF 토큰을 HttpOnly 가 아닌 쿠키
|
|
82
|
+
* (`XSRF-TOKEN`)로 발급하고, 클라이언트는 **비안전 메서드** 요청마다 그 값을 헤더(`X-XSRF-TOKEN`)에
|
|
83
|
+
* 되돌려 보낸다. 서버는 쿠키 값과 헤더 값의 동등성만 확인한다.
|
|
84
|
+
*
|
|
85
|
+
* 부착 규칙 (골든 벡터 XC-01~08):
|
|
86
|
+
* - 비안전 메서드(GET·HEAD·OPTIONS·TRACE 외)일 때만.
|
|
87
|
+
* - CSRF 쿠키가 있고 값이 비어 있지 않을 때만 (없으면 부착 안 함 — 오류 없음).
|
|
88
|
+
* - 요청 URL 이 **같은 출처**(상대 URL 포함)이거나 명시한 `allowedOrigins` 에 속할 때만 —
|
|
89
|
+
* 교차 출처로는 보내지 않는다(토큰 유출 방지).
|
|
90
|
+
* - `document` 가 없는 환경(SSR·Node)에서는 `readCookie` 주입이 없으면 아무것도 붙이지 않는다.
|
|
91
|
+
*
|
|
92
|
+
* SSR 안전: 모듈 로드 시점에 `document`/`location` 에 접근하지 않는다 (호출 시점에만 존재 확인).
|
|
93
|
+
*/
|
|
94
|
+
/** CSRF 토큰 쿠키 기본 이름 (contracts/session-auth.md §2.1). */
|
|
95
|
+
declare const DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
|
|
96
|
+
/** CSRF 요청 헤더 기본 이름 — Spring `CookieCsrfTokenRepository` 관례 (contracts/session-auth.md §2.1). */
|
|
97
|
+
declare const DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
|
|
98
|
+
/** CSRF 헤더 자동 부착 옵션 (apiClient `csrf` / useSse `csrf`). */
|
|
99
|
+
interface CsrfOptions {
|
|
100
|
+
/** CSRF 토큰 쿠키 이름. 기본 {@link DEFAULT_CSRF_COOKIE_NAME} (`"XSRF-TOKEN"`) — 서버 설정과 일치시킬 것. */
|
|
101
|
+
cookieName?: string;
|
|
102
|
+
/** CSRF 요청 헤더 이름. 기본 {@link DEFAULT_CSRF_HEADER_NAME} (`"X-XSRF-TOKEN"`). */
|
|
103
|
+
headerName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* 같은 출처 외에 헤더를 보내도 되는 **오리진** 목록 (예: `["https://api.example.com"]`) —
|
|
106
|
+
* 같은 사이트 서브도메인 토폴로지(`app.example.com` → `api.example.com`, contracts §8)용.
|
|
107
|
+
* 항목은 `scheme://host[:port]` 로 정규화해 비교한다(경로·끝 슬래시·대소문자·기본 포트 무관).
|
|
108
|
+
* 와일드카드는 지원하지 않는다. 기본: 없음 (같은 출처만).
|
|
109
|
+
*/
|
|
110
|
+
allowedOrigins?: readonly string[];
|
|
111
|
+
/**
|
|
112
|
+
* **원시 쿠키 문자열**(`"a=1; XSRF-TOKEN=abc"` 형태) 공급자 — `document.cookie` 대신 쓴다
|
|
113
|
+
* (React Native·테스트 등 `document` 가 없는 환경). null/undefined 반환은 쿠키 없음.
|
|
114
|
+
* 기본: `document` 가 있으면 `document.cookie`, 없으면 쿠키 없음.
|
|
115
|
+
*/
|
|
116
|
+
readCookie?: () => string | null | undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 원시 쿠키 문자열에서 `name` 쿠키 값을 읽는다 (contracts/session-auth.md §2.3).
|
|
120
|
+
*
|
|
121
|
+
* - `;` 로 나누고 각 조각의 앞뒤 공백을 제거한 뒤 **첫 `=`** 기준으로 이름·값을 가른다.
|
|
122
|
+
* - 이름은 **대소문자 구분 정확 일치**, 같은 이름이 여럿이면 **첫 값**(XC-08).
|
|
123
|
+
* - 값은 디코딩하지 않는다(`decodeURIComponent` 미적용). 빈 값(`name=`)은 `""` 를 반환한다.
|
|
124
|
+
* - `cookieString` 생략 시 `document` 가 있으면 `document.cookie` 를 쓰고, 없으면(SSR·Node) null.
|
|
125
|
+
* `document.cookie` 접근 자체가 실패하는 환경(샌드박스 iframe 등)도 null.
|
|
126
|
+
*
|
|
127
|
+
* @returns 쿠키 값, 해당 쿠키가 없으면 null.
|
|
128
|
+
*/
|
|
129
|
+
declare function readCookie(name: string, cookieString?: string): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* 비안전 메서드 여부 — GET·HEAD·OPTIONS·TRACE 외 전부 true (대소문자 무관).
|
|
132
|
+
* `method` 생략(또는 빈 문자열)은 fetch 기본값 GET 으로 보아 false.
|
|
133
|
+
*/
|
|
134
|
+
declare function isUnsafeMethod(method?: string): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* 요청 하나에 붙일 CSRF 헤더를 계산한다 — 붙여야 하면 `[헤더명, 쿠키값]`, 아니면 null.
|
|
137
|
+
*
|
|
138
|
+
* 판정 순서: 비안전 메서드? → CSRF 쿠키 있음(빈 값 제외)? → 같은 출처(상대 URL, 또는 `location` 이
|
|
139
|
+
* 있을 때 그 오리진과 같은 절대 URL) 또는 `allowedOrigins` 소속? → 부착 (골든 벡터 XC-01~05·07·08).
|
|
140
|
+
* 호출자가 이미 같은 헤더를 실었는지(XC-06)는 호출 측(apiClient·useSse)이 확인한다.
|
|
141
|
+
*
|
|
142
|
+
* 오류를 던지지 않는다 — `document`·`readCookie` 가 모두 없으면(SSR) null. URL 을 해석할 수 없으면
|
|
143
|
+
* 교차 출처로 간주해 null (토큰 유출 방지 쪽으로 실패).
|
|
144
|
+
*
|
|
145
|
+
* @param url 요청 URL (상대/절대). 상대 URL 은 같은 출처로 본다.
|
|
146
|
+
* @param method HTTP 메서드. 생략 시 GET.
|
|
147
|
+
* @param options `true` 또는 생략 = 기본값, 객체 = {@link CsrfOptions}.
|
|
148
|
+
*/
|
|
149
|
+
declare function csrfHeaderFor(url: string, method: string | undefined, options?: true | CsrfOptions): [name: string, value: string] | null;
|
|
150
|
+
|
|
78
151
|
/**
|
|
79
152
|
* 아웃바운드 회복탄력성 — 지수 백오프 + full jitter 재시도. 런타임 의존성 0(native 만).
|
|
80
153
|
*
|
|
@@ -229,8 +302,34 @@ interface ApiClientConfig {
|
|
|
229
302
|
* 상대 baseUrl("/api")도 지원.
|
|
230
303
|
*/
|
|
231
304
|
baseUrl: string;
|
|
232
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* 토큰 공급자 (`bearer` 모드). 지정 시 반환값이 truthy 면 `Authorization: Bearer <token>` 부착
|
|
307
|
+
* (호출자가 Authorization 을 이미 실었으면 덮어쓰지 않음). 쿠키 모드(`jwt-cookie`/`session`)의
|
|
308
|
+
* 웹 클라이언트는 쓰지 않는다 — `credentials`·`csrf` 참조.
|
|
309
|
+
*/
|
|
233
310
|
getToken?: () => string | null | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* fetch `credentials` 기본값 — 쿠키 운반 인증 모드(`jwt-cookie`/`session`, contracts/session-auth.md §5)용.
|
|
313
|
+
* 같은 출처 API 는 `"same-origin"`(fetch 기본값과 같음), 서브도메인 등 교차 출처 API 는 `"include"`.
|
|
314
|
+
*
|
|
315
|
+
* - 미지정 = fetch 에 `credentials` 속성 자체를 넘기지 않는다 (현행 동작 — fetch 기본값 `"same-origin"`).
|
|
316
|
+
* - 호출별 `init.credentials` 가 있으면 그 값이 우선한다.
|
|
317
|
+
*/
|
|
318
|
+
credentials?: RequestCredentials;
|
|
319
|
+
/**
|
|
320
|
+
* CSRF double-submit 헤더 자동 부착 (contracts/session-auth.md §4·§5, 골든 벡터 XC-01~08).
|
|
321
|
+
* `true` = 기본값(쿠키 `XSRF-TOKEN` → 헤더 `X-XSRF-TOKEN`, 같은 출처만), 객체 = {@link CsrfOptions}
|
|
322
|
+
* (쿠키·헤더 이름, 서브도메인 API 용 `allowedOrigins`, `document` 없는 환경용 `readCookie`).
|
|
323
|
+
* 미지정/false = 부착 안 함 (현행 동작).
|
|
324
|
+
*
|
|
325
|
+
* - **비안전 메서드**(GET·HEAD·OPTIONS·TRACE 외)이고 CSRF 쿠키가 있으며 요청 URL(baseUrl 조인 후)이
|
|
326
|
+
* 같은 출처(상대 URL 포함)이거나 `allowedOrigins` 에 속할 때만 붙인다 — 교차 출처로는 보내지 않는다.
|
|
327
|
+
* - 호출자가 이미 같은 이름의 헤더를 실었으면 덮어쓰지 않는다 (호출자 우선, XC-06).
|
|
328
|
+
* - 쿠키는 **시도마다 다시 읽는다** — 재시도 사이에 서버가 토큰을 회전해도 새 값을 싣는다.
|
|
329
|
+
* - `document` 가 없는 환경(SSR·Node)에서는 `readCookie` 주입이 없으면 아무것도 붙이지 않는다(오류 없음).
|
|
330
|
+
* - `getToken`(bearer)과 독립적으로 동작한다.
|
|
331
|
+
*/
|
|
332
|
+
csrf?: boolean | CsrfOptions;
|
|
234
333
|
/** 401 수신 시 throw 직전에 호출되는 콜백 (로그아웃/리다이렉트 등 소비자 정책 주입). */
|
|
235
334
|
onUnauthorized?: (error: ApiError) => void;
|
|
236
335
|
/** fetch 구현체 주입 (테스트용). 기본 globalThis.fetch. */
|
|
@@ -315,14 +414,22 @@ interface ApiClient {
|
|
|
315
414
|
*
|
|
316
415
|
* 동작 (contracts/common-response.schema.json, trace.md):
|
|
317
416
|
* - 매 요청에 traceId 를 생성해 traceIdHeader(기본 X-Trace-Id)로 부착한다.
|
|
417
|
+
* - `Content-Type: application/json` 은 호출자가 Content-Type 을 지정하지 않았고 body 가
|
|
418
|
+
* **문자열**일 때만 기본 부착한다 (JSON.stringify 관용). body 없는 요청(GET 등)엔 붙이지
|
|
419
|
+
* 않아 교차 출처 GET 의 CORS preflight 를 유발하지 않으며, URLSearchParams/Blob/FormData/
|
|
420
|
+
* ArrayBuffer/ReadableStream 등은 fetch 기본값에 맡긴다. 문자열이지만 JSON 이 아닌 본문은
|
|
421
|
+
* Content-Type 을 직접 지정할 것.
|
|
318
422
|
* - 응답이 CommonResponse 봉투면 success 검사 후 data 를 언랩한다
|
|
319
423
|
* (data 부재 시 undefined). 비봉투 JSON 은 그대로 반환.
|
|
320
424
|
* - 실패 봉투 / HTTP 에러는 code·message·traceId 를 담은 ApiError 를 throw.
|
|
321
425
|
* - 401 은 throw 직전에 onUnauthorized 콜백을 호출한다.
|
|
426
|
+
* - 인증 운반 (contracts/session-auth.md): `bearer` = `getToken` 으로 Authorization 헤더,
|
|
427
|
+
* `jwt-cookie`/`session` = `credentials` + `csrf` (쿠키는 브라우저가 싣고 CSRF 헤더만 자동 부착).
|
|
322
428
|
* - retry 지정 시 요청을 core retry() 로 감싼다 ({@link ApiClientRetryOptions} —
|
|
323
429
|
* 멱등 메서드 기본, Retry-After 하한, 시도 간 동일 traceId).
|
|
324
430
|
*
|
|
325
|
-
* 저장소 접근·경로·이벤트명 하드코딩 없음 — 전부 config
|
|
431
|
+
* 저장소 접근·경로·이벤트명 하드코딩 없음 — 전부 config 주입 (opt-in `csrf` 의 CSRF 쿠키 읽기만 예외 —
|
|
432
|
+
* 기본 `document.cookie`, `readCookie` 로 교체 가능).
|
|
326
433
|
*/
|
|
327
434
|
declare function createApiClient(config: ApiClientConfig): ApiClient;
|
|
328
435
|
|
|
@@ -575,9 +682,13 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
|
|
|
575
682
|
* 경량 서킷 브레이커. 런타임 의존성 0 (native 만).
|
|
576
683
|
*
|
|
577
684
|
* Java `com.rscc.common.util.CircuitBreaker` / Python `rscc_common.circuit_breaker` 와
|
|
578
|
-
* 시맨틱 동일 (3언어 공유 벡터 CB-01~
|
|
685
|
+
* 시맨틱 동일 (3언어 공유 벡터 CB-01~13 — 각 언어 테스트에 수동 동기화. Python 은
|
|
579
686
|
* 시간 단위가 초 — `초 × 1000 == ms`).
|
|
580
687
|
*
|
|
688
|
+
* - **권장 사용법 — `execute(fn)`**: 게이트(allowRequest) → 실행 → 결과 보고를 한 번에
|
|
689
|
+
* 처리해 보고를 **정확히 1회** 보장한다. 차단 시 {@link CircuitOpenError} 로 reject
|
|
690
|
+
* (fn 미호출), 취소(`name === "AbortError"`)는 성공·실패 어느 쪽에도 집계하지 않는다
|
|
691
|
+
* (onIgnore — CB-12). 수동 게이트(allowRequest + 보고)는 execute 로 감쌀 수 없는 경우에만.
|
|
581
692
|
* - **연속(consecutive) 실패 카운트**: `onFailure()` 가 failureThreshold 에 도달(`>=`)하면
|
|
582
693
|
* OPEN. `onSuccess()` 는 카운터를 0 으로 리셋한다 (슬라이딩 윈도 비채택 — 단순성).
|
|
583
694
|
* - **lazy 전이**: OPEN → HALF_OPEN 전이는 `allowRequest()` 호출 시점에 판정한다
|
|
@@ -586,38 +697,51 @@ declare function createTtlCache<K, V>(options: TtlCacheOptions): TtlCache<K, V>;
|
|
|
586
697
|
* - **HALF_OPEN 프로브 1개**: 프로브가 진행 중이면 나머지 `allowRequest()` 는 false.
|
|
587
698
|
* 프로브 성공 → CLOSED(카운터 0), 프로브 실패 → OPEN 재진입(openedAt 갱신 —
|
|
588
699
|
* 전체 openDurationMs 재대기).
|
|
589
|
-
* - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
* 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만
|
|
700
|
+
* - **결과 보고 규율**: `allowRequest()==true` 를 받은 호출만 `onSuccess()`/`onFailure()`/
|
|
701
|
+
* `onIgnore()` 중 하나를 **정확히 1회** 호출한다 (수동 게이트라면 try/catch 로 보고 보장 —
|
|
702
|
+
* 성공·실패로 판정할 수 없는 취소 등은 `onIgnore()` 로 그랜트만 반납). OPEN 대기 중 늦게
|
|
703
|
+
* 도착한 보고는 상태·openedAt·카운터를 바꾸지 않고(no-op — CB-08) 미보고 그랜트 잔량만
|
|
704
|
+
* 소진한다.
|
|
593
705
|
* - **스테일 그랜트 격리**: CLOSED 시절 그랜트가 아직 보고되지 않은 동안에는
|
|
594
706
|
* openDurationMs 가 경과해도 HALF_OPEN 전이(프로브)가 보류된다 — 스테일 보고가 프로브
|
|
595
|
-
* 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는
|
|
707
|
+
* 결과로 오인되는 것을 막아 HALF_OPEN 중 도착하는 보고는 (아래 용서 이후의 매우 늦은
|
|
708
|
+
* 보고를 제외하면) 항상 프로브의 것이다 (CB-09).
|
|
709
|
+
* - **유실 그랜트 용서 (2× 규칙)**: 보고가 끝내 오지 않는 그랜트(보고 누락·유실)가 위 보류를
|
|
710
|
+
* 무기한 지속시켜 서킷이 OPEN 에 영구 고착되는 것을 막는다 — OPEN 진입 후
|
|
711
|
+
* `2 × openDurationMs` 경과 시점에도 미보고 그랜트가 남아 있으면 이를 유실로 간주해 0 으로
|
|
712
|
+
* 용서하고 정상 HALF_OPEN 전이 + 프로브 획득을 수행한다 (CB-10). 용서 뒤 매우 늦은 스테일
|
|
713
|
+
* 보고가 HALF_OPEN 중 도착하면 프로브 결과로 취급되지만 무해하다 — 스테일 성공은 하류가
|
|
714
|
+
* 실제로 응답했다는 증거이고, 스테일 실패는 보수적인 OPEN 재진입일 뿐이다 (스테일
|
|
715
|
+
* onIgnore 는 프로브 슬롯을 조기 해제해 프로브가 1개 더 허가될 뿐 — 최초 보고가 판정).
|
|
716
|
+
* - **유실 프로브 용서**: HALF_OPEN 프로브가 openDurationMs(`>=`) 동안 보고되지 않으면 유실로
|
|
717
|
+
* 간주해 다음 allowRequest() 에 새 프로브를 넘긴다 — 보고 누락 프로브 1개로 HALF_OPEN 에 영구
|
|
718
|
+
* 고착되는 것을 막는다 (CB-13). 옛 프로브의 늦은 보고는 새 프로브 결과로 취급되며 위와 같은
|
|
719
|
+
* 이유로 무해하다.
|
|
720
|
+
*
|
|
721
|
+
* @example 권장 — execute 로 게이트·보고를 위임한다.
|
|
722
|
+
* ```ts
|
|
723
|
+
* const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
|
|
724
|
+
* const user = await cb.execute(() => api.request<User>("/users/1")); // OPEN 이면 CircuitOpenError
|
|
725
|
+
* ```
|
|
596
726
|
*
|
|
597
|
-
* @example 서킷 + 재시도 조합 —
|
|
598
|
-
*
|
|
727
|
+
* @example 서킷 + 재시도 조합 — 시도마다 execute 로 게이트·보고. OPEN 차단
|
|
728
|
+
* ({@link CircuitOpenError})은 shouldRetry 에서 비재시도로 걸러 즉시 실패시킨다(빠른 실패 유지).
|
|
599
729
|
* ```ts
|
|
600
730
|
* const cb = createCircuitBreaker({ failureThreshold: 5, openDurationMs: 30_000 });
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* try {
|
|
606
|
-
* const r = await call();
|
|
607
|
-
* cb.onSuccess();
|
|
608
|
-
* return r;
|
|
609
|
-
* } catch (error) {
|
|
610
|
-
* cb.onFailure();
|
|
611
|
-
* throw error;
|
|
612
|
-
* }
|
|
613
|
-
* },
|
|
614
|
-
* {
|
|
615
|
-
* shouldRetry: (error) => error !== circuitOpen, // OPEN 차단은 즉시 실패
|
|
616
|
-
* onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
|
|
617
|
-
* }
|
|
618
|
-
* );
|
|
731
|
+
* await retry(() => cb.execute(() => call()), {
|
|
732
|
+
* shouldRetry: (error) => !(error instanceof CircuitOpenError), // OPEN 차단은 즉시 실패
|
|
733
|
+
* onRetry: (error, attempt, delayMs) => console.warn(`재시도 ${attempt} (${delayMs}ms)`),
|
|
734
|
+
* });
|
|
619
735
|
* ```
|
|
620
736
|
*/
|
|
737
|
+
/**
|
|
738
|
+
* 서킷 차단 거부 오류 — `execute()` 가 allowRequest()==false(OPEN 대기·프로브 보류·HALF_OPEN
|
|
739
|
+
* 프로브 경합)일 때 reject 하며, 이때 작업 함수는 **호출되지 않는다** (CB-12).
|
|
740
|
+
* Java `CircuitOpenException` / Python `CircuitOpenError` 패리티. 재시도 조합에서 비재시도 신호로 쓴다.
|
|
741
|
+
*/
|
|
742
|
+
declare class CircuitOpenError extends Error {
|
|
743
|
+
constructor(message?: string);
|
|
744
|
+
}
|
|
621
745
|
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
622
746
|
interface CircuitBreakerOptions {
|
|
623
747
|
/** OPEN 전이 임계 — 연속 실패가 이 값에 도달(`>=`)하면 OPEN. 필수 양의 정수 (아니면 RangeError). */
|
|
@@ -631,14 +755,32 @@ interface CircuitBreaker {
|
|
|
631
755
|
/**
|
|
632
756
|
* 요청 통과 여부 — CLOSED 는 true, OPEN 대기 중 false, openDurationMs 경과 시
|
|
633
757
|
* HALF_OPEN 전이 + 프로브 슬롯 획득(true) — 단 CLOSED 시절 미보고 그랜트가 남아 있으면
|
|
634
|
-
* 전이를 보류하고 false (CB-09)
|
|
635
|
-
*
|
|
758
|
+
* 전이를 보류하고 false (CB-09), 보류는 OPEN 진입 후 `2 × openDurationMs` 까지만 —
|
|
759
|
+
* 그 시점엔 미보고 그랜트를 유실로 용서하고 전이 + 프로브 획득(true) (CB-10).
|
|
760
|
+
* HALF_OPEN 프로브 경합 시 나머지 false — 단 프로브가 openDurationMs 경과(`>=`)까지
|
|
761
|
+
* 미보고면 유실로 간주해 이 호출에 새 프로브를 넘긴다(true) (CB-13).
|
|
762
|
+
* true 를 받은 호출만 결과를 onSuccess()/onFailure()/onIgnore() 중 하나로 보고해야 한다.
|
|
636
763
|
*/
|
|
637
764
|
allowRequest(): boolean;
|
|
638
765
|
/** 성공 보고 — CLOSED 카운터 리셋, HALF_OPEN 프로브 성공 시 CLOSED 복귀. OPEN 중엔 무시. */
|
|
639
766
|
onSuccess(): void;
|
|
640
767
|
/** 실패 보고 — CLOSED 연속 카운트 증가(임계 도달 시 OPEN), HALF_OPEN 프로브 실패 시 OPEN 재진입. OPEN 중엔 무시. */
|
|
641
768
|
onFailure(): void;
|
|
769
|
+
/**
|
|
770
|
+
* 결과 없는 그랜트 반납 — 취소(abort) 등 성공·실패로 판정할 수 없는 호출이 보고 대신
|
|
771
|
+
* 호출한다 (CB-11). CLOSED: 미보고 그랜트만 반납(연속 실패 카운터 무변화). HALF_OPEN:
|
|
772
|
+
* 프로브 슬롯 해제 — HALF_OPEN 유지, 다음 allowRequest() 가 새 프로브를 획득한다.
|
|
773
|
+
* OPEN: 미보고 그랜트 잔량만 소진(그 외 무변화).
|
|
774
|
+
*/
|
|
775
|
+
onIgnore(): void;
|
|
776
|
+
/**
|
|
777
|
+
* fn 래핑 (권장 사용법) — allowRequest() 게이트 → 실행 → 결과 보고를 **정확히 1회** 보장한다.
|
|
778
|
+
* 거부는 reject(CircuitOpenError) — 동기 throw 아님, 거부 시 fn 은 호출되지 않는다.
|
|
779
|
+
* resolve → onSuccess() 후 값 반환. throw/reject 는 `name === "AbortError"` 면 onIgnore(),
|
|
780
|
+
* 그 외(TimeoutError 포함)는 onFailure() 후 **같은 값을 래핑 없이** 다시 throw 한다 (CB-12).
|
|
781
|
+
* fn 의 동기 throw 도 동일하게 처리된다.
|
|
782
|
+
*/
|
|
783
|
+
execute<T>(fn: () => T | PromiseLike<T>): Promise<T>;
|
|
642
784
|
/** 현재 상태 조회 — lazy 전이 특성상 openDurationMs 경과 후에도 allowRequest() 전엔 OPEN 으로 보고된다. */
|
|
643
785
|
state(): CircuitState;
|
|
644
786
|
}
|
|
@@ -807,7 +949,7 @@ declare function generateIdempotencyKey(): string;
|
|
|
807
949
|
* 시맨틱(입력→판정)은 3언어 동일하며 반환 형태만 다르다."
|
|
808
950
|
*
|
|
809
951
|
* Node 전용 API(`timingSafeEqual` 등) 미사용 — 브라우저/Node 공용
|
|
810
|
-
* (`globalThis.crypto.subtle`, Node
|
|
952
|
+
* (`globalThis.crypto.subtle`, Node 20+ / 모던 브라우저).
|
|
811
953
|
*/
|
|
812
954
|
/** 웹훅 서명 헤더명 기본값 (contracts/webhook-signature.md). */
|
|
813
955
|
declare const WEBHOOK_SIGNATURE_HEADER = "X-Rscc-Signature";
|
|
@@ -1307,4 +1449,4 @@ declare function composeHangul(s: string): string;
|
|
|
1307
1449
|
*/
|
|
1308
1450
|
declare function matchesHangul(query: string, target: string): boolean;
|
|
1309
1451
|
|
|
1310
|
-
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, type CircuitState, type CommonResponse, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type JosaPair, type ListQueryOptions, type PageResponse, type PhoneType, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createTokenBucket, createTtlCache, decodeJwtPayload, decomposeHangul, formatPhoneNumber, generateIdempotencyKey, getTokenExpiry, isBulkResult, isChosungQuery, isForeignerRrn, isRetryableStatus, isTokenExpired, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseFlag, parseRetryAfterMs, parseSseFrame, parseWireDateTime, pickJosa, readSseStream, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|
|
1452
|
+
export { type ApiClient, type ApiClientConfig, type ApiClientRetryOptions, ApiError, type ApiErrorInfo, type ApiRequestInfo, type ApiResponseInfo, type ApiResult, type BulkResult, type BulkResultBuilder, type BulkResultItem, type Bulkhead, BulkheadFullError, type BulkheadOptions, type BusinessDays, type BusinessDaysOptions, type CircuitBreaker, type CircuitBreakerOptions, CircuitOpenError, type CircuitState, type CommonResponse, type CsrfOptions, DEFAULT_CSRF_COOKIE_NAME, DEFAULT_CSRF_HEADER_NAME, type FeatureFlagReader, type FieldErrorDetail, type FileKind, type JosaPair, type ListQueryOptions, type PageResponse, type PhoneType, ResultCode, type RetryOptions, type SortParam, type SseCallbacks, type SseFrameEvent, type SseSource, type TokenBucket, type TokenBucketOptions, type TtlCache, type TtlCacheOptions, type UploadValidationResult, type ValidationErrorData, WEBHOOK_SIGNATURE_HEADER, abbreviateAmount, ageByYear, ageInsurance, ageMan, attachJosa, buildListQuery, bulkFailures, classifyPhoneNumber, composeHangul, createApiClient, createBulkResultBuilder, createBulkhead, createBusinessDays, createCircuitBreaker, createFeatureFlags, createTokenBucket, createTtlCache, csrfHeaderFor, decodeJwtPayload, decomposeHangul, formatPhoneNumber, generateIdempotencyKey, getTokenExpiry, isBulkResult, isChosungQuery, isForeignerRrn, isRetryableStatus, isTokenExpired, isUnsafeMethod, isValidBusinessNumber, isValidCorporateNumber, isValidRrn, isValidationErrorData, kindsForExtension, maskCardNumber, maskEmail, maskName, maskPhone, maskSecret, matchesHangul, normalizeBusinessNumber, normalizePhoneNumber, normalizeRrn, parseFlag, parseRetryAfterMs, parseSseFrame, parseWireDateTime, pickJosa, readCookie, readSseStream, retry, rrnBirthDate, rrnChecksumOkLegacy, sanitizeLogValue, signWebhook, sniffFile, stripZone, toChosung, toE164, toFormalNotation, toKoreanWords, toWireDate, toWireDateTime, validateUpload, verifyWebhook };
|