connectbase-client 0.16.1 → 1.2.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
@@ -61,8 +61,21 @@ var HttpClient = class {
61
61
  this.refreshPromise = null;
62
62
  this.config = { ...config };
63
63
  this.storageKey = this.buildStorageKey();
64
+ this.warnIfUnsafePersistence();
64
65
  this.restoreTokens();
65
66
  }
67
+ warnIfUnsafePersistence() {
68
+ if (typeof window === "undefined") return;
69
+ if (this.config.persistence === "localStorage") {
70
+ console.warn(
71
+ `[connect-base-client] persistence="localStorage" \uB294 XSS \uC2DC \uD1A0\uD070 \uC601\uAD6C \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4. refresh token \uC740 \uC11C\uBC84 HttpOnly \uCFE0\uD0A4\uB85C \uBC1C\uAE09\uBC1B\uACE0 \uAE30\uBCF8\uAC12('none')\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.`
72
+ );
73
+ } else if (this.config.persistence === "sessionStorage") {
74
+ console.warn(
75
+ '[connect-base-client] persistence="sessionStorage" \uB294 XSS \uC2DC \uD604\uC7AC \uD0ED \uC138\uC158 \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4.'
76
+ );
77
+ }
78
+ }
66
79
  updateConfig(config) {
67
80
  this.config = { ...this.config, ...config };
68
81
  }
@@ -78,7 +91,7 @@ var HttpClient = class {
78
91
  }
79
92
  // ===== Token Persistence =====
80
93
  get persistence() {
81
- return this.config.persistence ?? "localStorage";
94
+ return this.config.persistence ?? "none";
82
95
  }
