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/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
- * Google Cloud Console에 별도로 redirect_uri를 등록할 필요가 없습니다.
3748
- * OAuth 완료 지정한 콜백 URL로 토큰과 함께 리다이렉트됩니다.
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
- * if (result.error) {
3768
- * console.error('로그인 실패:', result.error)
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): Promise<OAuthCallbackResponse>;
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
- * Google Cloud Console에 별도로 redirect_uri를 등록할 필요가 없습니다.
3748
- * OAuth 완료 지정한 콜백 URL로 토큰과 함께 리다이렉트됩니다.
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
- * if (result.error) {
3768
- * console.error('로그인 실패:', result.error)
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): Promise<OAuthCallbackResponse>;
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 response = await fetch(`${this.config.baseUrl}${url}`, {
603
- ...init,
604
- credentials: "include",
605
- signal
606
- });
607
- status = response.status;
608
- return await this.handleResponse(response);
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
- * Google Cloud Console에 별도로 redirect_uri를 등록할 필요가 없습니다.
4688
- * OAuth 완료 지정한 콜백 URL로 토큰과 함께 리다이렉트됩니다.
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
- * if (result.error) {
4708
- * console.error('로그인 실패:', result.error)
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
- const params = new URLSearchParams({ app_callback: callbackUrl });
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${queryStr ? "?" + queryStr : ""}`
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
- void this.http.bootstrapRefreshCookie();
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
- void this.http.bootstrapRefreshCookie();
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
- void this.http.bootstrapRefreshCookie();
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
- void this.http.tryRestoreSessionFromCookie();
10665
+ this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
10597
10666
  }
10598
10667
  }
10599
10668
  /**