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.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
  }
@@ -4641,9 +4690,10 @@ var OAuthAPI = class {
4641
4690
  }
4642
4691
  /**
4643
4692
  * 소셜 로그인 (리다이렉트 방식) - 권장
4644
- * Google Cloud Console에 별도로 redirect_uri를 등록할 필요가 없습니다.
4645
- * OAuth 완료 지정한 콜백 URL로 토큰과 함께 리다이렉트됩니다.
4646
- * 모든 배포 환경에서 안정적으로 동작합니다.
4693
+ *
4694
+ * 기존 회원만 로그인 (`intent=signin` 명시). 앱에서 처음인 사용자는 콜백에서
4695
+ * `error=account_not_found` 받아 가입 안내 UX 로 분기해야 한다. 신규 가입까지 한 번에
4696
+ * 처리하려면 [signUp] 을 사용한다.
4647
4697
  *
4648
4698
  * @param provider - OAuth 프로바이더 (google, naver, github, discord)
4649
4699
  * @param callbackUrl - OAuth 완료 후 리다이렉트될 앱의 URL
@@ -4651,28 +4701,35 @@ var OAuthAPI = class {
4651
4701
  *
4652
4702
  * @example
4653
4703
  * ```typescript
4654
- * // 로그인 버튼 클릭 시
4704
+ * // "로그인" 버튼 클릭 시
4655
4705
  * await cb.oauth.signIn('google', 'https://myapp.com/oauth/callback')
4656
- * // Google 로그인 후 https://myapp.com/oauth/callback?access_token=...&refresh_token=... 로 리다이렉트됨
4657
4706
  * ```
4658
4707
  *
4659
4708
  * @example
4660
4709
  * ```typescript
4661
4710
  * // callback 페이지에서
4662
- * const result = cb.oauth.getCallbackResult()
4663
- * if (result) {
4664
- * if (result.error) {
4665
- * console.error('로그인 실패:', result.error)
4666
- * } else {
4667
- * console.log('로그인 성공:', result.member_id)
4668
- * // 메인 페이지로 이동
4669
- * window.location.href = '/'
4670
- * }
4711
+ * const result = await cb.oauth.getCallbackResult()
4712
+ * if (result?.error === 'account_not_found') {
4713
+ * // 가입되지 않은 사용자 — 회원가입 화면으로 안내
4714
+ * window.location.href = '/signup'
4671
4715
  * }
4672
4716
  * ```
4673
4717
  */
