connectbase-client 3.29.0 → 3.31.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 +24 -0
- package/README.md +6 -3
- package/dist/cli.js +35 -10
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +85 -3
- package/dist/index.mjs +85 -3
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -122,6 +122,13 @@ interface HttpClientConfig {
|
|
|
122
122
|
* 서버 환경에서만 사용 (전체 권한, 절대 노출 금지).
|
|
123
123
|
*/
|
|
124
124
|
secretKey?: string;
|
|
125
|
+
/**
|
|
126
|
+
* 이 클라이언트가 속한 앱 ID (선택). 설정 시, refresh 응답이 **다른 앱**의 access token 을
|
|
127
|
+
* 돌려주면(예: 같은 브라우저의 다른 앱 세션 쿠키로 복구된 토큰) 해당 토큰을 채택하지 않고
|
|
128
|
+
* 세션 없음으로 처리한다. 백엔드 re-issue 가 app-scoped 가드로 1차 차단하지만, SDK 측에서도
|
|
129
|
+
* 방어적으로 한 번 더 막는다 (platform-issue 019e86d1). appId 미설정 시 이 가드는 비활성.
|
|
130
|
+
*/
|
|
131
|
+
appId?: string;
|
|
125
132
|
accessToken?: string;
|
|
126
133
|
refreshToken?: string;
|
|
127
134
|
/**
|
|
@@ -3840,6 +3847,12 @@ declare function escapeToExternalBrowser(currentUrl?: string): boolean;
|
|
|
3840
3847
|
*/
|
|
3841
3848
|
declare class OAuthAPI {
|
|
3842
3849
|
private http;
|
|
3850
|
+
/**
|
|
3851
|
+
* 부팅 안전망(consumeRedirectCallbackOnBoot)이 리다이렉트 콜백을 자동 소비했을 때의 promise.
|
|
3852
|
+
* 앱이 그 후 `getCallbackResult()` 를 호출해도 중복으로 `bootstrapRefreshCookie` 를
|
|
3853
|
+
* 발화하지 않도록 이 promise 를 공유한다 (이중 re-issue/rotation 방지).
|
|
3854
|
+
*/
|
|
3855
|
+
private bootConsumePromise;
|
|
3843
3856
|
constructor(http: HttpClient);
|
|
3844
3857
|
/**
|
|
3845
3858
|
* 활성화된 OAuth 프로바이더 목록 조회
|
|
@@ -3964,6 +3977,28 @@ declare class OAuthAPI {
|
|
|
3964
3977
|
state?: string;
|
|
3965
3978
|
error?: string;
|
|
3966
3979
|
} | null>;
|
|
3980
|
+
/**
|
|
3981
|
+
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 부팅 안전망.
|
|
3982
|
+
*
|
|
3983
|
+
* OAuth 리다이렉트 콜백 페이지에서 앱이 `getCallbackResult()` 를 호출하지 않아도,
|
|
3984
|
+
* `new ConnectBase()` 만으로 URL 의 토큰(`?access_token=&refresh_token=&member_id=`)을
|
|
3985
|
+
* 적재해 세션을 확립한다.
|
|
3986
|
+
*
|
|
3987
|
+
* **왜 필요한가:** 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 콜백
|
|
3988
|
+
* 경로에 홈 `index.html` 을 서빙한다. 그 페이지가 콜백을 처리하지 않으면 URL 토큰이 영영
|
|
3989
|
+
* 소비되지 않아 "로그인 → 다시 로그인 페이지" 무한 루프가 발생한다 (여러 사용자 동시 보고).
|
|
3990
|
+
* 이 안전망은 콜백 처리 코드를 빠뜨린 앱도 동작하게 한다.
|
|
3991
|
+
*
|
|
3992
|
+
* **token rotation race(2026-05-16) 가 없는 이유:** 사전 `re-issue`(cookie 복구) 없이 URL
|
|
3993
|
+
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업
|
|
3994
|
+
* (`window.opener` 존재)은 `ConnectBase` 생성자에서 제외되어 호출되지 않는다 (팝업 콜백은
|
|
3995
|
+
* `getCallbackResult()` 의 postMessage 경로 전담). code-only(`?code=`)·에러(`?error=`)
|
|
3996
|
+
* 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
3997
|
+
*
|
|
3998
|
+
* @returns 토큰 적재 성공 시 true, no-op(토큰 없음 등)이면 false.
|
|
3999
|
+
*/
|
|
4000
|
+
consumeRedirectCallbackOnBoot(): Promise<boolean>;
|
|
4001
|
+
private doConsumeRedirectOnBoot;
|
|
3967
4002
|
/**
|
|
3968
4003
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
3969
4004
|
*
|
|
@@ -8805,9 +8840,18 @@ declare class ConnectBase {
|
|
|
8805
8840
|
*
|
|
8806
8841
|
* `?access_token=...&refresh_token=...` (legacy 토큰-in-URL 흐름) 또는
|
|
8807
8842
|
* `?code=...` (OAUTH_CODE_ONLY 흐름) 또는 OAuth 에러 응답 (`?error=...&state=...`)
|
|
8808
|
-
* 패턴이면 true. 콜백 페이지에서 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
8843
|
+
* 패턴이면 true. 콜백 페이지에서 일반 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
8809
8844
|
*/
|
|
8810
8845
|
private isOAuthCallbackUrl;
|
|
8846
|
+
/**
|
|
8847
|
+
* 현재 페이지가 토큰-in-URL OAuth 리다이렉트 콜백(팝업 아님)인지 판별.
|
|
8848
|
+
*
|
|
8849
|
+
* `?access_token=&refresh_token=` 이 있고 `window.opener` 가 없으면 true — 부팅 안전망
|
|
8850
|
+
* (`oauth.consumeRedirectCallbackOnBoot`)으로 토큰을 자동 소비할 대상이다. 팝업 콜백
|
|
8851
|
+
* (`window.opener` 존재)은 `getCallbackResult()` 의 postMessage 경로가 전담하므로 제외한다.
|
|
8852
|
+
* code-only(`?code=`)·에러(`?error=`) 콜백도 제외(네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
8853
|
+
*/
|
|
8854
|
+
private isOAuthRedirectCallbackUrl;
|
|
8811
8855
|
/**
|
|
8812
8856
|
* 새로고침/탭 재개 후 cookie 만으로 세션을 복원한다 (브라우저 전용).
|
|
8813
8857
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -122,6 +122,13 @@ interface HttpClientConfig {
|
|
|
122
122
|
* 서버 환경에서만 사용 (전체 권한, 절대 노출 금지).
|
|
123
123
|
*/
|
|
124
124
|
secretKey?: string;
|
|
125
|
+
/**
|
|
126
|
+
* 이 클라이언트가 속한 앱 ID (선택). 설정 시, refresh 응답이 **다른 앱**의 access token 을
|
|
127
|
+
* 돌려주면(예: 같은 브라우저의 다른 앱 세션 쿠키로 복구된 토큰) 해당 토큰을 채택하지 않고
|
|
128
|
+
* 세션 없음으로 처리한다. 백엔드 re-issue 가 app-scoped 가드로 1차 차단하지만, SDK 측에서도
|
|
129
|
+
* 방어적으로 한 번 더 막는다 (platform-issue 019e86d1). appId 미설정 시 이 가드는 비활성.
|
|
130
|
+
*/
|
|
131
|
+
appId?: string;
|
|
125
132
|
accessToken?: string;
|
|
126
133
|
refreshToken?: string;
|
|
127
134
|
/**
|
|
@@ -3840,6 +3847,12 @@ declare function escapeToExternalBrowser(currentUrl?: string): boolean;
|
|
|
3840
3847
|
*/
|
|
3841
3848
|
declare class OAuthAPI {
|
|
3842
3849
|
private http;
|
|
3850
|
+
/**
|
|
3851
|
+
* 부팅 안전망(consumeRedirectCallbackOnBoot)이 리다이렉트 콜백을 자동 소비했을 때의 promise.
|
|
3852
|
+
* 앱이 그 후 `getCallbackResult()` 를 호출해도 중복으로 `bootstrapRefreshCookie` 를
|
|
3853
|
+
* 발화하지 않도록 이 promise 를 공유한다 (이중 re-issue/rotation 방지).
|
|
3854
|
+
*/
|
|
3855
|
+
private bootConsumePromise;
|
|
3843
3856
|
constructor(http: HttpClient);
|
|
3844
3857
|
/**
|
|
3845
3858
|
* 활성화된 OAuth 프로바이더 목록 조회
|
|
@@ -3964,6 +3977,28 @@ declare class OAuthAPI {
|
|
|
3964
3977
|
state?: string;
|
|
3965
3978
|
error?: string;
|
|
3966
3979
|
} | null>;
|
|
3980
|
+
/**
|
|
3981
|
+
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 부팅 안전망.
|
|
3982
|
+
*
|
|
3983
|
+
* OAuth 리다이렉트 콜백 페이지에서 앱이 `getCallbackResult()` 를 호출하지 않아도,
|
|
3984
|
+
* `new ConnectBase()` 만으로 URL 의 토큰(`?access_token=&refresh_token=&member_id=`)을
|
|
3985
|
+
* 적재해 세션을 확립한다.
|
|
3986
|
+
*
|
|
3987
|
+
* **왜 필요한가:** 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 콜백
|
|
3988
|
+
* 경로에 홈 `index.html` 을 서빙한다. 그 페이지가 콜백을 처리하지 않으면 URL 토큰이 영영
|
|
3989
|
+
* 소비되지 않아 "로그인 → 다시 로그인 페이지" 무한 루프가 발생한다 (여러 사용자 동시 보고).
|
|
3990
|
+
* 이 안전망은 콜백 처리 코드를 빠뜨린 앱도 동작하게 한다.
|
|
3991
|
+
*
|
|
3992
|
+
* **token rotation race(2026-05-16) 가 없는 이유:** 사전 `re-issue`(cookie 복구) 없이 URL
|
|
3993
|
+
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업
|
|
3994
|
+
* (`window.opener` 존재)은 `ConnectBase` 생성자에서 제외되어 호출되지 않는다 (팝업 콜백은
|
|
3995
|
+
* `getCallbackResult()` 의 postMessage 경로 전담). code-only(`?code=`)·에러(`?error=`)
|
|
3996
|
+
* 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
3997
|
+
*
|
|
3998
|
+
* @returns 토큰 적재 성공 시 true, no-op(토큰 없음 등)이면 false.
|
|
3999
|
+
*/
|
|
4000
|
+
consumeRedirectCallbackOnBoot(): Promise<boolean>;
|
|
4001
|
+
private doConsumeRedirectOnBoot;
|
|
3967
4002
|
/**
|
|
3968
4003
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
3969
4004
|
*
|
|
@@ -8805,9 +8840,18 @@ declare class ConnectBase {
|
|
|
8805
8840
|
*
|
|
8806
8841
|
* `?access_token=...&refresh_token=...` (legacy 토큰-in-URL 흐름) 또는
|
|
8807
8842
|
* `?code=...` (OAUTH_CODE_ONLY 흐름) 또는 OAuth 에러 응답 (`?error=...&state=...`)
|
|
8808
|
-
* 패턴이면 true. 콜백 페이지에서 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
8843
|
+
* 패턴이면 true. 콜백 페이지에서 일반 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
8809
8844
|
*/
|
|
8810
8845
|
private isOAuthCallbackUrl;
|
|
8846
|
+
/**
|
|
8847
|
+
* 현재 페이지가 토큰-in-URL OAuth 리다이렉트 콜백(팝업 아님)인지 판별.
|
|
8848
|
+
*
|
|
8849
|
+
* `?access_token=&refresh_token=` 이 있고 `window.opener` 가 없으면 true — 부팅 안전망
|
|
8850
|
+
* (`oauth.consumeRedirectCallbackOnBoot`)으로 토큰을 자동 소비할 대상이다. 팝업 콜백
|
|
8851
|
+
* (`window.opener` 존재)은 `getCallbackResult()` 의 postMessage 경로가 전담하므로 제외한다.
|
|
8852
|
+
* code-only(`?code=`)·에러(`?error=`) 콜백도 제외(네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
8853
|
+
*/
|
|
8854
|
+
private isOAuthRedirectCallbackUrl;
|
|
8811
8855
|
/**
|
|
8812
8856
|
* 새로고침/탭 재개 후 cookie 만으로 세션을 복원한다 (브라우저 전용).
|
|
8813
8857
|
*
|
package/dist/index.js
CHANGED
|
@@ -451,6 +451,15 @@ var HttpClient = class {
|
|
|
451
451
|
this.refreshLockedUntil = 0;
|
|
452
452
|
return null;
|
|
453
453
|
}
|
|
454
|
+
if (this.config.appId) {
|
|
455
|
+
const tokenAppId = decodeJwtPayload(data.access_token)?.app_id;
|
|
456
|
+
if (typeof tokenAppId === "string" && tokenAppId !== this.config.appId) {
|
|
457
|
+
this.clearTokens();
|
|
458
|
+
this.refreshFailureCount = 0;
|
|
459
|
+
this.refreshLockedUntil = 0;
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
454
463
|
const nextRefreshToken = typeof data.refresh_token === "string" && data.refresh_token.length > 0 ? data.refresh_token : this.config.refreshToken ?? "";
|
|
455
464
|
if (nextRefreshToken) {
|
|
456
465
|
this.setTokens(data.access_token, nextRefreshToken);
|
|
@@ -4801,6 +4810,12 @@ function escapeToExternalBrowser(currentUrl) {
|
|
|
4801
4810
|
var OAuthAPI = class {
|
|
4802
4811
|
constructor(http) {
|
|
4803
4812
|
this.http = http;
|
|
4813
|
+
/**
|
|
4814
|
+
* 부팅 안전망(consumeRedirectCallbackOnBoot)이 리다이렉트 콜백을 자동 소비했을 때의 promise.
|
|
4815
|
+
* 앱이 그 후 `getCallbackResult()` 를 호출해도 중복으로 `bootstrapRefreshCookie` 를
|
|
4816
|
+
* 발화하지 않도록 이 promise 를 공유한다 (이중 re-issue/rotation 방지).
|
|
4817
|
+
*/
|
|
4818
|
+
this.bootConsumePromise = null;
|
|
4804
4819
|
}
|
|
4805
4820
|
/**
|
|
4806
4821
|
* 활성화된 OAuth 프로바이더 목록 조회
|
|
@@ -5095,10 +5110,58 @@ var OAuthAPI = class {
|
|
|
5095
5110
|
window.close();
|
|
5096
5111
|
return result;
|
|
5097
5112
|
}
|
|
5113
|
+
if (this.bootConsumePromise) {
|
|
5114
|
+
const consumed = await this.bootConsumePromise;
|
|
5115
|
+
if (consumed) {
|
|
5116
|
+
return result;
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5098
5119
|
this.http.setTokens(accessToken, refreshToken);
|
|
5099
5120
|
await this.http.bootstrapRefreshCookie();
|
|
5100
5121
|
return result;
|
|
5101
5122
|
}
|
|
5123
|
+
/**
|
|
5124
|
+
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 부팅 안전망.
|
|
5125
|
+
*
|
|
5126
|
+
* OAuth 리다이렉트 콜백 페이지에서 앱이 `getCallbackResult()` 를 호출하지 않아도,
|
|
5127
|
+
* `new ConnectBase()` 만으로 URL 의 토큰(`?access_token=&refresh_token=&member_id=`)을
|
|
5128
|
+
* 적재해 세션을 확립한다.
|
|
5129
|
+
*
|
|
5130
|
+
* **왜 필요한가:** 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 콜백
|
|
5131
|
+
* 경로에 홈 `index.html` 을 서빙한다. 그 페이지가 콜백을 처리하지 않으면 URL 토큰이 영영
|
|
5132
|
+
* 소비되지 않아 "로그인 → 다시 로그인 페이지" 무한 루프가 발생한다 (여러 사용자 동시 보고).
|
|
5133
|
+
* 이 안전망은 콜백 처리 코드를 빠뜨린 앱도 동작하게 한다.
|
|
5134
|
+
*
|
|
5135
|
+
* **token rotation race(2026-05-16) 가 없는 이유:** 사전 `re-issue`(cookie 복구) 없이 URL
|
|
5136
|
+
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업
|
|
5137
|
+
* (`window.opener` 존재)은 `ConnectBase` 생성자에서 제외되어 호출되지 않는다 (팝업 콜백은
|
|
5138
|
+
* `getCallbackResult()` 의 postMessage 경로 전담). code-only(`?code=`)·에러(`?error=`)
|
|
5139
|
+
* 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
5140
|
+
*
|
|
5141
|
+
* @returns 토큰 적재 성공 시 true, no-op(토큰 없음 등)이면 false.
|
|
5142
|
+
*/
|
|
5143
|
+
consumeRedirectCallbackOnBoot() {
|
|
5144
|
+
if (!this.bootConsumePromise) {
|
|
5145
|
+
this.bootConsumePromise = this.doConsumeRedirectOnBoot();
|
|
5146
|
+
}
|
|
5147
|
+
return this.bootConsumePromise;
|
|
5148
|
+
}
|
|
5149
|
+
async doConsumeRedirectOnBoot() {
|
|
5150
|
+
try {
|
|
5151
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
5152
|
+
const params = new URLSearchParams(window.location.search);
|
|
5153
|
+
if (params.get("error")) return false;
|
|
5154
|
+
const accessToken = params.get("access_token");
|
|
5155
|
+
const refreshToken = params.get("refresh_token");
|
|
5156
|
+
const memberId = params.get("member_id");
|
|
5157
|
+
if (!accessToken || !refreshToken || !memberId) return false;
|
|
5158
|
+
this.http.setTokens(accessToken, refreshToken);
|
|
5159
|
+
await this.http.bootstrapRefreshCookie();
|
|
5160
|
+
return true;
|
|
5161
|
+
} catch {
|
|
5162
|
+
return false;
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5102
5165
|
/**
|
|
5103
5166
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
5104
5167
|
*
|
|
@@ -10826,6 +10889,7 @@ var ConnectBase = class {
|
|
|
10826
10889
|
baseUrl: config.baseUrl || env("CB_BASE_URL") || DEFAULT_BASE_URL,
|
|
10827
10890
|
publicKey: config.publicKey,
|
|
10828
10891
|
secretKey: config.secretKey,
|
|
10892
|
+
appId: config.appId,
|
|
10829
10893
|
persistence: config.persistence,
|
|
10830
10894
|
autoRestoreSession: config.autoRestoreSession,
|
|
10831
10895
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
@@ -10860,8 +10924,12 @@ var ConnectBase = class {
|
|
|
10860
10924
|
this.support = new SupportAPI(this.http);
|
|
10861
10925
|
this.auth._attachAnalytics(this.analytics);
|
|
10862
10926
|
const shouldAutoRestore = config.autoRestoreSession ?? true;
|
|
10863
|
-
if (shouldAutoRestore && typeof window !== "undefined"
|
|
10864
|
-
|
|
10927
|
+
if (shouldAutoRestore && typeof window !== "undefined") {
|
|
10928
|
+
if (this.isOAuthRedirectCallbackUrl()) {
|
|
10929
|
+
this.http.setBootRestorePromise(this.oauth.consumeRedirectCallbackOnBoot());
|
|
10930
|
+
} else if (!this.isOAuthCallbackUrl()) {
|
|
10931
|
+
this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
|
|
10932
|
+
}
|
|
10865
10933
|
}
|
|
10866
10934
|
}
|
|
10867
10935
|
/**
|
|
@@ -10869,7 +10937,7 @@ var ConnectBase = class {
|
|
|
10869
10937
|
*
|
|
10870
10938
|
* `?access_token=...&refresh_token=...` (legacy 토큰-in-URL 흐름) 또는
|
|
10871
10939
|
* `?code=...` (OAUTH_CODE_ONLY 흐름) 또는 OAuth 에러 응답 (`?error=...&state=...`)
|
|
10872
|
-
* 패턴이면 true. 콜백 페이지에서 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
10940
|
+
* 패턴이면 true. 콜백 페이지에서 일반 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
10873
10941
|
*/
|
|
10874
10942
|
isOAuthCallbackUrl() {
|
|
10875
10943
|
if (typeof window === "undefined" || !window.location) return false;
|
|
@@ -10879,6 +10947,20 @@ var ConnectBase = class {
|
|
|
10879
10947
|
if (params.has("error") && params.has("state")) return true;
|
|
10880
10948
|
return false;
|
|
10881
10949
|
}
|
|
10950
|
+
/**
|
|
10951
|
+
* 현재 페이지가 토큰-in-URL OAuth 리다이렉트 콜백(팝업 아님)인지 판별.
|
|
10952
|
+
*
|
|
10953
|
+
* `?access_token=&refresh_token=` 이 있고 `window.opener` 가 없으면 true — 부팅 안전망
|
|
10954
|
+
* (`oauth.consumeRedirectCallbackOnBoot`)으로 토큰을 자동 소비할 대상이다. 팝업 콜백
|
|
10955
|
+
* (`window.opener` 존재)은 `getCallbackResult()` 의 postMessage 경로가 전담하므로 제외한다.
|
|
10956
|
+
* code-only(`?code=`)·에러(`?error=`) 콜백도 제외(네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
10957
|
+
*/
|
|
10958
|
+
isOAuthRedirectCallbackUrl() {
|
|
10959
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
10960
|
+
if (window.opener) return false;
|
|
10961
|
+
const params = new URLSearchParams(window.location.search);
|
|
10962
|
+
return params.has("access_token") && params.has("refresh_token");
|
|
10963
|
+
}
|
|
10882
10964
|
/**
|
|
10883
10965
|
* 새로고침/탭 재개 후 cookie 만으로 세션을 복원한다 (브라우저 전용).
|
|
10884
10966
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -405,6 +405,15 @@ var HttpClient = class {
|
|
|
405
405
|
this.refreshLockedUntil = 0;
|
|
406
406
|
return null;
|
|
407
407
|
}
|
|
408
|
+
if (this.config.appId) {
|
|
409
|
+
const tokenAppId = decodeJwtPayload(data.access_token)?.app_id;
|
|
410
|
+
if (typeof tokenAppId === "string" && tokenAppId !== this.config.appId) {
|
|
411
|
+
this.clearTokens();
|
|
412
|
+
this.refreshFailureCount = 0;
|
|
413
|
+
this.refreshLockedUntil = 0;
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
408
417
|
const nextRefreshToken = typeof data.refresh_token === "string" && data.refresh_token.length > 0 ? data.refresh_token : this.config.refreshToken ?? "";
|
|
409
418
|
if (nextRefreshToken) {
|
|
410
419
|
this.setTokens(data.access_token, nextRefreshToken);
|
|
@@ -4755,6 +4764,12 @@ function escapeToExternalBrowser(currentUrl) {
|
|
|
4755
4764
|
var OAuthAPI = class {
|
|
4756
4765
|
constructor(http) {
|
|
4757
4766
|
this.http = http;
|
|
4767
|
+
/**
|
|
4768
|
+
* 부팅 안전망(consumeRedirectCallbackOnBoot)이 리다이렉트 콜백을 자동 소비했을 때의 promise.
|
|
4769
|
+
* 앱이 그 후 `getCallbackResult()` 를 호출해도 중복으로 `bootstrapRefreshCookie` 를
|
|
4770
|
+
* 발화하지 않도록 이 promise 를 공유한다 (이중 re-issue/rotation 방지).
|
|
4771
|
+
*/
|
|
4772
|
+
this.bootConsumePromise = null;
|
|
4758
4773
|
}
|
|
4759
4774
|
/**
|
|
4760
4775
|
* 활성화된 OAuth 프로바이더 목록 조회
|
|
@@ -5049,10 +5064,58 @@ var OAuthAPI = class {
|
|
|
5049
5064
|
window.close();
|
|
5050
5065
|
return result;
|
|
5051
5066
|
}
|
|
5067
|
+
if (this.bootConsumePromise) {
|
|
5068
|
+
const consumed = await this.bootConsumePromise;
|
|
5069
|
+
if (consumed) {
|
|
5070
|
+
return result;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5052
5073
|
this.http.setTokens(accessToken, refreshToken);
|
|
5053
5074
|
await this.http.bootstrapRefreshCookie();
|
|
5054
5075
|
return result;
|
|
5055
5076
|
}
|
|
5077
|
+
/**
|
|
5078
|
+
* (SDK 내부용 — `ConnectBase` 생성자가 호출) 부팅 안전망.
|
|
5079
|
+
*
|
|
5080
|
+
* OAuth 리다이렉트 콜백 페이지에서 앱이 `getCallbackResult()` 를 호출하지 않아도,
|
|
5081
|
+
* `new ConnectBase()` 만으로 URL 의 토큰(`?access_token=&refresh_token=&member_id=`)을
|
|
5082
|
+
* 적재해 세션을 확립한다.
|
|
5083
|
+
*
|
|
5084
|
+
* **왜 필요한가:** 정적 호스팅(웹 스토리지)은 SPA fallback 으로 `/auth/callback` 같은 콜백
|
|
5085
|
+
* 경로에 홈 `index.html` 을 서빙한다. 그 페이지가 콜백을 처리하지 않으면 URL 토큰이 영영
|
|
5086
|
+
* 소비되지 않아 "로그인 → 다시 로그인 페이지" 무한 루프가 발생한다 (여러 사용자 동시 보고).
|
|
5087
|
+
* 이 안전망은 콜백 처리 코드를 빠뜨린 앱도 동작하게 한다.
|
|
5088
|
+
*
|
|
5089
|
+
* **token rotation race(2026-05-16) 가 없는 이유:** 사전 `re-issue`(cookie 복구) 없이 URL
|
|
5090
|
+
* 토큰을 *직접* 소비한다 — `getCallbackResult()` 의 리다이렉트 경로와 동일한 동작이다. 팝업
|
|
5091
|
+
* (`window.opener` 존재)은 `ConnectBase` 생성자에서 제외되어 호출되지 않는다 (팝업 콜백은
|
|
5092
|
+
* `getCallbackResult()` 의 postMessage 경로 전담). code-only(`?code=`)·에러(`?error=`)
|
|
5093
|
+
* 콜백도 생성자에서 제외된다 (네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
5094
|
+
*
|
|
5095
|
+
* @returns 토큰 적재 성공 시 true, no-op(토큰 없음 등)이면 false.
|
|
5096
|
+
*/
|
|
5097
|
+
consumeRedirectCallbackOnBoot() {
|
|
5098
|
+
if (!this.bootConsumePromise) {
|
|
5099
|
+
this.bootConsumePromise = this.doConsumeRedirectOnBoot();
|
|
5100
|
+
}
|
|
5101
|
+
return this.bootConsumePromise;
|
|
5102
|
+
}
|
|
5103
|
+
async doConsumeRedirectOnBoot() {
|
|
5104
|
+
try {
|
|
5105
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
5106
|
+
const params = new URLSearchParams(window.location.search);
|
|
5107
|
+
if (params.get("error")) return false;
|
|
5108
|
+
const accessToken = params.get("access_token");
|
|
5109
|
+
const refreshToken = params.get("refresh_token");
|
|
5110
|
+
const memberId = params.get("member_id");
|
|
5111
|
+
if (!accessToken || !refreshToken || !memberId) return false;
|
|
5112
|
+
this.http.setTokens(accessToken, refreshToken);
|
|
5113
|
+
await this.http.bootstrapRefreshCookie();
|
|
5114
|
+
return true;
|
|
5115
|
+
} catch {
|
|
5116
|
+
return false;
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5056
5119
|
/**
|
|
5057
5120
|
* 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
|
|
5058
5121
|
*
|
|
@@ -10780,6 +10843,7 @@ var ConnectBase = class {
|
|
|
10780
10843
|
baseUrl: config.baseUrl || env("CB_BASE_URL") || DEFAULT_BASE_URL,
|
|
10781
10844
|
publicKey: config.publicKey,
|
|
10782
10845
|
secretKey: config.secretKey,
|
|
10846
|
+
appId: config.appId,
|
|
10783
10847
|
persistence: config.persistence,
|
|
10784
10848
|
autoRestoreSession: config.autoRestoreSession,
|
|
10785
10849
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
@@ -10814,8 +10878,12 @@ var ConnectBase = class {
|
|
|
10814
10878
|
this.support = new SupportAPI(this.http);
|
|
10815
10879
|
this.auth._attachAnalytics(this.analytics);
|
|
10816
10880
|
const shouldAutoRestore = config.autoRestoreSession ?? true;
|
|
10817
|
-
if (shouldAutoRestore && typeof window !== "undefined"
|
|
10818
|
-
|
|
10881
|
+
if (shouldAutoRestore && typeof window !== "undefined") {
|
|
10882
|
+
if (this.isOAuthRedirectCallbackUrl()) {
|
|
10883
|
+
this.http.setBootRestorePromise(this.oauth.consumeRedirectCallbackOnBoot());
|
|
10884
|
+
} else if (!this.isOAuthCallbackUrl()) {
|
|
10885
|
+
this.http.setBootRestorePromise(this.http.tryRestoreSessionFromCookie());
|
|
10886
|
+
}
|
|
10819
10887
|
}
|
|
10820
10888
|
}
|
|
10821
10889
|
/**
|
|
@@ -10823,7 +10891,7 @@ var ConnectBase = class {
|
|
|
10823
10891
|
*
|
|
10824
10892
|
* `?access_token=...&refresh_token=...` (legacy 토큰-in-URL 흐름) 또는
|
|
10825
10893
|
* `?code=...` (OAUTH_CODE_ONLY 흐름) 또는 OAuth 에러 응답 (`?error=...&state=...`)
|
|
10826
|
-
* 패턴이면 true. 콜백 페이지에서 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
10894
|
+
* 패턴이면 true. 콜백 페이지에서 일반 `autoRestoreSession` 을 건너뛰어 rotation race 를 피한다.
|
|
10827
10895
|
*/
|
|
10828
10896
|
isOAuthCallbackUrl() {
|
|
10829
10897
|
if (typeof window === "undefined" || !window.location) return false;
|
|
@@ -10833,6 +10901,20 @@ var ConnectBase = class {
|
|
|
10833
10901
|
if (params.has("error") && params.has("state")) return true;
|
|
10834
10902
|
return false;
|
|
10835
10903
|
}
|
|
10904
|
+
/**
|
|
10905
|
+
* 현재 페이지가 토큰-in-URL OAuth 리다이렉트 콜백(팝업 아님)인지 판별.
|
|
10906
|
+
*
|
|
10907
|
+
* `?access_token=&refresh_token=` 이 있고 `window.opener` 가 없으면 true — 부팅 안전망
|
|
10908
|
+
* (`oauth.consumeRedirectCallbackOnBoot`)으로 토큰을 자동 소비할 대상이다. 팝업 콜백
|
|
10909
|
+
* (`window.opener` 존재)은 `getCallbackResult()` 의 postMessage 경로가 전담하므로 제외한다.
|
|
10910
|
+
* code-only(`?code=`)·에러(`?error=`) 콜백도 제외(네트워크 교환/에러 분기는 앱이 명시 호출).
|
|
10911
|
+
*/
|
|
10912
|
+
isOAuthRedirectCallbackUrl() {
|
|
10913
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
10914
|
+
if (window.opener) return false;
|
|
10915
|
+
const params = new URLSearchParams(window.location.search);
|
|
10916
|
+
return params.has("access_token") && params.has("refresh_token");
|
|
10917
|
+
}
|
|
10836
10918
|
/**
|
|
10837
10919
|
* 새로고침/탭 재개 후 cookie 만으로 세션을 복원한다 (브라우저 전용).
|
|
10838
10920
|
*
|