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/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(statusCode, message, code, details) {
174
+ constructor(statusCode2, message, code, details) {
174
175
  super(message);
175
- this.statusCode = 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
- return this.doRealtimeConnect();
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);
4769
+ }
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
+ );
4649
4785
  }
4650
- doRealtimeConnect() {
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
- }, 15e3);
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 ticketUrl = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
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(ticketUrl, {
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
- return new Promise((resolve, reject) => {
5924
- if (this.ws?.readyState === WebSocket.OPEN) {
5925
- resolve();
5926
- return;
5927
- }
5928
- if (!this.config.appId) {
5929
- reject(
5930
- new Error(
5931
- "cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
5932
- )
5933
- );
5934
- return;
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
- const url = this.buildConnectionUrl(roomId);
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 (this.config.publicKey) {
6206
- params.set("public_key", this.config.publicKey);
6207
- }
6208
- if (this.config.accessToken) {
6209
- params.set("token", this.config.accessToken);
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(["rate_limited", "auth_unavailable"]);
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.doConnect(Math.max(deadline - Date.now(), 1));
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
- async doConnect(timeoutMs = this.options.timeout) {
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?access_token=${encodeURIComponent(this.options.accessToken)}&client_id=${this.clientId}`;
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?public_key=${encodeURIComponent(publicKey)}&client_id=${this.clientId}`;
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.doConnect();
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;
@@ -13331,8 +13550,8 @@ var SupportAPI = class {
13331
13550
  }
13332
13551
  };
13333
13552
  function detectSdkVersion() {
13334
- if ("6.13.0") {
13335
- return "6.13.0";
13553
+ if ("6.14.0") {
13554
+ return "6.14.0";
13336
13555
  }
13337
13556
  return "unknown";
13338
13557
  }
@@ -14301,9 +14520,35 @@ var WebRTCAPI = class _WebRTCAPI {
14301
14520
  }
14302
14521
  return this.connectWebSocket();
14303
14522
  }
14304
- 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) {
14305
14550
  return new Promise((resolve, reject) => {
14306
- const wsUrl = this.buildWebSocketUrl();
14551
+ const wsUrl = this.buildWebSocketUrl(ticket);
14307
14552
  const generation = ++this.wsGeneration;
14308
14553
  const ws = new WebSocket(wsUrl);
14309
14554
  this.ws = ws;
@@ -14372,12 +14617,14 @@ var WebRTCAPI = class _WebRTCAPI {
14372
14617
  ws.onerror = null;
14373
14618
  ws.onclose = null;
14374
14619
  }
14375
- buildWebSocketUrl() {
14620
+ buildWebSocketUrl(ticket = null) {
14376
14621
  const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
14377
14622
  const publicKey = this.http.getPublicKey();
14378
14623
  const accessToken = this.http.getAccessToken();
14379
14624
  let authParam = "";
14380
- if (accessToken) {
14625
+ if (ticket) {
14626
+ authParam = `ticket=${encodeURIComponent(ticket)}`;
14627
+ } else if (accessToken) {
14381
14628
  authParam = `access_token=${encodeURIComponent(accessToken)}`;
14382
14629
  } else if (publicKey) {
14383
14630
  authParam = `public_key=${encodeURIComponent(publicKey)}`;
@@ -15035,11 +15282,35 @@ var WebSocketTransport = class {
15035
15282
  this.onClose = onClose;
15036
15283
  this.onError = onError;
15037
15284
  }
15038
- 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) {
15039
15311
  return new Promise((resolve, reject) => {
15040
- const url = this.buildUrl();
15041
15312
  try {
15042
- this.ws = new WebSocket(url);
15313
+ this.ws = new WebSocket(this.buildUrl(ticket));
15043
15314
  this.ws.binaryType = "arraybuffer";
15044
15315
  } catch (error) {
15045
15316
  reject(error);
@@ -15069,16 +15340,20 @@ var WebSocketTransport = class {
15069
15340
  this.ws.addEventListener("message", onMessage);
15070
15341
  });
15071
15342
  }
15072
- buildUrl() {
15343
+ buildUrl(ticket = null) {
15073
15344
  const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
15074
15345
  const wsUrl = baseUrl.replace(/^http/, "ws");
15075
15346
  const params = new URLSearchParams();
15076
15347
  params.set("client_id", requireClientId(this.config.clientId));
15077
- if (this.config.publicKey) {
15078
- params.set("public_key", this.config.publicKey);
15079
- }
15080
- if (this.config.accessToken) {
15081
- params.set("token", this.config.accessToken);
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
+ }
15082
15357
  }
15083
15358
  if (!this.config.appId) {
15084
15359
  throw new Error(
@@ -15758,6 +16033,7 @@ var index_default = ConnectBase;
15758
16033
  RolesAPI,
15759
16034
  SessionManager,
15760
16035
  VideoProcessingError,
16036
+ WsTicketError,
15761
16037
  detectInAppBrowser,
15762
16038
  escapeToExternalBrowser,
15763
16039
  isWebTransportSupported,