connectbase-client 3.22.0 → 3.23.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>;
@@ -3762,7 +3781,7 @@ declare class OAuthAPI {
3762
3781
  * @example
3763
3782
  * ```typescript
3764
3783
  * // callback 페이지에서
3765
- * const result = cb.oauth.getCallbackResult()
3784
+ * const result = await cb.oauth.getCallbackResult()
3766
3785
  * if (result) {
3767
3786
  * if (result.error) {
3768
3787
  * console.error('로그인 실패:', result.error)
@@ -3805,23 +3824,32 @@ declare class OAuthAPI {
3805
3824
  * 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
3806
3825
  * 토큰이 있으면 자동으로 저장됩니다.
3807
3826
  *
3827
+ * 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
3828
+ * cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
3829
+ * 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
3830
+ * 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
3831
+ *
3832
+ * 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
3833
+ * 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
3834
+ * 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
3835
+ * 019e638d, 2026-05-26).
3836
+ *
3808
3837
  * @returns OAuth 결과 (토큰, member_id 등) 또는 null
3809
3838
  *
3810
3839
  * @example
3811
3840
  * ```typescript
3812
3841
  * // callback 페이지에서
3813
- * const result = cb.oauth.getCallbackResult()
3842
+ * const result = await cb.oauth.getCallbackResult()
3814
3843
  * if (result) {
3815
3844
  * if (result.error) {
3816
3845
  * alert('로그인 실패: ' + result.error)
3817
3846
  * } else {
3818
- * console.log('로그인 성공:', result.member_id)
3819
3847
  * window.location.href = '/'
3820
3848
  * }
3821
3849
  * }
3822
3850
  * ```
3823
3851
  */
3824
- getCallbackResult(): {
3852
+ getCallbackResult(): Promise<{
3825
3853
  access_token?: string;
3826
3854
  refresh_token?: string;
3827
3855
  member_id?: string;
@@ -3829,7 +3857,7 @@ declare class OAuthAPI {
3829
3857
  email?: string;
3830
3858
  state?: string;
3831
3859
  error?: string;
3832
- } | null;
3860
+ } | null>;
3833
3861
  /**
3834
3862
  * 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
3835
3863
  *
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>;
@@ -3762,7 +3781,7 @@ declare class OAuthAPI {
3762
3781
  * @example
3763
3782
  * ```typescript
3764
3783
  * // callback 페이지에서
3765
- * const result = cb.oauth.getCallbackResult()
3784
+ * const result = await cb.oauth.getCallbackResult()
3766
3785
  * if (result) {
3767
3786
  * if (result.error) {
3768
3787
  * console.error('로그인 실패:', result.error)
@@ -3805,23 +3824,32 @@ declare class OAuthAPI {
3805
3824
  * 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
3806
3825
  * 토큰이 있으면 자동으로 저장됩니다.
3807
3826
  *
3827
+ * 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
3828
+ * cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
3829
+ * 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
3830
+ * 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
3831
+ *
3832
+ * 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
3833
+ * 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
3834
+ * 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
3835
+ * 019e638d, 2026-05-26).
3836
+ *
3808
3837
  * @returns OAuth 결과 (토큰, member_id 등) 또는 null
3809
3838
  *
3810
3839
  * @example
3811
3840
  * ```typescript
3812
3841
  * // callback 페이지에서
3813
- * const result = cb.oauth.getCallbackResult()
3842
+ * const result = await cb.oauth.getCallbackResult()
3814
3843
  * if (result) {
3815
3844
  * if (result.error) {
3816
3845
  * alert('로그인 실패: ' + result.error)
3817
3846
  * } else {
3818
- * console.log('로그인 성공:', result.member_id)
3819
3847
  * window.location.href = '/'
3820
3848
  * }
3821
3849
  * }
3822
3850
  * ```
3823
3851
  */
3824
- getCallbackResult(): {
3852
+ getCallbackResult(): Promise<{
3825
3853
  access_token?: string;
3826
3854
  refresh_token?: string;
3827
3855
  member_id?: string;
@@ -3829,7 +3857,7 @@ declare class OAuthAPI {
3829
3857
  email?: string;
3830
3858
  state?: string;
3831
3859
  error?: string;
3832
- } | null;
3860
+ } | null>;
3833
3861
  /**
3834
3862
  * 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
3835
3863
  *
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
  }
@@ -4702,7 +4751,7 @@ var OAuthAPI = class {
4702
4751
  * @example
4703
4752
  * ```typescript
4704
4753
  * // callback 페이지에서
4705
- * const result = cb.oauth.getCallbackResult()
4754
+ * const result = await cb.oauth.getCallbackResult()
4706
4755
  * if (result) {
4707
4756
  * if (result.error) {
4708
4757
  * console.error('로그인 실패:', result.error)
@@ -4794,7 +4843,10 @@ var OAuthAPI = class {
4794
4843
  ...typeof rawEmail === "string" && rawEmail.length > 0 ? { email: rawEmail } : {}
4795
4844
  };
4796
4845
  this.http.setTokens(result.access_token, result.refresh_token);
4797
- void this.http.bootstrapRefreshCookie();
4846
+ try {
4847
+ await this.http.bootstrapRefreshCookie();
4848
+ } catch {
4849
+ }
4798
4850
  resolve(result);
4799
4851
  };
4800
4852
  window.addEventListener("message", handleMessage);
@@ -4827,23 +4879,32 @@ var OAuthAPI = class {
4827
4879
  * 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
4828
4880
  * 토큰이 있으면 자동으로 저장됩니다.
4829
4881
  *
4882
+ * 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
4883
+ * cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
4884
+ * 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
4885
+ * 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
4886
+ *
4887
+ * 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
4888
+ * 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
4889
+ * 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
4890
+ * 019e638d, 2026-05-26).
4891
+ *
4830
4892
  * @returns OAuth 결과 (토큰, member_id 등) 또는 null
4831
4893
  *
4832
4894
  * @example
4833
4895
  * ```typescript
4834
4896
  * // callback 페이지에서
4835
- * const result = cb.oauth.getCallbackResult()
4897
+ * const result = await cb.oauth.getCallbackResult()
4836
4898
  * if (result) {
4837
4899
  * if (result.error) {
4838
4900
  * alert('로그인 실패: ' + result.error)
4839
4901
  * } else {
4840
- * console.log('로그인 성공:', result.member_id)
4841
4902
  * window.location.href = '/'
4842
4903
  * }
4843
4904
  * }
4844
4905
  * ```
4845
4906
  */
4846
- getCallbackResult() {
4907
+ async getCallbackResult() {
4847
4908
  const params = new URLSearchParams(window.location.search);
4848
4909
  const error = params.get("error");
4849
4910
  if (error) {
@@ -4875,7 +4936,7 @@ var OAuthAPI = class {
4875
4936
  return result;
4876
4937
  }
4877
4938
  this.http.setTokens(accessToken, refreshToken);
4878
- void this.http.bootstrapRefreshCookie();
4939
+ await this.http.bootstrapRefreshCookie();
4879
4940
  return result;
4880
4941
  }
4881
4942
  /**
@@ -4914,7 +4975,7 @@ var OAuthAPI = class {
4914
4975
  return result;
4915
4976
  }
4916
4977
  this.http.setTokens(result.access_token, result.refresh_token);
4917
- void this.http.bootstrapRefreshCookie();
4978
+ await this.http.bootstrapRefreshCookie();
4918
4979
  return result;
4919
4980
  }
4920
4981
  };
@@ -10593,7 +10654,7 @@ var ConnectBase = class {
10593
10654
  this.auth._attachAnalytics(this.analytics);
10594
10655
  const shouldAutoRestore = config.autoRestoreSession ?? true;
10595
10656
  if (shouldAutoRestore && typeof window !== "undefined" && !this.isOAuthCallbackUrl()) {
10596
- void this.http.tryRestoreSessionFromCookie();
10657
+ this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
10597
10658
  }
10598
10659
  }
10599
10660
  /**
package/dist/index.mjs CHANGED
@@ -128,11 +128,26 @@ var HttpClient = class {
128
128
  * `client.support.getRecentCalls()` 로 외부 노출.
129
129
  */
130
130
  this.recentCalls = new RecentCallsBuffer();
131
+ /**
132
+ * 부팅 시 fire-and-forget 으로 시작한 cookie 기반 복구 promise. `prepareHeaders` 가
133
+ * 인증 호출을 보내기 직전에 이 promise 를 await 해, 페이지 진입 직후 첫 API 호출이
134
+ * 메모리 토큰 빈 상태로 401 받는 race 를 막는다 (platform-issue 019e638d, 2026-05-26).
135
+ * 한 번 settle 되면 그 결과(메모리 적재 또는 미로그인)가 항상 반영되어 있다.
136
+ */
137
+ this.bootRestorePromise = null;
131
138
  this.config = { ...config };
132
139
  this.storageKey = this.buildStorageKey();
133
140
  this.warnIfUnsafePersistence();
134
141
  this.restoreTokens();
135
142
  }
143
+ /**
144
+ * 페이지 진입 시 fire-and-forget 으로 시작된 cookie 복구 promise 를 SDK 가 등록한다.
145
+ * 같은 promise 가 `prepareHeaders` 에서 await 되어, 첫 인증 호출이 cookie 복구
146
+ * 완료 후 발화한다.
147
+ */
148
+ setBootRestorePromise(p) {
149
+ this.bootRestorePromise = p.catch(() => false);
150
+ }
136
151
  /** 최근 호출 ring buffer 스냅샷 (시간순). */
137
152
  getRecentCalls() {
138
153
  return this.recentCalls.snapshot();
@@ -475,6 +490,12 @@ var HttpClient = class {
475
490
  if (credential) {
476
491
  headers.set("X-Public-Key", credential);
477
492
  }
493
+ if (!config?.skipAuth && !this.config.accessToken && this.config.publicKey && typeof window !== "undefined" && this.bootRestorePromise) {
494
+ try {
495
+ await this.bootRestorePromise;
496
+ } catch {
497
+ }
498
+ }
478
499
  if (!config?.skipAuth && this.config.accessToken) {
479
500
  let token = this.config.accessToken;
480
501
  if (this.isTokenExpired(token) && this.config.refreshToken) {
@@ -547,22 +568,35 @@ var HttpClient = class {
547
568
  }
548
569
  /**
549
570
  * AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합.
571
+ *
572
+ * 401 자동 복구: 인증 호출이 401 을 받으면 cookie 기반 복구를 *한 번* 시도하고 retry 한다.
573
+ * 메모리 토큰이 만료/누락 + cookie 는 살아 있는 경우를 자동으로 회복시켜,
574
+ * 페이지 진입 직후 race 로 401 을 받은 첫 호출이 사용자 흐름을 차단하지 않게 한다
575
+ * (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
550
576
  */
551
577
  async doFetch(url, init, config) {
552
- const { signal, cleanup } = createTimeoutController({
553
- timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
554
- signal: config?.signal
555
- });
556
578
  const startedAt = Date.now();
557
579
  let status = 0;
558
580
  try {
559
- const response = await fetch(`${this.config.baseUrl}${url}`, {
560
- ...init,
561
- credentials: "include",
562
- signal
563
- });
564
- status = response.status;
565
- return await this.handleResponse(response);
581
+ const first = await this.tryFetchOnce(url, init, config);
582
+ status = first.status;
583
+ if (!first.ok && first.status === 401 && !config?.skipAuth && this.config.publicKey && typeof window !== "undefined") {
584
+ const recovered = await this.tryRestoreSessionFromCookie();
585
+ if (recovered) {
586
+ const retryHeaders = await this.prepareHeaders(config);
587
+ if (init.body instanceof FormData) {
588
+ retryHeaders.delete("Content-Type");
589
+ }
590
+ const second = await this.tryFetchOnce(
591
+ url,
592
+ { ...init, headers: retryHeaders },
593
+ config
594
+ );
595
+ status = second.status;
596
+ return await this.handleResponse(second.response);
597
+ }
598
+ }
599
+ return await this.handleResponse(first.response);
566
600
  } finally {
567
601
  this.recentCalls.push({
568
602
  method: (init.method || "GET").toUpperCase(),
@@ -571,6 +605,21 @@ var HttpClient = class {
571
605
  duration_ms: Date.now() - startedAt,
572
606
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
573
607
  });
608
+ }
609
+ }
610
+ async tryFetchOnce(url, init, config) {
611
+ const { signal, cleanup } = createTimeoutController({
612
+ timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
613
+ signal: config?.signal
614
+ });
615
+ try {
616
+ const response = await fetch(`${this.config.baseUrl}${url}`, {
617
+ ...init,
618
+ credentials: "include",
619
+ signal
620
+ });
621
+ return { response, ok: response.ok, status: response.status };
622
+ } finally {
574
623
  cleanup();
575
624
  }
576
625
  }
@@ -4659,7 +4708,7 @@ var OAuthAPI = class {
4659
4708
  * @example
4660
4709
  * ```typescript
4661
4710
  * // callback 페이지에서
4662
- * const result = cb.oauth.getCallbackResult()
4711
+ * const result = await cb.oauth.getCallbackResult()
4663
4712
  * if (result) {
4664
4713
  * if (result.error) {
4665
4714
  * console.error('로그인 실패:', result.error)
@@ -4751,7 +4800,10 @@ var OAuthAPI = class {
4751
4800
  ...typeof rawEmail === "string" && rawEmail.length > 0 ? { email: rawEmail } : {}
4752
4801
  };
4753
4802
  this.http.setTokens(result.access_token, result.refresh_token);
4754
- void this.http.bootstrapRefreshCookie();
4803
+ try {
4804
+ await this.http.bootstrapRefreshCookie();
4805
+ } catch {
4806
+ }
4755
4807
  resolve(result);
4756
4808
  };
4757
4809
  window.addEventListener("message", handleMessage);
@@ -4784,23 +4836,32 @@ var OAuthAPI = class {
4784
4836
  * 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
4785
4837
  * 토큰이 있으면 자동으로 저장됩니다.
4786
4838
  *
4839
+ * 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
4840
+ * cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
4841
+ * 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
4842
+ * 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
4843
+ *
4844
+ * 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
4845
+ * 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
4846
+ * 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
4847
+ * 019e638d, 2026-05-26).
4848
+ *
4787
4849
  * @returns OAuth 결과 (토큰, member_id 등) 또는 null
4788
4850
  *
4789
4851
  * @example
4790
4852
  * ```typescript
4791
4853
  * // callback 페이지에서
4792
- * const result = cb.oauth.getCallbackResult()
4854
+ * const result = await cb.oauth.getCallbackResult()
4793
4855
  * if (result) {
4794
4856
  * if (result.error) {
4795
4857
  * alert('로그인 실패: ' + result.error)
4796
4858
  * } else {
4797
- * console.log('로그인 성공:', result.member_id)
4798
4859
  * window.location.href = '/'
4799
4860
  * }
4800
4861
  * }
4801
4862
  * ```
4802
4863
  */
4803
- getCallbackResult() {
4864
+ async getCallbackResult() {
4804
4865
  const params = new URLSearchParams(window.location.search);
4805
4866
  const error = params.get("error");
4806
4867
  if (error) {
@@ -4832,7 +4893,7 @@ var OAuthAPI = class {
4832
4893
  return result;
4833
4894
  }
4834
4895
  this.http.setTokens(accessToken, refreshToken);
4835
- void this.http.bootstrapRefreshCookie();
4896
+ await this.http.bootstrapRefreshCookie();
4836
4897
  return result;
4837
4898
  }
4838
4899
  /**
@@ -4871,7 +4932,7 @@ var OAuthAPI = class {
4871
4932
  return result;
4872
4933
  }
4873
4934
  this.http.setTokens(result.access_token, result.refresh_token);
4874
- void this.http.bootstrapRefreshCookie();
4935
+ await this.http.bootstrapRefreshCookie();
4875
4936
  return result;
4876
4937
  }
4877
4938
  };
@@ -10550,7 +10611,7 @@ var ConnectBase = class {
10550
10611
  this.auth._attachAnalytics(this.analytics);
10551
10612
  const shouldAutoRestore = config.autoRestoreSession ?? true;
10552
10613
  if (shouldAutoRestore && typeof window !== "undefined" && !this.isOAuthCallbackUrl()) {
10553
- void this.http.tryRestoreSessionFromCookie();
10614
+ this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
10554
10615
  }
10555
10616
  }
10556
10617
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.22.0",
3
+ "version": "3.23.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",