4674
4718
  async signIn(provider, callbackUrl, state) {
4675
- const params = new URLSearchParams({ app_callback: callbackUrl });
4719
+ return this.startCentralOAuth(provider, callbackUrl, state, "signin");
4720
+ }
4721
+ /**
4722
+ * 소셜 회원가입 (리다이렉트 방식) — 사용자가 명시적으로 가입 의사를 표시한 경우에만 호출.
4723
+ *
4724
+ * 기존 회원이면 그대로 로그인 (idempotent). 없으면 신규 AppMember 생성.
4725
+ * "로그인" 버튼에는 [signIn] 을 쓰고, "회원가입" 버튼에서만 이 메서드를 호출해야 silent
4726
+ * auto-signup 회귀를 방지할 수 있다.
4727
+ */
4728
+ async signUp(provider, callbackUrl, state) {
4729
+ return this.startCentralOAuth(provider, callbackUrl, state, "signup");
4730
+ }
4731
+ async startCentralOAuth(provider, callbackUrl, state, intent) {
4732
+ const params = new URLSearchParams({ app_callback: callbackUrl, intent });
4676
4733
  if (state) {
4677
4734
  params.append("state", state);
4678
4735
  }
@@ -4705,14 +4762,14 @@ var OAuthAPI = class {
4705
4762
  * const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth/callback')
4706
4763
  * ```
4707
4764
  */
4708
- async signInWithPopup(provider, callbackUrl) {
4765
+ async signInWithPopup(provider, callbackUrl, options = {}) {
4709
4766
  const params = new URLSearchParams();
4767
+ params.set("intent", options.intent ?? "signin");
4710
4768
  if (callbackUrl) {
4711
4769
  params.set("app_callback", callbackUrl);
4712
4770
  }
4713
- const queryStr = params.toString();
4714
4771
  const response = await this.http.get(
4715
- `/v1/public/oauth/${provider}/authorize/central${queryStr ? "?" + queryStr : ""}`
4772
+ `/v1/public/oauth/${provider}/authorize/central?${params.toString()}`
4716
4773
  );
4717
4774
  const width = 500;
4718
4775
  const height = 600;
@@ -4751,7 +4808,10 @@ var OAuthAPI = class {
4751
4808
  ...typeof rawEmail === "string" && rawEmail.length > 0 ? { email: rawEmail } : {}
4752
4809
  };
4753
4810
  this.http.setTokens(result.access_token, result.refresh_token);
4754
- void this.http.bootstrapRefreshCookie();
4811
+ try {
4812
+ await this.http.bootstrapRefreshCookie();
4813
+ } catch {
4814
+ }
4755
4815
  resolve(result);
4756
4816
  };
4757
4817
  window.addEventListener("message", handleMessage);
@@ -4784,23 +4844,32 @@ var OAuthAPI = class {
4784
4844
  * 중앙 콜백 방식에서 리다이렉트 후 URL 파라미터에서 결과를 추출합니다.
4785
4845
  * 토큰이 있으면 자동으로 저장됩니다.
4786
4846
  *
4847
+ * 본 메서드는 `Promise` 를 반환한다. 토큰 저장 직후 `cb_member_refresh_token`
4848
+ * cookie 부트스트랩(`/v1/auth/re-issue` 1회)을 *await* 하기 위함이다. 표준 예제
4849
+ * 코드는 결과를 await 한 다음 `window.location.href='/'` 로 이동하므로, cookie 가
4850
+ * 브라우저에 안전하게 저장된 뒤 페이지가 전환된다.
4851
+ *
4852
+ * 이전(3.22.x) 의 fire-and-forget 부트스트랩은 모바일/느린 네트워크에서 navigation
4853
+ * 이 fetch 보다 먼저 발화해 cookie 가 발급되지 않고, 새 페이지 진입 시 cookie
4854
+ * 없는 상태로 `getMe()` 가 401 으로 떨어지는 회귀가 있었다 (platform-issue
4855
+ * 019e638d, 2026-05-26).
4856
+ *
4787
4857
  * @returns OAuth 결과 (토큰, member_id 등) 또는 null
4788
4858
  *
4789
4859
  * @example
4790
4860
  * ```typescript
4791
4861
  * // callback 페이지에서
4792
- * const result = cb.oauth.getCallbackResult()
4862
+ * const result = await cb.oauth.getCallbackResult()
4793
4863
  * if (result) {
4794
4864
  * if (result.error) {
4795
4865
  * alert('로그인 실패: ' + result.error)
4796
4866
  * } else {
4797
- * console.log('로그인 성공:', result.member_id)
4798
4867
  * window.location.href = '/'
4799
4868
  * }
4800
4869
  * }
4801
4870
  * ```
4802
4871
  */
4803
- getCallbackResult() {
4872
+ async getCallbackResult() {
4804
4873
  const params = new URLSearchParams(window.location.search);
4805
4874
  const error = params.get("error");
4806
4875
  if (error) {
@@ -4832,7 +4901,7 @@ var OAuthAPI = class {
4832
4901
  return result;
4833
4902
  }
4834
4903
  this.http.setTokens(accessToken, refreshToken);
4835
- void this.http.bootstrapRefreshCookie();
4904
+ await this.http.bootstrapRefreshCookie();
4836
4905
  return result;
4837
4906
  }
4838
4907
  /**
@@ -4871,7 +4940,7 @@ var OAuthAPI = class {
4871
4940
  return result;
4872
4941
  }
4873
4942
  this.http.setTokens(result.access_token, result.refresh_token);
4874
- void this.http.bootstrapRefreshCookie();
4943
+ await this.http.bootstrapRefreshCookie();
4875
4944
  return result;
4876
4945
  }
4877
4946
  };
@@ -10550,7 +10619,7 @@ var ConnectBase = class {
10550
10619
  this.auth._attachAnalytics(this.analytics);
10551
10620
  const shouldAutoRestore = config.autoRestoreSession ?? true;
10552
10621
  if (shouldAutoRestore && typeof window !== "undefined" && !this.isOAuthCallbackUrl()) {
10553
- void this.http.tryRestoreSessionFromCookie();
10622
+ this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
10554
10623
  }
10555
10624
  }
10556
10625
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.22.1",
3
+ "version": "3.24.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",