connectbase-client 3.37.0 → 3.39.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
@@ -32,7 +32,6 @@ __export(index_exports, {
32
32
  GameError: () => GameError,
33
33
  GameRoom: () => GameRoom,
34
34
  GameRoomTransport: () => GameRoomTransport,
35
- GuestSessionConflictError: () => GuestSessionConflictError,
36
35
  NativeAPI: () => NativeAPI,
37
36
  SessionManager: () => SessionManager,
38
37
  VideoProcessingError: () => VideoProcessingError,
@@ -60,15 +59,6 @@ var AuthError = class extends Error {
60
59
  this.name = "AuthError";
61
60
  }
62
61
  };
63
- var GuestSessionConflictError = class extends Error {
64
- constructor() {
65
- super(
66
- "signInAsGuestMember refused: an active non-guest session is present in this client. Pass { allowOverrideExistingSession: true } to explicitly replace it, or call signOut() first."
67
- );
68
- this.code = "GUEST_SESSION_CONFLICT";
69
- this.name = "GuestSessionConflictError";
70
- }
71
- };
72
62
  var GameError = class extends Error {
73
63
  constructor(init) {
74
64
  super(init.message || init.code || "GameError");
@@ -822,21 +812,9 @@ function assertShape(value, shape, endpoint) {
822
812
  }
823
813
 
824
814
  // src/api/auth.ts
825
- var GUEST_MEMBER_TOKEN_KEY_PREFIX = "cb_guest_";
826
- function credentialToStorageKeyHash(credential) {
827
- let hash = 0;
828
- for (let i = 0; i < credential.length; i++) {
829
- const char = credential.charCodeAt(i);
830
- hash = (hash << 5) - hash + char;
831
- hash = hash & hash;
832
- }
833
- return Math.abs(hash).toString(36);
834
- }
835
815
  var AuthAPI = class {
836
816
  constructor(http) {
837
817
  this.http = http;
838
- this.guestMemberLoginPromise = null;
839
- this.cachedGuestMemberTokenKey = null;
840
818
  this.analytics = null;
841
819
  }
842
820
  /**
@@ -870,9 +848,7 @@ var AuthAPI = class {
870
848
  * @example
871
849
  * ```typescript
872
850
  * const settings = await client.auth.getAuthSettings()
873
- * if (settings.allow_guest_login) {
874
- * await client.auth.signInAsGuestMember()
875
- * } else if (settings.allow_id_password_login) {
851
+ * if (settings.allow_id_password_login) {
876
852
  * // 로그인 폼 표시
877
853
  * } else if (settings.enabled_oauth_providers.includes('GOOGLE')) {
878
854
  * // 구글 소셜 로그인 버튼 표시
@@ -950,66 +926,6 @@ var AuthAPI = class {
950
926
  this.notifyVisitorTracker(response.member_id);
951
927
  return response;
952
928
  }
953
- /**
954
- * 앱 멤버 게스트 가입
955
- * API Key로 인증된 앱에 게스트 멤버로 가입합니다.
956
- * 실시간 채팅 등에서 JWT 토큰 기반 인증이 필요할 때 사용합니다.
957
- * 로컬 스토리지에 저장된 토큰이 있으면 기존 계정으로 재로그인을 시도합니다.
958
- * 동시 호출 시 중복 요청 방지 (race condition 방지)
959
- *
960
- * **활성 세션 가드 (3.22.0+)**
961
- * OAuth/email/username 로 이미 로그인된 세션이 살아있는 채 이 메서드가 호출되면
962
- * 메인 토큰 슬롯이 silent 로 게스트 토큰으로 덮어써져 사용자가 둔갑되는 회귀가
963
- * 있었다 (platform-issue 019e6c5d). 이를 막기 위해 활성 비-게스트 세션이 감지되면
964
- * 기본적으로 `GuestSessionConflictError` 를 던진다. 명시적으로 교체하려면
965
- * `{ allowOverrideExistingSession: true }` 를 전달하거나 먼저 `signOut()` 을 호출할 것.
966
- *
967
- * @example
968
- * ```typescript
969
- * const guest = await client.auth.signInAsGuestMember()
970
- * await client.realtime.connect({ accessToken: guest.access_token })
971
- *
972
- * // 활성 세션을 명시적으로 교체 (예: "게스트로 계속" 버튼)
973
- * await client.auth.signInAsGuestMember({ allowOverrideExistingSession: true })
974
- * ```
975
- */
976
- async signInAsGuestMember(opts) {
977
- if (this.guestMemberLoginPromise) {
978
- return this.guestMemberLoginPromise;
979
- }
980
- if (!opts?.allowOverrideExistingSession && this.hasActiveNonGuestSession()) {
981
- throw new GuestSessionConflictError();
982
- }
983
- this.guestMemberLoginPromise = this.executeGuestMemberLogin();
984
- try {
985
- return await this.guestMemberLoginPromise;
986
- } finally {
987
- this.guestMemberLoginPromise = null;
988
- }
989
- }
990
- /**
991
- * 현재 in-memory access token 이 "게스트 토큰이 아닌 다른 세션" 으로 판단되는지 확인.
992
- *
993
- * 판별 규칙:
994
- * - access token 자체가 없으면 false (어떤 세션도 없음)
995
- * - access token 이 만료됐으면 false (재로그인이 필요한 dead 상태)
996
- * - persistence='none' 인 경우: 토큰은 메모리에만 존재. 우리는 그 토큰이
997
- * 게스트인지 회원인지 분별할 메타가 메모리에 없다 → 보수적으로 true 처리.
998
- * (회원이 가지고 있던 세션을 게스트가 덮어쓰는 게 더 큰 사고이므로
999
- * false-positive < silent overwrite.)
1000
- * - persistence='localStorage'|'sessionStorage' 인 경우: 저장된 게스트 토큰과
1001
- * 현재 access token 이 일치하면 게스트 세션(false), 다르면 비-게스트(true).
1002
- */
1003
- hasActiveNonGuestSession() {
1004
- const access = this.http.getAccessToken();
1005
- if (!access) return false;
1006
- if (this.isTokenExpired(access)) return false;
1007
- const stored = this.getStoredGuestMemberTokens();
1008
- if (!stored) {
1009
- return true;
1010
- }
1011
- return stored.accessToken !== access;
1012
- }
1013
929
  /**
1014
930
  * 현재 로그인한 멤버 정보 조회
1015
931
  * custom_data를 포함한 멤버 정보를 반환합니다.
@@ -1106,124 +1022,6 @@ var AuthAPI = class {
1106
1022
  this.notifyVisitorTracker(null);
1107
1023
  }
1108
1024
  }
1109
- /**
1110
- * 저장된 게스트 멤버 토큰 삭제
1111
- */
1112
- clearGuestMemberTokens() {
1113
- const storage = this.http.getPersistenceStorage();
1114
- if (!storage) return;
1115
- storage.removeItem(this.getGuestMemberTokenKey());
1116
- }
1117
- // ===== Private Methods =====
1118
- async executeGuestMemberLogin() {
1119
- const storedData = this.getStoredGuestMemberTokens();
1120
- if (storedData) {
1121
- if (!this.isTokenExpired(storedData.accessToken)) {
1122
- try {
1123
- this.http.setTokens(storedData.accessToken, storedData.refreshToken);
1124
- const memberInfo = await this.http.get(
1125
- "/v1/public/app-members/me"
1126
- );
1127
- if (memberInfo.is_active) {
1128
- this.notifyVisitorTracker(memberInfo.member_id);
1129
- return {
1130
- member_id: memberInfo.member_id,
1131
- access_token: storedData.accessToken,
1132
- refresh_token: storedData.refreshToken
1133
- };
1134
- }
1135
- this.clearGuestMemberTokens();
1136
- } catch {
1137
- this.http.clearTokens();
1138
- }
1139
- }
1140
- if (storedData.refreshToken && !this.isTokenExpired(storedData.refreshToken)) {
1141
- try {
1142
- const refreshed = await this.http.post(
1143
- "/v1/auth/re-issue",
1144
- {},
1145
- { headers: { "Authorization": `Bearer ${storedData.refreshToken}` }, skipAuth: true }
1146
- );
1147
- assertShape(
1148
- refreshed,
1149
- {
1150
- access_token: { type: "string" },
1151
- refresh_token: { type: "string" }
1152
- },
1153
- "auth.signInAsGuestMember.reissue"
1154
- );
1155
- this.http.setTokens(refreshed.access_token, refreshed.refresh_token);
1156
- this.storeGuestMemberTokens(refreshed.access_token, refreshed.refresh_token, storedData.memberId);
1157
- this.notifyVisitorTracker(storedData.memberId);
1158
- return {
1159
- member_id: storedData.memberId,
1160
- access_token: refreshed.access_token,
1161
- refresh_token: refreshed.refresh_token
1162
- };
1163
- } catch {
1164
- this.clearGuestMemberTokens();
1165
- }
1166
- } else {
1167
- this.clearGuestMemberTokens();
1168
- }
1169
- }
1170
- const response = await this.http.post(
1171
- "/v1/public/app-members",
1172
- {},
1173
- { skipAuth: true }
1174
- );
1175
- assertShape(
1176
- response,
1177
- {
1178
- access_token: { type: "string" },
1179
- refresh_token: { type: "string" },
1180
- member_id: { type: "string-or-number" }
1181
- },
1182
- "auth.signInAsGuestMember.new"
1183
- );
1184
- this.http.setTokens(response.access_token, response.refresh_token);
1185
- this.storeGuestMemberTokens(response.access_token, response.refresh_token, response.member_id);
1186
- this.notifyVisitorTracker(response.member_id);
1187
- return response;
1188
- }
1189
- isTokenExpired(token) {
1190
- try {
1191
- const payload = JSON.parse(atob(token.split(".")[1]));
1192
- const currentTime = Date.now() / 1e3;
1193
- return payload.exp < currentTime;
1194
- } catch {
1195
- return true;
1196
- }
1197
- }
1198
- getGuestMemberTokenKey() {
1199
- if (this.cachedGuestMemberTokenKey) {
1200
- return this.cachedGuestMemberTokenKey;
1201
- }
1202
- const credential = this.http.getCredential();
1203
- if (!credential) {
1204
- this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
1205
- } else {
1206
- const keyHash = credentialToStorageKeyHash(credential);
1207
- this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
1208
- }
1209
- return this.cachedGuestMemberTokenKey;
1210
- }
1211
- getStoredGuestMemberTokens() {
1212
- const storage = this.http.getPersistenceStorage();
1213
- if (!storage) return null;
1214
- const stored = storage.getItem(this.getGuestMemberTokenKey());
1215
- if (!stored) return null;
1216
- try {
1217
- return JSON.parse(stored);
1218
- } catch {
1219
- return null;
1220
- }
1221
- }
1222
- storeGuestMemberTokens(accessToken, refreshToken, memberId) {
1223
- const storage = this.http.getPersistenceStorage();
1224
- if (!storage) return;
1225
- storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
1226
- }
1227
1025
  };
1228
1026
 
1229
1027
  // src/api/database.ts
@@ -6201,6 +5999,55 @@ var PushAPI = class {
6201
5999
  };
6202
6000
  return this.http.post(`/v1/apps/${appId}/push/send`, body);
6203
6001
  }
6002
+ /**
6003
+ * 토픽 구독자 전체에게 푸시 알림을 발송한다 (서버 사이드 전용).
6004
+ *
6005
+ * 백엔드 라우트 `POST /v1/apps/:appID/push/send-to-topic` 는 콘솔 JWT, User Secret Key(`cb_sk_`),
6006
+ * 또는 service_role 토큰(`ctx.cbAdmin`)을 받는다. `sendToMembers` 와 동일하게 브라우저 SDK 의
6007
+ * Public Key(`cb_pk_`) **단독** 인스턴스에서는 호출이 차단되며, 위임 Bearer 토큰을 가진
6008
+ * 인스턴스(`ctx.cbAdmin`/`ctx.cb`)는 cb_pk_ 가 앱 식별용으로만 실리므로 허용된다.
6009
+ *
6010
+ * 정기 알림(일일 리마인더 등)을 ConnectBase Function 스케줄러에서 `ctx.cbAdmin` 으로 발송하는
6011
+ * 것이 대표 유스케이스다 — 시크릿 키를 콘솔에서 별도로 발급해 함수 secret 에 넣을 필요가 없다.
6012
+ *
6013
+ * @param appId 발송 대상 앱 ID.
6014
+ * @param topicName 발송 대상 토픽 이름 (예: 'daily-0900').
6015
+ * @param payload 푸시 내용/옵션.
6016
+ *
6017
+ * @example
6018
+ * ```ts
6019
+ * // ConnectBase Function (service_role=true) — 시크릿 키 없이 ctx.cbAdmin 으로 토픽 발송
6020
+ * const result = await ctx.cbAdmin.push.sendToTopic(ctx.appId, 'daily-0900', {
6021
+ * title: '오늘의 학습 리마인더',
6022
+ * body: '잊지 말고 학습을 이어가세요!',
6023
+ * data: { route: '/today' },
6024
+ * })
6025
+ * console.log(result.message_id, result.sent_count)
6026
+ * ```
6027
+ */
6028
+ async sendToTopic(appId, topicName, payload) {
6029
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
6030
+ throw new Error(
6031
+ "cb.push.sendToTopic() \uB294 User Secret Key(cb_sk_), \uCF58\uC194 JWT, \uB610\uB294 service_role(ctx.cbAdmin) \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Public Key(cb_pk_) \uB2E8\uB3C5 SDK \uC778\uC2A4\uD134\uC2A4\uB85C\uB294 \uD638\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Functions \uD658\uACBD\uC5D0\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."
6032
+ );
6033
+ }
6034
+ if (!topicName) {
6035
+ throw new Error("cb.push.sendToTopic(): topicName \uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
6036
+ }
6037
+ const body = {
6038
+ topic_name: topicName,
6039
+ title: payload.title,
6040
+ body: payload.body,
6041
+ ...payload.imageUrl !== void 0 ? { image_url: payload.imageUrl } : {},
6042
+ ...payload.data !== void 0 ? { data: payload.data } : {},
6043
+ ...payload.platforms !== void 0 ? { platforms: payload.platforms } : {},
6044
+ ...payload.ttlSeconds !== void 0 ? { ttl: payload.ttlSeconds } : {},
6045
+ ...payload.priority !== void 0 ? { priority: payload.priority } : {},
6046
+ ...payload.clickAction !== void 0 ? { click_action: payload.clickAction } : {},
6047
+ ...payload.scheduledAt !== void 0 ? { scheduled_at: payload.scheduledAt } : {}
6048
+ };
6049
+ return this.http.post(`/v1/apps/${appId}/push/send-to-topic`, body);
6050
+ }
6204
6051
  // ============ Helper Methods ============
6205
6052
  /**
6206
6053
  * 브라우저 고유 ID 생성 (localStorage에 저장)
@@ -11338,7 +11185,6 @@ var index_default = ConnectBase;
11338
11185
  GameError,
11339
11186
  GameRoom,
11340
11187
  GameRoomTransport,
11341
- GuestSessionConflictError,
11342
11188
  NativeAPI,
11343
11189
  SessionManager,
11344
11190
  VideoProcessingError,
package/dist/index.mjs CHANGED
@@ -14,15 +14,6 @@ var AuthError = class extends Error {
14
14
  this.name = "AuthError";
15
15
  }
16
16
  };
17
- var GuestSessionConflictError = class extends Error {
18
- constructor() {
19
- super(
20
- "signInAsGuestMember refused: an active non-guest session is present in this client. Pass { allowOverrideExistingSession: true } to explicitly replace it, or call signOut() first."
21
- );
22
- this.code = "GUEST_SESSION_CONFLICT";
23
- this.name = "GuestSessionConflictError";
24
- }
25
- };
26
17
  var GameError = class extends Error {
27
18
  constructor(init) {
28
19
  super(init.message || init.code || "GameError");
@@ -776,21 +767,9 @@ function assertShape(value, shape, endpoint) {
776
767
  }
777
768
 
778
769
  // src/api/auth.ts
779
- var GUEST_MEMBER_TOKEN_KEY_PREFIX = "cb_guest_";
780
- function credentialToStorageKeyHash(credential) {
781
- let hash = 0;
782
- for (let i = 0; i < credential.length; i++) {
783
- const char = credential.charCodeAt(i);
784
- hash = (hash << 5) - hash + char;
785
- hash = hash & hash;
786
- }
787
- return Math.abs(hash).toString(36);
788
- }
789
770
  var AuthAPI = class {
790
771
  constructor(http) {
791
772
  this.http = http;
792
- this.guestMemberLoginPromise = null;
793
- this.cachedGuestMemberTokenKey = null;
794
773
  this.analytics = null;
795
774
  }
796
775
  /**
@@ -824,9 +803,7 @@ var AuthAPI = class {
824
803
  * @example
825
804
  * ```typescript
826
805
  * const settings = await client.auth.getAuthSettings()
827
- * if (settings.allow_guest_login) {
828
- * await client.auth.signInAsGuestMember()
829
- * } else if (settings.allow_id_password_login) {
806
+ * if (settings.allow_id_password_login) {
830
807
  * // 로그인 폼 표시
831
808
  * } else if (settings.enabled_oauth_providers.includes('GOOGLE')) {
832
809
  * // 구글 소셜 로그인 버튼 표시
@@ -904,66 +881,6 @@ var AuthAPI = class {
904
881
  this.notifyVisitorTracker(response.member_id);
905
882
  return response;
906
883
  }
907
- /**
908
- * 앱 멤버 게스트 가입
909
- * API Key로 인증된 앱에 게스트 멤버로 가입합니다.
910
- * 실시간 채팅 등에서 JWT 토큰 기반 인증이 필요할 때 사용합니다.
911
- * 로컬 스토리지에 저장된 토큰이 있으면 기존 계정으로 재로그인을 시도합니다.
912
- * 동시 호출 시 중복 요청 방지 (race condition 방지)
913
- *
914
- * **활성 세션 가드 (3.22.0+)**
915
- * OAuth/email/username 로 이미 로그인된 세션이 살아있는 채 이 메서드가 호출되면
916
- * 메인 토큰 슬롯이 silent 로 게스트 토큰으로 덮어써져 사용자가 둔갑되는 회귀가
917
- * 있었다 (platform-issue 019e6c5d). 이를 막기 위해 활성 비-게스트 세션이 감지되면
918
- * 기본적으로 `GuestSessionConflictError` 를 던진다. 명시적으로 교체하려면
919
- * `{ allowOverrideExistingSession: true }` 를 전달하거나 먼저 `signOut()` 을 호출할 것.
920
- *
921
- * @example
922
- * ```typescript
923
- * const guest = await client.auth.signInAsGuestMember()
924
- * await client.realtime.connect({ accessToken: guest.access_token })
925
- *
926
- * // 활성 세션을 명시적으로 교체 (예: "게스트로 계속" 버튼)
927
- * await client.auth.signInAsGuestMember({ allowOverrideExistingSession: true })
928
- * ```
929
- */
930
- async signInAsGuestMember(opts) {
931
- if (this.guestMemberLoginPromise) {
932
- return this.guestMemberLoginPromise;
933
- }
934
- if (!opts?.allowOverrideExistingSession && this.hasActiveNonGuestSession()) {
935
- throw new GuestSessionConflictError();
936
- }
937
- this.guestMemberLoginPromise = this.executeGuestMemberLogin();
938
- try {
939
- return await this.guestMemberLoginPromise;
940
- } finally {
941
- this.guestMemberLoginPromise = null;
942
- }
943
- }
944
- /**
945
- * 현재 in-memory access token 이 "게스트 토큰이 아닌 다른 세션" 으로 판단되는지 확인.
946
- *
947
- * 판별 규칙:
948
- * - access token 자체가 없으면 false (어떤 세션도 없음)
949
- * - access token 이 만료됐으면 false (재로그인이 필요한 dead 상태)
950
- * - persistence='none' 인 경우: 토큰은 메모리에만 존재. 우리는 그 토큰이
951
- * 게스트인지 회원인지 분별할 메타가 메모리에 없다 → 보수적으로 true 처리.
952
- * (회원이 가지고 있던 세션을 게스트가 덮어쓰는 게 더 큰 사고이므로
953
- * false-positive < silent overwrite.)
954
- * - persistence='localStorage'|'sessionStorage' 인 경우: 저장된 게스트 토큰과
955
- * 현재 access token 이 일치하면 게스트 세션(false), 다르면 비-게스트(true).
956
- */
957
- hasActiveNonGuestSession() {
958
- const access = this.http.getAccessToken();
959
- if (!access) return false;
960
- if (this.isTokenExpired(access)) return false;
961
- const stored = this.getStoredGuestMemberTokens();
962
- if (!stored) {
963
- return true;
964
- }
965
- return stored.accessToken !== access;
966
- }
967
884
  /**
968
885
  * 현재 로그인한 멤버 정보 조회
969
886
  * custom_data를 포함한 멤버 정보를 반환합니다.
@@ -1060,124 +977,6 @@ var AuthAPI = class {
1060
977
  this.notifyVisitorTracker(null);
1061
978
  }
1062
979
  }
1063
- /**
1064
- * 저장된 게스트 멤버 토큰 삭제
1065
- */
1066
- clearGuestMemberTokens() {
1067
- const storage = this.http.getPersistenceStorage();
1068
- if (!storage) return;
1069
- storage.removeItem(this.getGuestMemberTokenKey());
1070
- }
1071
- // ===== Private Methods =====
1072
- async executeGuestMemberLogin() {
1073
- const storedData = this.getStoredGuestMemberTokens();
1074
- if (storedData) {
1075
- if (!this.isTokenExpired(storedData.accessToken)) {
1076
- try {
1077
- this.http.setTokens(storedData.accessToken, storedData.refreshToken);
1078
- const memberInfo = await this.http.get(
1079
- "/v1/public/app-members/me"
1080
- );
1081
- if (memberInfo.is_active) {
1082
- this.notifyVisitorTracker(memberInfo.member_id);
1083
- return {
1084
- member_id: memberInfo.member_id,
1085
- access_token: storedData.accessToken,
1086
- refresh_token: storedData.refreshToken
1087
- };
1088
- }
1089
- this.clearGuestMemberTokens();
1090
- } catch {
1091
- this.http.clearTokens();
1092
- }
1093
- }
1094
- if (storedData.refreshToken && !this.isTokenExpired(storedData.refreshToken)) {
1095
- try {
1096
- const refreshed = await this.http.post(
1097
- "/v1/auth/re-issue",
1098
- {},
1099
- { headers: { "Authorization": `Bearer ${storedData.refreshToken}` }, skipAuth: true }
1100
- );
1101
- assertShape(
1102
- refreshed,
1103
- {
1104
- access_token: { type: "string" },
1105
- refresh_token: { type: "string" }
1106
- },
1107
- "auth.signInAsGuestMember.reissue"
1108
- );
1109
- this.http.setTokens(refreshed.access_token, refreshed.refresh_token);
1110
- this.storeGuestMemberTokens(refreshed.access_token, refreshed.refresh_token, storedData.memberId);
1111
- this.notifyVisitorTracker(storedData.memberId);
1112
- return {
1113
- member_id: storedData.memberId,
1114
- access_token: refreshed.access_token,
1115
- refresh_token: refreshed.refresh_token
1116
- };
1117
- } catch {
1118
- this.clearGuestMemberTokens();
1119
- }
1120
- } else {
1121
- this.clearGuestMemberTokens();
1122
- }
1123
- }
1124
- const response = await this.http.post(
1125
- "/v1/public/app-members",
1126
- {},
1127
- { skipAuth: true }
1128
- );
1129
- assertShape(
1130
- response,
1131
- {
1132
- access_token: { type: "string" },
1133
- refresh_token: { type: "string" },
1134
- member_id: { type: "string-or-number" }
1135
- },
1136
- "auth.signInAsGuestMember.new"
1137
- );
1138
- this.http.setTokens(response.access_token, response.refresh_token);
1139
- this.storeGuestMemberTokens(response.access_token, response.refresh_token, response.member_id);
1140
- this.notifyVisitorTracker(response.member_id);
1141
- return response;
1142
- }
1143
- isTokenExpired(token) {
1144
- try {
1145
- const payload = JSON.parse(atob(token.split(".")[1]));
1146
- const currentTime = Date.now() / 1e3;
1147
- return payload.exp < currentTime;
1148
- } catch {
1149
- return true;
1150
- }
1151
- }
1152
- getGuestMemberTokenKey() {
1153
- if (this.cachedGuestMemberTokenKey) {
1154
- return this.cachedGuestMemberTokenKey;
1155
- }
1156
- const credential = this.http.getCredential();
1157
- if (!credential) {
1158
- this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}default`;
1159
- } else {
1160
- const keyHash = credentialToStorageKeyHash(credential);
1161
- this.cachedGuestMemberTokenKey = `${GUEST_MEMBER_TOKEN_KEY_PREFIX}${keyHash}`;
1162
- }
1163
- return this.cachedGuestMemberTokenKey;
1164
- }
1165
- getStoredGuestMemberTokens() {
1166
- const storage = this.http.getPersistenceStorage();
1167
- if (!storage) return null;
1168
- const stored = storage.getItem(this.getGuestMemberTokenKey());
1169
- if (!stored) return null;
1170
- try {
1171
- return JSON.parse(stored);
1172
- } catch {
1173
- return null;
1174
- }
1175
- }
1176
- storeGuestMemberTokens(accessToken, refreshToken, memberId) {
1177
- const storage = this.http.getPersistenceStorage();
1178
- if (!storage) return;
1179
- storage.setItem(this.getGuestMemberTokenKey(), JSON.stringify({ accessToken, refreshToken, memberId }));
1180
- }
1181
980
  };
1182
981
 
1183
982
  // src/api/database.ts
@@ -6155,6 +5954,55 @@ var PushAPI = class {
6155
5954
  };
6156
5955
  return this.http.post(`/v1/apps/${appId}/push/send`, body);
6157
5956
  }
5957
+ /**
5958
+ * 토픽 구독자 전체에게 푸시 알림을 발송한다 (서버 사이드 전용).
5959
+ *
5960
+ * 백엔드 라우트 `POST /v1/apps/:appID/push/send-to-topic` 는 콘솔 JWT, User Secret Key(`cb_sk_`),
5961
+ * 또는 service_role 토큰(`ctx.cbAdmin`)을 받는다. `sendToMembers` 와 동일하게 브라우저 SDK 의
5962
+ * Public Key(`cb_pk_`) **단독** 인스턴스에서는 호출이 차단되며, 위임 Bearer 토큰을 가진
5963
+ * 인스턴스(`ctx.cbAdmin`/`ctx.cb`)는 cb_pk_ 가 앱 식별용으로만 실리므로 허용된다.
5964
+ *
5965
+ * 정기 알림(일일 리마인더 등)을 ConnectBase Function 스케줄러에서 `ctx.cbAdmin` 으로 발송하는
5966
+ * 것이 대표 유스케이스다 — 시크릿 키를 콘솔에서 별도로 발급해 함수 secret 에 넣을 필요가 없다.
5967
+ *
5968
+ * @param appId 발송 대상 앱 ID.
5969
+ * @param topicName 발송 대상 토픽 이름 (예: 'daily-0900').
5970
+ * @param payload 푸시 내용/옵션.
5971
+ *
5972
+ * @example
5973
+ * ```ts
5974
+ * // ConnectBase Function (service_role=true) — 시크릿 키 없이 ctx.cbAdmin 으로 토픽 발송
5975
+ * const result = await ctx.cbAdmin.push.sendToTopic(ctx.appId, 'daily-0900', {
5976
+ * title: '오늘의 학습 리마인더',
5977
+ * body: '잊지 말고 학습을 이어가세요!',
5978
+ * data: { route: '/today' },
5979
+ * })
5980
+ * console.log(result.message_id, result.sent_count)
5981
+ * ```
5982
+ */
5983
+ async sendToTopic(appId, topicName, payload) {
5984
+ if (this.http.hasPublicKey() && !this.http.hasJWT()) {
5985
+ throw new Error(
5986
+ "cb.push.sendToTopic() \uB294 User Secret Key(cb_sk_), \uCF58\uC194 JWT, \uB610\uB294 service_role(ctx.cbAdmin) \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Public Key(cb_pk_) \uB2E8\uB3C5 SDK \uC778\uC2A4\uD134\uC2A4\uB85C\uB294 \uD638\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Functions \uD658\uACBD\uC5D0\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."
5987
+ );
5988
+ }
5989
+ if (!topicName) {
5990
+ throw new Error("cb.push.sendToTopic(): topicName \uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
5991
+ }
5992
+ const body = {
5993
+ topic_name: topicName,
5994
+ title: payload.title,
5995
+ body: payload.body,
5996
+ ...payload.imageUrl !== void 0 ? { image_url: payload.imageUrl } : {},
5997
+ ...payload.data !== void 0 ? { data: payload.data } : {},
5998
+ ...payload.platforms !== void 0 ? { platforms: payload.platforms } : {},
5999
+ ...payload.ttlSeconds !== void 0 ? { ttl: payload.ttlSeconds } : {},
6000
+ ...payload.priority !== void 0 ? { priority: payload.priority } : {},
6001
+ ...payload.clickAction !== void 0 ? { click_action: payload.clickAction } : {},
6002
+ ...payload.scheduledAt !== void 0 ? { scheduled_at: payload.scheduledAt } : {}
6003
+ };
6004
+ return this.http.post(`/v1/apps/${appId}/push/send-to-topic`, body);
6005
+ }
6158
6006
  // ============ Helper Methods ============
6159
6007
  /**
6160
6008
  * 브라우저 고유 ID 생성 (localStorage에 저장)
@@ -11291,7 +11139,6 @@ export {
11291
11139
  GameError,
11292
11140
  GameRoom,
11293
11141
  GameRoomTransport,
11294
- GuestSessionConflictError,
11295
11142
  NativeAPI,
11296
11143
  SessionManager,
11297
11144
  VideoProcessingError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.37.0",
3
+ "version": "3.39.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",