connectbase-client 3.22.1 → 3.24.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/CHANGELOG.md +30 -0
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +53 -19
- package/dist/index.d.ts +53 -19
- package/dist/index.js +105 -36
- package/dist/index.mjs +105 -36
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -159,7 +159,20 @@ declare class HttpClient {
|
|
|
159
159
|
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
160
160
|
*/
|
|
161
161
|
private recentCalls;
|
|
162
|
+
/**
|
|
163
|
+
* 부팅 시 fire-and-forget 으로 시작한 cookie 기반 복구 promise. `prepareHeaders` 가
|
|
164
|
+
* 인증 호출을 보내기 직전에 이 promise 를 await 해, 페이지 진입 직후 첫 API 호출이
|
|
165
|
+
* 메모리 토큰 빈 상태로 401 받는 race 를 막는다 (platform-issue 019e638d, 2026-05-26).
|
|
166
|
+
* 한 번 settle 되면 그 결과(메모리 적재 또는 미로그인)가 항상 반영되어 있다.
|
|
167
|
+
*/
|
|
168
|
+
private bootRestorePromise;
|
|
162
169
|
constructor(config: HttpClientConfig);
|
|
170
|
+
/**
|
|
171
|
+
* 페이지 진입 시 fire-and-forget 으로 시작된 cookie 복구 promise 를 SDK 가 등록한다.
|
|
172
|
+
* 같은 promise 가 `prepareHeaders` 에서 await 되어, 첫 인증 호출이 cookie 복구
|
|
173
|
+
* 완료 후 발화한다.
|
|
174
|
+
*/
|
|
175
|
+
setBootRestorePromise(p: Promise<boolean>): void;
|
|
163
176
|
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
164
177
|
getRecentCalls(): RecentApiCall[];
|
|
165
178
|
/** 최근 호출 buffer clear (테스트/프라이버시 처리). */
|
|
@@ -254,8 +267,14 @@ declare class HttpClient {
|
|
|
254
267
|
private handleResponse;
|
|
255
268
|
/**
|
|
256
269
|
* AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
|
|
270
|
+
*
|
|
271
|
+
* 401 자동 복구: 인증 호출이 401 을 받으면 cookie 기반 복구를 *한 번* 시도하고 retry 한다.
|
|
272
|
+
* 메모리 토큰이 만료/누락 + cookie 는 살아 있는 경우를 자동으로 회복시켜,
|
|
273
|
+
* 페이지 진입 직후 race 로 401 을 받은 첫 호출이 사용자 흐름을 차단하지 않게 한다
|
|
274
|
+
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
257
275
|
*/
|
|
258
276
|
private doFetch;
|
|
277
|
+
private tryFetchOnce;
|
|
259
278
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
260
279
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
261
280
|
put<T>(url: string, data: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -3744,9 +3763,10 @@ declare class OAuthAPI {
|
|
|
3744
3763
|
getEnabledProviders(): Promise<EnabledProvidersResponse>;
|
|
3745
3764
|
/**
|
|
3746
3765
|
* 소셜 로그인 (리다이렉트 방식) - 권장
|
|
3747
|
-
*
|
|
3748
|
-
*
|
|
3749
|
-
*
|
|
3766
|
+
*
|
|
3767
|
+
* 기존 회원만 로그인 (`intent=signin` 명시). 이 앱에서 처음인 사용자는 콜백에서
|
|
3768
|
+
* `error=account_not_found` 를 받아 가입 안내 UX 로 분기해야 한다. 신규 가입까지 한 번에
|
|
3769
|
+
* 처리하려면 [signUp] 을 사용한다.
|
|
3750
3770
|
*
|
|
3751
3771
|
* @param provider - OAuth 프로바이더 (google, naver, github, discord)
|
|
3752
3772
|
* @param callbackUrl - OAuth 완료 후 리다이렉트될 앱의 URL
|
|
@@ -3754,27 +3774,30 @@ declare class OAuthAPI {
|
|
|
3754
3774
|
*
|
|
3755
3775
|
* @example
|
|
3756
3776
|
* ```typescript
|
|
3757
|
-
* // 로그인 버튼 클릭 시
|
|
3777
|
+
* // "로그인" 버튼 클릭 시
|
|
3758
3778
|
* await cb.oauth.signIn('google', 'https://myapp.com/oauth/callback')
|
|
3759
|
-
* // Google 로그인 후 https://myapp.com/oauth/callback?access_token=...&refresh_token=... 로 리다이렉트됨
|
|
3760
3779
|
* ```
|
|
3761
3780
|
*
|
|
3762
3781
|
* @example
|
|
3763
3782
|
* ```typescript
|
|
3764
3783
|
* // callback 페이지에서
|
|
3765
|
-
* const result = cb.oauth.getCallbackResult()
|
|
3766
|
-
* if (result) {
|
|
3767
|
-
*
|
|
3768
|
-
*
|
|
3769
|
-
* } else {
|
|
3770
|
-
* console.log('로그인 성공:', result.member_id)
|
|
3771
|
-
* // 메인 페이지로 이동
|
|
3772
|
-
* window.location.href = '/'
|
|
3773
|
-
* }
|
|
3784
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
3785
|
+
* if (result?.error === 'account_not_found') {
|
|
3786
|
+
* // 가입되지 않은 사용자 — 회원가입 화면으로 안내
|
|
3787
|
+
* window.location.href = '/signup'
|
|
3774
3788
|
* }
|
|
3775
3789
|
* ```
|
|
3776
3790
|
*/
|
|
3777
3791
|
signIn(provider: OAuthProvider, callbackUrl: string, state?: string): Promise<void>;
|
|
3792
|
+
/**
|
|
3793
|
+
* 소셜 회원가입 (리다이렉트 방식) — 사용자가 명시적으로 가입 의사를 표시한 경우에만 호출.
|
|
3794
|
+
*
|
|
3795
|
+
* 기존 회원이면 그대로 로그인 (idempotent). 없으면 신규 AppMember 생성.
|
|
3796
|
+
* "로그인" 버튼에는 [signIn] 을 쓰고, "회원가입" 버튼에서만 이 메서드를 호출해야 silent
|
|
3797
|
+
* auto-signup 회귀를 방지할 수 있다.
|
|
3798
|
+
*/
|
|
3799
|
+
signUp(provider: OAuthProvider, callbackUrl: string, state?: string): Promise<void>;
|
|
3800
|
+
private startCentralOAuth;
|
|
3778
3801
|
/**
|
|
3779
3802
|
* 소셜 로그인 (팝업 방식)
|
|
3780
3803
|
* 팝업 창에서 소셜 로그인을 처리하고 결과를 Promise로 반환합니다.
|
|
@@ -3799,29 +3822,40 @@ declare class OAuthAPI {
|
|
|
3799
3822
|
* const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth/callback')
|
|
3800
3823
|
* ```
|
|
3801
3824
|
*/
|
|
3802
|
-
signInWithPopup(provider: OAuthProvider, callbackUrl?: string
|
|
3825
|
+
signInWithPopup(provider: OAuthProvider, callbackUrl?: string, options?: {
|
|
3826
|
+
intent?: 'signin' | 'signup';
|
|
3827
|
+
}): Promise<OAuthCallbackResponse>;
|
|
3803
3828
|
/**
|
|
3804
3829
|
* 콜백 URL에서 OAuth 결과 추출
|
|
3805
3830
|
* 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
|
|
3806
3831
|
* 토큰이 있으면 자동으로 저장됩니다.
|
|
3807
3832
|
*
|
|
3833
|
+
* 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
|
|
3834
|
+
* cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
|
|
3835
|
+
* 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
|
|
3836
|
+
* 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
|
|
3837
|
+
*
|
|
3838
|
+
* 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
|
|
3839
|
+
* 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
|
|
3840
|
+
* 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
|
|
3841
|
+
* 019e638d, 2026-05-26).
|
|
3842
|
+
*
|
|
3808
3843
|
* @returns OAuth 결과 (토큰, member_id 등) 또는 null
|
|
3809
3844
|
*
|
|
3810
3845
|
* @example
|
|
3811
3846
|
* ```typescript
|
|
3812
3847
|
* // callback 페이지에서
|
|
3813
|
-
* const result = cb.oauth.getCallbackResult()
|
|
3848
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
3814
3849
|
* if (result) {
|
|
3815
3850
|
* if (result.error) {
|
|
3816
3851
|
* alert('로그인 실패: ' + result.error)
|
|
3817
3852
|
* } else {
|
|
3818
|
-
* console.log('로그인 성공:', result.member_id)
|
|
3819
3853
|
* window.location.href = '/'
|
|
3820
3854
|
* }
|
|
3821
3855
|
* }
|
|
3822
3856
|
* ```
|
|
3823
3857
|
*/
|
|
3824
|
-
getCallbackResult(): {
|
|
3858
|
+
getCallbackResult(): Promise<{
|
|
3825
3859
|
access_token?: string;
|
|
3826
3860
|
refresh_token?: string;
|
|
3827
3861
|
member_id?: string;
|
|
@@ -3829,7 +3863,7 @@ declare class OAuthAPI {
|
|
|
3829
3863
|
email?: string;
|
|
3830
3864
|
state?: string;
|
|
3831
3865
|
error?: string;
|
|
3832
|
-
} | null
|
|
3866
|
+
} | null>;
|
|
3833
3867
|
/**
|
|
3834
3868
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
3835
3869
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -159,7 +159,20 @@ declare class HttpClient {
|
|
|
159
159
|
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
160
160
|
*/
|
|
161
161
|
private recentCalls;
|
|
162
|
+
/**
|
|
163
|
+
* 부팅 시 fire-and-forget 으로 시작한 cookie 기반 복구 promise. `prepareHeaders` 가
|
|
164
|
+
* 인증 호출을 보내기 직전에 이 promise 를 await 해, 페이지 진입 직후 첫 API 호출이
|
|
165
|
+
* 메모리 토큰 빈 상태로 401 받는 race 를 막는다 (platform-issue 019e638d, 2026-05-26).
|
|
166
|
+
* 한 번 settle 되면 그 결과(메모리 적재 또는 미로그인)가 항상 반영되어 있다.
|
|
167
|
+
*/
|
|
168
|
+
private bootRestorePromise;
|
|
162
169
|
constructor(config: HttpClientConfig);
|
|
170
|
+
/**
|
|
171
|
+
* 페이지 진입 시 fire-and-forget 으로 시작된 cookie 복구 promise 를 SDK 가 등록한다.
|
|
172
|
+
* 같은 promise 가 `prepareHeaders` 에서 await 되어, 첫 인증 호출이 cookie 복구
|
|
173
|
+
* 완료 후 발화한다.
|
|
174
|
+
*/
|
|
175
|
+
setBootRestorePromise(p: Promise<boolean>): void;
|
|
163
176
|
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
164
177
|
getRecentCalls(): RecentApiCall[];
|
|
165
178
|
/** 최근 호출 buffer clear (테스트/프라이버시 처리). */
|
|
@@ -254,8 +267,14 @@ declare class HttpClient {
|
|
|
254
267
|
private handleResponse;
|
|
255
268
|
/**
|
|
256
269
|
* AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
|
|
270
|
+
*
|
|
271
|
+
* 401 자동 복구: 인증 호출이 401 을 받으면 cookie 기반 복구를 *한 번* 시도하고 retry 한다.
|
|
272
|
+
* 메모리 토큰이 만료/누락 + cookie 는 살아 있는 경우를 자동으로 회복시켜,
|
|
273
|
+
* 페이지 진입 직후 race 로 401 을 받은 첫 호출이 사용자 흐름을 차단하지 않게 한다
|
|
274
|
+
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
257
275
|
*/
|
|
258
276
|
private doFetch;
|
|
277
|
+
private tryFetchOnce;
|
|
259
278
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
260
279
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
261
280
|
put<T>(url: string, data: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -3744,9 +3763,10 @@ declare class OAuthAPI {
|
|
|
3744
3763
|
getEnabledProviders(): Promise<EnabledProvidersResponse>;
|
|
3745
3764
|
/**
|
|
3746
3765
|
* 소셜 로그인 (리다이렉트 방식) - 권장
|
|
3747
|
-
*
|
|
3748
|
-
*
|
|
3749
|
-
*
|
|
3766
|
+
*
|
|
3767
|
+
* 기존 회원만 로그인 (`intent=signin` 명시). 이 앱에서 처음인 사용자는 콜백에서
|
|
3768
|
+
* `error=account_not_found` 를 받아 가입 안내 UX 로 분기해야 한다. 신규 가입까지 한 번에
|
|
3769
|
+
* 처리하려면 [signUp] 을 사용한다.
|
|
3750
3770
|
*
|
|
3751
3771
|
* @param provider - OAuth 프로바이더 (google, naver, github, discord)
|
|
3752
3772
|
* @param callbackUrl - OAuth 완료 후 리다이렉트될 앱의 URL
|
|
@@ -3754,27 +3774,30 @@ declare class OAuthAPI {
|
|
|
3754
3774
|
*
|
|
3755
3775
|
* @example
|
|
3756
3776
|
* ```typescript
|
|
3757
|
-
* // 로그인 버튼 클릭 시
|
|
3777
|
+
* // "로그인" 버튼 클릭 시
|
|
3758
3778
|
* await cb.oauth.signIn('google', 'https://myapp.com/oauth/callback')
|
|
3759
|
-
* // Google 로그인 후 https://myapp.com/oauth/callback?access_token=...&refresh_token=... 로 리다이렉트됨
|
|
3760
3779
|
* ```
|
|
3761
3780
|
*
|
|
3762
3781
|
* @example
|
|
3763
3782
|
* ```typescript
|
|
3764
3783
|
* // callback 페이지에서
|
|
3765
|
-
* const result = cb.oauth.getCallbackResult()
|
|
3766
|
-
* if (result) {
|
|
3767
|
-
*
|
|
3768
|
-
*
|
|
3769
|
-
* } else {
|
|
3770
|
-
* console.log('로그인 성공:', result.member_id)
|
|
3771
|
-
* // 메인 페이지로 이동
|
|
3772
|
-
* window.location.href = '/'
|
|
3773
|
-
* }
|
|
3784
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
3785
|
+
* if (result?.error === 'account_not_found') {
|
|
3786
|
+
* // 가입되지 않은 사용자 — 회원가입 화면으로 안내
|
|
3787
|
+
* window.location.href = '/signup'
|
|
3774
3788
|
* }
|
|
3775
3789
|
* ```
|
|
3776
3790
|
*/
|
|
3777
3791
|
signIn(provider: OAuthProvider, callbackUrl: string, state?: string): Promise<void>;
|
|
3792
|
+
/**
|
|
3793
|
+
* 소셜 회원가입 (리다이렉트 방식) — 사용자가 명시적으로 가입 의사를 표시한 경우에만 호출.
|
|
3794
|
+
*
|
|
3795
|
+
* 기존 회원이면 그대로 로그인 (idempotent). 없으면 신규 AppMember 생성.
|
|
3796
|
+
* "로그인" 버튼에는 [signIn] 을 쓰고, "회원가입" 버튼에서만 이 메서드를 호출해야 silent
|
|
3797
|
+
* auto-signup 회귀를 방지할 수 있다.
|
|
3798
|
+
*/
|
|
3799
|
+
signUp(provider: OAuthProvider, callbackUrl: string, state?: string): Promise<void>;
|
|
3800
|
+
private startCentralOAuth;
|
|
3778
3801
|
/**
|
|
3779
3802
|
* 소셜 로그인 (팝업 방식)
|
|
3780
3803
|
* 팝업 창에서 소셜 로그인을 처리하고 결과를 Promise로 반환합니다.
|
|
@@ -3799,29 +3822,40 @@ declare class OAuthAPI {
|
|
|
3799
3822
|
* const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth/callback')
|
|
3800
3823
|
* ```
|
|
3801
3824
|
*/
|
|
3802
|
-
signInWithPopup(provider: OAuthProvider, callbackUrl?: string
|
|
3825
|
+
signInWithPopup(provider: OAuthProvider, callbackUrl?: string, options?: {
|
|
3826
|
+
intent?: 'signin' | 'signup';
|
|
3827
|
+
}): Promise<OAuthCallbackResponse>;
|
|
3803
3828
|
/**
|
|
3804
3829
|
* 콜백 URL에서 OAuth 결과 추출
|
|
3805
3830
|
* 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
|
|
3806
3831
|
* 토큰이 있으면 자동으로 저장됩니다.
|
|
3807
3832
|
*
|
|
3833
|
+
* 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
|
|
3834
|
+
* cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
|
|
3835
|
+
* 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
|
|
3836
|
+
* 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
|
|
3837
|
+
*
|
|
3838
|
+
* 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
|
|
3839
|
+
* 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
|
|
3840
|
+
* 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
|
|
3841
|
+
* 019e638d, 2026-05-26).
|
|
3842
|
+
*
|
|
3808
3843
|
* @returns OAuth 결과 (토큰, member_id 등) 또는 null
|
|
3809
3844
|
*
|
|
3810
3845
|
* @example
|
|
3811
3846
|
* ```typescript
|
|
3812
3847
|
* // callback 페이지에서
|
|
3813
|
-
* const result = cb.oauth.getCallbackResult()
|
|
3848
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
3814
3849
|
* if (result) {
|
|
3815
3850
|
* if (result.error) {
|
|
3816
3851
|
* alert('로그인 실패: ' + result.error)
|
|
3817
3852
|
* } else {
|
|
3818
|
-
* console.log('로그인 성공:', result.member_id)
|
|
3819
3853
|
* window.location.href = '/'
|
|
3820
3854
|
* }
|
|
3821
3855
|
* }
|
|
3822
3856
|
* ```
|
|
3823
3857
|
*/
|
|
3824
|
-
getCallbackResult(): {
|
|
3858
|
+
getCallbackResult(): Promise<{
|
|
3825
3859
|
access_token?: string;
|
|
3826
3860
|
refresh_token?: string;
|
|
3827
3861
|
member_id?: string;
|
|
@@ -3829,7 +3863,7 @@ declare class OAuthAPI {
|
|
|
3829
3863
|
email?: string;
|
|
3830
3864
|
state?: string;
|
|
3831
3865
|
error?: string;
|
|
3832
|
-
} | null
|
|
3866
|
+
} | null>;
|
|
3833
3867
|
/**
|
|
3834
3868
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
3835
3869
|
*
|
package/dist/index.js
CHANGED
|
@@ -171,11 +171,26 @@ var HttpClient = class {
|
|
|
171
171
|
* `client.support.getRecentCalls()` 로 외부 노출.
|
|
172
172
|
*/
|
|
173
173
|
this.recentCalls = new RecentCallsBuffer();
|
|
174
|
+
/**
|
|
175
|
+
* 부팅 시 fire-and-forget 으로 시작한 cookie 기반 복구 promise. `prepareHeaders` 가
|
|
176
|
+
* 인증 호출을 보내기 직전에 이 promise 를 await 해, 페이지 진입 직후 첫 API 호출이
|
|
177
|
+
* 메모리 토큰 빈 상태로 401 받는 race 를 막는다 (platform-issue 019e638d, 2026-05-26).
|
|
178
|
+
* 한 번 settle 되면 그 결과(메모리 적재 또는 미로그인)가 항상 반영되어 있다.
|
|
179
|
+
*/
|
|
180
|
+
this.bootRestorePromise = null;
|
|
174
181
|
this.config = { ...config };
|
|
175
182
|
this.storageKey = this.buildStorageKey();
|
|
176
183
|
this.warnIfUnsafePersistence();
|
|
177
184
|
this.restoreTokens();
|
|
178
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* 페이지 진입 시 fire-and-forget 으로 시작된 cookie 복구 promise 를 SDK 가 등록한다.
|
|
188
|
+
* 같은 promise 가 `prepareHeaders` 에서 await 되어, 첫 인증 호출이 cookie 복구
|
|
189
|
+
* 완료 후 발화한다.
|
|
190
|
+
*/
|
|
191
|
+
setBootRestorePromise(p) {
|
|
192
|
+
this.bootRestorePromise = p.catch(() => false);
|
|
193
|
+
}
|
|
179
194
|
/** 최근 호출 ring buffer 스냅샷 (시간순). */
|
|
180
195
|
getRecentCalls() {
|
|
181
196
|
return this.recentCalls.snapshot();
|
|
@@ -518,6 +533,12 @@ var HttpClient = class {
|
|
|
518
533
|
if (credential) {
|
|
519
534
|
headers.set("X-Public-Key", credential);
|
|
520
535
|
}
|
|
536
|
+
if (!config?.skipAuth && !this.config.accessToken && this.config.publicKey && typeof window !== "undefined" && this.bootRestorePromise) {
|
|
537
|
+
try {
|
|
538
|
+
await this.bootRestorePromise;
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
521
542
|
if (!config?.skipAuth && this.config.accessToken) {
|
|
522
543
|
let token = this.config.accessToken;
|
|
523
544
|
if (this.isTokenExpired(token) && this.config.refreshToken) {
|
|
@@ -590,22 +611,35 @@ var HttpClient = class {
|
|
|
590
611
|
}
|
|
591
612
|
/**
|
|
592
613
|
* AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
|
|
614
|
+
*
|
|
615
|
+
* 401 자동 복구: 인증 호출이 401 을 받으면 cookie 기반 복구를 *한 번* 시도하고 retry 한다.
|
|
616
|
+
* 메모리 토큰이 만료/누락 + cookie 는 살아 있는 경우를 자동으로 회복시켜,
|
|
617
|
+
* 페이지 진입 직후 race 로 401 을 받은 첫 호출이 사용자 흐름을 차단하지 않게 한다
|
|
618
|
+
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
593
619
|
*/
|
|
594
620
|
async doFetch(url, init, config) {
|
|
595
|
-
const { signal, cleanup } = createTimeoutController({
|
|
596
|
-
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
597
|
-
signal: config?.signal
|
|
598
|
-
});
|
|
599
621
|
const startedAt = Date.now();
|
|
600
622
|
let status = 0;
|
|
601
623
|
try {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
624
|
+
const first = await this.tryFetchOnce(url, init, config);
|
|
625
|
+
status = first.status;
|
|
626
|
+
if (!first.ok && first.status === 401 && !config?.skipAuth && this.config.publicKey && typeof window !== "undefined") {
|
|
627
|
+
const recovered = await this.tryRestoreSessionFromCookie();
|
|
628
|
+
if (recovered) {
|
|
629
|
+
const retryHeaders = await this.prepareHeaders(config);
|
|
630
|
+
if (init.body instanceof FormData) {
|
|
631
|
+
retryHeaders.delete("Content-Type");
|
|
632
|
+
}
|
|
633
|
+
const second = await this.tryFetchOnce(
|
|
634
|
+
url,
|
|
635
|
+
{ ...init, headers: retryHeaders },
|
|
636
|
+
config
|
|
637
|
+
);
|
|
638
|
+
status = second.status;
|
|
639
|
+
return await this.handleResponse(second.response);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return await this.handleResponse(first.response);
|
|
609
643
|
} finally {
|
|
610
644
|
this.recentCalls.push({
|
|
611
645
|
method: (init.method || "GET").toUpperCase(),
|
|
@@ -614,6 +648,21 @@ var HttpClient = class {
|
|
|
614
648
|
duration_ms: Date.now() - startedAt,
|
|
615
649
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
616
650
|
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async tryFetchOnce(url, init, config) {
|
|
654
|
+
const { signal, cleanup } = createTimeoutController({
|
|
655
|
+
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
656
|
+
signal: config?.signal
|
|
657
|
+
});
|
|
658
|
+
try {
|
|
659
|
+
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
660
|
+
...init,
|
|
661
|
+
credentials: "include",
|
|
662
|
+
signal
|
|
663
|
+
});
|
|
664
|
+
return { response, ok: response.ok, status: response.status };
|
|
665
|
+
} finally {
|
|
617
666
|
cleanup();
|
|
618
667
|
}
|
|
619
668
|
}
|
|
@@ -4684,9 +4733,10 @@ var OAuthAPI = class {
|
|
|
4684
4733
|
}
|
|
4685
4734
|
/**
|
|
4686
4735
|
* 소셜 로그인 (리다이렉트 방식) - 권장
|
|
4687
|
-
*
|
|
4688
|
-
*
|
|
4689
|
-
*
|
|
4736
|
+
*
|
|
4737
|
+
* 기존 회원만 로그인 (`intent=signin` 명시). 이 앱에서 처음인 사용자는 콜백에서
|
|
4738
|
+
* `error=account_not_found` 를 받아 가입 안내 UX 로 분기해야 한다. 신규 가입까지 한 번에
|
|
4739
|
+
* 처리하려면 [signUp] 을 사용한다.
|
|
4690
4740
|
*
|
|
4691
4741
|
* @param provider - OAuth 프로바이더 (google, naver, github, discord)
|
|
4692
4742
|
* @param callbackUrl - OAuth 완료 후 리다이렉트될 앱의 URL
|
|
@@ -4694,28 +4744,35 @@ var OAuthAPI = class {
|
|
|
4694
4744
|
*
|
|
4695
4745
|
* @example
|
|
4696
4746
|
* ```typescript
|
|
4697
|
-
* // 로그인 버튼 클릭 시
|
|
4747
|
+
* // "로그인" 버튼 클릭 시
|
|
4698
4748
|
* await cb.oauth.signIn('google', 'https://myapp.com/oauth/callback')
|
|
4699
|
-
* // Google 로그인 후 https://myapp.com/oauth/callback?access_token=...&refresh_token=... 로 리다이렉트됨
|
|
4700
4749
|
* ```
|
|
4701
4750
|
*
|
|
4702
4751
|
* @example
|
|
4703
4752
|
* ```typescript
|
|
4704
4753
|
* // callback 페이지에서
|
|
4705
|
-
* const result = cb.oauth.getCallbackResult()
|
|
4706
|
-
* if (result) {
|
|
4707
|
-
*
|
|
4708
|
-
*
|
|
4709
|
-
* } else {
|
|
4710
|
-
* console.log('로그인 성공:', result.member_id)
|
|
4711
|
-
* // 메인 페이지로 이동
|
|
4712
|
-
* window.location.href = '/'
|
|
4713
|
-
* }
|
|
4754
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
4755
|
+
* if (result?.error === 'account_not_found') {
|
|
4756
|
+
* // 가입되지 않은 사용자 — 회원가입 화면으로 안내
|
|
4757
|
+
* window.location.href = '/signup'
|
|
4714
4758
|
* }
|
|
4715
4759
|
* ```
|
|
4716
4760
|
*/
|
|
4717
4761
|
async signIn(provider, callbackUrl, state) {
|
|
4718
|
-
|
|
4762
|
+
return this.startCentralOAuth(provider, callbackUrl, state, "signin");
|
|
4763
|
+
}
|
|
4764
|
+
/**
|
|
4765
|
+
* 소셜 회원가입 (리다이렉트 방식) — 사용자가 명시적으로 가입 의사를 표시한 경우에만 호출.
|
|
4766
|
+
*
|
|
4767
|
+
* 기존 회원이면 그대로 로그인 (idempotent). 없으면 신규 AppMember 생성.
|
|
4768
|
+
* "로그인" 버튼에는 [signIn] 을 쓰고, "회원가입" 버튼에서만 이 메서드를 호출해야 silent
|
|
4769
|
+
* auto-signup 회귀를 방지할 수 있다.
|
|
4770
|
+
*/
|
|
4771
|
+
async signUp(provider, callbackUrl, state) {
|
|
4772
|
+
return this.startCentralOAuth(provider, callbackUrl, state, "signup");
|
|
4773
|
+
}
|
|
4774
|
+
async startCentralOAuth(provider, callbackUrl, state, intent) {
|
|
4775
|
+
const params = new URLSearchParams({ app_callback: callbackUrl, intent });
|
|
4719
4776
|
if (state) {
|
|
4720
4777
|
params.append("state", state);
|
|
4721
4778
|
}
|
|
@@ -4748,14 +4805,14 @@ var OAuthAPI = class {
|
|
|
4748
4805
|
* const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth/callback')
|
|
4749
4806
|
* ```
|
|
4750
4807
|
*/
|
|
4751
|
-
async signInWithPopup(provider, callbackUrl) {
|
|
4808
|
+
async signInWithPopup(provider, callbackUrl, options = {}) {
|
|
4752
4809
|
const params = new URLSearchParams();
|
|
4810
|
+
params.set("intent", options.intent ?? "signin");
|
|
4753
4811
|
if (callbackUrl) {
|
|
4754
4812
|
params.set("app_callback", callbackUrl);
|
|
4755
4813
|
}
|
|
4756
|
-
const queryStr = params.toString();
|
|
4757
4814
|
const response = await this.http.get(
|
|
4758
|
-
`/v1/public/oauth/${provider}/authorize/central
|
|
4815
|
+
`/v1/public/oauth/${provider}/authorize/central?${params.toString()}`
|
|
4759
4816
|
);
|
|
4760
4817
|
const width = 500;
|
|
4761
4818
|
const height = 600;
|
|
@@ -4794,7 +4851,10 @@ var OAuthAPI = class {
|
|
|
4794
4851
|
...typeof rawEmail === "string" && rawEmail.length > 0 ? { email: rawEmail } : {}
|
|
4795
4852
|
};
|
|
4796
4853
|
this.http.setTokens(result.access_token, result.refresh_token);
|
|
4797
|
-
|
|
4854
|
+
try {
|
|
4855
|
+
await this.http.bootstrapRefreshCookie();
|
|
4856
|
+
} catch {
|
|
4857
|
+
}
|
|
4798
4858
|
resolve(result);
|
|
4799
4859
|
};
|
|
4800
4860
|
window.addEventListener("message", handleMessage);
|
|
@@ -4827,23 +4887,32 @@ var OAuthAPI = class {
|
|
|
4827
4887
|
* 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
|
|
4828
4888
|
* 토큰이 있으면 자동으로 저장됩니다.
|
|
4829
4889
|
*
|
|
4890
|
+
* 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
|
|
4891
|
+
* cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
|
|
4892
|
+
* 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
|
|
4893
|
+
* 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
|
|
4894
|
+
*
|
|
4895
|
+
* 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
|
|
4896
|
+
* 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
|
|
4897
|
+
* 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
|
|
4898
|
+
* 019e638d, 2026-05-26).
|
|
4899
|
+
*
|
|
4830
4900
|
* @returns OAuth 결과 (토큰, member_id 등) 또는 null
|
|
4831
4901
|
*
|
|
4832
4902
|
* @example
|
|
4833
4903
|
* ```typescript
|
|
4834
4904
|
* // callback 페이지에서
|
|
4835
|
-
* const result = cb.oauth.getCallbackResult()
|
|
4905
|
+
* const result = await cb.oauth.getCallbackResult()
|
|
4836
4906
|
* if (result) {
|
|
4837
4907
|
* if (result.error) {
|
|
4838
4908
|
* alert('로그인 실패: ' + result.error)
|
|
4839
4909
|
* } else {
|
|
4840
|
-
* console.log('로그인 성공:', result.member_id)
|
|
4841
4910
|
* window.location.href = '/'
|
|
4842
4911
|
* }
|
|
4843
4912
|
* }
|
|
4844
4913
|
* ```
|
|
4845
4914
|
*/
|
|
4846
|
-
getCallbackResult() {
|
|
4915
|
+
async getCallbackResult() {
|
|
4847
4916
|
const params = new URLSearchParams(window.location.search);
|
|
4848
4917
|
const error = params.get("error");
|
|
4849
4918
|
if (error) {
|
|
@@ -4875,7 +4944,7 @@ var OAuthAPI = class {
|
|
|
4875
4944
|
return result;
|
|
4876
4945
|
}
|
|
4877
4946
|
this.http.setTokens(accessToken, refreshToken);
|
|
4878
|
-
|
|
4947
|
+
await this.http.bootstrapRefreshCookie();
|
|
4879
4948
|
return result;
|
|
4880
4949
|
}
|
|
4881
4950
|
/**
|
|
@@ -4914,7 +4983,7 @@ var OAuthAPI = class {
|
|
|
4914
4983
|
return result;
|
|
4915
4984
|
}
|
|
4916
4985
|
this.http.setTokens(result.access_token, result.refresh_token);
|
|
4917
|
-
|
|
4986
|
+
await this.http.bootstrapRefreshCookie();
|
|
4918
4987
|
return result;
|
|
4919
4988
|
}
|
|
4920
4989
|
};
|
|
@@ -10593,7 +10662,7 @@ var ConnectBase = class {
|
|
|
10593
10662
|
this.auth._attachAnalytics(this.analytics);
|
|
10594
10663
|
const shouldAutoRestore = config.autoRestoreSession ?? true;
|
|
10595
10664
|
if (shouldAutoRestore && typeof window !== "undefined" && !this.isOAuthCallbackUrl()) {
|
|
10596
|
-
|
|
10665
|
+
this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
|
|
10597
10666
|
}
|
|
10598
10667
|
}
|
|
10599
10668
|
/**
|