connectbase-client 6.12.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 +71 -0
- package/README.md +8 -1
- package/dist/cli.js +1 -1
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +259 -67
- package/dist/index.d.ts +259 -67
- package/dist/index.js +441 -79
- package/dist/index.mjs +440 -79
- 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);
|
|
4597
4716
|
}
|
|
4598
|
-
|
|
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
|
+
);
|
|
4732
|
+
}
|
|
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;
|
|
@@ -12657,32 +12875,41 @@ var SubscriptionAPI = class {
|
|
|
12657
12875
|
* 구독 생성
|
|
12658
12876
|
* 정기결제 구독을 생성합니다.
|
|
12659
12877
|
*
|
|
12660
|
-
*
|
|
12661
|
-
*
|
|
12878
|
+
* **금액과 요금제는 서버가 정합니다.** 브라우저에서는 콘솔 가격표의 정기 상품 `product_id` 만 보내고, 서버가
|
|
12879
|
+
* 금액, 통화, 주기, 플랜명, 무료 체험, MoR PG 가격 ref 를 상품에서 정합니다. 금액을 직접 보내는 형태(레거시)는
|
|
12880
|
+
* 결제 설정의 금액 출처가 `client` 인 앱이거나 서버(시크릿 키, service_role 함수)에서 부를 때만 받습니다.
|
|
12881
|
+
*
|
|
12882
|
+
* @param data - 가격표 정기 상품(`product_id`) 또는 금액과 요금제, 빌링키, 고객 정보 등
|
|
12883
|
+
* @returns 생성된 구독 정보 (`product_id` 포함)
|
|
12662
12884
|
*
|
|
12663
12885
|
* @example
|
|
12664
12886
|
* ```typescript
|
|
12887
|
+
* // 가격표 구독 (권장) — 금액, 주기, 체험 기간은 콘솔 가격표의 premium_month 가 정한다
|
|
12665
12888
|
* const subscription = await client.subscription.create({
|
|
12666
12889
|
* billing_key_id: 'bk_xxxxx',
|
|
12667
|
-
*
|
|
12668
|
-
* amount: 9900,
|
|
12669
|
-
* billing_cycle: 'monthly',
|
|
12670
|
-
* billing_day: 15, // 매월 15일 결제
|
|
12671
|
-
* trial_days: 7 // 7일 무료 체험
|
|
12890
|
+
* product_id: 'premium_month',
|
|
12672
12891
|
* })
|
|
12673
12892
|
*
|
|
12674
|
-
* // MoR(dodo/paddle) 구독 + 할인 코드
|
|
12893
|
+
* // MoR(dodo/paddle) 구독 + 할인 코드 — PG 가격 ref 도 상품의 provider_price_refs 에서 서버가 고른다
|
|
12675
12894
|
* const promo = await client.subscription.create({
|
|
12895
|
+
* product_id: 'premium_month',
|
|
12896
|
+
* discount_code: 'LAUNCH50', // 첫 결제에 적용 (몇 주기까지인지는 PG 쿠폰 설정이 결정)
|
|
12897
|
+
* })
|
|
12898
|
+
*
|
|
12899
|
+
* // 금액 직접 지정 (레거시 앱, 서버 호출) — 가격표 전용 앱에서 브라우저가 보내면 403 client_amount_not_allowed
|
|
12900
|
+
* const legacy = await client.subscription.create({
|
|
12901
|
+
* billing_key_id: 'bk_xxxxx',
|
|
12676
12902
|
* plan_name: '프리미엄 플랜',
|
|
12677
12903
|
* amount: 9900,
|
|
12678
12904
|
* billing_cycle: 'monthly',
|
|
12679
|
-
* provider_price_ref: 'pdt_xxxxx', // PG 카탈로그의 recurring 상품
|
|
12680
|
-
* discount_code: 'LAUNCH50', // 첫 결제에 적용 (몇 주기까지인지는 PG 쿠폰 설정이 결정)
|
|
12681
|
-
* trial_days: 14, // Dodo 만 지원 — Paddle 은 400 trial_days_unsupported
|
|
12682
12905
|
* })
|
|
12683
12906
|
* ```
|
|
12684
12907
|
*
|
|
12685
12908
|
* @remarks
|
|
12909
|
+
* 권한은 구독의 `product_id` 로 지급합니다(`metadata` 는 클라이언트 값). 가격표 전용 앱의 브라우저 호출은
|
|
12910
|
+
* `start_now: false` 와 `billing_day` 를 쓸 수 없습니다(403 `client_schedule_not_allowed`) — 둘 다 한 번 낸 돈으로 쓰는
|
|
12911
|
+
* 기간을 늘립니다. 무료 체험은 상품의 `trial_days` 가 정합니다.
|
|
12912
|
+
*
|
|
12686
12913
|
* 할인 코드(`discount_code`)와 `trial_days` 의 프로바이더 지원 범위가 다릅니다:
|
|
12687
12914
|
* - **Dodo**: 할인 코드 O, 결제창 쿠폰 입력 토글 O, 구독별 체험 일수 O
|
|
12688
12915
|
* - **Paddle**: 할인 코드 O(코드→내부 ID 자동 해소), 쿠폰 입력칸은 항상 노출(숨김 불가),
|
|
@@ -12754,9 +12981,9 @@ var SubscriptionAPI = class {
|
|
|
12754
12981
|
*
|
|
12755
12982
|
* @example
|
|
12756
12983
|
* ```typescript
|
|
12984
|
+
* // 서버(시크릿 키, service_role 함수)에서 다른 가격표 상품으로 바꾸기 — 다음 청구부터 새 금액
|
|
12757
12985
|
* await client.subscription.update(subscriptionId, {
|
|
12758
|
-
*
|
|
12759
|
-
* amount: 29900
|
|
12986
|
+
* product_id: 'enterprise_month',
|
|
12760
12987
|
* })
|
|
12761
12988
|
*
|
|
12762
12989
|
* // 다음 결제일을 특정 날짜로 옮기기 (기간권·보상)
|
|
@@ -12942,11 +13169,10 @@ var SubscriptionAPI = class {
|
|
|
12942
13169
|
*
|
|
12943
13170
|
* @example
|
|
12944
13171
|
* ```typescript
|
|
13172
|
+
* // 가격표 정기 상품으로 바꾼다 — 새 PG 가격 ref 와 플랜명은 서버가 상품에서 정한다
|
|
12945
13173
|
* await client.subscription.changePlan(subscriptionId, {
|
|
12946
|
-
*
|
|
12947
|
-
*
|
|
12948
|
-
* amount: 24900,
|
|
12949
|
-
* proration_mode: 'prorated_immediately',
|
|
13174
|
+
* product_id: 'family_month',
|
|
13175
|
+
* proration_mode: 'prorated_immediately', // 브라우저는 지금 정산하는 모드만
|
|
12950
13176
|
* })
|
|
12951
13177
|
* ```
|
|
12952
13178
|
*/
|
|
@@ -12979,7 +13205,7 @@ var SubscriptionAPI = class {
|
|
|
12979
13205
|
* @example
|
|
12980
13206
|
* ```typescript
|
|
12981
13207
|
* const preview = await client.subscription.previewChangePlan(subscriptionId, {
|
|
12982
|
-
*
|
|
13208
|
+
* product_id: 'family_month',
|
|
12983
13209
|
* })
|
|
12984
13210
|
* // 통화를 함께 포맷한다 — 금액만 쓰면 다통화에서 거짓 금액이 된다
|
|
12985
13211
|
* format(preview.immediate_charge_amount, preview.currency)
|
|
@@ -13033,10 +13259,11 @@ var SubscriptionAPI = class {
|
|
|
13033
13259
|
*
|
|
13034
13260
|
* @example
|
|
13035
13261
|
* ```typescript
|
|
13262
|
+
* // 가격표 단건 상품으로 청구 — 금액과 상품명은 서버가 정한다
|
|
13036
13263
|
* const result = await client.subscription.chargeWithBillingKey({
|
|
13037
13264
|
* billing_key_id: 'bk_xxxxx',
|
|
13038
|
-
*
|
|
13039
|
-
*
|
|
13265
|
+
* product_id: 'gem_100',
|
|
13266
|
+
* quantity: 2,
|
|
13040
13267
|
* })
|
|
13041
13268
|
*
|
|
13042
13269
|
* if (result.status === 'done') {
|
|
@@ -13270,8 +13497,8 @@ var SupportAPI = class {
|
|
|
13270
13497
|
}
|
|
13271
13498
|
};
|
|
13272
13499
|
function detectSdkVersion() {
|
|
13273
|
-
if ("6.
|
|
13274
|
-
return "6.
|
|
13500
|
+
if ("6.14.0") {
|
|
13501
|
+
return "6.14.0";
|
|
13275
13502
|
}
|
|
13276
13503
|
return "unknown";
|
|
13277
13504
|
}
|
|
@@ -14165,7 +14392,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14165
14392
|
this.currentRoomId = null;
|
|
14166
14393
|
this.currentPeerId = null;
|
|
14167
14394
|
this.currentUserId = null;
|
|
14168
|
-
|
|
14395
|
+
/** connect 가 요청한 방송자 역할 (switchRoom 이 이어받는다) */
|
|
14396
|
+
this.broadcasterRequested = false;
|
|
14397
|
+
/** 서버가 정한 방송자 역할 (`joined` 의 is_broadcaster) */
|
|
14398
|
+
this.broadcasterGranted = false;
|
|
14399
|
+
/** 비공개 룸 허용 목록 요청 (이 연결에서만 쓴다) */
|
|
14400
|
+
this.allowedMemberIds = null;
|
|
14401
|
+
this.privateRoom = false;
|
|
14402
|
+
this.warningListeners = [];
|
|
14169
14403
|
this.localStream = null;
|
|
14170
14404
|
this.channelType = "interactive";
|
|
14171
14405
|
// 피어 연결 관리
|
|
@@ -14219,7 +14453,10 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14219
14453
|
this.setState("connecting");
|
|
14220
14454
|
this.currentRoomId = options.roomId;
|
|
14221
14455
|
this.currentUserId = options.userId || null;
|
|
14222
|
-
this.
|
|
14456
|
+
this.broadcasterRequested = options.isBroadcaster || false;
|
|
14457
|
+
this.broadcasterGranted = false;
|
|
14458
|
+
this.allowedMemberIds = options.allowedMemberIds ? [...options.allowedMemberIds] : null;
|
|
14459
|
+
this.privateRoom = false;
|
|
14223
14460
|
this.localStream = options.localStream || null;
|
|
14224
14461
|
if (this.iceServers.length === 0) {
|
|
14225
14462
|
try {
|
|
@@ -14230,9 +14467,35 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14230
14467
|
}
|
|
14231
14468
|
return this.connectWebSocket();
|
|
14232
14469
|
}
|
|
14233
|
-
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) {
|
|
14234
14497
|
return new Promise((resolve, reject) => {
|
|
14235
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
14498
|
+
const wsUrl = this.buildWebSocketUrl(ticket);
|
|
14236
14499
|
const generation = ++this.wsGeneration;
|
|
14237
14500
|
const ws = new WebSocket(wsUrl);
|
|
14238
14501
|
this.ws = ws;
|
|
@@ -14251,13 +14514,16 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14251
14514
|
return;
|
|
14252
14515
|
}
|
|
14253
14516
|
this.reconnectAttempts = 0;
|
|
14517
|
+
const joinData = {
|
|
14518
|
+
is_broadcaster: this.broadcasterRequested
|
|
14519
|
+
};
|
|
14520
|
+
if (this.allowedMemberIds) {
|
|
14521
|
+
joinData.allowed_member_ids = this.allowedMemberIds;
|
|
14522
|
+
}
|
|
14254
14523
|
this.sendSignaling({
|
|
14255
14524
|
type: "join",
|
|
14256
14525
|
room_id: this.currentRoomId,
|
|
14257
|
-
data:
|
|
14258
|
-
user_id: this.currentUserId,
|
|
14259
|
-
is_broadcaster: this.isBroadcaster
|
|
14260
|
-
}
|
|
14526
|
+
data: joinData
|
|
14261
14527
|
});
|
|
14262
14528
|
};
|
|
14263
14529
|
ws.onmessage = async (event) => {
|
|
@@ -14298,12 +14564,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14298
14564
|
ws.onerror = null;
|
|
14299
14565
|
ws.onclose = null;
|
|
14300
14566
|
}
|
|
14301
|
-
buildWebSocketUrl() {
|
|
14567
|
+
buildWebSocketUrl(ticket = null) {
|
|
14302
14568
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
14303
14569
|
const publicKey = this.http.getPublicKey();
|
|
14304
14570
|
const accessToken = this.http.getAccessToken();
|
|
14305
14571
|
let authParam = "";
|
|
14306
|
-
if (
|
|
14572
|
+
if (ticket) {
|
|
14573
|
+
authParam = `ticket=${encodeURIComponent(ticket)}`;
|
|
14574
|
+
} else if (accessToken) {
|
|
14307
14575
|
authParam = `access_token=${encodeURIComponent(accessToken)}`;
|
|
14308
14576
|
} else if (publicKey) {
|
|
14309
14577
|
authParam = `public_key=${encodeURIComponent(publicKey)}`;
|
|
@@ -14325,6 +14593,8 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14325
14593
|
if (data.channel_type) {
|
|
14326
14594
|
this.channelType = data.channel_type;
|
|
14327
14595
|
}
|
|
14596
|
+
this.broadcasterGranted = data.is_broadcaster === true;
|
|
14597
|
+
this.privateRoom = data.private === true;
|
|
14328
14598
|
const peers = data.peers || [];
|
|
14329
14599
|
for (const peer of peers) {
|
|
14330
14600
|
if (peer.peer_id !== this.currentPeerId) {
|
|
@@ -14365,9 +14635,16 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14365
14635
|
await this.handleICECandidate(msg.peer_id, msg.candidate);
|
|
14366
14636
|
}
|
|
14367
14637
|
break;
|
|
14638
|
+
case "warning": {
|
|
14639
|
+
const data = msg.data && typeof msg.data === "object" ? msg.data : {};
|
|
14640
|
+
this.emitWarning({
|
|
14641
|
+
code: typeof data.code === "string" ? data.code : "unknown",
|
|
14642
|
+
message: typeof data.message === "string" ? data.message : ""
|
|
14643
|
+
});
|
|
14644
|
+
break;
|
|
14645
|
+
}
|
|
14368
14646
|
case "error": {
|
|
14369
|
-
const
|
|
14370
|
-
const error = new Error(errorMsg);
|
|
14647
|
+
const error = new Error(_WebRTCAPI.signalingErrorMessage(msg.data));
|
|
14371
14648
|
this.emitError(error);
|
|
14372
14649
|
reject?.(error);
|
|
14373
14650
|
break;
|
|
@@ -14465,6 +14742,19 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14465
14742
|
}
|
|
14466
14743
|
this.remoteStreams.delete(remotePeerId);
|
|
14467
14744
|
}
|
|
14745
|
+
/**
|
|
14746
|
+
* 서버 error 메시지의 data 에서 안내 문구를 꺼낸다. 대부분은 문자열이지만 할당량, 동시접속 초과는
|
|
14747
|
+
* `{ error, message, ... }` 객체로 온다.
|
|
14748
|
+
*/
|
|
14749
|
+
static signalingErrorMessage(data) {
|
|
14750
|
+
if (typeof data === "string" && data) return data;
|
|
14751
|
+
if (data && typeof data === "object") {
|
|
14752
|
+
const obj = data;
|
|
14753
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
14754
|
+
if (typeof obj.error === "string" && obj.error) return obj.error;
|
|
14755
|
+
}
|
|
14756
|
+
return "Unknown error";
|
|
14757
|
+
}
|
|
14468
14758
|
sendSignaling(msg) {
|
|
14469
14759
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
14470
14760
|
this.ws.send(JSON.stringify(msg));
|
|
@@ -14530,6 +14820,8 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14530
14820
|
this.currentRoomId = null;
|
|
14531
14821
|
this.currentPeerId = null;
|
|
14532
14822
|
this.localStream = null;
|
|
14823
|
+
this.broadcasterGranted = false;
|
|
14824
|
+
this.privateRoom = false;
|
|
14533
14825
|
this.setState("disconnected");
|
|
14534
14826
|
}
|
|
14535
14827
|
/**
|
|
@@ -14547,9 +14839,15 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14547
14839
|
async switchRoom(roomId, options = {}) {
|
|
14548
14840
|
const localStream = options.localStream ?? this.localStream ?? void 0;
|
|
14549
14841
|
const userId = options.userId ?? this.currentUserId ?? void 0;
|
|
14550
|
-
const isBroadcaster = options.isBroadcaster ?? this.
|
|
14842
|
+
const isBroadcaster = options.isBroadcaster ?? this.broadcasterRequested;
|
|
14551
14843
|
this.disconnect();
|
|
14552
|
-
await this.connect({
|
|
14844
|
+
await this.connect({
|
|
14845
|
+
roomId,
|
|
14846
|
+
userId,
|
|
14847
|
+
isBroadcaster,
|
|
14848
|
+
localStream,
|
|
14849
|
+
allowedMemberIds: options.allowedMemberIds
|
|
14850
|
+
});
|
|
14553
14851
|
}
|
|
14554
14852
|
/**
|
|
14555
14853
|
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
@@ -14602,6 +14900,19 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14602
14900
|
getChannelType() {
|
|
14603
14901
|
return this.channelType;
|
|
14604
14902
|
}
|
|
14903
|
+
/**
|
|
14904
|
+
* 서버가 정한 방송자 역할인지 조회한다. 요청(`isBroadcaster: true`)과 다를 수 있다 — 익명 연결이나
|
|
14905
|
+
* 채널의 방송자 역할이 없는 회원은 시청자로 참가한다(`onWarning` 으로 이유가 온다).
|
|
14906
|
+
*/
|
|
14907
|
+
isBroadcaster() {
|
|
14908
|
+
return this.broadcasterGranted;
|
|
14909
|
+
}
|
|
14910
|
+
/**
|
|
14911
|
+
* 지금 룸이 회원 허용 목록이 있는 비공개 룸인지 조회한다.
|
|
14912
|
+
*/
|
|
14913
|
+
isPrivateRoom() {
|
|
14914
|
+
return this.privateRoom;
|
|
14915
|
+
}
|
|
14605
14916
|
/**
|
|
14606
14917
|
* 원격 스트림 조회
|
|
14607
14918
|
*/
|
|
@@ -14672,6 +14983,24 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14672
14983
|
this.errorListeners = this.errorListeners.filter((cb) => cb !== callback);
|
|
14673
14984
|
};
|
|
14674
14985
|
}
|
|
14986
|
+
/**
|
|
14987
|
+
* 서버가 join 요청을 바꿨다는 알림 이벤트 (예: 방송자 요청을 시청자로 낮춤)
|
|
14988
|
+
*
|
|
14989
|
+
* @example
|
|
14990
|
+
* ```typescript
|
|
14991
|
+
* cb.webrtc.onWarning(({ code, message }) => {
|
|
14992
|
+
* if (code === 'broadcaster_role_required') showToast(message)
|
|
14993
|
+
* })
|
|
14994
|
+
* ```
|
|
14995
|
+
*/
|
|
14996
|
+
onWarning(callback) {
|
|
14997
|
+
this.warningListeners.push(callback);
|
|
14998
|
+
return () => {
|
|
14999
|
+
this.warningListeners = this.warningListeners.filter(
|
|
15000
|
+
(cb) => cb !== callback
|
|
15001
|
+
);
|
|
15002
|
+
};
|
|
15003
|
+
}
|
|
14675
15004
|
/**
|
|
14676
15005
|
* 피어 참가 이벤트
|
|
14677
15006
|
*/
|
|
@@ -14717,6 +15046,9 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14717
15046
|
emitError(error) {
|
|
14718
15047
|
this.errorListeners.forEach((cb) => cb(error));
|
|
14719
15048
|
}
|
|
15049
|
+
emitWarning(warning) {
|
|
15050
|
+
this.warningListeners.forEach((cb) => cb(warning));
|
|
15051
|
+
}
|
|
14720
15052
|
emitPeerJoined(peerId, info) {
|
|
14721
15053
|
this.peerJoinedListeners.forEach((cb) => cb(peerId, info));
|
|
14722
15054
|
}
|
|
@@ -14897,11 +15229,35 @@ var WebSocketTransport = class {
|
|
|
14897
15229
|
this.onClose = onClose;
|
|
14898
15230
|
this.onError = onError;
|
|
14899
15231
|
}
|
|
14900
|
-
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) {
|
|
14901
15258
|
return new Promise((resolve, reject) => {
|
|
14902
|
-
const url = this.buildUrl();
|
|
14903
15259
|
try {
|
|
14904
|
-
this.ws = new WebSocket(
|
|
15260
|
+
this.ws = new WebSocket(this.buildUrl(ticket));
|
|
14905
15261
|
this.ws.binaryType = "arraybuffer";
|
|
14906
15262
|
} catch (error) {
|
|
14907
15263
|
reject(error);
|
|
@@ -14931,16 +15287,20 @@ var WebSocketTransport = class {
|
|
|
14931
15287
|
this.ws.addEventListener("message", onMessage);
|
|
14932
15288
|
});
|
|
14933
15289
|
}
|
|
14934
|
-
buildUrl() {
|
|
15290
|
+
buildUrl(ticket = null) {
|
|
14935
15291
|
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
14936
15292
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
14937
15293
|
const params = new URLSearchParams();
|
|
14938
15294
|
params.set("client_id", requireClientId(this.config.clientId));
|
|
14939
|
-
if (
|
|
14940
|
-
params.set("
|
|
14941
|
-
}
|
|
14942
|
-
|
|
14943
|
-
|
|
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
|
+
}
|
|
14944
15304
|
}
|
|
14945
15305
|
if (!this.config.appId) {
|
|
14946
15306
|
throw new Error(
|
|
@@ -15619,6 +15979,7 @@ export {
|
|
|
15619
15979
|
RolesAPI,
|
|
15620
15980
|
SessionManager,
|
|
15621
15981
|
VideoProcessingError,
|
|
15982
|
+
WsTicketError,
|
|
15622
15983
|
index_default as default,
|
|
15623
15984
|
detectInAppBrowser,
|
|
15624
15985
|
escapeToExternalBrowser,
|