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.mjs CHANGED
@@ -23,8 +23,21 @@ var HttpClient = class {
23
23
  this.refreshPromise = null;
24
24
  this.config = { ...config };
25
25
  this.storageKey = this.buildStorageKey();
26
+ this.warnIfUnsafePersistence();
26
27
  this.restoreTokens();
27
28
  }
29
+ warnIfUnsafePersistence() {
30
+ if (typeof window === "undefined") return;
31
+ if (this.config.persistence === "localStorage") {
32
+ console.warn(
33
+ `[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.`
34
+ );
35
+ } else if (this.config.persistence === "sessionStorage") {
36
+ console.warn(
37
+ '[connect-base-client] persistence="sessionStorage" \uB294 XSS \uC2DC \uD604\uC7AC \uD0ED \uC138\uC158 \uD0C8\uCDE8 \uC704\uD5D8\uC774 \uC788\uC2B5\uB2C8\uB2E4.'
38
+ );
39
+ }
40
+ }
28
41
  updateConfig(config) {
29
42
  this.config = { ...this.config, ...config };
30
43
  }
@@ -40,7 +53,7 @@ var HttpClient = class {
40
53
  }
41
54
  // ===== Token Persistence =====
42
55
  get persistence() {
43
- return this.config.persistence ?? "localStorage";
56
+ return this.config.persistence ?? "none";
44
57
  }
