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.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
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
*
|
|
4645
|
-
*
|
|
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
|
-
*
|
|
4665
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10622
|
+
this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
|
|
10554
10623
|
}
|
|
10555
10624
|
}
|
|
10556
10625
|
/**
|