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.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;
|
|
@@ -308,7 +309,7 @@ var RealtimeConnectionError = class extends Error {
|
|
|
308
309
|
}
|
|
309
310
|
};
|
|
310
311
|
function isRetryableRealtimeRejection(code) {
|
|
311
|
-
return code === "connection_limit_exceeded" || code === "server_at_capacity" || code === "rate_limited" || code === "auth_unavailable" || code === "internal_error";
|
|
312
|
+
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";
|
|
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",
|
|
@@ -4645,15 +4744,52 @@ var DatabaseAPI = class {
|
|
|
4645
4744
|
);
|
|
4646
4745
|
if (fresh) opts.accessToken = fresh;
|
|
4647
4746
|
}
|
|
4648
|
-
|
|
4747
|
+
let ticket;
|
|
4748
|
+
try {
|
|
4749
|
+
ticket = await this.realtimeTicket();
|
|
4750
|
+
} catch (e) {
|
|
4751
|
+
if (e instanceof WsTicketError) {
|
|
4752
|
+
if (e.status === 429 && this.realtimeOptions) {
|
|
4753
|
+
this.attemptRealtimeReconnect();
|
|
4754
|
+
} else {
|
|
4755
|
+
this.setRealtimeState("disconnected");
|
|
4756
|
+
}
|
|
4757
|
+
const err = new RealtimeConnectionError({
|
|
4758
|
+
code: e.code,
|
|
4759
|
+
message: e.message,
|
|
4760
|
+
closeCode: 4e3 + e.status,
|
|
4761
|
+
retryAfterMs: e.retryAfterMs
|
|
4762
|
+
});
|
|
4763
|
+
this.realtimeOnError?.(err);
|
|
4764
|
+
throw err;
|
|
4765
|
+
}
|
|
4766
|
+
throw e;
|
|
4767
|
+
}
|
|
4768
|
+
return this.doRealtimeConnect(ticket);
|
|
4649
4769
|
}
|
|
4650
|
-
|
|
4770
|
+
/**
|
|
4771
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰을
|
|
4772
|
+
* URL(`?access_token=`)에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가 티켓을 모르거나(구버전) 잠시 못
|
|
4773
|
+
* 주면 null 이고 예전처럼 붙는다. cross-app OAuth 토큰도 같은 경로다(서버가 티켓에서 토큰을 되살려 같은 검증을 한다).
|
|
4774
|
+
*/
|
|
4775
|
+
async realtimeTicket() {
|
|
4776
|
+
const opts = this.realtimeOptions;
|
|
4777
|
+
if (!opts?.accessToken) return null;
|
|
4778
|
+
const baseUrl = opts.dataServerUrl || this.http.getBaseUrl();
|
|
4779
|
+
return fetchWsTicket(
|
|
4780
|
+
ticketUrl(baseUrl, "/v1/database/realtime/ws-ticket"),
|
|
4781
|
+
"data.realtime",
|
|
4782
|
+
{ kind: "access_token", value: opts.accessToken },
|
|
4783
|
+
{ timeoutMs: REALTIME_CONNECT_TIMEOUT_MS }
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4786
|
+
doRealtimeConnect(ticket = null) {
|
|
4651
4787
|
if (!this.realtimeOptions)
|
|
4652
4788
|
return Promise.reject(new Error("No realtime options"));
|
|
4653
4789
|
this.setRealtimeState("connecting");
|
|
4654
4790
|
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
4655
4791
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
4656
|
-
let url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4792
|
+
let url = ticket ? `${wsUrl}/v1/database/realtime/ws?ticket=${encodeURIComponent(ticket)}` : `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
4657
4793
|
this.realtimeTokenRefresh = this.http.canRefreshMemberToken(
|
|
4658
4794
|
this.realtimeOptions.accessToken
|
|
4659
4795
|
);
|
|
@@ -4672,7 +4808,7 @@ var DatabaseAPI = class {
|
|
|
4672
4808
|
this.setRealtimeState("disconnected");
|
|
4673
4809
|
reject(new Error("Connection timeout"));
|
|
4674
4810
|
}
|
|
4675
|
-
},
|
|
4811
|
+
}, REALTIME_CONNECT_TIMEOUT_MS);
|
|
4676
4812
|
this.realtimeWs.onopen = () => {
|
|
4677
4813
|
if (!settled) {
|
|
4678
4814
|
settled = true;
|
|
@@ -5213,7 +5349,7 @@ var EndpointAPI = class {
|
|
|
5213
5349
|
);
|
|
5214
5350
|
}
|
|
5215
5351
|
const baseUrl = this.http.getBaseUrl().replace(/\/+$/, "");
|
|
5216
|
-
const
|
|
5352
|
+
const ticketUrl2 = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
|
|
5217
5353
|
const ticketHeaders = new Headers();
|
|
5218
5354
|
const pk = this.http.getPublicKey();
|
|
5219
5355
|
if (!pk) {
|
|
@@ -5222,7 +5358,7 @@ var EndpointAPI = class {
|
|
|
5222
5358
|
);
|
|
5223
5359
|
}
|
|
5224
5360
|
ticketHeaders.set("X-Public-Key", pk);
|
|
5225
|
-
const ticketRes = await fetch(
|
|
5361
|
+
const ticketRes = await fetch(ticketUrl2, {
|
|
5226
5362
|
method: "POST",
|
|
5227
5363
|
headers: ticketHeaders,
|
|
5228
5364
|
signal: opts.signal
|
|
@@ -5920,20 +6056,54 @@ var GameRoom = class {
|
|
|
5920
6056
|
* 게임 서버에 연결
|
|
5921
6057
|
*/
|
|
5922
6058
|
connect(roomId) {
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
6059
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
6060
|
+
return Promise.resolve();
|
|
6061
|
+
}
|
|
6062
|
+
if (!this.config.appId) {
|
|
6063
|
+
return Promise.reject(
|
|
6064
|
+
new Error(
|
|
6065
|
+
"cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
|
|
6066
|
+
)
|
|
6067
|
+
);
|
|
6068
|
+
}
|
|
6069
|
+
if (!this.config.clientId) {
|
|
6070
|
+
return Promise.reject(
|
|
6071
|
+
new Error(
|
|
6072
|
+
"cb.game: clientId is required to build a game connection URL. Pass it via `cb.game.createClient({ appId, clientId })`."
|
|
6073
|
+
)
|
|
6074
|
+
);
|
|
6075
|
+
}
|
|
6076
|
+
return this.connectionTicket().then(
|
|
6077
|
+
(ticket) => this.openSocket(roomId, ticket)
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
/**
|
|
6081
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
6082
|
+
* 토큰(`?token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
6083
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
6084
|
+
*/
|
|
6085
|
+
async connectionTicket() {
|
|
6086
|
+
const { accessToken, publicKey, appId, gameServerUrl } = this.config;
|
|
6087
|
+
if (!accessToken && !publicKey || !appId || !gameServerUrl) return null;
|
|
6088
|
+
try {
|
|
6089
|
+
return await fetchWsTicket(
|
|
6090
|
+
ticketUrl(
|
|
6091
|
+
gameServerUrl,
|
|
6092
|
+
`/v1/game/${encodeURIComponent(appId)}/ws-ticket`
|
|
6093
|
+
),
|
|
6094
|
+
"game.ws",
|
|
6095
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
6096
|
+
);
|
|
6097
|
+
} catch (e) {
|
|
6098
|
+
if (e instanceof WsTicketError) {
|
|
6099
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
5935
6100
|
}
|
|
5936
|
-
|
|
6101
|
+
throw e;
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
openSocket(roomId, ticket) {
|
|
6105
|
+
return new Promise((resolve, reject) => {
|
|
6106
|
+
const url = this.buildConnectionUrl(roomId, ticket);
|
|
5937
6107
|
this.ws = new WebSocket(url);
|
|
5938
6108
|
const onOpen = () => {
|
|
5939
6109
|
this._isConnected = true;
|
|
@@ -6189,7 +6359,7 @@ var GameRoom = class {
|
|
|
6189
6359
|
});
|
|
6190
6360
|
}
|
|
6191
6361
|
// Private methods
|
|
6192
|
-
buildConnectionUrl(roomId) {
|
|
6362
|
+
buildConnectionUrl(roomId, ticket) {
|
|
6193
6363
|
const baseUrl = this.config.gameServerUrl;
|
|
6194
6364
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
6195
6365
|
const params = new URLSearchParams();
|
|
@@ -6202,11 +6372,15 @@ var GameRoom = class {
|
|
|
6202
6372
|
if (roomId) {
|
|
6203
6373
|
params.set("room_id", roomId);
|
|
6204
6374
|
}
|
|
6205
|
-
if (
|
|
6206
|
-
params.set("
|
|
6207
|
-
}
|
|
6208
|
-
|
|
6209
|
-
|
|
6375
|
+
if (ticket) {
|
|
6376
|
+
params.set("ticket", ticket);
|
|
6377
|
+
} else {
|
|
6378
|
+
if (this.config.publicKey) {
|
|
6379
|
+
params.set("public_key", this.config.publicKey);
|
|
6380
|
+
}
|
|
6381
|
+
if (this.config.accessToken) {
|
|
6382
|
+
params.set("token", this.config.accessToken);
|
|
6383
|
+
}
|
|
6210
6384
|
}
|
|
6211
6385
|
if (!this.config.appId) {
|
|
6212
6386
|
throw new Error(
|
|
@@ -10063,7 +10237,14 @@ var ConnectCancelledError = class extends Error {
|
|
|
10063
10237
|
}
|
|
10064
10238
|
};
|
|
10065
10239
|
var MAX_HANDSHAKE_RETRIES = 2;
|
|
10066
|
-
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10240
|
+
var RETRY_IN_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
10241
|
+
"rate_limited",
|
|
10242
|
+
"auth_unavailable",
|
|
10243
|
+
// 1회용 티켓이 업그레이드에서 거부됐다(만료, 이미 쓰임, 저장소 일시 장애). 다음 시도는 새 티켓을 받거나, 저장소가
|
|
10244
|
+
// 없으면 예전 방식으로 붙는다(core/ws-ticket.ts).
|
|
10245
|
+
"invalid_ws_ticket",
|
|
10246
|
+
"ws_ticket_unavailable"
|
|
10247
|
+
]);
|
|
10067
10248
|
var CONNECTION_TOKEN_MIN_VALIDITY_SEC = 600;
|
|
10068
10249
|
var CONNECT_TOKEN_MIN_VALIDITY_SEC = 60;
|
|
10069
10250
|
var AUTH_REFRESH_RETRY_MS = 3e4;
|
|
@@ -11178,7 +11359,10 @@ var RealtimeAPI = class {
|
|
|
11178
11359
|
for (let attempt = 1; ; attempt++) {
|
|
11179
11360
|
try {
|
|
11180
11361
|
await this.prepareConnectToken();
|
|
11181
|
-
await this.
|
|
11362
|
+
const ticket = await this.connectTicket(
|
|
11363
|
+
Math.max(deadline - Date.now(), 1)
|
|
11364
|
+
);
|
|
11365
|
+
await this.doConnect(Math.max(deadline - Date.now(), 1), ticket);
|
|
11182
11366
|
return;
|
|
11183
11367
|
} catch (e) {
|
|
11184
11368
|
let wait = null;
|
|
@@ -11221,7 +11405,39 @@ var RealtimeAPI = class {
|
|
|
11221
11405
|
this.initialConnecting = false;
|
|
11222
11406
|
}
|
|
11223
11407
|
}
|
|
11224
|
-
|
|
11408
|
+
/**
|
|
11409
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버
|
|
11410
|
+
* 토큰과 공개 키를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 티켓은 한 번만 쓰이므로 시도마다
|
|
11411
|
+
* 새로 받는다.
|
|
11412
|
+
*
|
|
11413
|
+
* 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고, doConnect 가 예전처럼 자격증명을 URL 에 싣는다. 발급이
|
|
11414
|
+
* 자격증명을 거부하면 업그레이드 거부와 같은 {@link RealtimeConnectionError} 로 던진다 — 재시도 규칙이 같다.
|
|
11415
|
+
*/
|
|
11416
|
+
async connectTicket(timeoutMs) {
|
|
11417
|
+
const accessToken = this.options.accessToken;
|
|
11418
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
11419
|
+
if (!accessToken && !publicKey) return null;
|
|
11420
|
+
try {
|
|
11421
|
+
return await fetchWsTicket(
|
|
11422
|
+
ticketUrl(this.socketUrl, "/v1/realtime/ws-ticket"),
|
|
11423
|
+
"socket.realtime",
|
|
11424
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey },
|
|
11425
|
+
{ timeoutMs }
|
|
11426
|
+
);
|
|
11427
|
+
} catch (e) {
|
|
11428
|
+
if (e instanceof WsTicketError) {
|
|
11429
|
+
this.log(`WebSocket ticket rejected: ${e.code} (${e.status})`);
|
|
11430
|
+
throw new RealtimeConnectionError({
|
|
11431
|
+
code: e.code,
|
|
11432
|
+
message: e.message,
|
|
11433
|
+
closeCode: 4e3 + e.status,
|
|
11434
|
+
retryAfterMs: e.retryAfterMs
|
|
11435
|
+
});
|
|
11436
|
+
}
|
|
11437
|
+
throw e;
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
async doConnect(timeoutMs = this.options.timeout, ticket = null) {
|
|
11225
11441
|
return new Promise((resolve, reject) => {
|
|
11226
11442
|
this.state = "connecting";
|
|
11227
11443
|
this.notifyStateChange();
|
|
@@ -11230,9 +11446,10 @@ var RealtimeAPI = class {
|
|
|
11230
11446
|
const usingDirect = !!directWsBase;
|
|
11231
11447
|
const wsUrl = (directWsBase ?? this.socketUrl).replace(/^http/, "ws");
|
|
11232
11448
|
let url;
|
|
11449
|
+
const auth = ticket ? `ticket=${encodeURIComponent(ticket)}` : void 0;
|
|
11233
11450
|
this.tokenRefreshNegotiated = false;
|
|
11234
11451
|
if (this.options.accessToken) {
|
|
11235
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11452
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `access_token=${encodeURIComponent(this.options.accessToken)}`}&client_id=${this.clientId}`;
|
|
11236
11453
|
if (this.http.canRefreshMemberToken(this.options.accessToken)) {
|
|
11237
11454
|
this.tokenRefreshNegotiated = true;
|
|
11238
11455
|
url += "&token_refresh=1";
|
|
@@ -11248,7 +11465,7 @@ var RealtimeAPI = class {
|
|
|
11248
11465
|
reject(error);
|
|
11249
11466
|
return;
|
|
11250
11467
|
}
|
|
11251
|
-
url = `${wsUrl}/v1/realtime/auth
|
|
11468
|
+
url = `${wsUrl}/v1/realtime/auth?${auth ?? `public_key=${encodeURIComponent(publicKey)}`}&client_id=${this.clientId}`;
|
|
11252
11469
|
this.log("Using API Key authentication");
|
|
11253
11470
|
}
|
|
11254
11471
|
if (this.userId) {
|
|
@@ -11728,7 +11945,9 @@ var RealtimeAPI = class {
|
|
|
11728
11945
|
try {
|
|
11729
11946
|
await this.prepareConnectToken();
|
|
11730
11947
|
if (this.state !== "reconnecting") return;
|
|
11731
|
-
await this.
|
|
11948
|
+
const ticket = await this.connectTicket(this.options.timeout);
|
|
11949
|
+
if (this.state !== "reconnecting") return;
|
|
11950
|
+
await this.doConnect(void 0, ticket);
|
|
11732
11951
|
this.log("Reconnected successfully, restoring subscriptions...");
|
|
11733
11952
|
const snapshot = this.pendingRestore;
|
|
11734
11953
|
this.pendingRestore = null;
|
|
@@ -12709,32 +12928,41 @@ var SubscriptionAPI = class {
|
|
|
12709
12928
|
* 구독 생성
|
|
12710
12929
|
* 정기결제 구독을 생성합니다.
|
|
12711
12930
|
*
|
|
12712
|
-
*
|
|
12713
|
-
*
|
|
12931
|
+
* **금액과 요금제는 서버가 정합니다.** 브라우저에서는 콘솔 가격표의 정기 상품 `product_id` 만 보내고, 서버가
|
|
12932
|
+
* 금액, 통화, 주기, 플랜명, 무료 체험, MoR PG 가격 ref 를 상품에서 정합니다. 금액을 직접 보내는 형태(레거시)는
|
|
12933
|
+
* 결제 설정의 금액 출처가 `client` 인 앱이거나 서버(시크릿 키, service_role 함수)에서 부를 때만 받습니다.
|
|
12934
|
+
*
|
|
12935
|
+
* @param data - 가격표 정기 상품(`product_id`) 또는 금액과 요금제, 빌링키, 고객 정보 등
|
|
12936
|
+
* @returns 생성된 구독 정보 (`product_id` 포함)
|
|
12714
12937
|
*
|
|
12715
12938
|
* @example
|
|
12716
12939
|
* ```typescript
|
|
12940
|
+
* // 가격표 구독 (권장) — 금액, 주기, 체험 기간은 콘솔 가격표의 premium_month 가 정한다
|
|
12717
12941
|
* const subscription = await client.subscription.create({
|
|
12718
12942
|
* billing_key_id: 'bk_xxxxx',
|
|
12719
|
-
*
|
|
12720
|
-
* amount: 9900,
|
|
12721
|
-
* billing_cycle: 'monthly',
|
|
12722
|
-
* billing_day: 15, // 매월 15일 결제
|
|
12723
|
-
* trial_days: 7 // 7일 무료 체험
|
|
12943
|
+
* product_id: 'premium_month',
|
|
12724
12944
|
* })
|
|
12725
12945
|
*
|
|
12726
|
-
* // MoR(dodo/paddle) 구독 + 할인 코드
|
|
12946
|
+
* // MoR(dodo/paddle) 구독 + 할인 코드 — PG 가격 ref 도 상품의 provider_price_refs 에서 서버가 고른다
|
|
12727
12947
|
* const promo = await client.subscription.create({
|
|
12948
|
+
* product_id: 'premium_month',
|
|
12949
|
+
* discount_code: 'LAUNCH50', // 첫 결제에 적용 (몇 주기까지인지는 PG 쿠폰 설정이 결정)
|
|
12950
|
+
* })
|
|
12951
|
+
*
|
|
12952
|
+
* // 금액 직접 지정 (레거시 앱, 서버 호출) — 가격표 전용 앱에서 브라우저가 보내면 403 client_amount_not_allowed
|
|
12953
|
+
* const legacy = await client.subscription.create({
|
|
12954
|
+
* billing_key_id: 'bk_xxxxx',
|
|
12728
12955
|
* plan_name: '프리미엄 플랜',
|
|
12729
12956
|
* amount: 9900,
|
|
12730
12957
|
* billing_cycle: 'monthly',
|
|
12731
|
-
* provider_price_ref: 'pdt_xxxxx', // PG 카탈로그의 recurring 상품
|
|
12732
|
-
* discount_code: 'LAUNCH50', // 첫 결제에 적용 (몇 주기까지인지는 PG 쿠폰 설정이 결정)
|
|
12733
|
-
* trial_days: 14, // Dodo 만 지원 — Paddle 은 400 trial_days_unsupported
|
|
12734
12958
|
* })
|
|
12735
12959
|
* ```
|
|
12736
12960
|
*
|
|
12737
12961
|
* @remarks
|
|
12962
|
+
* 권한은 구독의 `product_id` 로 지급합니다(`metadata` 는 클라이언트 값). 가격표 전용 앱의 브라우저 호출은
|
|
12963
|
+
* `start_now: false` 와 `billing_day` 를 쓸 수 없습니다(403 `client_schedule_not_allowed`) — 둘 다 한 번 낸 돈으로 쓰는
|
|
12964
|
+
* 기간을 늘립니다. 무료 체험은 상품의 `trial_days` 가 정합니다.
|
|
12965
|
+
*
|
|
12738
12966
|
* 할인 코드(`discount_code`)와 `trial_days` 의 프로바이더 지원 범위가 다릅니다:
|
|
12739
12967
|
* - **Dodo**: 할인 코드 O, 결제창 쿠폰 입력 토글 O, 구독별 체험 일수 O
|
|
12740
12968
|
* - **Paddle**: 할인 코드 O(코드→내부 ID 자동 해소), 쿠폰 입력칸은 항상 노출(숨김 불가),
|
|
@@ -12806,9 +13034,9 @@ var SubscriptionAPI = class {
|
|
|
12806
13034
|
*
|
|
12807
13035
|
* @example
|
|
12808
13036
|
* ```typescript
|
|
13037
|
+
* // 서버(시크릿 키, service_role 함수)에서 다른 가격표 상품으로 바꾸기 — 다음 청구부터 새 금액
|
|
12809
13038
|
* await client.subscription.update(subscriptionId, {
|
|
12810
|
-
*
|
|
12811
|
-
* amount: 29900
|
|
13039
|
+
* product_id: 'enterprise_month',
|
|
12812
13040
|
* })
|
|
12813
13041
|
*
|
|
12814
13042
|
* // 다음 결제일을 특정 날짜로 옮기기 (기간권·보상)
|
|
@@ -12994,11 +13222,10 @@ var SubscriptionAPI = class {
|
|
|
12994
13222
|
*
|
|
12995
13223
|
* @example
|
|
12996
13224
|
* ```typescript
|
|
13225
|
+
* // 가격표 정기 상품으로 바꾼다 — 새 PG 가격 ref 와 플랜명은 서버가 상품에서 정한다
|
|
12997
13226
|
* await client.subscription.changePlan(subscriptionId, {
|
|
12998
|
-
*
|
|
12999
|
-
*
|
|
13000
|
-
* amount: 24900,
|
|
13001
|
-
* proration_mode: 'prorated_immediately',
|
|
13227
|
+
* product_id: 'family_month',
|
|
13228
|
+
* proration_mode: 'prorated_immediately', // 브라우저는 지금 정산하는 모드만
|
|
13002
13229
|
* })
|
|
13003
13230
|
* ```
|
|
13004
13231
|
*/
|
|
@@ -13031,7 +13258,7 @@ var SubscriptionAPI = class {
|
|
|
13031
13258
|
* @example
|
|
13032
13259
|
* ```typescript
|
|
13033
13260
|
* const preview = await client.subscription.previewChangePlan(subscriptionId, {
|
|
13034
|
-
*
|
|
13261
|
+
* product_id: 'family_month',
|
|
13035
13262
|
* })
|
|
13036
13263
|
* // 통화를 함께 포맷한다 — 금액만 쓰면 다통화에서 거짓 금액이 된다
|
|
13037
13264
|
* format(preview.immediate_charge_amount, preview.currency)
|
|
@@ -13085,10 +13312,11 @@ var SubscriptionAPI = class {
|
|
|
13085
13312
|
*
|
|
13086
13313
|
* @example
|
|
13087
13314
|
* ```typescript
|
|
13315
|
+
* // 가격표 단건 상품으로 청구 — 금액과 상품명은 서버가 정한다
|
|
13088
13316
|
* const result = await client.subscription.chargeWithBillingKey({
|
|
13089
13317
|
* billing_key_id: 'bk_xxxxx',
|
|
13090
|
-
*
|
|
13091
|
-
*
|
|
13318
|
+
* product_id: 'gem_100',
|
|
13319
|
+
* quantity: 2,
|
|
13092
13320
|
* })
|
|
13093
13321
|
*
|
|
13094
13322
|
* if (result.status === 'done') {
|
|
@@ -13322,8 +13550,8 @@ var SupportAPI = class {
|
|
|
13322
13550
|
}
|
|
13323
13551
|
};
|
|
13324
13552
|
function detectSdkVersion() {
|
|
13325
|
-
if ("6.
|
|
13326
|
-
return "6.
|
|
13553
|
+
if ("6.14.0") {
|
|
13554
|
+
return "6.14.0";
|
|
13327
13555
|
}
|
|
13328
13556
|
return "unknown";
|
|
13329
13557
|
}
|
|
@@ -14217,7 +14445,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14217
14445
|
this.currentRoomId = null;
|
|
14218
14446
|
this.currentPeerId = null;
|
|
14219
14447
|
this.currentUserId = null;
|
|
14220
|
-
|
|
14448
|
+
/** connect 가 요청한 방송자 역할 (switchRoom 이 이어받는다) */
|
|
14449
|
+
this.broadcasterRequested = false;
|
|
14450
|
+
/** 서버가 정한 방송자 역할 (`joined` 의 is_broadcaster) */
|
|
14451
|
+
this.broadcasterGranted = false;
|
|
14452
|
+
/** 비공개 룸 허용 목록 요청 (이 연결에서만 쓴다) */
|
|
14453
|
+
this.allowedMemberIds = null;
|
|
14454
|
+
this.privateRoom = false;
|
|
14455
|
+
this.warningListeners = [];
|
|
14221
14456
|
this.localStream = null;
|
|
14222
14457
|
this.channelType = "interactive";
|
|
14223
14458
|
// 피어 연결 관리
|
|
@@ -14271,7 +14506,10 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14271
14506
|
this.setState("connecting");
|
|
14272
14507
|
this.currentRoomId = options.roomId;
|
|
14273
14508
|
this.currentUserId = options.userId || null;
|
|
14274
|
-
this.
|
|
14509
|
+
this.broadcasterRequested = options.isBroadcaster || false;
|
|
14510
|
+
this.broadcasterGranted = false;
|
|
14511
|
+
this.allowedMemberIds = options.allowedMemberIds ? [...options.allowedMemberIds] : null;
|
|
14512
|
+
this.privateRoom = false;
|
|
14275
14513
|
this.localStream = options.localStream || null;
|
|
14276
14514
|
if (this.iceServers.length === 0) {
|
|
14277
14515
|
try {
|
|
@@ -14282,9 +14520,35 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14282
14520
|
}
|
|
14283
14521
|
return this.connectWebSocket();
|
|
14284
14522
|
}
|
|
14285
|
-
connectWebSocket() {
|
|
14523
|
+
async connectWebSocket() {
|
|
14524
|
+
const before = this.wsGeneration;
|
|
14525
|
+
const ticket = await this.signalingTicket();
|
|
14526
|
+
if (this.wsGeneration !== before) {
|
|
14527
|
+
throw new Error("\uC5F0\uACB0\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
|
|
14528
|
+
}
|
|
14529
|
+
return this.openSignalingSocket(ticket);
|
|
14530
|
+
}
|
|
14531
|
+
/**
|
|
14532
|
+
* 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰
|
|
14533
|
+
* (`?access_token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
|
|
14534
|
+
* 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 그 이유로 던진다.
|
|
14535
|
+
*/
|
|
14536
|
+
async signalingTicket() {
|
|
14537
|
+
const accessToken = this.http.getAccessToken();
|
|
14538
|
+
const publicKey = accessToken ? void 0 : this.http.getPublicKey();
|
|
14539
|
+
if (!accessToken && !publicKey || !this.appId) return null;
|
|
14540
|
+
return fetchWsTicket(
|
|
14541
|
+
ticketUrl(
|
|
14542
|
+
this.webrtcUrl,
|
|
14543
|
+
`/v1/apps/${encodeURIComponent(this.appId)}/signaling/ws-ticket`
|
|
14544
|
+
),
|
|
14545
|
+
"webrtc.signaling",
|
|
14546
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
14547
|
+
);
|
|
14548
|
+
}
|
|
14549
|
+
openSignalingSocket(ticket) {
|
|
14286
14550
|
return new Promise((resolve, reject) => {
|
|
14287
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
14551
|
+
const wsUrl = this.buildWebSocketUrl(ticket);
|
|
14288
14552
|
const generation = ++this.wsGeneration;
|
|
14289
14553
|
const ws = new WebSocket(wsUrl);
|
|
14290
14554
|
this.ws = ws;
|
|
@@ -14303,13 +14567,16 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14303
14567
|
return;
|
|
14304
14568
|
}
|
|
14305
14569
|
this.reconnectAttempts = 0;
|
|
14570
|
+
const joinData = {
|
|
14571
|
+
is_broadcaster: this.broadcasterRequested
|
|
14572
|
+
};
|
|
14573
|
+
if (this.allowedMemberIds) {
|
|
14574
|
+
joinData.allowed_member_ids = this.allowedMemberIds;
|
|
14575
|
+
}
|
|
14306
14576
|
this.sendSignaling({
|
|
14307
14577
|
type: "join",
|
|
14308
14578
|
room_id: this.currentRoomId,
|
|
14309
|
-
data:
|
|
14310
|
-
user_id: this.currentUserId,
|
|
14311
|
-
is_broadcaster: this.isBroadcaster
|
|
14312
|
-
}
|
|
14579
|
+
data: joinData
|
|
14313
14580
|
});
|
|
14314
14581
|
};
|
|
14315
14582
|
ws.onmessage = async (event) => {
|
|
@@ -14350,12 +14617,14 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14350
14617
|
ws.onerror = null;
|
|
14351
14618
|
ws.onclose = null;
|
|
14352
14619
|
}
|
|
14353
|
-
buildWebSocketUrl() {
|
|
14620
|
+
buildWebSocketUrl(ticket = null) {
|
|
14354
14621
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
14355
14622
|
const publicKey = this.http.getPublicKey();
|
|
14356
14623
|
const accessToken = this.http.getAccessToken();
|
|
14357
14624
|
let authParam = "";
|
|
14358
|
-
if (
|
|
14625
|
+
if (ticket) {
|
|
14626
|
+
authParam = `ticket=${encodeURIComponent(ticket)}`;
|
|
14627
|
+
} else if (accessToken) {
|
|
14359
14628
|
authParam = `access_token=${encodeURIComponent(accessToken)}`;
|
|
14360
14629
|
} else if (publicKey) {
|
|
14361
14630
|
authParam = `public_key=${encodeURIComponent(publicKey)}`;
|
|
@@ -14377,6 +14646,8 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14377
14646
|
if (data.channel_type) {
|
|
14378
14647
|
this.channelType = data.channel_type;
|
|
14379
14648
|
}
|
|
14649
|
+
this.broadcasterGranted = data.is_broadcaster === true;
|
|
14650
|
+
this.privateRoom = data.private === true;
|
|
14380
14651
|
const peers = data.peers || [];
|
|
14381
14652
|
for (const peer of peers) {
|
|
14382
14653
|
if (peer.peer_id !== this.currentPeerId) {
|
|
@@ -14417,9 +14688,16 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14417
14688
|
await this.handleICECandidate(msg.peer_id, msg.candidate);
|
|
14418
14689
|
}
|
|
14419
14690
|
break;
|
|
14691
|
+
case "warning": {
|
|
14692
|
+
const data = msg.data && typeof msg.data === "object" ? msg.data : {};
|
|
14693
|
+
this.emitWarning({
|
|
14694
|
+
code: typeof data.code === "string" ? data.code : "unknown",
|
|
14695
|
+
message: typeof data.message === "string" ? data.message : ""
|
|
14696
|
+
});
|
|
14697
|
+
break;
|
|
14698
|
+
}
|
|
14420
14699
|
case "error": {
|
|
14421
|
-
const
|
|
14422
|
-
const error = new Error(errorMsg);
|
|
14700
|
+
const error = new Error(_WebRTCAPI.signalingErrorMessage(msg.data));
|
|
14423
14701
|
this.emitError(error);
|
|
14424
14702
|
reject?.(error);
|
|
14425
14703
|
break;
|
|
@@ -14517,6 +14795,19 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14517
14795
|
}
|
|
14518
14796
|
this.remoteStreams.delete(remotePeerId);
|
|
14519
14797
|
}
|
|
14798
|
+
/**
|
|
14799
|
+
* 서버 error 메시지의 data 에서 안내 문구를 꺼낸다. 대부분은 문자열이지만 할당량, 동시접속 초과는
|
|
14800
|
+
* `{ error, message, ... }` 객체로 온다.
|
|
14801
|
+
*/
|
|
14802
|
+
static signalingErrorMessage(data) {
|
|
14803
|
+
if (typeof data === "string" && data) return data;
|
|
14804
|
+
if (data && typeof data === "object") {
|
|
14805
|
+
const obj = data;
|
|
14806
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
14807
|
+
if (typeof obj.error === "string" && obj.error) return obj.error;
|
|
14808
|
+
}
|
|
14809
|
+
return "Unknown error";
|
|
14810
|
+
}
|
|
14520
14811
|
sendSignaling(msg) {
|
|
14521
14812
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
14522
14813
|
this.ws.send(JSON.stringify(msg));
|
|
@@ -14582,6 +14873,8 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14582
14873
|
this.currentRoomId = null;
|
|
14583
14874
|
this.currentPeerId = null;
|
|
14584
14875
|
this.localStream = null;
|
|
14876
|
+
this.broadcasterGranted = false;
|
|
14877
|
+
this.privateRoom = false;
|
|
14585
14878
|
this.setState("disconnected");
|
|
14586
14879
|
}
|
|
14587
14880
|
/**
|
|
@@ -14599,9 +14892,15 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14599
14892
|
async switchRoom(roomId, options = {}) {
|
|
14600
14893
|
const localStream = options.localStream ?? this.localStream ?? void 0;
|
|
14601
14894
|
const userId = options.userId ?? this.currentUserId ?? void 0;
|
|
14602
|
-
const isBroadcaster = options.isBroadcaster ?? this.
|
|
14895
|
+
const isBroadcaster = options.isBroadcaster ?? this.broadcasterRequested;
|
|
14603
14896
|
this.disconnect();
|
|
14604
|
-
await this.connect({
|
|
14897
|
+
await this.connect({
|
|
14898
|
+
roomId,
|
|
14899
|
+
userId,
|
|
14900
|
+
isBroadcaster,
|
|
14901
|
+
localStream,
|
|
14902
|
+
allowedMemberIds: options.allowedMemberIds
|
|
14903
|
+
});
|
|
14605
14904
|
}
|
|
14606
14905
|
/**
|
|
14607
14906
|
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
@@ -14654,6 +14953,19 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14654
14953
|
getChannelType() {
|
|
14655
14954
|
return this.channelType;
|
|
14656
14955
|
}
|
|
14956
|
+
/**
|
|
14957
|
+
* 서버가 정한 방송자 역할인지 조회한다. 요청(`isBroadcaster: true`)과 다를 수 있다 — 익명 연결이나
|
|
14958
|
+
* 채널의 방송자 역할이 없는 회원은 시청자로 참가한다(`onWarning` 으로 이유가 온다).
|
|
14959
|
+
*/
|
|
14960
|
+
isBroadcaster() {
|
|
14961
|
+
return this.broadcasterGranted;
|
|
14962
|
+
}
|
|
14963
|
+
/**
|
|
14964
|
+
* 지금 룸이 회원 허용 목록이 있는 비공개 룸인지 조회한다.
|
|
14965
|
+
*/
|
|
14966
|
+
isPrivateRoom() {
|
|
14967
|
+
return this.privateRoom;
|
|
14968
|
+
}
|
|
14657
14969
|
/**
|
|
14658
14970
|
* 원격 스트림 조회
|
|
14659
14971
|
*/
|
|
@@ -14724,6 +15036,24 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14724
15036
|
this.errorListeners = this.errorListeners.filter((cb) => cb !== callback);
|
|
14725
15037
|
};
|
|
14726
15038
|
}
|
|
15039
|
+
/**
|
|
15040
|
+
* 서버가 join 요청을 바꿨다는 알림 이벤트 (예: 방송자 요청을 시청자로 낮춤)
|
|
15041
|
+
*
|
|
15042
|
+
* @example
|
|
15043
|
+
* ```typescript
|
|
15044
|
+
* cb.webrtc.onWarning(({ code, message }) => {
|
|
15045
|
+
* if (code === 'broadcaster_role_required') showToast(message)
|
|
15046
|
+
* })
|
|
15047
|
+
* ```
|
|
15048
|
+
*/
|
|
15049
|
+
onWarning(callback) {
|
|
15050
|
+
this.warningListeners.push(callback);
|
|
15051
|
+
return () => {
|
|
15052
|
+
this.warningListeners = this.warningListeners.filter(
|
|
15053
|
+
(cb) => cb !== callback
|
|
15054
|
+
);
|
|
15055
|
+
};
|
|
15056
|
+
}
|
|
14727
15057
|
/**
|
|
14728
15058
|
* 피어 참가 이벤트
|
|
14729
15059
|
*/
|
|
@@ -14769,6 +15099,9 @@ var WebRTCAPI = class _WebRTCAPI {
|
|
|
14769
15099
|
emitError(error) {
|
|
14770
15100
|
this.errorListeners.forEach((cb) => cb(error));
|
|
14771
15101
|
}
|
|
15102
|
+
emitWarning(warning) {
|
|
15103
|
+
this.warningListeners.forEach((cb) => cb(warning));
|
|
15104
|
+
}
|
|
14772
15105
|
emitPeerJoined(peerId, info) {
|
|
14773
15106
|
this.peerJoinedListeners.forEach((cb) => cb(peerId, info));
|
|
14774
15107
|
}
|
|
@@ -14949,11 +15282,35 @@ var WebSocketTransport = class {
|
|
|
14949
15282
|
this.onClose = onClose;
|
|
14950
15283
|
this.onError = onError;
|
|
14951
15284
|
}
|
|
14952
|
-
connect() {
|
|
15285
|
+
async connect() {
|
|
15286
|
+
const ticket = await this.connectionTicket();
|
|
15287
|
+
return this.openSocket(ticket);
|
|
15288
|
+
}
|
|
15289
|
+
/**
|
|
15290
|
+
* 연결 시도 한 번에 쓸 1회용 티켓(core/ws-ticket.ts). 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼
|
|
15291
|
+
* 자격증명을 URL 에 싣는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
|
|
15292
|
+
*/
|
|
15293
|
+
async connectionTicket() {
|
|
15294
|
+
const { accessToken, publicKey, appId } = this.config;
|
|
15295
|
+
if (!accessToken && !publicKey || !appId) return null;
|
|
15296
|
+
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
15297
|
+
try {
|
|
15298
|
+
return await fetchWsTicket(
|
|
15299
|
+
ticketUrl(baseUrl, `/v1/game/${encodeURIComponent(appId)}/ws-ticket`),
|
|
15300
|
+
"game.ws",
|
|
15301
|
+
accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
|
|
15302
|
+
);
|
|
15303
|
+
} catch (e) {
|
|
15304
|
+
if (e instanceof WsTicketError) {
|
|
15305
|
+
throw new GameError({ code: e.code, message: e.message });
|
|
15306
|
+
}
|
|
15307
|
+
throw e;
|
|
15308
|
+
}
|
|
15309
|
+
}
|
|
15310
|
+
openSocket(ticket) {
|
|
14953
15311
|
return new Promise((resolve, reject) => {
|
|
14954
|
-
const url = this.buildUrl();
|
|
14955
15312
|
try {
|
|
14956
|
-
this.ws = new WebSocket(
|
|
15313
|
+
this.ws = new WebSocket(this.buildUrl(ticket));
|
|
14957
15314
|
this.ws.binaryType = "arraybuffer";
|
|
14958
15315
|
} catch (error) {
|
|
14959
15316
|
reject(error);
|
|
@@ -14983,16 +15340,20 @@ var WebSocketTransport = class {
|
|
|
14983
15340
|
this.ws.addEventListener("message", onMessage);
|
|
14984
15341
|
});
|
|
14985
15342
|
}
|
|
14986
|
-
buildUrl() {
|
|
15343
|
+
buildUrl(ticket = null) {
|
|
14987
15344
|
const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
|
|
14988
15345
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
14989
15346
|
const params = new URLSearchParams();
|
|
14990
15347
|
params.set("client_id", requireClientId(this.config.clientId));
|
|
14991
|
-
if (
|
|
14992
|
-
params.set("
|
|
14993
|
-
}
|
|
14994
|
-
|
|
14995
|
-
|
|
15348
|
+
if (ticket) {
|
|
15349
|
+
params.set("ticket", ticket);
|
|
15350
|
+
} else {
|
|
15351
|
+
if (this.config.publicKey) {
|
|
15352
|
+
params.set("public_key", this.config.publicKey);
|
|
15353
|
+
}
|
|
15354
|
+
if (this.config.accessToken) {
|
|
15355
|
+
params.set("token", this.config.accessToken);
|
|
15356
|
+
}
|
|
14996
15357
|
}
|
|
14997
15358
|
if (!this.config.appId) {
|
|
14998
15359
|
throw new Error(
|
|
@@ -15672,6 +16033,7 @@ var index_default = ConnectBase;
|
|
|
15672
16033
|
RolesAPI,
|
|
15673
16034
|
SessionManager,
|
|
15674
16035
|
VideoProcessingError,
|
|
16036
|
+
WsTicketError,
|
|
15675
16037
|
detectInAppBrowser,
|
|
15676
16038
|
escapeToExternalBrowser,
|
|
15677
16039
|
isWebTransportSupported,
|