connectbase-client 6.13.0 → 6.14.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 +27 -0
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +50 -2
- package/dist/index.d.ts +50 -2
- package/dist/index.js +325 -49
- package/dist/index.mjs +324 -49
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -118,9 +118,9 @@ var AdsAPI = class {
|
|
|
118
118
|
|
|
119
119
|
// src/types/error.ts
|
|
120
120
|
var ApiError = class extends Error {
|
|
121
|
-
constructor(
|
|
121
|
+
constructor(statusCode2, message, code, details) {
|
|
122
122
|
super(message);
|
|
123
|
-
this.statusCode =
|
|
123
|
+
this.statusCode = statusCode2;
|
|
124
124
|
this.name = "ApiError";
|
|
125
125
|
this.code = code;
|
|
126
126
|
this.details = details;
|
|
@@ -256,7 +256,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
256
256
|
}
|
|
257
257
|
};
|
|
258
258
|
function isRetryableRealtimeRejection(code) {
|
|
259
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
259
|
+
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error" || code === "invalid_ws_ticket" || code === "ws_ticket_unavailable";
|
|
260
260
|
}
|
|
261
261
|
var RealtimeRequestError = class extends Error {
|
|
262
262
|
constructor(init) {
|
|
@@ -3646,9 +3646,107 @@ var HttpClient = class {
|
|
|
3646
3646
|
}
|
|
3647
3647
|
};
|
|
3648
3648
|
|
|
3649
|
+
// src/core/ws-ticket.ts
|
|
3650
|
+
var WsTicketError = class extends Error {
|
|
3651
|
+
constructor(init) {
|
|
3652
|
+
super(
|
|
3653
|
+
init.message || init.code || `WebSocket ticket request failed (${init.status})`
|
|
3654
|
+
);
|
|
3655
|
+
this.name = "WsTicketError";
|
|
3656
|
+
this.status = init.status;
|
|
3657
|
+
this.code = init.code || "ws_ticket_rejected";
|
|
3658
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
var DEFAULT_TICKET_TIMEOUT_MS = 5e3;
|
|
3662
|
+
var unsupported = /* @__PURE__ */ new Set();
|
|
3663
|
+
function supportKey(url, kind) {
|
|
3664
|
+
try {
|
|
3665
|
+
const u = new URL(url);
|
|
3666
|
+
return `${u.origin}|${kind}`;
|
|
3667
|
+
} catch {
|
|
3668
|
+
return `${url}|${kind}`;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
async function fetchWsTicket(url, kind, credential, options = {}) {
|
|
3672
|
+
const key = supportKey(url, kind);
|
|
3673
|
+
if (unsupported.has(key)) return null;
|
|
3674
|
+
const headers = {};
|
|
3675
|
+
if (credential.kind === "access_token") {
|
|
3676
|
+
headers.Authorization = `Bearer ${credential.value}`;
|
|
3677
|
+
} else {
|
|
3678
|
+
headers["X-Public-Key"] = credential.value;
|
|
3679
|
+
}
|
|
3680
|
+
const { signal, cleanup } = createTimeoutController({
|
|
3681
|
+
timeout: Math.max(
|
|
3682
|
+
1,
|
|
3683
|
+
Math.min(
|
|
3684
|
+
options.timeoutMs ?? DEFAULT_TICKET_TIMEOUT_MS,
|
|
3685
|
+
DEFAULT_TICKET_TIMEOUT_MS
|
|
3686
|
+
)
|
|
3687
|
+
),
|
|
3688
|
+
signal: options.signal
|
|
3689
|
+
});
|
|
3690
|
+
let response;
|
|
3691
|
+
try {
|
|
3692
|
+
response = await withAbort(
|
|
3693
|
+
fetch(url, { method: "POST", headers, credentials: "omit", signal }),
|
|
3694
|
+
signal
|
|
3695
|
+
);
|
|
3696
|
+
} catch (err) {
|
|
3697
|
+
cleanup();
|
|
3698
|
+
if (options.signal?.aborted) throw err;
|
|
3699
|
+
return null;
|
|
3700
|
+
}
|
|
3701
|
+
try {
|
|
3702
|
+
if (response.ok) {
|
|
3703
|
+
const body2 = await response.json().catch(() => null);
|
|
3704
|
+
return typeof body2?.ticket === "string" && body2.ticket !== "" ? body2.ticket : null;
|
|
3705
|
+
}
|
|
3706
|
+
if (response.status === 404 || response.status === 405 || response.status === 501) {
|
|
3707
|
+
unsupported.add(key);
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
if (response.status >= 500) return null;
|
|
3711
|
+
const body = await response.json().catch(() => null);
|
|
3712
|
+
const raw = typeof body?.error === "string" ? body.error : void 0;
|
|
3713
|
+
const isCode = raw !== void 0 && /^[a-z0-9_]+$/.test(raw);
|
|
3714
|
+
const code = isCode ? raw : statusCode(response.status);
|
|
3715
|
+
const message = typeof body?.message === "string" ? body.message : raw;
|
|
3716
|
+
const retryAfterMs = typeof body?.retry_after_ms === "number" && body.retry_after_ms > 0 ? body.retry_after_ms : void 0;
|
|
3717
|
+
throw new WsTicketError({
|
|
3718
|
+
status: response.status,
|
|
3719
|
+
code,
|
|
3720
|
+
message,
|
|
3721
|
+
retryAfterMs
|
|
3722
|
+
});
|
|
3723
|
+
} finally {
|
|
3724
|
+
cleanup();
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
function statusCode(status) {
|
|
3728
|
+
switch (status) {
|
|
3729
|
+
case 401:
|
|
3730
|
+
return "unauthorized";
|
|
3731
|
+
case 402:
|
|
3732
|
+
return "quota_exceeded";
|
|
3733
|
+
case 403:
|
|
3734
|
+
return "forbidden";
|
|
3735
|
+
case 429:
|
|
3736
|
+
return "rate_limited";
|
|
3737
|
+
default:
|
|
3738
|
+
return "ws_ticket_rejected";
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function ticketUrl(base, path) {
|
|
3742
|
+
const httpBase = base.replace(/^ws(s?):\/\//, "http$1://").replace(/\/+$/, "");
|
|
3743
|
+
return `${httpBase}${path}`;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3649
3746
|
// src/api/database.ts
|
|
3650
3747
|
var REALTIME_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
3651
3748
|
var REALTIME_CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
3749
|
+
var REALTIME_CONNECT_TIMEOUT_MS = 15e3;
|
|
3652
3750
|
var REALTIME_AUTH_REFRESH_RETRY_MS = 3e4;
|
|
3653
3751
|
var REALTIME_CLOSING_NOTICE_CODES = /* @__PURE__ */ new Set([
|
|
3654
3752
|
"TOKEN_EXPIRED",
|
|
@@ -4593,15 +4691,52 @@ var DatabaseAPI = class {
|
|
|
4593
4691
|
);
|
|
4594
4692
|
if (fresh) opts.accessToken = fresh;
|
|
4595
4693
|
}
|
|
4596
|
-
|
|
4694
|
+
let ticket;
|
|
4695
|
+
try {
|
|
4696
|
+
ticket = await this.realtimeTicket();
|
|
4697
|
+
} catch (e) {
|
|
4698
|
+
if (e instanceof WsTicketError) {
|
|
4699
|
+
if (e.status === 429 && this.realtimeOptions) {
|
|
4700
|
+
this.attemptRealtimeReconnect();
|
|
4701
|
+
} else {
|
|
4702
|
+
this.setRealtimeState("disconnected");
|
|
4703
|
+
}
|
|
4704
|
+
const err = new RealtimeConnectionError({
|
|
4705
|
+
code: e.code,
|
|
4706
|
+
message: e.message,
|
|
4707
|
+
closeCode: 4e3 + e.status,
|
|
4708
|
+
retryAfterMs: e.retryAfterMs
|
|
4709
|
+
});
|
|
4710
|
+
this.realtimeOnError?.(err);
|
|
4711
|
+
throw err;
|
|
4712
|
+
}
|
|
4713
|
+
throw e;
|
|
4714
|
+
}
|
|
4715
|
+
return this.doRealtimeConnect(ticket);
|
|
4716
|
+
}
|
|
4717
|
+
/**
|
|
4718
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰을
|
|
4719
|
+
* URL(`?access_token=`)에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가 티켓을 모르거나(구버전) 잠시 못
|
|
4720
|
+
* 주면 null 이고 예전처럼 붙는다. cross-app OAuth 토큰도 같은 경로다(서버가 티켓에서 토큰을 되살려 같은 검증을 한다).
|
|
4721
|
+
*/
|
|
4722
|
+
async realtimeTicket() {
|
|
4723
|
+
const opts = this.realtimeOptions;
|
|
4724
|
+
if (!opts?.accessToken) return null;
|
|
4725
|
+
const baseUrl = opts.dataServerUrl || this.http.getBaseUrl();
|
|
4726
|
+
return fetchWsTicket(
|
|
4727
|
+
ticketUrl(baseUrl, "/v1/database/realtime/ws-ticket"),
|
|
4728
|
+
"data.realtime",
|
|
4729
|
+
{ kind: "access_token", value: opts.accessToken },
|
|
4730
|
+
{ timeoutMs: REALTIME_CONNECT_TIMEOUT_MS }
|
|
4731
|
+
);
|
|
4597
4732
|
}
|
|
4598
|
-
doRealtimeConnect() {
|
|
4733
|
+
doRealtimeConnect(ticket = null) {
|
|
4599
4734
|
if (!this.realtimeOptions)
|
|
4600
4735
|
return Promise.reject(new Error("No realtime options"));
|
|
4601
4736
|
this.setRealtimeState("connecting");
|
|
4602
4737
|
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
4603
4738
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
4604
|
-
let url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4739
|
+
let url = ticket ? `${wsUrl}/v1/database/realtime/ws?ticket=${encodeURIComponent(ticket)}` : `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4605
4740
|
this.realtimeTokenRefresh = this.http.canRefreshMemberToken(
|
|
4606
4741
|
this.realtimeOptions.accessToken
|
|
4607
4742
|
);
|
|
@@ -4620,7 +4755,7 @@ var DatabaseAPI = class {
|
|
|
4620
4755
|
this.setRealtimeState("disconnected");
|
|
4621
4756
|
reject(new Error("Connection timeout"));
|
|
4622
4757
|
}
|
|
4623
|
-
},
|
|
4758
|
+
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4624
4759
|
this.realtimeWs.onopen = () => {
|
|
4625
4760
|
if (!settled) {
|
|
4626
4761
|
settled = true;
|
|
@@ -5161,7 +5296,7 @@ var EndpointAPI = class {
|
|
|
5161
5296
|
);
|
|
5162
5297
|
}
|
|
5163
5298
|
const baseUrl = this.http.getBaseUrl().replace(/\/+$/, "");
|
|
5164
|
-
const
|
|
5299
|
+
const ticketUrl2 = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
|
|
5165
5300
|
const ticketHeaders = new Headers();
|
|
5166
5301
|
const pk = this.http.getPublicKey();
|
|
5167
5302
|
if (!pk) {
|
|
@@ -5170,7 +5305,7 @@ var EndpointAPI = class {
|
|
|
5170
5305
|
);
|
|
5171
5306
|
}
|
|
5172
5307
|
ticketHeaders.set("X-Public-Key", pk);
|
|
5173
|
-
const ticketRes = await fetch(
|
|
5308
|
+
const ticketRes = await fetch(ticketUrl2, {
|
|
5174
5309
|
method: "POST",
|
|
5175
5310
|
headers: ticketHeaders,
|
|
5176
5311
|
signal: opts.signal
|
|
@@ -5868,20 +6003,54 @@ var GameRoom = class {
|
|
|
5868
6003
|
* 게임 서버에 연결
|
|
5869
6004
|
*/
|
|
5870
6005
|
connect(roomId) {
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6006
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
6007
|
+
return Promise.resolve();
|
|
6008
|
+
}
|
|
6009
|
+
if (!this.config.appId) {
|
|
6010
|
+
return Promise.reject(
|
|
6011
|
+
new Error(
|
|
6012
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6013
|
+
)
|
|
6014
|
+
);
|
|
6015
|
+
}
|
|
6016
|
+
if (!this.config.clientId) {
|
|
6017
|
+
return Promise.reject(
|
|
6018
|
+
new Error(
|
|
6019
|
+
"cb.game: clientId is required to build a game connection URL. Pass it via `cb.game.createClient({ appId, clientId })`."
|
|
6020
|
+
)
|
|
6021
|
+
);
|
|
6022
|
+
}
|
|
6023
|
+
return this.connectionTicket().then(
|
|
6024
|
+
(ticket) => this.openSocket(roomId, ticket)
|
|
6025
|
+
);
|
|
6026
|
+
}
|
|
6027
|
+
/**
|
|
6028
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
6029
|
+
* 토큰(`?token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
6030
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
6031
|
+
*/
|
|
6032
|
+
async connectionTicket() {
|
|
6033
|
+
const { accessToken, publicKey, appId, gameServerUrl } = this.config;
|
|
6034
|
+
if (!accessToken && !publicKey || !appId || !gameServerUrl) return null;
|
|
6035
|
+
try {
|
|
6036
|
+
return await fetchWsTicket(
|
|
6037
|
+
ticketUrl(
|
|
6038
|
+
gameServerUrl,
|
|
6039
|
+
`/v1/game/${encodeURIComponent(appId)}/ws-ticket`
|
|
6040
|
+
),
|
|
6041
|
+
"game.ws",
|
|
6042
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
6043
|
+
);
|
|
6044
|
+
} catch (e) {
|
|
6045
|
+
if (e instanceof WsTicketError) {
|
|
6046
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
5883
6047
|
}
|
|
5884
|
-
|
|
6048
|
+
throw e;
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
openSocket(roomId, ticket) {
|
|
6052
|
+
return new Promise((resolve, reject) => {
|
|
6053
|
+
const url = this.buildConnectionUrl(roomId, ticket);
|
|
5885
6054
|
this.ws = new WebSocket(url);
|
|
5886
6055
|
const onOpen = () => {
|
|
5887
6056
|
this._isConnected = true;
|
|
@@ -6137,7 +6306,7 @@ var GameRoom = class {
|
|
|
6137
6306
|
});
|
|
6138
6307
|
}
|
|
6139
6308
|
// Private methods
|
|
6140
|
-
buildConnectionUrl(roomId) {
|
|
6309
|
+
buildConnectionUrl(roomId, ticket) {
|
|
6141
6310
|
const baseUrl = this.config.gameServerUrl;
|
|
6142
6311
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
6143
6312
|
const params = new URLSearchParams();
|
|
@@ -6150,11 +6319,15 @@ var GameRoom = class {
|
|
|
6150
6319
|
if (roomId) {
|
|
6151
6320
|
params.set("room_id", roomId);
|
|
6152
6321
|
}
|
|
6153
|
-
if (
|
|
6154
|
-
params.set("
|
|
6155
|
-
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6322
|
+
if (ticket) {
|
|
6323
|
+
params.set("ticket", ticket);
|
|
6324
|
+
} else {
|
|
6325
|
+
if (this.config.publicKey) {
|
|
6326
|
+
params.set("public_key", this.config.publicKey);
|
|
6327
|
+
}
|
|
6328
|
+
if (this.config.accessToken) {
|
|
6329
|
+
params.set("token", this.config.accessToken);
|
|
6330
|
+
}
|
|
6158
6331
|
}
|
|
6159
6332
|
if (!this.config.appId) {
|
|
6160
6333
|
throw new Error(
|
|
@@ -10011,7 +10184,14 @@ var ConnectCancelledError = class extends Error {
|
|
|
10011
10184
|
}
|
|
10012
10185
|
};
|
|
10013
10186
|
var MAX_HANDSHAKE_RETRIES = 2;
|
|
10014
|
-
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10187
|
+
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10188
|
+
"rate_limited",
|
|
10189
|
+
"auth_unavailable",
|
|
10190
|
+
// 1회용 티켓이 업그레이드에서 거부됐다(만료, 이미 쓰임, 저장소 일시 장애). 다음 시도는 새 티켓을 받거나, 저장소가
|
|
10191
|
+
// 없으면 예전 방식으로 붙는다(core/ws-ticket.ts).
|
|
10192
|
+
"invalid_ws_ticket",
|
|
10193
|
+
"ws_ticket_unavailable"
|
|
10194
|
+
]);
|
|
10015
10195
|
var CONNECTION_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
10016
10196
|
var CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
10017
10197
|
var AUTH_REFRESH_RETRY_MS = 3e4;
|
|
@@ -11126,7 +11306,10 @@ var RealtimeAPI = class {
|
|
|
11126
11306
|
for (let attempt = 1; ; attempt++) {
|
|
11127
11307
|
try {
|
|
11128
11308
|
await this.prepareConnectToken();
|
|
11129
|
-
await this.
|
|
11309
|
+
const ticket = await this.connectTicket(
|
|
11310
|
+
Math.max(deadline - Date.now(), 1)
|
|
11311
|
+
);
|
|
11312
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1), ticket);
|
|
11130
11313
|
return;
|
|
11131
11314
|
} catch (e) {
|
|
11132
11315
|
let wait = null;
|
|
@@ -11169,7 +11352,39 @@ var RealtimeAPI = class {
|
|
|
11169
11352
|
this.initialConnecting = false;
|
|
11170
11353
|
}
|
|
11171
11354
|
}
|
|
11172
|
-
|
|
11355
|
+
/**
|
|
11356
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
11357
|
+
* 토큰과 공개 키를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 티켓은 한 번만 쓰이므로 시도마다
|
|
11358
|
+
* 새로 받는다.
|
|
11359
|
+
*
|
|
11360
|
+
* 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고, doConnect 가 예전처럼 자격증명을 URL 에 싣는다. 발급이
|
|
11361
|
+
* 자격증명을 거부하면 업그레이드 거부와 같은 {@link RealtimeConnectionError} 로 던진다 — 재시도 규칙이 같다.
|
|
11362
|
+
*/
|
|
11363
|
+
async connectTicket(timeoutMs) {
|
|
11364
|
+
const accessToken = this.options.accessToken;
|
|
11365
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
11366
|
+
if (!accessToken && !publicKey) return null;
|
|
11367
|
+
try {
|
|
11368
|
+
return await fetchWsTicket(
|
|
11369
|
+
ticketUrl(this.socketUrl, "/v1/realtime/ws-ticket"),
|
|
11370
|
+
"socket.realtime",
|
|
11371
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey },
|
|
11372
|
+
{ timeoutMs }
|
|
11373
|
+
);
|
|
11374
|
+
} catch (e) {
|
|
11375
|
+
if (e instanceof WsTicketError) {
|
|
11376
|
+
this.log(`WebSocket ticket rejected: ${e.code} (${e.status})`);
|
|
11377
|
+
throw new RealtimeConnectionError({
|
|
11378
|
+
code: e.code,
|
|
11379
|
+
message: e.message,
|
|
11380
|
+
closeCode: 4e3 + e.status,
|
|
11381
|
+
retryAfterMs: e.retryAfterMs
|
|
11382
|
+
});
|
|
11383
|
+
}
|
|
11384
|
+
throw e;
|
|
11385
|
+
}
|
|
11386
|
+
}
|
|
11387
|
+
async doConnect(timeoutMs = this.options.timeout, ticket = null) {
|
|
11173
11388
|
return new Promise((resolve, reject) => {
|
|
11174
11389
|
this.state = "connecting";
|
|
11175
11390
|
this.notifyStateChange();
|
|
@@ -11178,9 +11393,10 @@ var RealtimeAPI = class {
|
|
|
11178
11393
|
const usingDirect = !!directWsBase;
|
|
11179
11394
|
const wsUrl = (directWsBase ?? this.socketUrl).replace(/^http/, "ws");
|
|
11180
11395
|
let url;
|
|
11396
|
+
const auth = ticket ? `ticket=${encodeURIComponent(ticket)}` : void 0;
|
|
11181
11397
|
this.tokenRefreshNegotiated = false;
|
|
11182
11398
|
if (this.options.accessToken) {
|
|
11183
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11399
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `access_token=${encodeURIComponent(this.options.accessToken)}`}&client_id=${this.clientId}`;
|
|
11184
11400
|
if (this.http.canRefreshMemberToken(this.options.accessToken)) {
|
|
11185
11401
|
this.tokenRefreshNegotiated = true;
|
|
11186
11402
|
url += "&token_refresh=1";
|
|
@@ -11196,7 +11412,7 @@ var RealtimeAPI = class {
|
|
|
11196
11412
|
reject(error);
|
|
11197
11413
|
return;
|
|
11198
11414
|
}
|
|
11199
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11415
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `public_key=${encodeURIComponent(publicKey)}`}&client_id=${this.clientId}`;
|
|
11200
11416
|
this.log("Using API Key authentication");
|
|
11201
11417
|
}
|
|
11202
11418
|
if (this.userId) {
|
|
@@ -11676,7 +11892,9 @@ var RealtimeAPI = class {
|
|
|
11676
11892
|
try {
|
|
11677
11893
|
await this.prepareConnectToken();
|
|
11678
11894
|
if (this.state !== "reconnecting") return;
|
|
11679
|
-
await this.
|
|
11895
|
+
const ticket = await this.connectTicket(this.options.timeout);
|
|
11896
|
+
if (this.state !== "reconnecting") return;
|
|
11897
|
+
await this.doConnect(void 0, ticket);
|
|
11680
11898
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
11681
11899
|
const snapshot = this.pendingRestore;
|
|
11682
11900
|
this.pendingRestore = null;
|
|
@@ -13279,8 +13497,8 @@ var SupportAPI = class {
|
|
|
13279
13497
|
}
|
|
13280
13498
|
};
|
|
13281
13499
|
function detectSdkVersion() {
|
|
13282
|
-
if ("6.
|
|
13283
|
-
return "6.
|
|
13500
|
+
if ("6.14.0") {
|
|
13501
|
+
return "6.14.0";
|
|
13284
13502
|
}
|
|
13285
13503
|
return "unknown";
|
|
13286
13504
|
}
|
|
@@ -14249,9 +14467,35 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14249
14467
|
}
|
|
14250
14468
|
return this.connectWebSocket();
|
|
14251
14469
|
}
|
|
14252
|
-
connectWebSocket() {
|
|
14470
|
+
async connectWebSocket() {
|
|
14471
|
+
const before = this.wsGeneration;
|
|
14472
|
+
const ticket = await this.signalingTicket();
|
|
14473
|
+
if (this.wsGeneration !== before) {
|
|
14474
|
+
throw new Error("\uC5F0\uACB0\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
14475
|
+
}
|
|
14476
|
+
return this.openSignalingSocket(ticket);
|
|
14477
|
+
}
|
|
14478
|
+
/**
|
|
14479
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰
|
|
14480
|
+
* (`?access_token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
14481
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 그 이유로 던진다.
|
|
14482
|
+
*/
|
|
14483
|
+
async signalingTicket() {
|
|
14484
|
+
const accessToken = this.http.getAccessToken();
|
|
14485
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
14486
|
+
if (!accessToken && !publicKey || !this.appId) return null;
|
|
14487
|
+
return fetchWsTicket(
|
|
14488
|
+
ticketUrl(
|
|
14489
|
+
this.webrtcUrl,
|
|
14490
|
+
`/v1/apps/${encodeURIComponent(this.appId)}/signaling/ws-ticket`
|
|
14491
|
+
),
|
|
14492
|
+
"webrtc.signaling",
|
|
14493
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
14494
|
+
);
|
|
14495
|
+
}
|
|
14496
|
+
openSignalingSocket(ticket) {
|
|
14253
14497
|
return new Promise((resolve, reject) => {
|
|
14254
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
14498
|
+
const wsUrl = this.buildWebSocketUrl(ticket);
|
|
14255
14499
|
const generation = ++this.wsGeneration;
|
|
14256
14500
|
const ws = new WebSocket(wsUrl);
|
|
14257
14501
|
this.ws = ws;
|
|
@@ -14320,12 +14564,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14320
14564
|
ws.onerror = null;
|
|
14321
14565
|
ws.onclose = null;
|
|
14322
14566
|
}
|
|
14323
|
-
buildWebSocketUrl() {
|
|
14567
|
+
buildWebSocketUrl(ticket = null) {
|
|
14324
14568
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
14325
14569
|
const publicKey = this.http.getPublicKey();
|
|
14326
14570
|
const accessToken = this.http.getAccessToken();
|
|
14327
14571
|
let authParam = "";
|
|
14328
|
-
if (
|
|
14572
|
+
if (ticket) {
|
|
14573
|
+
authParam = `ticket=${encodeURIComponent(ticket)}`;
|
|
14574
|
+
} else if (accessToken) {
|
|
14329
14575
|
authParam = `access_token=${encodeURIComponent(accessToken)}`;
|
|
14330
14576
|
} else if (publicKey) {
|
|
14331
14577
|
authParam = `public_key=${encodeURIComponent(publicKey)}`;
|
|
@@ -14983,11 +15229,35 @@ var WebSocketTransport = class {
|
|
|
14983
15229
|
this.onClose = onClose;
|
|
14984
15230
|
this.onError = onError;
|
|
14985
15231
|
}
|
|
14986
|
-
connect() {
|
|
15232
|
+
async connect() {
|
|
15233
|
+
const ticket = await this.connectionTicket();
|
|
15234
|
+
return this.openSocket(ticket);
|
|
15235
|
+
}
|
|
15236
|
+
/**
|
|
15237
|
+
* 연결 시도 한 번에 쓸 1회용 티켓(core/ws-ticket.ts). 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼
|
|
15238
|
+
* 자격증명을 URL 에 싣는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
15239
|
+
*/
|
|
15240
|
+
async connectionTicket() {
|
|
15241
|
+
const { accessToken, publicKey, appId } = this.config;
|
|
15242
|
+
if (!accessToken && !publicKey || !appId) return null;
|
|
15243
|
+
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15244
|
+
try {
|
|
15245
|
+
return await fetchWsTicket(
|
|
15246
|
+
ticketUrl(baseUrl, `/v1/game/${encodeURIComponent(appId)}/ws-ticket`),
|
|
15247
|
+
"game.ws",
|
|
15248
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
15249
|
+
);
|
|
15250
|
+
} catch (e) {
|
|
15251
|
+
if (e instanceof WsTicketError) {
|
|
15252
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
15253
|
+
}
|
|
15254
|
+
throw e;
|
|
15255
|
+
}
|
|
15256
|
+
}
|
|
15257
|
+
openSocket(ticket) {
|
|
14987
15258
|
return new Promise((resolve, reject) => {
|
|
14988
|
-
const url = this.buildUrl();
|
|
14989
15259
|
try {
|
|
14990
|
-
this.ws = new WebSocket(
|
|
15260
|
+
this.ws = new WebSocket(this.buildUrl(ticket));
|
|
14991
15261
|
this.ws.binaryType = "arraybuffer";
|
|
14992
15262
|
} catch (error) {
|
|
14993
15263
|
reject(error);
|
|
@@ -15017,16 +15287,20 @@ var WebSocketTransport = class {
|
|
|
15017
15287
|
this.ws.addEventListener("message", onMessage);
|
|
15018
15288
|
});
|
|
15019
15289
|
}
|
|
15020
|
-
buildUrl() {
|
|
15290
|
+
buildUrl(ticket = null) {
|
|
15021
15291
|
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15022
15292
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
15023
15293
|
const params = new URLSearchParams();
|
|
15024
15294
|
params.set("client_id", requireClientId(this.config.clientId));
|
|
15025
|
-
if (
|
|
15026
|
-
params.set("
|
|
15027
|
-
}
|
|
15028
|
-
|
|
15029
|
-
|
|
15295
|
+
if (ticket) {
|
|
15296
|
+
params.set("ticket", ticket);
|
|
15297
|
+
} else {
|
|
15298
|
+
if (this.config.publicKey) {
|
|
15299
|
+
params.set("public_key", this.config.publicKey);
|
|
15300
|
+
}
|
|
15301
|
+
if (this.config.accessToken) {
|
|
15302
|
+
params.set("token", this.config.accessToken);
|
|
15303
|
+
}
|
|
15030
15304
|
}
|
|
15031
15305
|
if (!this.config.appId) {
|
|
15032
15306
|
throw new Error(
|
|
@@ -15705,6 +15979,7 @@ export {
|
|
|
15705
15979
|
RolesAPI,
|
|
15706
15980
|
SessionManager,
|
|
15707
15981
|
VideoProcessingError,
|
|
15982
|
+
WsTicketError,
|
|
15708
15983
|
index_default as default,
|
|
15709
15984
|
detectInAppBrowser,
|
|
15710
15985
|
escapeToExternalBrowser,
|