connectbase-client 6.13.0 → 6.15.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 +52 -0
- package/README.md +17 -1
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.js +399 -64
- package/dist/index.mjs +398 -64
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ __export(index_exports, {
|
|
|
41
41
|
RolesAPI: () => RolesAPI,
|
|
42
42
|
SessionManager: () => SessionManager,
|
|
43
43
|
VideoProcessingError: () => VideoProcessingError,
|
|
44
|
+
WsTicketError: () => WsTicketError,
|
|
44
45
|
default: () => index_default,
|
|
45
46
|
detectInAppBrowser: () => detectInAppBrowser,
|
|
46
47
|
escapeToExternalBrowser: () => escapeToExternalBrowser,
|
|
@@ -170,9 +171,9 @@ var AdsAPI = class {
|
|
|
170
171
|
|
|
171
172
|
// src/types/error.ts
|
|
172
173
|
var ApiError = class extends Error {
|
|
173
|
-
constructor(
|
|
174
|
+
constructor(statusCode2, message, code, details) {
|
|
174
175
|
super(message);
|
|
175
|
-
this.statusCode =
|
|
176
|
+
this.statusCode = statusCode2;
|
|
176
177
|
this.name = "ApiError";
|
|
177
178
|
this.code = code;
|
|
178
179
|
this.details = details;
|
|
@@ -290,7 +291,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
290
291
|
this.name = "RealtimeConnectionError";
|
|
291
292
|
this.code = init.code || "unknown";
|
|
292
293
|
this.closeCode = init.closeCode;
|
|
293
|
-
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code);
|
|
294
|
+
this.retryable = init.retryable ?? isRetryableRealtimeRejection(this.code, init.closeCode);
|
|
294
295
|
this.details = init.details;
|
|
295
296
|
this.retryAfterMs = init.retryAfterMs;
|
|
296
297
|
}
|
|
@@ -307,8 +308,8 @@ var RealtimeConnectionError = class extends Error {
|
|
|
307
308
|
};
|
|
308
309
|
}
|
|
309
310
|
};
|
|
310
|
-
function isRetryableRealtimeRejection(code) {
|
|
311
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
311
|
+
function isRetryableRealtimeRejection(code, closeCode) {
|
|
312
|
+
return closeCode === 4429 || closeCode === 4500 || closeCode === 4503 || code === "connection_limit_exceeded" || code === "rate_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
312
313
|
}
|
|
313
314
|
var RealtimeRequestError = class extends Error {
|
|
314
315
|
constructor(init) {
|
|
@@ -3698,9 +3699,107 @@ var HttpClient = class {
|
|
|
3698
3699
|
}
|
|
3699
3700
|
};
|
|
3700
3701
|
|
|
3702
|
+
// src/core/ws-ticket.ts
|
|
3703
|
+
var WsTicketError = class extends Error {
|
|
3704
|
+
constructor(init) {
|
|
3705
|
+
super(
|
|
3706
|
+
init.message || init.code || `WebSocket ticket request failed (${init.status})`
|
|
3707
|
+
);
|
|
3708
|
+
this.name = "WsTicketError";
|
|
3709
|
+
this.status = init.status;
|
|
3710
|
+
this.code = init.code || "ws_ticket_rejected";
|
|
3711
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
var DEFAULT_TICKET_TIMEOUT_MS = 5e3;
|
|
3715
|
+
var unsupported = /* @__PURE__ */ new Set();
|
|
3716
|
+
function supportKey(url, kind) {
|
|
3717
|
+
try {
|
|
3718
|
+
const u = new URL(url);
|
|
3719
|
+
return `${u.origin}|${kind}`;
|
|
3720
|
+
} catch {
|
|
3721
|
+
return `${url}|${kind}`;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
async function fetchWsTicket(url, kind, credential, options = {}) {
|
|
3725
|
+
const key = supportKey(url, kind);
|
|
3726
|
+
if (unsupported.has(key)) return null;
|
|
3727
|
+
const headers = {};
|
|
3728
|
+
if (credential.kind === "access_token") {
|
|
3729
|
+
headers.Authorization = `Bearer ${credential.value}`;
|
|
3730
|
+
} else {
|
|
3731
|
+
headers["X-Public-Key"] = credential.value;
|
|
3732
|
+
}
|
|
3733
|
+
const { signal, cleanup } = createTimeoutController({
|
|
3734
|
+
timeout: Math.max(
|
|
3735
|
+
1,
|
|
3736
|
+
Math.min(
|
|
3737
|
+
options.timeoutMs ?? DEFAULT_TICKET_TIMEOUT_MS,
|
|
3738
|
+
DEFAULT_TICKET_TIMEOUT_MS
|
|
3739
|
+
)
|
|
3740
|
+
),
|
|
3741
|
+
signal: options.signal
|
|
3742
|
+
});
|
|
3743
|
+
let response;
|
|
3744
|
+
try {
|
|
3745
|
+
response = await withAbort(
|
|
3746
|
+
fetch(url, { method: "POST", headers, credentials: "omit", signal }),
|
|
3747
|
+
signal
|
|
3748
|
+
);
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
cleanup();
|
|
3751
|
+
if (options.signal?.aborted) throw err;
|
|
3752
|
+
return null;
|
|
3753
|
+
}
|
|
3754
|
+
try {
|
|
3755
|
+
if (response.ok) {
|
|
3756
|
+
const body2 = await response.json().catch(() => null);
|
|
3757
|
+
return typeof body2?.ticket === "string" && body2.ticket !== "" ? body2.ticket : null;
|
|
3758
|
+
}
|
|
3759
|
+
if (response.status === 404 || response.status === 405 || response.status === 501) {
|
|
3760
|
+
unsupported.add(key);
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
if (response.status >= 500) return null;
|
|
3764
|
+
const body = await response.json().catch(() => null);
|
|
3765
|
+
const raw = typeof body?.error === "string" ? body.error : void 0;
|
|
3766
|
+
const isCode = raw !== void 0 && /^[a-z0-9_]+$/.test(raw);
|
|
3767
|
+
const code = isCode ? raw : statusCode(response.status);
|
|
3768
|
+
const message = typeof body?.message === "string" ? body.message : raw;
|
|
3769
|
+
const retryAfterMs = typeof body?.retry_after_ms === "number" && body.retry_after_ms > 0 ? body.retry_after_ms : void 0;
|
|
3770
|
+
throw new WsTicketError({
|
|
3771
|
+
status: response.status,
|
|
3772
|
+
code,
|
|
3773
|
+
message,
|
|
3774
|
+
retryAfterMs
|
|
3775
|
+
});
|
|
3776
|
+
} finally {
|
|
3777
|
+
cleanup();
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
function statusCode(status) {
|
|
3781
|
+
switch (status) {
|
|
3782
|
+
case 401:
|
|
3783
|
+
return "unauthorized";
|
|
3784
|
+
case 402:
|
|
3785
|
+
return "quota_exceeded";
|
|
3786
|
+
case 403:
|
|
3787
|
+
return "forbidden";
|
|
3788
|
+
case 429:
|
|
3789
|
+
return "rate_limited";
|
|
3790
|
+
default:
|
|
3791
|
+
return "ws_ticket_rejected";
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
function ticketUrl(base, path) {
|
|
3795
|
+
const httpBase = base.replace(/^ws(s?):\/\//, "http$1://").replace(/\/+$/, "");
|
|
3796
|
+
return `${httpBase}${path}`;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3701
3799
|
// src/api/database.ts
|
|
3702
3800
|
var REALTIME_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
3703
3801
|
var REALTIME_CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
3802
|
+
var REALTIME_CONNECT_TIMEOUT_MS = 15e3;
|
|
3704
3803
|
var REALTIME_AUTH_REFRESH_RETRY_MS = 3e4;
|
|
3705
3804
|
var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
3706
3805
|
"TOKEN_EXPIRED",
|
|
@@ -3708,6 +3807,20 @@ var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
|
3708
3807
|
]);
|
|
3709
3808
|
var CLOSE_CODE_TOKEN_EXPIRED = 4401;
|
|
3710
3809
|
var CLOSE_CODE_MEMBER_REVOKED = 4403;
|
|
3810
|
+
function toRealtimeRejection(msg) {
|
|
3811
|
+
if (msg.type !== "error" || msg.request_id || typeof msg.close_code !== "number") {
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
const details = msg.details;
|
|
3815
|
+
return new RealtimeConnectionError({
|
|
3816
|
+
code: typeof msg.code === "string" ? msg.code : void 0,
|
|
3817
|
+
message: typeof msg.message === "string" ? msg.message : void 0,
|
|
3818
|
+
closeCode: msg.close_code,
|
|
3819
|
+
retryable: typeof msg.retryable === "boolean" ? msg.retryable : void 0,
|
|
3820
|
+
details: details && typeof details === "object" && !Array.isArray(details) ? details : void 0,
|
|
3821
|
+
retryAfterMs: typeof msg.retry_after_ms === "number" && msg.retry_after_ms > 0 ? msg.retry_after_ms : void 0
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3711
3824
|
var DatabaseAPI = class {
|
|
3712
3825
|
constructor(http) {
|
|
3713
3826
|
this.realtimeWs = null;
|
|
@@ -4645,23 +4758,59 @@ var DatabaseAPI = class {
|
|
|
4645
4758
|
);
|
|
4646
4759
|
if (fresh) opts.accessToken = fresh;
|
|
4647
4760
|
}
|
|
4648
|
-
|
|
4761
|
+
let ticket;
|
|
4762
|
+
try {
|
|
4763
|
+
ticket = await this.realtimeTicket();
|
|
4764
|
+
} catch (e) {
|
|
4765
|
+
if (e instanceof WsTicketError) {
|
|
4766
|
+
const err = new RealtimeConnectionError({
|
|
4767
|
+
code: e.code,
|
|
4768
|
+
message: e.message,
|
|
4769
|
+
closeCode: 4e3 + e.status,
|
|
4770
|
+
retryAfterMs: e.retryAfterMs
|
|
4771
|
+
});
|
|
4772
|
+
this.handleRealtimeRejection(err);
|
|
4773
|
+
throw err;
|
|
4774
|
+
}
|
|
4775
|
+
throw e;
|
|
4776
|
+
}
|
|
4777
|
+
return this.doRealtimeConnect(ticket);
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰을
|
|
4781
|
+
* URL(`?access_token=`)에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가 티켓을 모르거나(구버전) 잠시 못
|
|
4782
|
+
* 주면 null 이고 예전처럼 붙는다. cross-app OAuth 토큰도 같은 경로다(서버가 티켓에서 토큰을 되살려 같은 검증을 한다).
|
|
4783
|
+
*/
|
|
4784
|
+
async realtimeTicket() {
|
|
4785
|
+
const opts = this.realtimeOptions;
|
|
4786
|
+
if (!opts?.accessToken) return null;
|
|
4787
|
+
const baseUrl = opts.dataServerUrl || this.http.getBaseUrl();
|
|
4788
|
+
return fetchWsTicket(
|
|
4789
|
+
ticketUrl(baseUrl, "/v1/database/realtime/ws-ticket"),
|
|
4790
|
+
"data.realtime",
|
|
4791
|
+
{ kind: "access_token", value: opts.accessToken },
|
|
4792
|
+
{ timeoutMs: REALTIME_CONNECT_TIMEOUT_MS }
|
|
4793
|
+
);
|
|
4649
4794
|
}
|
|
4650
|
-
doRealtimeConnect() {
|
|
4795
|
+
doRealtimeConnect(ticket = null) {
|
|
4651
4796
|
if (!this.realtimeOptions)
|
|
4652
4797
|
return Promise.reject(new Error("No realtime options"));
|
|
4653
4798
|
this.setRealtimeState("connecting");
|
|
4654
4799
|
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
4655
4800
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
4656
|
-
let url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4801
|
+
let url = ticket ? `${wsUrl}/v1/database/realtime/ws?ticket=${encodeURIComponent(ticket)}` : `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4657
4802
|
this.realtimeTokenRefresh = this.http.canRefreshMemberToken(
|
|
4658
4803
|
this.realtimeOptions.accessToken
|
|
4659
4804
|
);
|
|
4660
4805
|
if (this.realtimeTokenRefresh) url += "&token_refresh=1";
|
|
4806
|
+
url += "&reject_mode=close";
|
|
4661
4807
|
return new Promise((resolve, reject) => {
|
|
4662
4808
|
try {
|
|
4663
|
-
|
|
4809
|
+
const ws = new WebSocket(url);
|
|
4810
|
+
this.realtimeWs = ws;
|
|
4664
4811
|
let settled = false;
|
|
4812
|
+
let accepted = false;
|
|
4813
|
+
let rejection = null;
|
|
4665
4814
|
const connectTimeout = setTimeout(() => {
|
|
4666
4815
|
if (!settled) {
|
|
4667
4816
|
settled = true;
|
|
@@ -4672,8 +4821,9 @@ var DatabaseAPI = class {
|
|
|
4672
4821
|
this.setRealtimeState("disconnected");
|
|
4673
4822
|
reject(new Error("Connection timeout"));
|
|
4674
4823
|
}
|
|
4675
|
-
},
|
|
4676
|
-
|
|
4824
|
+
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4825
|
+
const accept = () => {
|
|
4826
|
+
accepted = true;
|
|
4677
4827
|
if (!settled) {
|
|
4678
4828
|
settled = true;
|
|
4679
4829
|
clearTimeout(connectTimeout);
|
|
@@ -4686,15 +4836,28 @@ var DatabaseAPI = class {
|
|
|
4686
4836
|
this.resubscribeAll();
|
|
4687
4837
|
resolve();
|
|
4688
4838
|
};
|
|
4689
|
-
|
|
4839
|
+
ws.onopen = () => {
|
|
4840
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
|
|
4841
|
+
};
|
|
4842
|
+
ws.onmessage = (event) => {
|
|
4843
|
+
let msg;
|
|
4690
4844
|
try {
|
|
4691
|
-
|
|
4692
|
-
this.handleRealtimeMessage(msg);
|
|
4845
|
+
msg = JSON.parse(event.data);
|
|
4693
4846
|
} catch {
|
|
4694
4847
|
this.debugLog("Failed to parse realtime message");
|
|
4848
|
+
return;
|
|
4695
4849
|
}
|
|
4850
|
+
if (!accepted) {
|
|
4851
|
+
const rejected = toRealtimeRejection(msg);
|
|
4852
|
+
if (rejected) {
|
|
4853
|
+
rejection = rejected;
|
|
4854
|
+
return;
|
|
4855
|
+
}
|
|
4856
|
+
accept();
|
|
4857
|
+
}
|
|
4858
|
+
this.handleRealtimeMessage(msg);
|
|
4696
4859
|
};
|
|
4697
|
-
|
|
4860
|
+
ws.onclose = (event) => {
|
|
4698
4861
|
this.debugLog(
|
|
4699
4862
|
`Database realtime disconnected (code=${event?.code}, reason=${event?.reason})`
|
|
4700
4863
|
);
|
|
@@ -4703,12 +4866,25 @@ var DatabaseAPI = class {
|
|
|
4703
4866
|
this.clearRealtimeAuthRetry();
|
|
4704
4867
|
const notice = this.realtimeCloseNotice;
|
|
4705
4868
|
this.realtimeCloseNotice = null;
|
|
4869
|
+
const code = event?.code;
|
|
4870
|
+
if (!accepted && code >= 4e3 && code <= 4999) {
|
|
4871
|
+
const err = rejection ?? new RealtimeConnectionError({
|
|
4872
|
+
code: event.reason || void 0,
|
|
4873
|
+
closeCode: code
|
|
4874
|
+
});
|
|
4875
|
+
if (!settled) {
|
|
4876
|
+
settled = true;
|
|
4877
|
+
clearTimeout(connectTimeout);
|
|
4878
|
+
reject(err);
|
|
4879
|
+
}
|
|
4880
|
+
this.handleRealtimeRejection(err);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4706
4883
|
if (!settled) {
|
|
4707
4884
|
settled = true;
|
|
4708
4885
|
clearTimeout(connectTimeout);
|
|
4709
4886
|
reject(new Error("Connection closed during handshake"));
|
|
4710
4887
|
}
|
|
4711
|
-
const code = event?.code;
|
|
4712
4888
|
if (this.realtimeOptions && this.realtimeState !== "disconnected" && (code === CLOSE_CODE_MEMBER_REVOKED || code === CLOSE_CODE_TOKEN_EXPIRED && !this.http.canRefreshMemberToken(
|
|
4713
4889
|
this.realtimeOptions.accessToken
|
|
4714
4890
|
))) {
|
|
@@ -4726,7 +4902,7 @@ var DatabaseAPI = class {
|
|
|
4726
4902
|
this.attemptRealtimeReconnect();
|
|
4727
4903
|
}
|
|
4728
4904
|
};
|
|
4729
|
-
|
|
4905
|
+
ws.onerror = () => {
|
|
4730
4906
|
this.debugLog("Database realtime error");
|
|
4731
4907
|
this.realtimeOnError?.(new Error("WebSocket connection error"));
|
|
4732
4908
|
};
|
|
@@ -4736,6 +4912,21 @@ var DatabaseAPI = class {
|
|
|
4736
4912
|
}
|
|
4737
4913
|
});
|
|
4738
4914
|
}
|
|
4915
|
+
/**
|
|
4916
|
+
* 서버가 연결을 거부했다(티켓 발급 거부, 또는 reject_mode=close 로 받은 업그레이드 거부). cb.realtime 과 같은 규칙이다 —
|
|
4917
|
+
* 다시 해서 될 거부(`retryable`: 일시 장애, 속도 제한, 동시접속 한도, 만료된 티켓)는 서버가 알려 준 시각(`retryAfterMs`)
|
|
4918
|
+
* 이후로 재연결을 이어 가고, 아니면(자격증명, 멤버 폐기, 할당량) 멈추고 `onRealtimeError` 로 알린다. 재시도를 다 써도
|
|
4919
|
+
* 거부되면 마지막 거부를 알린다.
|
|
4920
|
+
*/
|
|
4921
|
+
handleRealtimeRejection(err) {
|
|
4922
|
+
if (!this.realtimeOptions || this.realtimeState === "disconnected") return;
|
|
4923
|
+
if (err.retryable) {
|
|
4924
|
+
this.attemptRealtimeReconnect(err);
|
|
4925
|
+
return;
|
|
4926
|
+
}
|
|
4927
|
+
this.setRealtimeState("disconnected");
|
|
4928
|
+
this.realtimeOnError?.(err);
|
|
4929
|
+
}
|
|
4739
4930
|
/**
|
|
4740
4931
|
* 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise
|
|
4741
4932
|
*/
|
|
@@ -4908,21 +5099,25 @@ var DatabaseAPI = class {
|
|
|
4908
5099
|
break;
|
|
4909
5100
|
}
|
|
4910
5101
|
}
|
|
4911
|
-
|
|
5102
|
+
/**
|
|
5103
|
+
* 지수 백오프로 다시 붙는다. cause 는 이번 시도를 끝낸 서버의 거부다 — 알려 준 재시도 시각(`retryAfterMs`)보다 일찍 다시
|
|
5104
|
+
* 하지 않고, 재시도를 다 쓰면 일반 오류 대신 그 거부를 알린다.
|
|
5105
|
+
*/
|
|
5106
|
+
attemptRealtimeReconnect(cause) {
|
|
4912
5107
|
const maxRetries = this.realtimeOptions?.maxRetries ?? 5;
|
|
4913
5108
|
const retryInterval = this.realtimeOptions?.retryInterval ?? 1e3;
|
|
4914
5109
|
if (this.realtimeRetryCount >= maxRetries) {
|
|
4915
5110
|
this.setRealtimeState("disconnected");
|
|
4916
5111
|
this.realtimeOnError?.(
|
|
4917
|
-
new Error("Realtime connection lost. Max retries exceeded.")
|
|
5112
|
+
cause ?? new Error("Realtime connection lost. Max retries exceeded.")
|
|
4918
5113
|
);
|
|
4919
5114
|
return;
|
|
4920
5115
|
}
|
|
4921
5116
|
this.setRealtimeState("connecting");
|
|
4922
5117
|
this.realtimeRetryCount++;
|
|
4923
|
-
const delay = Math.
|
|
4924
|
-
retryInterval * 2 ** (this.realtimeRetryCount - 1),
|
|
4925
|
-
|
|
5118
|
+
const delay = Math.max(
|
|
5119
|
+
Math.min(retryInterval * 2 ** (this.realtimeRetryCount - 1), 3e4),
|
|
5120
|
+
cause?.retryAfterMs ?? 0
|
|
4926
5121
|
);
|
|
4927
5122
|
this.debugLog(
|
|
4928
5123
|
`Reconnecting in ${delay}ms (attempt ${this.realtimeRetryCount}/${maxRetries})`
|
|
@@ -5213,7 +5408,7 @@ var EndpointAPI = class {
|
|
|
5213
5408
|
);
|
|
5214
5409
|
}
|
|
5215
5410
|
const baseUrl = this.http.getBaseUrl().replace(/\/+$/, "");
|
|
5216
|
-
const
|
|
5411
|
+
const ticketUrl2 = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
|
|
5217
5412
|
const ticketHeaders = new Headers();
|
|
5218
5413
|
const pk = this.http.getPublicKey();
|
|
5219
5414
|
if (!pk) {
|
|
@@ -5222,7 +5417,7 @@ var EndpointAPI = class {
|
|
|
5222
5417
|
);
|
|
5223
5418
|
}
|
|
5224
5419
|
ticketHeaders.set("X-Public-Key", pk);
|
|
5225
|
-
const ticketRes = await fetch(
|
|
5420
|
+
const ticketRes = await fetch(ticketUrl2, {
|
|
5226
5421
|
method: "POST",
|
|
5227
5422
|
headers: ticketHeaders,
|
|
5228
5423
|
signal: opts.signal
|
|
@@ -5920,20 +6115,54 @@ var GameRoom = class {
|
|
|
5920
6115
|
* 게임 서버에 연결
|
|
5921
6116
|
*/
|
|
5922
6117
|
connect(roomId) {
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
6118
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
6119
|
+
return Promise.resolve();
|
|
6120
|
+
}
|
|
6121
|
+
if (!this.config.appId) {
|
|
6122
|
+
return Promise.reject(
|
|
6123
|
+
new Error(
|
|
6124
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6125
|
+
)
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
if (!this.config.clientId) {
|
|
6129
|
+
return Promise.reject(
|
|
6130
|
+
new Error(
|
|
6131
|
+
"cb.game: clientId is required to build a game connection URL. Pass it via `cb.game.createClient({ appId, clientId })`."
|
|
6132
|
+
)
|
|
6133
|
+
);
|
|
6134
|
+
}
|
|
6135
|
+
return this.connectionTicket().then(
|
|
6136
|
+
(ticket) => this.openSocket(roomId, ticket)
|
|
6137
|
+
);
|
|
6138
|
+
}
|
|
6139
|
+
/**
|
|
6140
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
6141
|
+
* 토큰(`?token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
6142
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
6143
|
+
*/
|
|
6144
|
+
async connectionTicket() {
|
|
6145
|
+
const { accessToken, publicKey, appId, gameServerUrl } = this.config;
|
|
6146
|
+
if (!accessToken && !publicKey || !appId || !gameServerUrl) return null;
|
|
6147
|
+
try {
|
|
6148
|
+
return await fetchWsTicket(
|
|
6149
|
+
ticketUrl(
|
|
6150
|
+
gameServerUrl,
|
|
6151
|
+
`/v1/game/${encodeURIComponent(appId)}/ws-ticket`
|
|
6152
|
+
),
|
|
6153
|
+
"game.ws",
|
|
6154
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
6155
|
+
);
|
|
6156
|
+
} catch (e) {
|
|
6157
|
+
if (e instanceof WsTicketError) {
|
|
6158
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
5935
6159
|
}
|
|
5936
|
-
|
|
6160
|
+
throw e;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
openSocket(roomId, ticket) {
|
|
6164
|
+
return new Promise((resolve, reject) => {
|
|
6165
|
+
const url = this.buildConnectionUrl(roomId, ticket);
|
|
5937
6166
|
this.ws = new WebSocket(url);
|
|
5938
6167
|
const onOpen = () => {
|
|
5939
6168
|
this._isConnected = true;
|
|
@@ -6189,7 +6418,7 @@ var GameRoom = class {
|
|
|
6189
6418
|
});
|
|
6190
6419
|
}
|
|
6191
6420
|
// Private methods
|
|
6192
|
-
buildConnectionUrl(roomId) {
|
|
6421
|
+
buildConnectionUrl(roomId, ticket) {
|
|
6193
6422
|
const baseUrl = this.config.gameServerUrl;
|
|
6194
6423
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
6195
6424
|
const params = new URLSearchParams();
|
|
@@ -6202,11 +6431,15 @@ var GameRoom = class {
|
|
|
6202
6431
|
if (roomId) {
|
|
6203
6432
|
params.set("room_id", roomId);
|
|
6204
6433
|
}
|
|
6205
|
-
if (
|
|
6206
|
-
params.set("
|
|
6207
|
-
}
|
|
6208
|
-
|
|
6209
|
-
|
|
6434
|
+
if (ticket) {
|
|
6435
|
+
params.set("ticket", ticket);
|
|
6436
|
+
} else {
|
|
6437
|
+
if (this.config.publicKey) {
|
|
6438
|
+
params.set("public_key", this.config.publicKey);
|
|
6439
|
+
}
|
|
6440
|
+
if (this.config.accessToken) {
|
|
6441
|
+
params.set("token", this.config.accessToken);
|
|
6442
|
+
}
|
|
6210
6443
|
}
|
|
6211
6444
|
if (!this.config.appId) {
|
|
6212
6445
|
throw new Error(
|
|
@@ -10063,7 +10296,14 @@ var ConnectCancelledError = class extends Error {
|
|
|
10063
10296
|
}
|
|
10064
10297
|
};
|
|
10065
10298
|
var MAX_HANDSHAKE_RETRIES = 2;
|
|
10066
|
-
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10299
|
+
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10300
|
+
"rate_limited",
|
|
10301
|
+
"auth_unavailable",
|
|
10302
|
+
// 1회용 티켓이 업그레이드에서 거부됐다(만료, 이미 쓰임, 저장소 일시 장애). 다음 시도는 새 티켓을 받거나, 저장소가
|
|
10303
|
+
// 없으면 예전 방식으로 붙는다(core/ws-ticket.ts).
|
|
10304
|
+
"invalid_ws_ticket",
|
|
10305
|
+
"ws_ticket_unavailable"
|
|
10306
|
+
]);
|
|
10067
10307
|
var CONNECTION_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
10068
10308
|
var CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
10069
10309
|
var AUTH_REFRESH_RETRY_MS = 3e4;
|
|
@@ -11178,7 +11418,10 @@ var RealtimeAPI = class {
|
|
|
11178
11418
|
for (let attempt = 1; ; attempt++) {
|
|
11179
11419
|
try {
|
|
11180
11420
|
await this.prepareConnectToken();
|
|
11181
|
-
await this.
|
|
11421
|
+
const ticket = await this.connectTicket(
|
|
11422
|
+
Math.max(deadline - Date.now(), 1)
|
|
11423
|
+
);
|
|
11424
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1), ticket);
|
|
11182
11425
|
return;
|
|
11183
11426
|
} catch (e) {
|
|
11184
11427
|
let wait = null;
|
|
@@ -11221,7 +11464,39 @@ var RealtimeAPI = class {
|
|
|
11221
11464
|
this.initialConnecting = false;
|
|
11222
11465
|
}
|
|
11223
11466
|
}
|
|
11224
|
-
|
|
11467
|
+
/**
|
|
11468
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
11469
|
+
* 토큰과 공개 키를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 티켓은 한 번만 쓰이므로 시도마다
|
|
11470
|
+
* 새로 받는다.
|
|
11471
|
+
*
|
|
11472
|
+
* 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고, doConnect 가 예전처럼 자격증명을 URL 에 싣는다. 발급이
|
|
11473
|
+
* 자격증명을 거부하면 업그레이드 거부와 같은 {@link RealtimeConnectionError} 로 던진다 — 재시도 규칙이 같다.
|
|
11474
|
+
*/
|
|
11475
|
+
async connectTicket(timeoutMs) {
|
|
11476
|
+
const accessToken = this.options.accessToken;
|
|
11477
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
11478
|
+
if (!accessToken && !publicKey) return null;
|
|
11479
|
+
try {
|
|
11480
|
+
return await fetchWsTicket(
|
|
11481
|
+
ticketUrl(this.socketUrl, "/v1/realtime/ws-ticket"),
|
|
11482
|
+
"socket.realtime",
|
|
11483
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey },
|
|
11484
|
+
{ timeoutMs }
|
|
11485
|
+
);
|
|
11486
|
+
} catch (e) {
|
|
11487
|
+
if (e instanceof WsTicketError) {
|
|
11488
|
+
this.log(`WebSocket ticket rejected: ${e.code} (${e.status})`);
|
|
11489
|
+
throw new RealtimeConnectionError({
|
|
11490
|
+
code: e.code,
|
|
11491
|
+
message: e.message,
|
|
11492
|
+
closeCode: 4e3 + e.status,
|
|
11493
|
+
retryAfterMs: e.retryAfterMs
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
throw e;
|
|
11497
|
+
}
|
|
11498
|
+
}
|
|
11499
|
+
async doConnect(timeoutMs = this.options.timeout, ticket = null) {
|
|
11225
11500
|
return new Promise((resolve, reject) => {
|
|
11226
11501
|
this.state = "connecting";
|
|
11227
11502
|
this.notifyStateChange();
|
|
@@ -11230,9 +11505,10 @@ var RealtimeAPI = class {
|
|
|
11230
11505
|
const usingDirect = !!directWsBase;
|
|
11231
11506
|
const wsUrl = (directWsBase ?? this.socketUrl).replace(/^http/, "ws");
|
|
11232
11507
|
let url;
|
|
11508
|
+
const auth = ticket ? `ticket=${encodeURIComponent(ticket)}` : void 0;
|
|
11233
11509
|
this.tokenRefreshNegotiated = false;
|
|
11234
11510
|
if (this.options.accessToken) {
|
|
11235
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11511
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `access_token=${encodeURIComponent(this.options.accessToken)}`}&client_id=${this.clientId}`;
|
|
11236
11512
|
if (this.http.canRefreshMemberToken(this.options.accessToken)) {
|
|
11237
11513
|
this.tokenRefreshNegotiated = true;
|
|
11238
11514
|
url += "&token_refresh=1";
|
|
@@ -11248,7 +11524,7 @@ var RealtimeAPI = class {
|
|
|
11248
11524
|
reject(error);
|
|
11249
11525
|
return;
|
|
11250
11526
|
}
|
|
11251
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11527
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `public_key=${encodeURIComponent(publicKey)}`}&client_id=${this.clientId}`;
|
|
11252
11528
|
this.log("Using API Key authentication");
|
|
11253
11529
|
}
|
|
11254
11530
|
if (this.userId) {
|
|
@@ -11728,7 +12004,9 @@ var RealtimeAPI = class {
|
|
|
11728
12004
|
try {
|
|
11729
12005
|
await this.prepareConnectToken();
|
|
11730
12006
|
if (this.state !== "reconnecting") return;
|
|
11731
|
-
await this.
|
|
12007
|
+
const ticket = await this.connectTicket(this.options.timeout);
|
|
12008
|
+
if (this.state !== "reconnecting") return;
|
|
12009
|
+
await this.doConnect(void 0, ticket);
|
|
11732
12010
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
11733
12011
|
const snapshot = this.pendingRestore;
|
|
11734
12012
|
this.pendingRestore = null;
|
|
@@ -13331,8 +13609,8 @@ var SupportAPI = class {
|
|
|
13331
13609
|
}
|
|
13332
13610
|
};
|
|
13333
13611
|
function detectSdkVersion() {
|
|
13334
|
-
if ("6.
|
|
13335
|
-
return "6.
|
|
13612
|
+
if ("6.15.0") {
|
|
13613
|
+
return "6.15.0";
|
|
13336
13614
|
}
|
|
13337
13615
|
return "unknown";
|
|
13338
13616
|
}
|
|
@@ -14301,9 +14579,35 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14301
14579
|
}
|
|
14302
14580
|
return this.connectWebSocket();
|
|
14303
14581
|
}
|
|
14304
|
-
connectWebSocket() {
|
|
14582
|
+
async connectWebSocket() {
|
|
14583
|
+
const before = this.wsGeneration;
|
|
14584
|
+
const ticket = await this.signalingTicket();
|
|
14585
|
+
if (this.wsGeneration !== before) {
|
|
14586
|
+
throw new Error("\uC5F0\uACB0\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
14587
|
+
}
|
|
14588
|
+
return this.openSignalingSocket(ticket);
|
|
14589
|
+
}
|
|
14590
|
+
/**
|
|
14591
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰
|
|
14592
|
+
* (`?access_token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
14593
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 그 이유로 던진다.
|
|
14594
|
+
*/
|
|
14595
|
+
async signalingTicket() {
|
|
14596
|
+
const accessToken = this.http.getAccessToken();
|
|
14597
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
14598
|
+
if (!accessToken && !publicKey || !this.appId) return null;
|
|
14599
|
+
return fetchWsTicket(
|
|
14600
|
+
ticketUrl(
|
|
14601
|
+
this.webrtcUrl,
|
|
14602
|
+
`/v1/apps/${encodeURIComponent(this.appId)}/signaling/ws-ticket`
|
|
14603
|
+
),
|
|
14604
|
+
"webrtc.signaling",
|
|
14605
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
14606
|
+
);
|
|
14607
|
+
}
|
|
14608
|
+
openSignalingSocket(ticket) {
|
|
14305
14609
|
return new Promise((resolve, reject) => {
|
|
14306
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
14610
|
+
const wsUrl = this.buildWebSocketUrl(ticket);
|
|
14307
14611
|
const generation = ++this.wsGeneration;
|
|
14308
14612
|
const ws = new WebSocket(wsUrl);
|
|
14309
14613
|
this.ws = ws;
|
|
@@ -14372,12 +14676,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14372
14676
|
ws.onerror = null;
|
|
14373
14677
|
ws.onclose = null;
|
|
14374
14678
|
}
|
|
14375
|
-
buildWebSocketUrl() {
|
|
14679
|
+
buildWebSocketUrl(ticket = null) {
|
|
14376
14680
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
14377
14681
|
const publicKey = this.http.getPublicKey();
|
|
14378
14682
|
const accessToken = this.http.getAccessToken();
|
|
14379
14683
|
let authParam = "";
|
|
14380
|
-
if (
|
|
14684
|
+
if (ticket) {
|
|
14685
|
+
authParam = `ticket=${encodeURIComponent(ticket)}`;
|
|
14686
|
+
} else if (accessToken) {
|
|
14381
14687
|
authParam = `access_token=${encodeURIComponent(accessToken)}`;
|
|
14382
14688
|
} else if (publicKey) {
|
|
14383
14689
|
authParam = `public_key=${encodeURIComponent(publicKey)}`;
|
|
@@ -15035,11 +15341,35 @@ var WebSocketTransport = class {
|
|
|
15035
15341
|
this.onClose = onClose;
|
|
15036
15342
|
this.onError = onError;
|
|
15037
15343
|
}
|
|
15038
|
-
connect() {
|
|
15344
|
+
async connect() {
|
|
15345
|
+
const ticket = await this.connectionTicket();
|
|
15346
|
+
return this.openSocket(ticket);
|
|
15347
|
+
}
|
|
15348
|
+
/**
|
|
15349
|
+
* 연결 시도 한 번에 쓸 1회용 티켓(core/ws-ticket.ts). 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼
|
|
15350
|
+
* 자격증명을 URL 에 싣는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
15351
|
+
*/
|
|
15352
|
+
async connectionTicket() {
|
|
15353
|
+
const { accessToken, publicKey, appId } = this.config;
|
|
15354
|
+
if (!accessToken && !publicKey || !appId) return null;
|
|
15355
|
+
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15356
|
+
try {
|
|
15357
|
+
return await fetchWsTicket(
|
|
15358
|
+
ticketUrl(baseUrl, `/v1/game/${encodeURIComponent(appId)}/ws-ticket`),
|
|
15359
|
+
"game.ws",
|
|
15360
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
15361
|
+
);
|
|
15362
|
+
} catch (e) {
|
|
15363
|
+
if (e instanceof WsTicketError) {
|
|
15364
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
15365
|
+
}
|
|
15366
|
+
throw e;
|
|
15367
|
+
}
|
|
15368
|
+
}
|
|
15369
|
+
openSocket(ticket) {
|
|
15039
15370
|
return new Promise((resolve, reject) => {
|
|
15040
|
-
const url = this.buildUrl();
|
|
15041
15371
|
try {
|
|
15042
|
-
this.ws = new WebSocket(
|
|
15372
|
+
this.ws = new WebSocket(this.buildUrl(ticket));
|
|
15043
15373
|
this.ws.binaryType = "arraybuffer";
|
|
15044
15374
|
} catch (error) {
|
|
15045
15375
|
reject(error);
|
|
@@ -15069,16 +15399,20 @@ var WebSocketTransport = class {
|
|
|
15069
15399
|
this.ws.addEventListener("message", onMessage);
|
|
15070
15400
|
});
|
|
15071
15401
|
}
|
|
15072
|
-
buildUrl() {
|
|
15402
|
+
buildUrl(ticket = null) {
|
|
15073
15403
|
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15074
15404
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
15075
15405
|
const params = new URLSearchParams();
|
|
15076
15406
|
params.set("client_id", requireClientId(this.config.clientId));
|
|
15077
|
-
if (
|
|
15078
|
-
params.set("
|
|
15079
|
-
}
|
|
15080
|
-
|
|
15081
|
-
|
|
15407
|
+
if (ticket) {
|
|
15408
|
+
params.set("ticket", ticket);
|
|
15409
|
+
} else {
|
|
15410
|
+
if (this.config.publicKey) {
|
|
15411
|
+
params.set("public_key", this.config.publicKey);
|
|
15412
|
+
}
|
|
15413
|
+
if (this.config.accessToken) {
|
|
15414
|
+
params.set("token", this.config.accessToken);
|
|
15415
|
+
}
|
|
15082
15416
|
}
|
|
15083
15417
|
if (!this.config.appId) {
|
|
15084
15418
|
throw new Error(
|
|
@@ -15758,6 +16092,7 @@ var index_default = ConnectBase;
|
|
|
15758
16092
|
RolesAPI,
|
|
15759
16093
|
SessionManager,
|
|
15760
16094
|
VideoProcessingError,
|
|
16095
|
+
WsTicketError,
|
|
15761
16096
|
detectInAppBrowser,
|
|
15762
16097
|
escapeToExternalBrowser,
|
|
15763
16098
|
isWebTransportSupported,
|