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.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(statusCode, message, code, details) {
121
+ constructor(statusCode2, message, code, details) {
122
122
  super(message);
123
- this.statusCode = 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
- return this.doRealtimeConnect();
4694
+ let ticket;
4695
+ try {
4696
+ ticket = await this.realtimeTicket();
4697
+ } catch (e) {
4698
+ if (e instanceof WsTicketError) {
4699
+ if (e.status === 429 && this.realtimeOptions) {
4700
+ this.attemptRealtimeReconnect();
4701
+ } else {
4702
+ this.setRealtimeState("disconnected");
4703
+ }
4704
+ const err = new RealtimeConnectionError({
4705
+ code: e.code,
4706
+ message: e.message,
4707
+ closeCode: 4e3 + e.status,
4708
+ retryAfterMs: e.retryAfterMs
4709
+ });
4710
+ this.realtimeOnError?.(err);
4711
+ throw err;
4712
+ }
4713
+ throw e;
4714
+ }
4715
+ return this.doRealtimeConnect(ticket);
4716
+ }
4717
+ /**
4718
+ * 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰을
4719
+ * URL(`?access_token=`)에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가 티켓을 모르거나(구버전) 잠시 못
4720
+ * 주면 null 이고 예전처럼 붙는다. cross-app OAuth 토큰도 같은 경로다(서버가 티켓에서 토큰을 되살려 같은 검증을 한다).
4721
+ */
4722
+ async realtimeTicket() {
4723
+ const opts = this.realtimeOptions;
4724
+ if (!opts?.accessToken) return null;
4725
+ const baseUrl = opts.dataServerUrl || this.http.getBaseUrl();
4726
+ return fetchWsTicket(
4727
+ ticketUrl(baseUrl, "/v1/database/realtime/ws-ticket"),
4728
+ "data.realtime",
4729
+ { kind: "access_token", value: opts.accessToken },
4730
+ { timeoutMs: REALTIME_CONNECT_TIMEOUT_MS }
4731
+ );
4597
4732
  }
4598
- doRealtimeConnect() {
4733
+ doRealtimeConnect(ticket = null) {
4599
4734
  if (!this.realtimeOptions)
4600
4735
  return Promise.reject(new Error("No realtime options"));
4601
4736
  this.setRealtimeState("connecting");
4602
4737
  const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
4603
4738
  const wsUrl = baseUrl.replace(/^http/, "ws");
4604
- let url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
4739
+ let url = ticket ? `${wsUrl}/v1/database/realtime/ws?ticket=${encodeURIComponent(ticket)}` : `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
4605
4740
  this.realtimeTokenRefresh = this.http.canRefreshMemberToken(
4606
4741
  this.realtimeOptions.accessToken
4607
4742
  );
@@ -4620,7 +4755,7 @@ var DatabaseAPI = class {
4620
4755
  this.setRealtimeState("disconnected");
4621
4756
  reject(new Error("Connection timeout"));
4622
4757
  }
4623
- }, 15e3);
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 ticketUrl = `${baseUrl}/v1/proxy/${encodeURIComponent(label)}/ws-ticket`;
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(ticketUrl, {
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
- return new Promise((resolve, reject) => {
5872
- if (this.ws?.readyState === WebSocket.OPEN) {
5873
- resolve();
5874
- return;
5875
- }
5876
- if (!this.config.appId) {
5877
- reject(
5878
- new Error(
5879
- "cb.game: appId is required to connect. Pass it to `new ConnectBase({ appId })` or per-client via `cb.game.createClient({ appId, clientId })`."
5880
- )
5881
- );
5882
- return;
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
- const url = this.buildConnectionUrl(roomId);
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 (this.config.publicKey) {
6154
- params.set("public_key", this.config.publicKey);
6155
- }
6156
- if (this.config.accessToken) {
6157
- params.set("token", this.config.accessToken);
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(["rate_limited", "auth_unavailable"]);
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.doConnect(Math.max(deadline - Date.now(), 1));
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
- async doConnect(timeoutMs = this.options.timeout) {
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?access_token=${encodeURIComponent(this.options.accessToken)}&client_id=${this.clientId}`;
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?public_key=${encodeURIComponent(publicKey)}&client_id=${this.clientId}`;
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.doConnect();
11895
+ const ticket = await this.connectTicket(this.options.timeout);
11896
+ if (this.state !== "reconnecting") return;
11897
+ await this.doConnect(void 0, ticket);
11680
11898
  this.log("Reconnected successfully, restoring subscriptions...");
11681
11899
  const snapshot = this.pendingRestore;
11682
11900
  this.pendingRestore = null;
@@ -13279,8 +13497,8 @@ var SupportAPI = class {
13279
13497
  }
13280
13498
  };
13281
13499
  function detectSdkVersion() {
13282
- if ("6.13.0") {
13283
- return "6.13.0";
13500
+ if ("6.14.0") {
13501
+ return "6.14.0";
13284
13502
  }
13285
13503
  return "unknown";
13286
13504
  }
@@ -14249,9 +14467,35 @@ var WebRTCAPI = class _WebRTCAPI {
14249
14467
  }
14250
14468
  return this.connectWebSocket();
14251
14469
  }
14252
- connectWebSocket() {
14470
+ async connectWebSocket() {
14471
+ const before = this.wsGeneration;
14472
+ const ticket = await this.signalingTicket();
14473
+ if (this.wsGeneration !== before) {
14474
+ throw new Error("\uC5F0\uACB0\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
14475
+ }
14476
+ return this.openSignalingSocket(ticket);
14477
+ }
14478
+ /**
14479
+ * 연결 시도 한 번에 쓸 1회용 티켓을 받는다(core/ws-ticket.ts). 브라우저 WebSocket 은 헤더를 못 붙여 예전에는 멤버 토큰
14480
+ * (`?access_token=`)과 공개 키(`?public_key=`)를 URL 에 실었다 — 이제 헤더로 티켓을 받아 `?ticket=` 만 싣는다. 서버가
14481
+ * 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼 붙는다. 발급이 자격증명을 거부하면 그 이유로 던진다.
14482
+ */
14483
+ async signalingTicket() {
14484
+ const accessToken = this.http.getAccessToken();
14485
+ const publicKey = accessToken ? void 0 : this.http.getPublicKey();
14486
+ if (!accessToken && !publicKey || !this.appId) return null;
14487
+ return fetchWsTicket(
14488
+ ticketUrl(
14489
+ this.webrtcUrl,
14490
+ `/v1/apps/${encodeURIComponent(this.appId)}/signaling/ws-ticket`
14491
+ ),
14492
+ "webrtc.signaling",
14493
+ accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
14494
+ );
14495
+ }
14496
+ openSignalingSocket(ticket) {
14253
14497
  return new Promise((resolve, reject) => {
14254
- const wsUrl = this.buildWebSocketUrl();
14498
+ const wsUrl = this.buildWebSocketUrl(ticket);
14255
14499
  const generation = ++this.wsGeneration;
14256
14500
  const ws = new WebSocket(wsUrl);
14257
14501
  this.ws = ws;
@@ -14320,12 +14564,14 @@ var WebRTCAPI = class _WebRTCAPI {
14320
14564
  ws.onerror = null;
14321
14565
  ws.onclose = null;
14322
14566
  }
14323
- buildWebSocketUrl() {
14567
+ buildWebSocketUrl(ticket = null) {
14324
14568
  const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
14325
14569
  const publicKey = this.http.getPublicKey();
14326
14570
  const accessToken = this.http.getAccessToken();
14327
14571
  let authParam = "";
14328
- if (accessToken) {
14572
+ if (ticket) {
14573
+ authParam = `ticket=${encodeURIComponent(ticket)}`;
14574
+ } else if (accessToken) {
14329
14575
  authParam = `access_token=${encodeURIComponent(accessToken)}`;
14330
14576
  } else if (publicKey) {
14331
14577
  authParam = `public_key=${encodeURIComponent(publicKey)}`;
@@ -14983,11 +15229,35 @@ var WebSocketTransport = class {
14983
15229
  this.onClose = onClose;
14984
15230
  this.onError = onError;
14985
15231
  }
14986
- connect() {
15232
+ async connect() {
15233
+ const ticket = await this.connectionTicket();
15234
+ return this.openSocket(ticket);
15235
+ }
15236
+ /**
15237
+ * 연결 시도 한 번에 쓸 1회용 티켓(core/ws-ticket.ts). 서버가 티켓을 모르거나(구버전) 잠시 못 주면 null 이고 예전처럼
15238
+ * 자격증명을 URL 에 싣는다. 발급이 자격증명을 거부하면 {@link GameError} 로 던진다.
15239
+ */
15240
+ async connectionTicket() {
15241
+ const { accessToken, publicKey, appId } = this.config;
15242
+ if (!accessToken && !publicKey || !appId) return null;
15243
+ const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
15244
+ try {
15245
+ return await fetchWsTicket(
15246
+ ticketUrl(baseUrl, `/v1/game/${encodeURIComponent(appId)}/ws-ticket`),
15247
+ "game.ws",
15248
+ accessToken ? { kind: "access_token", value: accessToken } : { kind: "public_key", value: publicKey }
15249
+ );
15250
+ } catch (e) {
15251
+ if (e instanceof WsTicketError) {
15252
+ throw new GameError({ code: e.code, message: e.message });
15253
+ }
15254
+ throw e;
15255
+ }
15256
+ }
15257
+ openSocket(ticket) {
14987
15258
  return new Promise((resolve, reject) => {
14988
- const url = this.buildUrl();
14989
15259
  try {
14990
- this.ws = new WebSocket(url);
15260
+ this.ws = new WebSocket(this.buildUrl(ticket));
14991
15261
  this.ws.binaryType = "arraybuffer";
14992
15262
  } catch (error) {
14993
15263
  reject(error);
@@ -15017,16 +15287,20 @@ var WebSocketTransport = class {
15017
15287
  this.ws.addEventListener("message", onMessage);
15018
15288
  });
15019
15289
  }
15020
- buildUrl() {
15290
+ buildUrl(ticket = null) {
15021
15291
  const baseUrl = this.config.gameServerUrl || "wss://game.connectbase.world";
15022
15292
  const wsUrl = baseUrl.replace(/^http/, "ws");
15023
15293
  const params = new URLSearchParams();
15024
15294
  params.set("client_id", requireClientId(this.config.clientId));
15025
- if (this.config.publicKey) {
15026
- params.set("public_key", this.config.publicKey);
15027
- }
15028
- if (this.config.accessToken) {
15029
- params.set("token", this.config.accessToken);
15295
+ if (ticket) {
15296
+ params.set("ticket", ticket);
15297
+ } else {
15298
+ if (this.config.publicKey) {
15299
+ params.set("public_key", this.config.publicKey);
15300
+ }
15301
+ if (this.config.accessToken) {
15302
+ params.set("token", this.config.accessToken);
15303
+ }
15030
15304
  }
15031
15305
  if (!this.config.appId) {
15032
15306
  throw new Error(
@@ -15705,6 +15979,7 @@ export {
15705
15979
  RolesAPI,
15706
15980
  SessionManager,
15707
15981
  VideoProcessingError,
15982
+ WsTicketError,
15708
15983
  index_default as default,
15709
15984
  detectInAppBrowser,
15710
15985
  escapeToExternalBrowser,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "6.13.0",
3
+ "version": "6.14.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",