connectbase-client 0.16.1 → 1.3.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
+ // 1.3.0 부터 `acceptPartyInvite/declinePartyInvite` 추가. 직접 join 엔드포인트는 없음 — `joinParty` 는 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,16 +6573,49 @@ 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) {
6576
+ /**
6577
+ * @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptPartyInvite` 플로우 사용
6578
+ */
6579
+ async joinParty(_partyId, _playerId) {
6580
+ throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptPartyInvite flow.");
6581
+ }
6582
+ /**
6583
+ * 파티 초대 수락
6584
+ *
6585
+ * `inviteToParty` 로 생성된 초대 ID 를 수락하여 파티에 합류한다.
6586
+ * 로비 초대(`acceptInvite`) 와 다른 엔드포인트 (`/v1/game/:appID/invites/:inviteID/accept`).
6587
+ */
6588
+ async acceptPartyInvite(inviteId, playerId, displayName) {
6487
6589
  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}`);
6590
+ const qs = new URLSearchParams({ player_id: playerId });
6591
+ if (displayName) qs.set("display_name", displayName);
6592
+ const response = await fetch(
6593
+ `${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/accept?${qs.toString()}`,
6594
+ { method: "POST", headers: this.getHeaders() }
6595
+ );
6596
+ if (!response.ok) {
6597
+ const err = await response.json().catch(() => ({}));
6598
+ throw new Error(err.error || `Failed to accept party invite: ${response.statusText}`);
6599
+ }
6494
6600
  return response.json();
6495
6601
  }
6602
+ /**
6603
+ * 파티 초대 거절
6604
+ *
6605
+ * `inviteToParty` 로 생성된 초대 ID 를 거절한다.
6606
+ */
6607
+ async declinePartyInvite(inviteId, playerId) {
6608
+ const id = this.appId || "";
6609
+ const qs = new URLSearchParams({ player_id: playerId });
6610
+ const response = await fetch(
6611
+ `${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/decline?${qs.toString()}`,
6612
+ { method: "POST", headers: this.getHeaders() }
6613
+ );
6614
+ if (!response.ok) {
6615
+ const err = await response.json().catch(() => ({}));
6616
+ throw new Error(err.error || `Failed to decline party invite: ${response.statusText}`);
6617
+ }
6618
+ }
6496
6619
  async leaveParty(partyId, playerId) {
6497
6620
  const id = this.appId || "";
6498
6621
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/leave`, {
@@ -6525,14 +6648,15 @@ var GameAPI = class {
6525
6648
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
6526
6649
  method: "POST",
6527
6650
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6528
- body: JSON.stringify({ player_id: playerId, message })
6651
+ body: JSON.stringify({ sender_id: playerId, content: message })
6529
6652
  });
6530
6653
  if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
6531
6654
  }
6532
6655
  // --- Spectator API ---
6656
+ // 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
6533
6657
  async joinSpectator(roomId, playerId) {
6534
6658
  const id = this.appId || "";
6535
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectate`, {
6659
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators`, {
6536
6660
  method: "POST",
6537
6661
  headers: { ...this.getHeaders(), "Content-Type": "application/json" },
6538
6662
  body: JSON.stringify({ player_id: playerId })
@@ -6542,10 +6666,9 @@ var GameAPI = class {
6542
6666
  }
6543
6667
  async leaveSpectator(roomId, spectatorId) {
6544
6668
  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 })
6669
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/rooms/${roomId}/spectators/${spectatorId}`, {
6670
+ method: "DELETE",
6671
+ headers: this.getHeaders()
6549
6672
  });
6550
6673
  if (!response.ok) throw new Error(`Failed to leave spectator: ${response.statusText}`);
6551
6674
  }
@@ -6559,27 +6682,30 @@ var GameAPI = class {
6559
6682
  return data.spectators || [];
6560
6683
  }
6561
6684
  // --- Ranking/Leaderboard API ---
6562
- async getLeaderboard(top) {
6685
+ // 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
6686
+ async getLeaderboard(gameType, top = 100, season = "default") {
6563
6687
  const id = this.appId || "";
6564
- const limit = top || 100;
6565
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?limit=${limit}`, {
6688
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
6689
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
6566
6690
  headers: this.getHeaders()
6567
6691
  });
6568
6692
  if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
6569
6693
  const data = await response.json();
6570
6694
  return data.entries || [];
6571
6695
  }