83
96
  getStorage() {
84
97
  if (typeof window === "undefined") return null;
@@ -86,6 +99,13 @@ var HttpClient = class {
86
99
  if (this.persistence === "sessionStorage" && typeof sessionStorage !== "undefined") return sessionStorage;
87
100
  return null;
88
101
  }
102
+ /**
103
+ * AuthAPI 등 내부 사용자가 "현재 persistence 설정된 스토리지"를 확인할 수 있도록 노출.
104
+ * persistence='none' 이면 null. XSS 완화 기본값을 공유하기 위함.
105
+ */
106
+ getPersistenceStorage() {
107
+ return this.getStorage();
108
+ }
89
109
  buildStorageKey() {
90
110
  const credential = this.config.publicKey ?? this.config.secretKey;
91
111
  if (!credential) return TOKEN_STORAGE_KEY;
@@ -164,6 +184,12 @@ var HttpClient = class {
164
184
  getAccessToken() {
165
185
  return this.config.accessToken;
166
186
  }
187
+ /**
188
+ * JWT(Access Token) 가 설정되어 있는지 확인
189
+ */
190
+ hasJWT() {
191
+ return !!this.config.accessToken;
192
+ }
167
193
  /**
168
194
  * Base URL 반환
169
195
  */
@@ -347,10 +373,10 @@ function notifyVisitorTracker(memberId) {
347
373
  }
348
374
  }
349
375
  }
350
- function simpleHash(str) {
376
+ function credentialToStorageKeyHash(credential) {
351
377
  let hash = 0;
352
- for (let i = 0; i < str.length; i++) {
353
- const char = str.charCodeAt(i);
378
+ for (let i = 0; i < credential.length; i++) {
379
+ const char = credential.charCodeAt(i);
354
380
  hash = (hash << 5) - hash + char;
355
381
  hash = hash & hash;
356
382
  }
@@ -506,8 +532,9 @@ var AuthAPI = class {
506
532
  * 저장된 게스트 멤버 토큰 삭제
507
533
  */
508
534
  clearGuestMemberTokens() {
509
- if (typeof sessionStorage === "undefined") return;
510
- sessionStorage.removeItem(this.getGuestMemberTokenKey());
535
+ const storage = this.http.getPersistenceStorage();
536
+ if (!storage) return;
537
+ storage.removeItem(this.getGuestMemberTokenKey());
511
538
  }
512
539
  // ===== Private Methods =====
513
540
  async executeGuestMemberLogin() {
@@ -581,14 +608,15 @@ var AuthAPI = class {
581
608
  if (!credential) {
582
609
  this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
583
610
  } else {
584
- const keyHash = simpleHash(credential);
611
+ const keyHash = credentialToStorageKeyHash(credential);
585
612
  this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
586
613
  }
587
614
  return this.cachedGuestMemberTokenKey;
588
615
  }
589
616
  getStoredGuestMemberTokens() {
590
- if (typeof sessionStorage === "undefined") return null;
591
- const stored = sessionStorage.getItem(this.getGuestMemberTokenKey());
617
+ const storage = this.http.getPersistenceStorage();
618
+ if (!storage) return null;
619
+ const stored = storage.getItem(this.getGuestMemberTokenKey());
592
620
  if (!stored) return null;
593
621
  try {
594
622
  return JSON.parse(stored);
@@ -597,8 +625,9 @@ var AuthAPI = class {
597
625
  }
598
626
  }
599
627
  storeGuestMemberTokens(accessToken, refreshToken, memberId) {
600
- if (typeof sessionStorage === "undefined") return;
601
- sessionStorage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
628
+ const storage = this.http.getPersistenceStorage();
629
+ if (!storage) return;
630
+ storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
602
631
  }
603
632
  };
604
633
 
@@ -1856,21 +1885,33 @@ var StorageAPI = class {
1856
1885
  }
1857
1886
  /**
1858
1887
  * 파일/폴더 이동
1888
+ *
1889
+ * Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
1859
1890
  */
1860
1891
  async moveFile(storageId, fileId, data) {
1861
- const prefix = this.getPublicPrefix();
1892
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
1893
+ throw new Error(
1894
+ "storage.moveFile requires JWT (console token) auth; not available with Public Key alone."
1895
+ );
1896
+ }
1862
1897
  await this.http.post(
1863
- `${prefix}/storages/files/${storageId}/items/${fileId}/move`,
1898
+ `/v1/storages/files/${storageId}/items/${fileId}/move`,
1864
1899
  data
1865
1900
  );
1866
1901
  }
1867
1902
  /**
1868
1903
  * 파일/폴더 이름 변경
1904
+ *
1905
+ * Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
1869
1906
  */
1870
1907
  async renameFile(storageId, fileId, data) {
1871
- const prefix = this.getPublicPrefix();
1908
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
1909
+ throw new Error(
1910
+ "storage.renameFile requires JWT (console token) auth; not available with Public Key alone."
1911
+ );
1912
+ }
1872
1913
  return this.http.patch(
1873
- `${prefix}/storages/files/${storageId}/items/${fileId}/rename`,
1914
+ `/v1/storages/files/${storageId}/items/${fileId}/rename`,
1874
1915
  data
1875
1916
  );
1876
1917
  }
@@ -3303,7 +3344,10 @@ var WebRTCAPI = class {
3303
3344
  * ICE 서버 목록 조회
3304
3345
  */
3305
3346
  async getICEServers() {
3306
- const response = await this.http.get("/v1/ice-servers");
3347
+ if (!this.appId) {
3348
+ throw new Error("WebRTC getICEServers \uC5D0\uB294 appId \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. ConnectBase \uCD08\uAE30\uD654 \uC2DC appId \uB97C \uC124\uC815\uD558\uC138\uC694.");
3349
+ }
3350
+ const response = await this.http.get(`/v1/apps/${this.appId}/ice-servers`);
3307
3351
  this.iceServers = response.ice_servers;
3308
3352
  return response.ice_servers;
3309
3353
  }
@@ -3774,13 +3818,13 @@ var WebRTCAPI = class {
3774
3818
  * 앱의 WebRTC 통계 조회
3775
3819
  */
3776
3820
  async getStats(appID) {
3777
- return this.http.get(`/v1/apps/${appID}/webrtc/stats`);
3821
+ return this.http.get(`/v1/apps/${appID}/stats`);
3778
3822
  }
3779
3823
  /**
3780
3824
  * 앱의 활성 룸 목록 조회
3781
3825
  */
3782
3826
  async getRooms(appID) {
3783
- return this.http.get(`/v1/apps/${appID}/webrtc/rooms`);
3827
+ return this.http.get(`/v1/apps/${appID}/rooms`);
3784
3828
  }
3785
3829
  };
3786
3830
 
@@ -4219,6 +4263,43 @@ var OAuthAPI = class {
4219
4263
  this.http.setTokens(accessToken, refreshToken);
4220
4264
  return result;
4221
4265
  }
4266
+ /**
4267
+ * 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
4268
+ *
4269
+ * 서버가 `OAUTH_CODE_ONLY=true` 모드면 리다이렉트 URL 에 토큰 대신 `code` 만 포함됩니다.
4270
+ * 이 메서드는 code 를 `POST /v1/auth/oauth/exchange` 로 교환하고 토큰을 자동 저장합니다.
4271
+ *
4272
+ * 권장 호출 순서:
4273
+ * 1) `cb.oauth.exchangeCodeFromCallback()` 먼저 — code-only 모드면 성공
4274
+ * 2) null 반환 시 `cb.oauth.getCallbackResult()` 폴백 (legacy 토큰-in-URL 모드)
4275
+ *
4276
+ * @example
4277
+ * ```typescript
4278
+ * const result = await cb.oauth.exchangeCodeFromCallback()
4279
+ * ?? cb.oauth.getCallbackResult()
4280
+ * ```
4281
+ */
4282
+ async exchangeCodeFromCallback() {
4283
+ const params = new URLSearchParams(window.location.search);
4284
+ const code = params.get("code");
4285
+ if (!code) return null;
4286
+ if (params.get("error")) return null;
4287
+ const resp = await this.http.post("/v1/auth/oauth/exchange", { code });
4288
+ const result = {
4289
+ access_token: resp.access_token,
4290
+ refresh_token: resp.refresh_token,
4291
+ member_id: resp.member_id,
4292
+ is_new_member: resp.is_new_member,
4293
+ state: params.get("state") || void 0
4294
+ };
4295
+ if (window.opener) {
4296
+ window.opener.postMessage({ type: "oauth-callback", ...result }, "*");
4297
+ window.close();
4298
+ return result;
4299
+ }
4300
+ this.http.setTokens(result.access_token, result.refresh_token);
4301
+ return result;
4302
+ }
4222
4303
  };
4223
4304
 
4224
4305
  // src/api/payment.ts
@@ -4241,38 +4322,67 @@ var PaymentAPI = class {
4241
4322
  *
4242
4323
  * @example
4243
4324
  * ```typescript
4244
- * const prepareResult = await client.payment.prepare({
4325
+ * const result = await cb.payment.prepare({
4245
4326
  * amount: 10000,
4246
- * order_name: '테스트 상품',
4327
+ * order_name: '프리미엄 1개월',
4247
4328
  * customer_email: 'test@example.com',
4248
4329
  * customer_name: '홍길동'
4249
4330
  * })
4250
4331
  *
4251
- * // 토스 결제 위젯 초기화
4252
- * const tossPayments = TossPayments(prepareResult.toss_client_key)
4253
- * await tossPayments.requestPayment('카드', {
4254
- * amount: prepareResult.amount,
4255
- * orderId: prepareResult.order_id,
4256
- * orderName: prepareResult.order_name,
4257
- * customerKey: prepareResult.customer_key,
4258
- * successUrl: prepareResult.success_url,
4259
- * failUrl: prepareResult.fail_url
4260
- * })
4332
+ * // Toss V2 결제 플로우
4333
+ * if (result.payment_provider === 'toss') {
4334
+ * const tossPayments = TossPayments(result.toss_client_key)
4335
+ * const payment = tossPayments.payment({ customerKey: result.customer_key })
4336
+ * await payment.requestPayment({
4337
+ * method: 'CARD',
4338
+ * amount: { currency: 'KRW', value: result.amount },
4339
+ * orderId: result.order_id,
4340
+ * orderName: result.order_name,
4341
+ * successUrl: result.success_url,
4342
+ * failUrl: result.fail_url,
4343
+ * })
4344
+ * }
4261
4345
  *
4262
- * // Stripe 결제 플로우:
4263
- * // const result = await client.payment.prepare({ amount: 1000, order_name: 'Product' })
4264
- * // if (result.payment_provider === 'stripe') {
4265
- * // const stripe = await loadStripe(result.stripe_publishable_key)
4266
- * // const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
4267
- * // // Mount PaymentElement, then:
4268
- * // // stripe.confirmPayment({ elements, redirect: 'if_required' })
4269
- * // }
4346
+ * // Stripe 결제 플로우
4347
+ * if (result.payment_provider === 'stripe') {
4348
+ * const stripe = await loadStripe(result.stripe_publishable_key)
4349
+ * const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
4350
+ * // Mount PaymentElement, then:
4351
+ * // stripe.confirmPayment({ elements, redirect: 'if_required' })
4352
+ * }
4270
4353
  * ```
4271
4354
  */
4272
4355
  async prepare(data) {
4273
4356
  const prefix = this.getPublicPrefix();
4274
4357
  return this.http.post(`${prefix}/payments/prepare`, data);
4275
4358
  }
4359
+ /**
4360
+ * Stripe-hosted 결제 페이지 세션 생성 (client_secret 미노출 플로우).
4361
+ *
4362
+ * Elements 플로우(`prepare`) 대신 Stripe-hosted Checkout 페이지로 리다이렉트하고 싶을 때 사용.
4363
+ * 응답에 `session_url` 이 포함되며, 브라우저를 해당 URL 로 이동시키면 Stripe 가 카드 입력·결제를 처리한 뒤
4364
+ * `success_url` / `cancel_url` 로 복귀한다. `client_secret` 은 서버·클라이언트 어디에도 노출되지 않는다.
4365
+ *
4366
+ * @param data - 결제 세션 정보 (금액, 통화, 상품명, success/cancel URL 등)
4367
+ * @returns session_id, session_url (redirect 대상)
4368
+ *
4369
+ * @example
4370
+ * ```typescript
4371
+ * const sess = await client.payment.createCheckoutSession({
4372
+ * amount: 1000,
4373
+ * currency: 'USD',
4374
+ * product_name: 'Premium Subscription',
4375
+ * success_url: 'https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}',
4376
+ * cancel_url: 'https://app.example.com/cancel',
4377
+ * customer_email: 'user@example.com',
4378
+ * })
4379
+ * window.location.href = sess.session_url
4380
+ * ```
4381
+ */
4382
+ async createCheckoutSession(data) {
4383
+ const prefix = this.getPublicPrefix();
4384
+ return this.http.post(`${prefix}/payments/checkout-session`, data);
4385
+ }
4276
4386
  /**
4277
4387
  * 결제 승인
4278
4388
  * 토스에서 결제 완료 후 콜백으로 받은 정보로 결제를 최종 승인합니다.
@@ -6077,41 +6187,19 @@ var GameAPI = class {
6077
6187
  }
6078
6188
  /**
6079
6189
  * 룸 생성 (HTTP, gRPC 대안)
6190
+ *
6191
+ * @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
6080
6192
  */
6081
- async createRoom(appId, config = {}) {
6082
- const id = appId || this.appId || "";
6083
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms`, {
6084
- method: "POST",
6085
- headers: {
6086
- ...this.getHeaders(),
6087
- "Content-Type": "application/json"
6088
- },
6089
- body: JSON.stringify({
6090
- app_id: appId,
6091
- category_id: config.categoryId,
6092
- room_id: config.roomId,
6093
- tick_rate: config.tickRate,
6094
- max_players: config.maxPlayers,
6095
- metadata: config.metadata
6096
- })
6097
- });
6098
- if (!response.ok) {
6099
- throw new Error(`Failed to create room: ${response.statusText}`);
6100
- }
6101
- return response.json();
6193
+ async createRoom(_appId, _config = {}) {
6194
+ throw new Error("cb.game.createRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
6102
6195
  }
6103
6196
  /**
6104
6197
  * 룸 삭제 (HTTP)
6198
+ *
6199
+ * @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
6105
6200
  */
6106
- async deleteRoom(roomId) {
6107
- const id = this.appId || "";
6108
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}`, {
6109
- method: "DELETE",
6110
- headers: this.getHeaders()
6111
- });
6112
- if (!response.ok) {
6113
- throw new Error(`Failed to delete room: ${response.statusText}`);
6114
- }
6201
+ async deleteRoom(_roomId) {
6202
+ throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
6115
6203
  }
6116
6204
  // ==================== Matchmaking ====================
6117
6205
  /**
@@ -6511,6 +6599,8 @@ var GameAPI = class {
6511
6599
  }));
6512
6600
  }
6513
6601
  // --- Party API ---
6602
+ // 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
6603
+ // `joinParty` 는 초대 수락(`acceptInvite`) 플로우로 대체되었으며 직접 join 엔드포인트는 없음 — throw 유지.
6514
6604
  async createParty(playerId, maxSize) {
6515
6605
  const id = this.appId || "";
6516
6606
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
@@ -6521,15 +6611,11 @@ var GameAPI = class {
6521
6611
  if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
6522
6612
  return response.json();
6523
6613
  }
6524
- async joinParty(partyId, playerId) {
6525
- const id = this.appId || "";
6526
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/join`, {
6527
- method: "POST",
6528
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6529
- body: JSON.stringify({ player_id: playerId })
6530
- });
6531
- if (!response.ok) throw new Error(`Failed to join party: ${response.statusText}`);
6532
- return response.json();
6614
+ /**
6615
+ * @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptInvite` 플로우 사용
6616
+ */
6617
+ async joinParty(_partyId, _playerId) {
6618
+ throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptInvite flow.");
6533
6619
  }
6534
6620
  async leaveParty(partyId, playerId) {
6535
6621
  const id = this.appId || "";
@@ -6563,14 +6649,15 @@ var GameAPI = class {
6563
6649
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
6564
6650
  method: "POST",
6565
6651
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6566
- body: JSON.stringify({ player_id: playerId, message })
6652
+ body: JSON.stringify({ sender_id: playerId, content: message })
6567
6653
  });
6568
6654
  if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
6569
6655
  }
6570
6656
  // --- Spectator API ---
6657
+ // 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
6571
6658
  async joinSpectator(roomId, playerId) {
6572
6659
  const id = this.appId || "";
6573
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectate`, {
6660
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators`, {
6574
6661
  method: "POST",
6575
6662
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6576
6663
  body: JSON.stringify({ player_id: playerId })
@@ -6580,10 +6667,9 @@ var GameAPI = class {
6580
6667
  }
6581
6668
  async leaveSpectator(roomId, spectatorId) {
6582
6669
  const id = this.appId || "";
6583
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectate/stop`, {
6584
- method: "POST",
6585
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6586
- body: JSON.stringify({ spectator_id: spectatorId })
6670
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators/${spectatorId}`, {
6671
+ method: "DELETE",
6672
+ headers: this.getHeaders()
6587
6673
  });
6588
6674
  if (!response.ok) throw new Error(`Failed to leave spectator: ${response.statusText}`);
6589
6675
  }
@@ -6597,27 +6683,30 @@ var GameAPI = class {
6597
6683
  return data.spectators || [];
6598
6684
  }
6599
6685
  // --- Ranking/Leaderboard API ---
6600
- async getLeaderboard(top) {
6686
+ // 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
6687
+ async getLeaderboard(gameType, top = 100, season = "default") {
6601
6688
  const id = this.appId || "";
6602
- const limit = top || 100;
6603
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?limit=${limit}`, {
6689
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
6690
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
6604
6691
  headers: this.getHeaders()
6605
6692
  });
6606
6693
  if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
6607
6694
  const data = await response.json();
6608
6695
  return data.entries || [];
6609
6696
  }
6610
- async getPlayerStats(playerId) {
6697
+ async getPlayerStats(playerId, gameType, season = "default") {
6611
6698
  const id = this.appId || "";
6612
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats`, {
6699
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6700
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
6613
6701
  headers: this.getHeaders()
6614
6702
  });
6615
6703
  if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
6616
6704
  return response.json();
6617
6705
  }
6618
- async getPlayerRank(playerId) {
6706
+ async getPlayerRank(playerId, gameType, season = "default") {
6619
6707
  const id = this.appId || "";
6620
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank`, {
6708
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6709
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
6621
6710
  headers: this.getHeaders()
6622
6711
  });
6623
6712
  if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
@@ -6653,11 +6742,16 @@ var GameAPI = class {
6653
6742
  if (!response.ok) throw new Error(`Failed to set mute: ${response.statusText}`);
6654
6743
  }
6655
6744
  // --- Replay API ---
6745
+ // 1.1.0 부터 활성화. 백엔드에서 `REPLAY_STORAGE_PATH` 환경변수가 설정된 경우에만 동작.
6746
+ // 미설정 시 404 응답 → SDK 가 명시적 에러를 throw.
6656
6747
  async listReplays(roomId) {
6657
6748
  const id = this.appId || "";
6658
6749
  let url = `${this.gameServerUrl}/v1/game/${id}/replays`;
6659
6750
  if (roomId) url += `?room_id=${roomId}`;
6660
6751
  const response = await fetch(url, { headers: this.getHeaders() });
6752
+ if (response.status === 404) {
6753
+ throw new Error("cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).");
6754
+ }
6661
6755
  if (!response.ok) throw new Error(`Failed to list replays: ${response.statusText}`);
6662
6756
  const data = await response.json();
6663
6757
  return data.replays || [];
@@ -6667,6 +6761,9 @@ var GameAPI = class {
6667
6761
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}`, {
6668
6762
  headers: this.getHeaders()
6669
6763
  });
6764
+ if (response.status === 404) {
6765
+ throw new Error("cb.game.getReplay: replay not found or replay storage unconfigured.");
6766
+ }
6670
6767
  if (!response.ok) throw new Error(`Failed to get replay: ${response.statusText}`);
6671
6768
  return response.json();
6672
6769
  }
@@ -6675,6 +6772,9 @@ var GameAPI = class {
6675
6772
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/download`, {
6676
6773
  headers: this.getHeaders()
6677
6774
  });
6775
+ if (response.status === 404) {
6776
+ throw new Error("cb.game.downloadReplay: replay not found or replay storage unconfigured.");
6777
+ }
6678
6778
  if (!response.ok) throw new Error(`Failed to download replay: ${response.statusText}`);
6679
6779
  return response.arrayBuffer();
6680
6780
  }
@@ -6683,6 +6783,9 @@ var GameAPI = class {
6683
6783
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/highlights`, {
6684
6784
  headers: this.getHeaders()
6685
6785
  });
6786
+ if (response.status === 404) {
6787
+ throw new Error("cb.game.getReplayHighlights: replay not found or replay storage unconfigured.");
6788
+ }
6686
6789
  if (!response.ok) throw new Error(`Failed to get highlights: ${response.statusText}`);
6687
6790
  const data = await response.json();
6688
6791
  return data.highlights || [];
@@ -6713,15 +6816,18 @@ var AdsAPI = class {
6713
6816
  return this.http.hasPublicKey() ? "/v1/public" : "/v1";
6714
6817
  }
6715
6818
  /**
6716
- * AdSense 연결 상태 확인
6819
+ * AdSense / AdMob 연결 상태 확인 (중첩 구조)
6717
6820
  *
6718
- * @returns 연결 상태, 이메일, AdSense 계정 ID
6821
+ * @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID
6719
6822
  *
6720
6823
  * @example
6721
6824
  * ```typescript
6722
6825
  * const status = await cb.ads.getConnectionStatus()
6723
- * if (status.is_connected) {
6724
- * console.log('연결됨:', status.email)
6826
+ * if (status.adsense.is_connected) {
6827
+ * console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id)
6828
+ * }
6829
+ * if (status.admob.is_connected) {
6830
+ * console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id)
6725
6831
  * }
6726
6832
  * ```
6727
6833
  */