connectbase-client 5.10.0 → 5.12.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 +80 -0
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +123 -9
- package/dist/index.mjs +123 -9
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -264,6 +264,18 @@ declare class HttpClient {
|
|
|
264
264
|
*/
|
|
265
265
|
private bootRestorePromise;
|
|
266
266
|
private bootRestoreAlwaysAwait;
|
|
267
|
+
/**
|
|
268
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
269
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
270
|
+
*/
|
|
271
|
+
private directBaseUrl;
|
|
272
|
+
/**
|
|
273
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
274
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
275
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
276
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
277
|
+
*/
|
|
278
|
+
private directDisabled;
|
|
267
279
|
/**
|
|
268
280
|
* 클라이언트 생성 시 명시된 요청 타임아웃(ms). 미지정이면 `undefined`.
|
|
269
281
|
*
|
|
@@ -406,6 +418,41 @@ declare class HttpClient {
|
|
|
406
418
|
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
407
419
|
*/
|
|
408
420
|
private doFetch;
|
|
421
|
+
/**
|
|
422
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
423
|
+
*
|
|
424
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
425
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
426
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
427
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
428
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
429
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
430
|
+
*/
|
|
431
|
+
private resolveOrigin;
|
|
432
|
+
/**
|
|
433
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
434
|
+
*
|
|
435
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
436
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
437
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
438
|
+
* 즉시 회수할 수도 있다.
|
|
439
|
+
*
|
|
440
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
441
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
442
|
+
* 일은 없어야 한다.
|
|
443
|
+
*/
|
|
444
|
+
private learnDirectOrigin;
|
|
445
|
+
/**
|
|
446
|
+
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
447
|
+
*
|
|
448
|
+
* `cb.endpoint.call()`(`/v1/proxy/*`) 전용이다. 그 경로는 dumb pipe 라 요청 헤더가
|
|
449
|
+
* 사용자 PC 의 모델 서버로 그대로 전달된다 — `prepareHeaders()` 를 태우면 SDK 의
|
|
450
|
+
* `Authorization` 이 남의 서버로 나가고, 호출자가 자기 모델 서버용으로 설정한
|
|
451
|
+
* `Authorization` 을 덮어쓴다. 그래서 fetchRaw 를 재사용하지 않는다.
|
|
452
|
+
*
|
|
453
|
+
* @param path `/v1/proxy/...` 형태의 절대 경로 (origin 없이)
|
|
454
|
+
*/
|
|
455
|
+
fetchPassthrough(path: string, init: RequestInit): Promise<Response>;
|
|
409
456
|
private tryFetchOnce;
|
|
410
457
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
411
458
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -3110,6 +3157,8 @@ declare class EndpointAPI {
|
|
|
3110
3157
|
* @param init - fetch() 의 RequestInit + path. path 는 사용자 모델 서버의 엔드포인트 경로 (예: "/prompt", "/v1/chat/completions").
|
|
3111
3158
|
*/
|
|
3112
3159
|
call(label: string, init: EndpointCallInit): Promise<Response>;
|
|
3160
|
+
/** `/v1/proxy/<label><path>` 절대 경로(origin 없이). URL 조립 규칙의 단일 출처. */
|
|
3161
|
+
private proxyPath;
|
|
3113
3162
|
/**
|
|
3114
3163
|
* 라벨 + path 의 최종 호출 URL `${baseUrl}/v1/proxy/${label}${path}` 을 조립해서
|
|
3115
3164
|
* 반환. URL 을 다른 시스템 (Service Worker, 백엔드 워커, 로깅) 에 넘기거나
|
package/dist/index.d.ts
CHANGED
|
@@ -264,6 +264,18 @@ declare class HttpClient {
|
|
|
264
264
|
*/
|
|
265
265
|
private bootRestorePromise;
|
|
266
266
|
private bootRestoreAlwaysAwait;
|
|
267
|
+
/**
|
|
268
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
269
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
270
|
+
*/
|
|
271
|
+
private directBaseUrl;
|
|
272
|
+
/**
|
|
273
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
274
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
275
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
276
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
277
|
+
*/
|
|
278
|
+
private directDisabled;
|
|
267
279
|
/**
|
|
268
280
|
* 클라이언트 생성 시 명시된 요청 타임아웃(ms). 미지정이면 `undefined`.
|
|
269
281
|
*
|
|
@@ -406,6 +418,41 @@ declare class HttpClient {
|
|
|
406
418
|
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
407
419
|
*/
|
|
408
420
|
private doFetch;
|
|
421
|
+
/**
|
|
422
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
423
|
+
*
|
|
424
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
425
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
426
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
427
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
428
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
429
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
430
|
+
*/
|
|
431
|
+
private resolveOrigin;
|
|
432
|
+
/**
|
|
433
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
434
|
+
*
|
|
435
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
436
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
437
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
438
|
+
* 즉시 회수할 수도 있다.
|
|
439
|
+
*
|
|
440
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
441
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
442
|
+
* 일은 없어야 한다.
|
|
443
|
+
*/
|
|
444
|
+
private learnDirectOrigin;
|
|
445
|
+
/**
|
|
446
|
+
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
447
|
+
*
|
|
448
|
+
* `cb.endpoint.call()`(`/v1/proxy/*`) 전용이다. 그 경로는 dumb pipe 라 요청 헤더가
|
|
449
|
+
* 사용자 PC 의 모델 서버로 그대로 전달된다 — `prepareHeaders()` 를 태우면 SDK 의
|
|
450
|
+
* `Authorization` 이 남의 서버로 나가고, 호출자가 자기 모델 서버용으로 설정한
|
|
451
|
+
* `Authorization` 을 덮어쓴다. 그래서 fetchRaw 를 재사용하지 않는다.
|
|
452
|
+
*
|
|
453
|
+
* @param path `/v1/proxy/...` 형태의 절대 경로 (origin 없이)
|
|
454
|
+
*/
|
|
455
|
+
fetchPassthrough(path: string, init: RequestInit): Promise<Response>;
|
|
409
456
|
private tryFetchOnce;
|
|
410
457
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
411
458
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -3110,6 +3157,8 @@ declare class EndpointAPI {
|
|
|
3110
3157
|
* @param init - fetch() 의 RequestInit + path. path 는 사용자 모델 서버의 엔드포인트 경로 (예: "/prompt", "/v1/chat/completions").
|
|
3111
3158
|
*/
|
|
3112
3159
|
call(label: string, init: EndpointCallInit): Promise<Response>;
|
|
3160
|
+
/** `/v1/proxy/<label><path>` 절대 경로(origin 없이). URL 조립 규칙의 단일 출처. */
|
|
3161
|
+
private proxyPath;
|
|
3113
3162
|
/**
|
|
3114
3163
|
* 라벨 + path 의 최종 호출 URL `${baseUrl}/v1/proxy/${label}${path}` 을 조립해서
|
|
3115
3164
|
* 반환. URL 을 다른 시스템 (Service Worker, 백엔드 워커, 로깅) 에 넘기거나
|
package/dist/index.js
CHANGED
|
@@ -3121,7 +3121,7 @@ var EndpointAPI = class {
|
|
|
3121
3121
|
`EndpointAPI.call: path must start with '/', got ${JSON.stringify(init.path)}`
|
|
3122
3122
|
);
|
|
3123
3123
|
}
|
|
3124
|
-
const
|
|
3124
|
+
const path = this.proxyPath(label, init.path);
|
|
3125
3125
|
const headers = new Headers(init.headers ?? {});
|
|
3126
3126
|
if (!headers.has("X-Public-Key")) {
|
|
3127
3127
|
const pk = this.http.getPublicKey();
|
|
@@ -3132,7 +3132,7 @@ var EndpointAPI = class {
|
|
|
3132
3132
|
}
|
|
3133
3133
|
headers.set("X-Public-Key", pk);
|
|
3134
3134
|
}
|
|
3135
|
-
return
|
|
3135
|
+
return this.http.fetchPassthrough(path, {
|
|
3136
3136
|
method: init.method ?? "GET",
|
|
3137
3137
|
headers,
|
|
3138
3138
|
body: init.body,
|
|
@@ -3141,6 +3141,16 @@ var EndpointAPI = class {
|
|
|
3141
3141
|
redirect: "follow"
|
|
3142
3142
|
});
|
|
3143
3143
|
}
|
|
3144
|
+
/** `/v1/proxy/<label><path>` 절대 경로(origin 없이). URL 조립 규칙의 단일 출처. */
|
|
3145
|
+
proxyPath(label, path) {
|
|
3146
|
+
if (!label) throw new Error("EndpointAPI.call: label required");
|
|
3147
|
+
if (!path || !path.startsWith("/")) {
|
|
3148
|
+
throw new Error(
|
|
3149
|
+
`EndpointAPI.call: path must start with '/', got ${JSON.stringify(path)}`
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
return `/v1/proxy/${encodeURIComponent(label)}${path}`;
|
|
3153
|
+
}
|
|
3144
3154
|
/**
|
|
3145
3155
|
* 라벨 + path 의 최종 호출 URL `${baseUrl}/v1/proxy/${label}${path}` 을 조립해서
|
|
3146
3156
|
* 반환. URL 을 다른 시스템 (Service Worker, 백엔드 워커, 로깅) 에 넘기거나
|
|
@@ -11441,12 +11451,15 @@ var COOKIE_BEARING_PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
|
11441
11451
|
"/v1/public/app-members/signin",
|
|
11442
11452
|
"/v1/public/app-members/signout"
|
|
11443
11453
|
]);
|
|
11454
|
+
function isAbortError(err) {
|
|
11455
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
11456
|
+
}
|
|
11444
11457
|
function fetchCredentialsForPath(url) {
|
|
11445
11458
|
const path = url.split("?")[0];
|
|
11446
11459
|
if (COOKIE_BEARING_PUBLIC_PATHS.has(path)) {
|
|
11447
11460
|
return "include";
|
|
11448
11461
|
}
|
|
11449
|
-
return path.startsWith("/v1/public/") ? "omit" : "include";
|
|
11462
|
+
return path.startsWith("/v1/public/") || path.startsWith("/v1/proxy/") ? "omit" : "include";
|
|
11450
11463
|
}
|
|
11451
11464
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
11452
11465
|
function gatewayCodeFromStatus(status) {
|
|
@@ -11512,6 +11525,18 @@ var HttpClient = class {
|
|
|
11512
11525
|
// OAuth 콜백 부트스트랩처럼 세션 힌트 유무와 무관하게 첫 인증 호출이 반드시 기다려야
|
|
11513
11526
|
// 하는 boot promise 인지 여부 (setBootRestorePromise 의 alwaysAwait 옵션).
|
|
11514
11527
|
this.bootRestoreAlwaysAwait = false;
|
|
11528
|
+
/**
|
|
11529
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
11530
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
11531
|
+
*/
|
|
11532
|
+
this.directBaseUrl = null;
|
|
11533
|
+
/**
|
|
11534
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
11535
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
11536
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
11537
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
11538
|
+
*/
|
|
11539
|
+
this.directDisabled = false;
|
|
11515
11540
|
this.config = { ...config };
|
|
11516
11541
|
this.storageKey = this.buildStorageKey();
|
|
11517
11542
|
this.warnIfUnsafePersistence();
|
|
@@ -12102,18 +12127,97 @@ var HttpClient = class {
|
|
|
12102
12127
|
});
|
|
12103
12128
|
}
|
|
12104
12129
|
}
|
|
12130
|
+
/**
|
|
12131
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
12132
|
+
*
|
|
12133
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
12134
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
12135
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
12136
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
12137
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
12138
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
12139
|
+
*/
|
|
12140
|
+
resolveOrigin(url) {
|
|
12141
|
+
if (this.directBaseUrl && !this.directDisabled && fetchCredentialsForPath(url) === "omit") {
|
|
12142
|
+
return this.directBaseUrl;
|
|
12143
|
+
}
|
|
12144
|
+
return this.config.baseUrl;
|
|
12145
|
+
}
|
|
12146
|
+
/**
|
|
12147
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
12148
|
+
*
|
|
12149
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
12150
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
12151
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
12152
|
+
* 즉시 회수할 수도 있다.
|
|
12153
|
+
*
|
|
12154
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
12155
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
12156
|
+
* 일은 없어야 한다.
|
|
12157
|
+
*/
|
|
12158
|
+
learnDirectOrigin(response) {
|
|
12159
|
+
if (this.directDisabled || this.directBaseUrl) return;
|
|
12160
|
+
const advertised = response.headers?.get("X-CB-Direct-Base");
|
|
12161
|
+
if (!advertised) return;
|
|
12162
|
+
try {
|
|
12163
|
+
const next = new URL(advertised);
|
|
12164
|
+
const base = new URL(this.config.baseUrl);
|
|
12165
|
+
if (next.protocol !== "https:") return;
|
|
12166
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
12167
|
+
if (tail(next.hostname) !== tail(base.hostname)) return;
|
|
12168
|
+
this.directBaseUrl = next.origin;
|
|
12169
|
+
} catch {
|
|
12170
|
+
}
|
|
12171
|
+
}
|
|
12172
|
+
/**
|
|
12173
|
+
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
12174
|
+
*
|
|
12175
|
+
* `cb.endpoint.call()`(`/v1/proxy/*`) 전용이다. 그 경로는 dumb pipe 라 요청 헤더가
|
|
12176
|
+
* 사용자 PC 의 모델 서버로 그대로 전달된다 — `prepareHeaders()` 를 태우면 SDK 의
|
|
12177
|
+
* `Authorization` 이 남의 서버로 나가고, 호출자가 자기 모델 서버용으로 설정한
|
|
12178
|
+
* `Authorization` 을 덮어쓴다. 그래서 fetchRaw 를 재사용하지 않는다.
|
|
12179
|
+
*
|
|
12180
|
+
* @param path `/v1/proxy/...` 형태의 절대 경로 (origin 없이)
|
|
12181
|
+
*/
|
|
12182
|
+
async fetchPassthrough(path, init) {
|
|
12183
|
+
const origin = this.resolveOrigin(path);
|
|
12184
|
+
try {
|
|
12185
|
+
const response = await fetch(`${origin}${path}`, init);
|
|
12186
|
+
this.learnDirectOrigin(response);
|
|
12187
|
+
return response;
|
|
12188
|
+
} catch (err) {
|
|
12189
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12190
|
+
this.directDisabled = true;
|
|
12191
|
+
return fetch(`${this.config.baseUrl}${path}`, init);
|
|
12192
|
+
}
|
|
12193
|
+
throw err;
|
|
12194
|
+
}
|
|
12195
|
+
}
|
|
12105
12196
|
async tryFetchOnce(url, init, config) {
|
|
12106
12197
|
const { signal, cleanup } = createTimeoutController({
|
|
12107
12198
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
12108
12199
|
signal: config?.signal
|
|
12109
12200
|
});
|
|
12201
|
+
const origin = this.resolveOrigin(url);
|
|
12110
12202
|
try {
|
|
12111
|
-
const response = await fetch(`${
|
|
12203
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12112
12204
|
...init,
|
|
12113
12205
|
credentials: fetchCredentialsForPath(url),
|
|
12114
12206
|
signal
|
|
12115
12207
|
});
|
|
12208
|
+
this.learnDirectOrigin(response);
|
|
12116
12209
|
return { response, ok: response.ok, status: response.status };
|
|
12210
|
+
} catch (err) {
|
|
12211
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12212
|
+
this.directDisabled = true;
|
|
12213
|
+
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
12214
|
+
...init,
|
|
12215
|
+
credentials: fetchCredentialsForPath(url),
|
|
12216
|
+
signal
|
|
12217
|
+
});
|
|
12218
|
+
return { response, ok: response.ok, status: response.status };
|
|
12219
|
+
}
|
|
12220
|
+
throw err;
|
|
12117
12221
|
} finally {
|
|
12118
12222
|
cleanup();
|
|
12119
12223
|
}
|
|
@@ -12180,11 +12284,21 @@ var HttpClient = class {
|
|
|
12180
12284
|
const extra = new Headers(init.headers);
|
|
12181
12285
|
extra.forEach((value, key) => mergedHeaders.set(key, value));
|
|
12182
12286
|
}
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12287
|
+
const origin = this.resolveOrigin(url);
|
|
12288
|
+
try {
|
|
12289
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12290
|
+
...init,
|
|
12291
|
+
credentials: fetchCredentialsForPath(url),
|
|
12292
|
+
headers: mergedHeaders
|
|
12293
|
+
});
|
|
12294
|
+
this.learnDirectOrigin(response);
|
|
12295
|
+
return response;
|
|
12296
|
+
} catch (err) {
|
|
12297
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12298
|
+
this.directDisabled = true;
|
|
12299
|
+
}
|
|
12300
|
+
throw err;
|
|
12301
|
+
}
|
|
12188
12302
|
}
|
|
12189
12303
|
};
|
|
12190
12304
|
|
package/dist/index.mjs
CHANGED
|
@@ -3072,7 +3072,7 @@ var EndpointAPI = class {
|
|
|
3072
3072
|
`EndpointAPI.call: path must start with '/', got ${JSON.stringify(init.path)}`
|
|
3073
3073
|
);
|
|
3074
3074
|
}
|
|
3075
|
-
const
|
|
3075
|
+
const path = this.proxyPath(label, init.path);
|
|
3076
3076
|
const headers = new Headers(init.headers ?? {});
|
|
3077
3077
|
if (!headers.has("X-Public-Key")) {
|
|
3078
3078
|
const pk = this.http.getPublicKey();
|
|
@@ -3083,7 +3083,7 @@ var EndpointAPI = class {
|
|
|
3083
3083
|
}
|
|
3084
3084
|
headers.set("X-Public-Key", pk);
|
|
3085
3085
|
}
|
|
3086
|
-
return
|
|
3086
|
+
return this.http.fetchPassthrough(path, {
|
|
3087
3087
|
method: init.method ?? "GET",
|
|
3088
3088
|
headers,
|
|
3089
3089
|
body: init.body,
|
|
@@ -3092,6 +3092,16 @@ var EndpointAPI = class {
|
|
|
3092
3092
|
redirect: "follow"
|
|
3093
3093
|
});
|
|
3094
3094
|
}
|
|
3095
|
+
/** `/v1/proxy/<label><path>` 절대 경로(origin 없이). URL 조립 규칙의 단일 출처. */
|
|
3096
|
+
proxyPath(label, path) {
|
|
3097
|
+
if (!label) throw new Error("EndpointAPI.call: label required");
|
|
3098
|
+
if (!path || !path.startsWith("/")) {
|
|
3099
|
+
throw new Error(
|
|
3100
|
+
`EndpointAPI.call: path must start with '/', got ${JSON.stringify(path)}`
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
return `/v1/proxy/${encodeURIComponent(label)}${path}`;
|
|
3104
|
+
}
|
|
3095
3105
|
/**
|
|
3096
3106
|
* 라벨 + path 의 최종 호출 URL `${baseUrl}/v1/proxy/${label}${path}` 을 조립해서
|
|
3097
3107
|
* 반환. URL 을 다른 시스템 (Service Worker, 백엔드 워커, 로깅) 에 넘기거나
|
|
@@ -11392,12 +11402,15 @@ var COOKIE_BEARING_PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
|
11392
11402
|
"/v1/public/app-members/signin",
|
|
11393
11403
|
"/v1/public/app-members/signout"
|
|
11394
11404
|
]);
|
|
11405
|
+
function isAbortError(err) {
|
|
11406
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
11407
|
+
}
|
|
11395
11408
|
function fetchCredentialsForPath(url) {
|
|
11396
11409
|
const path = url.split("?")[0];
|
|
11397
11410
|
if (COOKIE_BEARING_PUBLIC_PATHS.has(path)) {
|
|
11398
11411
|
return "include";
|
|
11399
11412
|
}
|
|
11400
|
-
return path.startsWith("/v1/public/") ? "omit" : "include";
|
|
11413
|
+
return path.startsWith("/v1/public/") || path.startsWith("/v1/proxy/") ? "omit" : "include";
|
|
11401
11414
|
}
|
|
11402
11415
|
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
11403
11416
|
function gatewayCodeFromStatus(status) {
|
|
@@ -11463,6 +11476,18 @@ var HttpClient = class {
|
|
|
11463
11476
|
// OAuth 콜백 부트스트랩처럼 세션 힌트 유무와 무관하게 첫 인증 호출이 반드시 기다려야
|
|
11464
11477
|
// 하는 boot promise 인지 여부 (setBootRestorePromise 의 alwaysAwait 옵션).
|
|
11465
11478
|
this.bootRestoreAlwaysAwait = false;
|
|
11479
|
+
/**
|
|
11480
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
11481
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
11482
|
+
*/
|
|
11483
|
+
this.directBaseUrl = null;
|
|
11484
|
+
/**
|
|
11485
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
11486
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
11487
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
11488
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
11489
|
+
*/
|
|
11490
|
+
this.directDisabled = false;
|
|
11466
11491
|
this.config = { ...config };
|
|
11467
11492
|
this.storageKey = this.buildStorageKey();
|
|
11468
11493
|
this.warnIfUnsafePersistence();
|
|
@@ -12053,18 +12078,97 @@ var HttpClient = class {
|
|
|
12053
12078
|
});
|
|
12054
12079
|
}
|
|
12055
12080
|
}
|
|
12081
|
+
/**
|
|
12082
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
12083
|
+
*
|
|
12084
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
12085
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
12086
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
12087
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
12088
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
12089
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
12090
|
+
*/
|
|
12091
|
+
resolveOrigin(url) {
|
|
12092
|
+
if (this.directBaseUrl && !this.directDisabled && fetchCredentialsForPath(url) === "omit") {
|
|
12093
|
+
return this.directBaseUrl;
|
|
12094
|
+
}
|
|
12095
|
+
return this.config.baseUrl;
|
|
12096
|
+
}
|
|
12097
|
+
/**
|
|
12098
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
12099
|
+
*
|
|
12100
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
12101
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
12102
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
12103
|
+
* 즉시 회수할 수도 있다.
|
|
12104
|
+
*
|
|
12105
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
12106
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
12107
|
+
* 일은 없어야 한다.
|
|
12108
|
+
*/
|
|
12109
|
+
learnDirectOrigin(response) {
|
|
12110
|
+
if (this.directDisabled || this.directBaseUrl) return;
|
|
12111
|
+
const advertised = response.headers?.get("X-CB-Direct-Base");
|
|
12112
|
+
if (!advertised) return;
|
|
12113
|
+
try {
|
|
12114
|
+
const next = new URL(advertised);
|
|
12115
|
+
const base = new URL(this.config.baseUrl);
|
|
12116
|
+
if (next.protocol !== "https:") return;
|
|
12117
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
12118
|
+
if (tail(next.hostname) !== tail(base.hostname)) return;
|
|
12119
|
+
this.directBaseUrl = next.origin;
|
|
12120
|
+
} catch {
|
|
12121
|
+
}
|
|
12122
|
+
}
|
|
12123
|
+
/**
|
|
12124
|
+
* SDK 인증 헤더를 **주입하지 않는** raw fetch. 저지연 원본 선택과 폴백만 얹는다.
|
|
12125
|
+
*
|
|
12126
|
+
* `cb.endpoint.call()`(`/v1/proxy/*`) 전용이다. 그 경로는 dumb pipe 라 요청 헤더가
|
|
12127
|
+
* 사용자 PC 의 모델 서버로 그대로 전달된다 — `prepareHeaders()` 를 태우면 SDK 의
|
|
12128
|
+
* `Authorization` 이 남의 서버로 나가고, 호출자가 자기 모델 서버용으로 설정한
|
|
12129
|
+
* `Authorization` 을 덮어쓴다. 그래서 fetchRaw 를 재사용하지 않는다.
|
|
12130
|
+
*
|
|
12131
|
+
* @param path `/v1/proxy/...` 형태의 절대 경로 (origin 없이)
|
|
12132
|
+
*/
|
|
12133
|
+
async fetchPassthrough(path, init) {
|
|
12134
|
+
const origin = this.resolveOrigin(path);
|
|
12135
|
+
try {
|
|
12136
|
+
const response = await fetch(`${origin}${path}`, init);
|
|
12137
|
+
this.learnDirectOrigin(response);
|
|
12138
|
+
return response;
|
|
12139
|
+
} catch (err) {
|
|
12140
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12141
|
+
this.directDisabled = true;
|
|
12142
|
+
return fetch(`${this.config.baseUrl}${path}`, init);
|
|
12143
|
+
}
|
|
12144
|
+
throw err;
|
|
12145
|
+
}
|
|
12146
|
+
}
|
|
12056
12147
|
async tryFetchOnce(url, init, config) {
|
|
12057
12148
|
const { signal, cleanup } = createTimeoutController({
|
|
12058
12149
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
12059
12150
|
signal: config?.signal
|
|
12060
12151
|
});
|
|
12152
|
+
const origin = this.resolveOrigin(url);
|
|
12061
12153
|
try {
|
|
12062
|
-
const response = await fetch(`${
|
|
12154
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12063
12155
|
...init,
|
|
12064
12156
|
credentials: fetchCredentialsForPath(url),
|
|
12065
12157
|
signal
|
|
12066
12158
|
});
|
|
12159
|
+
this.learnDirectOrigin(response);
|
|
12067
12160
|
return { response, ok: response.ok, status: response.status };
|
|
12161
|
+
} catch (err) {
|
|
12162
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12163
|
+
this.directDisabled = true;
|
|
12164
|
+
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
12165
|
+
...init,
|
|
12166
|
+
credentials: fetchCredentialsForPath(url),
|
|
12167
|
+
signal
|
|
12168
|
+
});
|
|
12169
|
+
return { response, ok: response.ok, status: response.status };
|
|
12170
|
+
}
|
|
12171
|
+
throw err;
|
|
12068
12172
|
} finally {
|
|
12069
12173
|
cleanup();
|
|
12070
12174
|
}
|
|
@@ -12131,11 +12235,21 @@ var HttpClient = class {
|
|
|
12131
12235
|
const extra = new Headers(init.headers);
|
|
12132
12236
|
extra.forEach((value, key) => mergedHeaders.set(key, value));
|
|
12133
12237
|
}
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12238
|
+
const origin = this.resolveOrigin(url);
|
|
12239
|
+
try {
|
|
12240
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12241
|
+
...init,
|
|
12242
|
+
credentials: fetchCredentialsForPath(url),
|
|
12243
|
+
headers: mergedHeaders
|
|
12244
|
+
});
|
|
12245
|
+
this.learnDirectOrigin(response);
|
|
12246
|
+
return response;
|
|
12247
|
+
} catch (err) {
|
|
12248
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12249
|
+
this.directDisabled = true;
|
|
12250
|
+
}
|
|
12251
|
+
throw err;
|
|
12252
|
+
}
|
|
12139
12253
|
}
|
|
12140
12254
|
};
|
|
12141
12255
|
|