6572
- async getPlayerStats(playerId) {
6696
+ async getPlayerStats(playerId, gameType, season = "default") {
6573
6697
  const id = this.appId || "";
6574
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats`, {
6698
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6699
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
6575
6700
  headers: this.getHeaders()
6576
6701
  });
6577
6702
  if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
6578
6703
  return response.json();
6579
6704
  }
6580
- async getPlayerRank(playerId) {
6705
+ async getPlayerRank(playerId, gameType, season = "default") {
6581
6706
  const id = this.appId || "";
6582
- const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank`, {
6707
+ const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
6708
+ const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
6583
6709
  headers: this.getHeaders()
6584
6710
  });
6585
6711
  if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
@@ -6615,11 +6741,16 @@ var GameAPI = class {
6615
6741
  if (!response.ok) throw new Error(`Failed to set mute: ${response.statusText}`);
6616
6742
  }
6617
6743
  // --- Replay API ---
6744
+ // 1.1.0 부터 활성화. 백엔드에서 `REPLAY_STORAGE_PATH` 환경변수가 설정된 경우에만 동작.
6745
+ // 미설정 시 404 응답 → SDK 가 명시적 에러를 throw.
6618
6746
  async listReplays(roomId) {
6619
6747
  const id = this.appId || "";
6620
6748
  let url = `${this.gameServerUrl}/v1/game/${id}/replays`;
6621
6749
  if (roomId) url += `?room_id=${roomId}`;
6622
6750
  const response = await fetch(url, { headers: this.getHeaders() });
6751
+ if (response.status === 404) {
6752
+ throw new Error("cb.game.listReplays: replay storage is not configured on this server (REPLAY_STORAGE_PATH unset).");
6753
+ }
6623
6754
  if (!response.ok) throw new Error(`Failed to list replays: ${response.statusText}`);
6624
6755
  const data = await response.json();
6625
6756
  return data.replays || [];
@@ -6629,6 +6760,9 @@ var GameAPI = class {
6629
6760
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}`, {
6630
6761
  headers: this.getHeaders()
6631
6762
  });
6763
+ if (response.status === 404) {
6764
+ throw new Error("cb.game.getReplay: replay not found or replay storage unconfigured.");
6765
+ }
6632
6766
  if (!response.ok) throw new Error(`Failed to get replay: ${response.statusText}`);
6633
6767
  return response.json();
6634
6768
  }
@@ -6637,6 +6771,9 @@ var GameAPI = class {
6637
6771
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/download`, {
6638
6772
  headers: this.getHeaders()
6639
6773
  });
6774
+ if (response.status === 404) {
6775
+ throw new Error("cb.game.downloadReplay: replay not found or replay storage unconfigured.");
6776
+ }
6640
6777
  if (!response.ok) throw new Error(`Failed to download replay: ${response.statusText}`);
6641
6778
  return response.arrayBuffer();
6642
6779
  }
@@ -6645,6 +6782,9 @@ var GameAPI = class {
6645
6782
  const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/replays/${replayId}/highlights`, {
6646
6783
  headers: this.getHeaders()
6647
6784
  });
6785
+ if (response.status === 404) {
6786
+ throw new Error("cb.game.getReplayHighlights: replay not found or replay storage unconfigured.");
6787
+ }
6648
6788
  if (!response.ok) throw new Error(`Failed to get highlights: ${response.statusText}`);
6649
6789
  const data = await response.json();
6650
6790
  return data.highlights || [];
@@ -6675,15 +6815,18 @@ var AdsAPI = class {
6675
6815
  return this.http.hasPublicKey() ? "/v1/public" : "/v1";
6676
6816
  }
6677
6817
  /**
6678
- * AdSense 연결 상태 확인
6818
+ * AdSense / AdMob 연결 상태 확인 (중첩 구조)
6679
6819
  *
6680
- * @returns 연결 상태, 이메일, AdSense 계정 ID
6820
+ * @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID
6681
6821
  *
6682
6822
  * @example
6683
6823
  * ```typescript
6684
6824
  * const status = await cb.ads.getConnectionStatus()
6685
- * if (status.is_connected) {
6686
- * console.log('연결됨:', status.email)
6825
+ * if (status.adsense.is_connected) {
6826
+ * console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id)
6827
+ * }
6828
+ * if (status.admob.is_connected) {
6829
+ * console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id)
6687
6830
  * }
6688
6831
  * ```
6689
6832
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "0.16.1",
3
+ "version": "1.3.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",