45
58
  getStorage() {
46
59
  if (typeof window === "undefined") return null;
@@ -48,6 +61,13 @@ var HttpClient = class {
48
61
  if (this.persistence === "sessionStorage" && typeof sessionStorage !== "undefined") return sessionStorage;
49
62
  return null;
50
63
  }
64
+ /**
65
+ * AuthAPI 등 내부 사용자가 "현재 persistence 설정된 스토리지"를 확인할 수 있도록 노출.
66
+ * persistence='none' 이면 null. XSS 완화 기본값을 공유하기 위함.
67
+ */
68
+ getPersistenceStorage() {
69
+ return this.getStorage();
70
+ }
51
71
  buildStorageKey() {
52
72
  const credential = this.config.publicKey ?? this.config.secretKey;
53
73
  if (!credential) return TOKEN_STORAGE_KEY;
@@ -126,6 +146,12 @@ var HttpClient = class {
126
146
  getAccessToken() {
127
147
  return this.config.accessToken;
128
148
  }
149
+ /**
150
+ * JWT(Access Token) 가 설정되어 있는지 확인
151
+ */
152
+ hasJWT() {
153
+ return !!this.config.accessToken;
154
+ }
129
155
  /**
130
156
  * Base URL 반환
131
157
  */
@@ -309,10 +335,10 @@ function notifyVisitorTracker(memberId) {
309
335
  }
310
336
  }
311
337
  }
312
- function simpleHash(str) {
338
+ function credentialToStorageKeyHash(credential) {
313
339
  let hash = 0;
314
- for (let i = 0; i < str.length; i++) {
315
- const char = str.charCodeAt(i);
340
+ for (let i = 0; i < credential.length; i++) {
341
+ const char = credential.charCodeAt(i);
316
342
  hash = (hash << 5) - hash + char;
317
343
  hash = hash & hash;
318
344
  }
@@ -468,8 +494,9 @@ var AuthAPI = class {
468
494
  * 저장된 게스트 멤버 토큰 삭제
469
495
  */
470
496
  clearGuestMemberTokens() {
471
- if (typeof sessionStorage === "undefined") return;
472
- sessionStorage.removeItem(this.getGuestMemberTokenKey());
497
+ const storage = this.http.getPersistenceStorage();
498
+ if (!storage) return;
499
+ storage.removeItem(this.getGuestMemberTokenKey());
473
500
  }
474
501
  // ===== Private Methods =====
475
502
  async executeGuestMemberLogin() {
@@ -543,14 +570,15 @@ var AuthAPI = class {
543
570
  if (!credential) {
544
571
  this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
545
572
  } else {
546
- const keyHash = simpleHash(credential);
573
+ const keyHash = credentialToStorageKeyHash(credential);
547
574
  this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
548
575
  }
549
576
  return this.cachedGuestMemberTokenKey;
550
577
  }
551
578
  getStoredGuestMemberTokens() {
552
- if (typeof sessionStorage === "undefined") return null;
553
- const stored = sessionStorage.getItem(this.getGuestMemberTokenKey());
579
+ const storage = this.http.getPersistenceStorage();
580
+ if (!storage) return null;
581
+ const stored = storage.getItem(this.getGuestMemberTokenKey());
554
582
  if (!stored) return null;
555
583
  try {
556
584
  return JSON.parse(stored);
@@ -559,8 +587,9 @@ var AuthAPI = class {
559
587
  }
560
588
  }
561
589
  storeGuestMemberTokens(accessToken, refreshToken, memberId) {
562
- if (typeof sessionStorage === "undefined") return;
563
- sessionStorage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
590
+ const storage = this.http.getPersistenceStorage();
591
+ if (!storage) return;
592
+ storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
564
593
  }
565
594
  };
566
595
 
@@ -1818,21 +1847,33 @@ var StorageAPI = class {
1818
1847
  }
1819
1848
  /**
1820
1849
  * 파일/폴더 이동
1850
+ *
1851
+ * Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
1821
1852
  */
1822
1853
  async moveFile(storageId, fileId, data) {
1823
- const prefix = this.getPublicPrefix();
1854
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
1855
+ throw new Error(
1856
+ "storage.moveFile requires JWT (console token) auth; not available with Public Key alone."
1857
+ );
1858
+ }
1824
1859
  await this.http.post(
1825
- `${prefix}/storages/files/${storageId}/items/${fileId}/move`,
1860
+ `/v1/storages/files/${storageId}/items/${fileId}/move`,
1826
1861
  data
1827
1862
  );
1828
1863
  }
1829
1864
  /**
1830
1865
  * 파일/폴더 이름 변경
1866
+ *
1867
+ * Public Key 인증만으로는 이 엔드포인트가 노출되지 않습니다. JWT(콘솔 토큰)가 필요합니다.
1831
1868
  */
1832
1869
  async renameFile(storageId, fileId, data) {
1833
- const prefix = this.getPublicPrefix();
1870
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
1871
+ throw new Error(
1872
+ "storage.renameFile requires JWT (console token) auth; not available with Public Key alone."
1873
+ );
1874
+ }
1834
1875
  return this.http.patch(
1835
- `${prefix}/storages/files/${storageId}/items/${fileId}/rename`,
1876
+ `/v1/storages/files/${storageId}/items/${fileId}/rename`,
1836
1877
  data
1837
1878
  );
1838
1879
  }
@@ -3265,7 +3306,10 @@ var WebRTCAPI = class {
3265
3306
  * ICE 서버 목록 조회
3266
3307
  */
3267
3308
  async getICEServers() {
3268
- const response = await this.http.get("/v1/ice-servers");
3309
+ if (!this.appId) {
3310
+ 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.");
3311
+ }
3312
+ const response = await this.http.get(`/v1/apps/${this.appId}/ice-servers`);
3269
3313
  this.iceServers = response.ice_servers;
3270
3314
  return response.ice_servers;
3271
3315
  }
@@ -3736,13 +3780,13 @@ var WebRTCAPI = class {
3736
3780
  * 앱의 WebRTC 통계 조회
3737
3781
  */
3738
3782
  async getStats(appID) {
3739
- return this.http.get(`/v1/apps/${appID}/webrtc/stats`);
3783
+ return this.http.get(`/v1/apps/${appID}/stats`);
3740
3784
  }
3741
3785
  /**
3742
3786
  * 앱의 활성 룸 목록 조회
3743
3787
  */
3744
3788
  async getRooms(appID) {
3745
- return this.http.get(`/v1/apps/${appID}/webrtc/rooms`);
3789
+ return this.http.get(`/v1/apps/${appID}/rooms`);
3746
3790
  }
3747
3791
  };
3748
3792
 
@@ -4181,6 +4225,43 @@ var OAuthAPI = class {
4181
4225
  this.http.setTokens(accessToken, refreshToken);
4182
4226
  return result;
4183
4227
  }
4228
+ /**
4229
+ * 콜백 URL 에서 1회용 `code` 를 토큰으로 교환합니다 (`OAUTH_CODE_ONLY` 서버 모드용).
4230
+ *
4231
+ * 서버가 `OAUTH_CODE_ONLY=true` 모드면 리다이렉트 URL 에 토큰 대신 `code` 만 포함됩니다.
4232
+ * 이 메서드는 code 를 `POST /v1/auth/oauth/exchange` 로 교환하고 토큰을 자동 저장합니다.
4233
+ *
4234
+ * 권장 호출 순서:
4235
+ * 1) `cb.oauth.exchangeCodeFromCallback()` 먼저 — code-only 모드면 성공
4236
+ * 2) null 반환 시 `cb.oauth.getCallbackResult()` 폴백 (legacy 토큰-in-URL 모드)
4237
+ *
4238
+ * @example
4239
+ * ```typescript
4240
+ * const result = await cb.oauth.exchangeCodeFromCallback()
4241
+ * ?? cb.oauth.getCallbackResult()
4242
+ * ```
4243
+ */
4244
+ async exchangeCodeFromCallback() {
4245
+ const params = new URLSearchParams(window.location.search);
4246
+ const code = params.get("code");
4247
+ if (!code) return null;
4248
+ if (params.get("error")) return null;
4249
+ const resp = await this.http.post("/v1/auth/oauth/exchange", { code });
4250
+ const result = {
4251
+ access_token: resp.access_token,
4252
+ refresh_token: resp.refresh_token,
4253
+ member_id: resp.member_id,
4254
+ is_new_member: resp.is_new_member,
4255
+ state: params.get("state") || void 0
4256
+ };
4257
+ if (window.opener) {
4258
+ window.opener.postMessage({ type: "oauth-callback", ...result }, "*");
4259
+ window.close();
4260
+ return result;
4261
+ }
4262
+ this.http.setTokens(result.access_token, result.refresh_token);
4263
+ return result;
4264
+ }
4184
4265
  };
4185
4266
 
4186
4267
  // src/api/payment.ts
@@ -4203,38 +4284,67 @@ var PaymentAPI = class {
4203
4284
  *
4204
4285
  * @example
4205
4286
  * ```typescript
4206
- * const prepareResult = await client.payment.prepare({
4287
+ * const result = await cb.payment.prepare({
4207
4288
  * amount: 10000,
4208
- * order_name: '테스트 상품',
4289
+ * order_name: '프리미엄 1개월',
4209
4290
  * customer_email: 'test@example.com',
4210
4291
  * customer_name: '홍길동'
4211
4292
  * })
4212
4293
  *
4213
- * // 토스 결제 위젯 초기화
4214
- * const tossPayments = TossPayments(prepareResult.toss_client_key)
4215
- * await tossPayments.requestPayment('카드', {
4216
- * amount: prepareResult.amount,
4217
- * orderId: prepareResult.order_id,
4218
- * orderName: prepareResult.order_name,
4219
- * customerKey: prepareResult.customer_key,
4220
- * successUrl: prepareResult.success_url,
4221
- * failUrl: prepareResult.fail_url
4222
- * })
4294
+ * // Toss V2 결제 플로우
4295
+ * if (result.payment_provider === 'toss') {
4296
+ * const tossPayments = TossPayments(result.toss_client_key)
4297
+ * const payment = tossPayments.payment({ customerKey: result.customer_key })
4298
+ * await payment.requestPayment({
4299
+ * method: 'CARD',
4300
+ * amount: { currency: 'KRW', value: result.amount },
4301
+ * orderId: result.order_id,
4302
+ * orderName: result.order_name,
4303
+ * successUrl: result.success_url,
4304
+ * failUrl: result.fail_url,
4305
+ * })
4306
+ * }
4223
4307
  *
4224
- * // Stripe 결제 플로우:
4225
- * // const result = await client.payment.prepare({ amount: 1000, order_name: 'Product' })
4226
- * // if (result.payment_provider === 'stripe') {
4227
- * // const stripe = await loadStripe(result.stripe_publishable_key)
4228
- * // const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
4229
- * // // Mount PaymentElement, then:
4230
- * // // stripe.confirmPayment({ elements, redirect: 'if_required' })
4231
- * // }
4308
+ * // Stripe 결제 플로우
4309
+ * if (result.payment_provider === 'stripe') {
4310
+ * const stripe = await loadStripe(result.stripe_publishable_key)
4311
+ * const elements = stripe.elements({ clientSecret: result.stripe_client_secret })
4312
+ * // Mount PaymentElement, then:
4313
+ * // stripe.confirmPayment({ elements, redirect: 'if_required' })
4314
+ * }
4232
4315
  * ```
4233
4316
  */
4234
4317
  async prepare(data) {
4235
4318
  const prefix = this.getPublicPrefix();
4236
4319
  return this.http.post(`${prefix}/payments/prepare`, data);
4237
4320
  }
4321
+ /**
4322
+ * Stripe-hosted 결제 페이지 세션 생성 (client_secret 미노출 플로우).
4323
+ *
4324
+ * Elements 플로우(`prepare`) 대신 Stripe-hosted Checkout 페이지로 리다이렉트하고 싶을 때 사용.
4325
+ * 응답에 `session_url` 이 포함되며, 브라우저를 해당 URL 로 이동시키면 Stripe 가 카드 입력·결제를 처리한 뒤
4326
+ * `success_url` / `cancel_url` 로 복귀한다. `client_secret` 은 서버·클라이언트 어디에도 노출되지 않는다.
4327
+ *
4328
+ * @param data - 결제 세션 정보 (금액, 통화, 상품명, success/cancel URL 등)
4329
+ * @returns session_id, session_url (redirect 대상)
4330
+ *
4331
+ * @example
4332
+ * ```typescript
4333
+ * const sess = await client.payment.createCheckoutSession({
4334
+ * amount: 1000,
4335
+ * currency: 'USD',
4336
+ * product_name: 'Premium Subscription',
4337
+ * success_url: 'https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}',
4338
+ * cancel_url: 'https://app.example.com/cancel',
4339
+ * customer_email: 'user@example.com',
4340
+ * })
4341
+ * window.location.href = sess.session_url
4342
+ * ```
4343
+ */
4344
+ async createCheckoutSession(data) {
4345
+ const prefix = this.getPublicPrefix();
4346
+ return this.http.post(`${prefix}/payments/checkout-session`, data);
4347
+ }
4238
4348
  /**
4239
4349
  * 결제 승인
4240
4350
  * 토스에서 결제 완료 후 콜백으로 받은 정보로 결제를 최종 승인합니다.
@@ -6039,41 +6149,19 @@ var GameAPI = class {
6039
6149
  }
6040
6150
  /**
6041
6151
  * 룸 생성 (HTTP, gRPC 대안)
6152
+ *
6153
+ * @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
6042
6154
  */
6043
- async createRoom(appId, config = {}) {
6044
- const id = appId || this.appId || "";
6045
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms`, {
6046
- method: "POST",
6047
- headers: {
6048
- ...this.getHeaders(),
6049
- "Content-Type": "application/json"
6050
- },
6051
- body: JSON.stringify({
6052
- app_id: appId,
6053
- category_id: config.categoryId,
6054
- room_id: config.roomId,
6055
- tick_rate: config.tickRate,
6056
- max_players: config.maxPlayers,
6057
- metadata: config.metadata
6058
- })
6059
- });
6060
- if (!response.ok) {
6061
- throw new Error(`Failed to create room: ${response.statusText}`);
6062
- }
6063
- return response.json();
6155
+ async createRoom(_appId, _config = {}) {
6156
+ throw new Error("cb.game.createRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
6064
6157
  }
6065
6158
  /**
6066
6159
  * 룸 삭제 (HTTP)
6160
+ *
6161
+ * @deprecated 현재 SDK public 경로로 노출되지 않습니다. 콘솔(admin) 에서 진행하거나 백엔드 public 경로 오픈을 요청하세요.
6067
6162
  */
6068
- async deleteRoom(roomId) {
6069
- const id = this.appId || "";
6070
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}`, {
6071
- method: "DELETE",
6072
- headers: this.getHeaders()
6073
- });
6074
- if (!response.ok) {
6075
- throw new Error(`Failed to delete room: ${response.statusText}`);
6076
- }
6163
+ async deleteRoom(_roomId) {
6164
+ throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
6077
6165
  }
6078
6166
  // ==================== Matchmaking ====================
6079
6167
  /**
@@ -6473,6 +6561,8 @@ var GameAPI = class {
6473
6561
  }));
6474
6562
  }
6475
6563
  // --- Party API ---
6564
+ // 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
6565
+ // `joinParty` 는 초대 수락(`acceptInvite`) 플로우로 대체되었으며 직접 join 엔드포인트는 없음 — throw 유지.
6476
6566
  async createParty(playerId, maxSize) {
6477
6567
  const id = this.appId || "";
6478
6568
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
@@ -6483,15 +6573,11 @@ var GameAPI = class {
6483
6573
  if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
6484
6574
  return response.json();
6485
6575
  }
6486
- async joinParty(partyId, playerId) {
6487
- const id = this.appId || "";
6488
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/join`, {
6489
- method: "POST",
6490
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6491
- body: JSON.stringify({ player_id: playerId })
6492
- });
6493
- if (!response.ok) throw new Error(`Failed to join party: ${response.statusText}`);
6494
- return response.json();
6576
+ /**
6577
+ * @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptInvite` 플로우 사용
6578
+ */
6579
+ async joinParty(_partyId, _playerId) {
6580
+ throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptInvite flow.");
6495
6581
  }
6496
6582
  async leaveParty(partyId, playerId) {
6497
6583
  const id = this.appId || "";
@@ -6525,14 +6611,15 @@ var GameAPI = class {
6525
6611
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
6526
6612
  method: "POST",
6527
6613
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6528
- body: JSON.stringify({ player_id: playerId, message })
6614
+ body: JSON.stringify({ sender_id: playerId, content: message })
6529
6615
  });
6530
6616
  if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
6531
6617
  }
6532
6618
  // --- Spectator API ---
6619
+ // 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
6533
6620
  async joinSpectator(roomId, playerId) {
6534
6621
  const id = this.appId || "";
6535
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectate`, {
6622
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators`, {
6536
6623
  method: "POST",
6537
6624
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6538
6625
  body: JSON.stringify({ player_id: playerId })
@@ -6542,10 +6629,9 @@ var GameAPI = class {
6542
6629
  }
6543
6630
  async leaveSpectator(roomId, spectatorId) {
6544
6631
  const id = this.appId || "";
6545
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectate/stop`, {
6546
- method: "POST",
6547
- headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6548
- body: JSON.stringify({ spectator_id: spectatorId })
6632
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators/${spectatorId}`, {
6633
+ method: "DELETE",
6634
+ headers: this.getHeaders()
6549
6635
  });
6550
6636
  if (!response.ok) throw new Error(`Failed to leave spectator: ${response.statusText}`);
6551
6637
  }
@@ -6559,27 +6645,30 @@ var GameAPI = class {
6559
6645
  return data.spectators || [];
6560
6646
  }
6561
6647
  // --- Ranking/Leaderboard API ---
6562
- async getLeaderboard(top) {
6648
+ // 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
6649
+ async getLeaderboard(gameType, top = 100, season = "default") {
6563
6650
  const id = this.appId || "";
6564
- const limit = top || 100;
6565
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?limit=${limit}`, {
6651
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
6652
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
6566
6653
  headers: this.getHeaders()
6567
6654
  });
6568
6655
  if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
6569
6656
  const data = await response.json();
6570
6657
  return data.entries || [];
6571
6658
  }
6572
- async getPlayerStats(playerId) {
6659
+ async getPlayerStats(playerId, gameType, season = "default") {
6573
6660
  const id = this.appId || "";
6574
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats`, {
6661
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6662
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
6575
6663
  headers: this.getHeaders()
6576
6664
  });
6577
6665
  if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
6578
6666
  return response.json();
6579
6667
  }
6580
- async getPlayerRank(playerId) {
6668
+ async getPlayerRank(playerId, gameType, season = "default") {
6581
6669
  const id = this.appId || "";
6582
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank`, {
6670
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6671
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
6583
6672
  headers: this.getHeaders()
6584
6673
  });
6585
6674
  if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
@@ -6615,11 +6704,16 @@ var GameAPI = class {
6615
6704
  if (!response.ok) throw new Error(`Failed to set mute: ${response.statusText}`);
6616
6705
  }
6617
6706
  // --- Replay API ---
6707
+ // 1.1.0 부터 활성화. 백엔드에서 `REPLAY_STORAGE_PATH` 환경변수가 설정된 경우에만 동작.
6708
+ // 미설정 시 404 응답 → SDK 가 명시적 에러를 throw.
6618
6709
  async listReplays(roomId) {
6619
6710
  const id = this.appId || "";
6620
6711
  let url = `${this.gameServerUrl}/v1/game/${id}/replays`;
6621
6712
  if (roomId) url += `?room_id=${roomId}`;
6622
6713
  const response = await fetch(url, { headers: this.getHeaders() });
6714
+ if (response.status === 404) {
6715
+ throw new Error("cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).");
6716
+ }
6623
6717
  if (!response.ok) throw new Error(`Failed to list replays: ${response.statusText}`);
6624
6718
  const data = await response.json();
6625
6719
  return data.replays || [];
@@ -6629,6 +6723,9 @@ var GameAPI = class {
6629
6723
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}`, {
6630
6724
  headers: this.getHeaders()
6631
6725
  });
6726
+ if (response.status === 404) {
6727
+ throw new Error("cb.game.getReplay: replay not found or replay storage unconfigured.");
6728
+ }
6632
6729
  if (!response.ok) throw new Error(`Failed to get replay: ${response.statusText}`);
6633
6730
  return response.json();
6634
6731
  }
@@ -6637,6 +6734,9 @@ var GameAPI = class {
6637
6734
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/download`, {
6638
6735
  headers: this.getHeaders()
6639
6736
  });
6737
+ if (response.status === 404) {
6738
+ throw new Error("cb.game.downloadReplay: replay not found or replay storage unconfigured.");
6739
+ }
6640
6740
  if (!response.ok) throw new Error(`Failed to download replay: ${response.statusText}`);
6641
6741
  return response.arrayBuffer();
6642
6742
  }
@@ -6645,6 +6745,9 @@ var GameAPI = class {
6645
6745
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/highlights`, {
6646
6746
  headers: this.getHeaders()
6647
6747
  });
6748
+ if (response.status === 404) {
6749
+ throw new Error("cb.game.getReplayHighlights: replay not found or replay storage unconfigured.");
6750
+ }
6648
6751
  if (!response.ok) throw new Error(`Failed to get highlights: ${response.statusText}`);
6649
6752
  const data = await response.json();
6650
6753
  return data.highlights || [];
@@ -6675,15 +6778,18 @@ var AdsAPI = class {
6675
6778
  return this.http.hasPublicKey() ? "/v1/public" : "/v1";
6676
6779
  }
6677
6780
  /**
6678
- * AdSense 연결 상태 확인
6781
+ * AdSense / AdMob 연결 상태 확인 (중첩 구조)
6679
6782
  *
6680
- * @returns 연결 상태, 이메일, AdSense 계정 ID
6783
+ * @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID
6681
6784
  *
6682
6785
  * @example
6683
6786
  * ```typescript
6684
6787
  * const status = await cb.ads.getConnectionStatus()
6685
- * if (status.is_connected) {
6686
- * console.log('연결됨:', status.email)
6788
+ * if (status.adsense.is_connected) {
6789
+ * console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id)
6790
+ * }
6791
+ * if (status.admob.is_connected) {
6792
+ * console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id)
6687
6793
  * }
6688
6794
  * ```
6689
6795
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "0.16.1",
3
+ "version": "1.2.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",