connectbase-client 6.0.0 → 6.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
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  GameRoom: () => GameRoom,
36
36
  GameRoomTransport: () => GameRoomTransport,
37
37
  NativeAPI: () => NativeAPI,
38
+ OrganizationsAPI: () => OrganizationsAPI,
38
39
  RolesAPI: () => RolesAPI,
39
40
  SessionManager: () => SessionManager,
40
41
  VideoProcessingError: () => VideoProcessingError,
@@ -1686,20 +1687,48 @@ var AuthAPI = class {
1686
1687
  });
1687
1688
  }
1688
1689
  /**
1689
- * 앱 멤버 회원가입 (아이디/비밀번호 기반)
1690
+ * 앱 멤버 회원가입 (아이디/이메일 + 비밀번호 기반)
1690
1691
  * 앱에 새로운 멤버를 등록합니다.
1691
1692
  *
1693
+ * `login_id` 와 `email` 중 **최소 하나**는 있어야 합니다. 어느 쪽을 주느냐에 따라
1694
+ * 만들어지는 로그인 수단이 달라집니다 — 표는 {@link MemberSignUpRequest} 참고.
1695
+ *
1696
+ * `email` 을 함께 저장해야 나중에 {@link AuthAPI.requestPasswordReset} 로 비밀번호를
1697
+ * 복구할 수 있습니다. `login_id` 만으로 가입한 멤버는 복구 수단이 없습니다.
1698
+ *
1699
+ * 이메일을 넘겨도 **인증 메일이 자동 발송되지는 않습니다.** 발신 주소가 플랫폼 단일
1700
+ * 주소라 앱이 요청하지 않은 메일을 보내지 않는다는 정책입니다 — 필요하면 가입 직후
1701
+ * {@link AuthAPI.sendEmailVerification} 을 직접 호출하세요.
1702
+ *
1692
1703
  * @example
1693
1704
  * ```typescript
1705
+ * // 기존 방식 — 아이디만 (동작 그대로, 단 비밀번호 복구 불가)
1694
1706
  * const result = await client.auth.signUpMember({
1695
1707
  * login_id: 'myuser123',
1696
1708
  * password: 'password123',
1697
1709
  * nickname: 'John'
1698
1710
  * })
1699
- * console.log('가입 완료:', result.member_id)
1711
+ *
1712
+ * // 아이디 + 복구용 이메일 (로그인은 여전히 login_id 로)
1713
+ * await client.auth.signUpMember({
1714
+ * login_id: 'myuser123',
1715
+ * email: 'john@example.com',
1716
+ * password: 'password123'
1717
+ * })
1718
+ *
1719
+ * // 이메일만 — 이메일이 곧 로그인 아이디
1720
+ * await client.auth.signUpMember({
1721
+ * email: 'john@example.com',
1722
+ * password: 'password123'
1723
+ * })
1700
1724
  * ```
1701
1725
  */
1702
1726
  async signUpMember(data) {
1727
+ if (!data.login_id && !data.email) {
1728
+ throw new Error(
1729
+ "signUpMember requires at least one of login_id or email."
1730
+ );
1731
+ }
1703
1732
  const response = await this.http.post(
1704
1733
  "/v1/public/app-members/signup",
1705
1734
  data,
@@ -1719,19 +1748,34 @@ var AuthAPI = class {
1719
1748
  return response;
1720
1749
  }
1721
1750
  /**
1722
- * 앱 멤버 로그인 (아이디/비밀번호 기반)
1751
+ * 앱 멤버 로그인 (아이디 또는 이메일 + 비밀번호)
1723
1752
  * 기존 멤버로 로그인합니다.
1724
1753
  *
1754
+ * `login_id` 가 1순위 조회 키이고, `email` 은 이메일로 가입한 멤버를 위한 별칭입니다.
1755
+ * 서버는 USERNAME 을 먼저 찾고 없을 때만 EMAIL 로 폴백하므로 **기존 `login_id` 호출은
1756
+ * 동작이 그대로**입니다. 둘 다 비어 있으면 400 입니다.
1757
+ *
1725
1758
  * @example
1726
1759
  * ```typescript
1760
+ * // 아이디로 로그인 (기존 방식)
1727
1761
  * const result = await client.auth.signInMember({
1728
1762
  * login_id: 'myuser123',
1729
1763
  * password: 'password123'
1730
1764
  * })
1731
- * console.log('로그인 성공:', result.member_id)
1765
+ *
1766
+ * // 이메일로 로그인
1767
+ * await client.auth.signInMember({
1768
+ * email: 'john@example.com',
1769
+ * password: 'password123'
1770
+ * })
1732
1771
  * ```
1733
1772
  */
1734
1773
  async signInMember(data) {
1774
+ if (!data.login_id && !data.email) {
1775
+ throw new Error(
1776
+ "signInMember requires at least one of login_id or email."
1777
+ );
1778
+ }
1735
1779
  const response = await this.http.post(
1736
1780
  "/v1/public/app-members/signin",
1737
1781
  data,
@@ -1750,6 +1794,169 @@ var AuthAPI = class {
1750
1794
  this.notifyVisitorTracker(response.member_id);
1751
1795
  return response;
1752
1796
  }
1797
+ // ── 자격증명 복구/변경 ────────────────────────────────────────────────────
1798
+ //
1799
+ // # 메일 링크는 "앱이 운영하는 페이지"를 거친다
1800
+ //
1801
+ // 재설정/인증 메일의 링크는 이 API 를 직접 열지 않습니다. 브라우저 주소창으로는
1802
+ // `X-Public-Key` 헤더를 붙일 수 없기 때문입니다. 링크는 앱의 페이지(= SDK 가 이미
1803
+ // 초기화돼 있는 곳)로 가고, 그 페이지가 쿼리에서 `token` 을 꺼내 아래 confirm 메서드를
1804
+ // 호출하는 구조입니다.
1805
+ //
1806
+ // 그래서 `redirect_url` 로 넘길 주소는 **앱에 등록된 도메인이어야 합니다**
1807
+ // (자세한 규칙은 {@link MemberRedirectOption}). 도메인을 등록하지 않으면 링크가
1808
+ // 플랫폼 기본 페이지로 폴백합니다.
1809
+ /**
1810
+ * 비밀번호 재설정 메일을 보냅니다. (로그인 불필요)
1811
+ *
1812
+ * # 계정 열거 방지 — 응답으로는 아무것도 알 수 없다
1813
+ *
1814
+ * 가입되지 않은 이메일이든, 소셜 전용 계정(비밀번호 없음)이든, 정지된 멤버든
1815
+ * **똑같은 200 과 똑같은 문구**가 돌아옵니다. 메일이 실제로 나갔는지 여부는 응답으로
1816
+ * 판별할 수 없습니다 — "가입 여부 확인" 용도로 쓸 수 없다는 뜻이며, 의도된 설계입니다.
1817
+ * UI 에서도 "가입된 계정이라면 메일을 보냈습니다" 처럼 안내하세요.
1818
+ *
1819
+ * 메일 링크의 토큰은 **1시간** 만료이고 **1회만** 쓸 수 있습니다.
1820
+ *
1821
+ * @example
1822
+ * ```typescript
1823
+ * // 앱이 자체 재설정 페이지를 운영하는 경우 (도메인 등록 선행 필수)
1824
+ * await client.auth.requestPasswordReset({
1825
+ * email: 'john@example.com',
1826
+ * redirect_url: 'https://myapp.com/reset-password'
1827
+ * })
1828
+ * toast('가입된 계정이라면 재설정 링크를 보냈습니다')
1829
+ * ```
1830
+ */
1831
+ async requestPasswordReset(data) {
1832
+ return this.http.post(
1833
+ "/v1/public/app-members/password-reset/request",
1834
+ data,
1835
+ { skipAuth: true }
1836
+ );
1837
+ }
1838
+ /**
1839
+ * 재설정 토큰으로 새 비밀번호를 확정합니다. (로그인 불필요)
1840
+ *
1841
+ * `redirect_url` 페이지의 `?token=` 쿼리 값을 그대로 넘기면 됩니다.
1842
+ *
1843
+ * 성공하면 **그 멤버의 모든 세션이 서버에서 폐기**됩니다. 다른 기기 로그인까지 전부
1844
+ * 끊기며, 이 SDK 인스턴스에 남아 있던 토큰도 함께 정리하므로 새 비밀번호로 다시
1845
+ * 로그인해야 합니다.
1846
+ *
1847
+ * 토큰이 만료(1시간)됐거나 이미 사용됐거나 위조/타앱 토큰이면 400 이 납니다. 사유는
1848
+ * 구분되지 않습니다 — 토큰을 탐색하는 단서를 주지 않기 위해서입니다.
1849
+ *
1850
+ * @example
1851
+ * ```typescript
1852
+ * const token = new URLSearchParams(location.search).get('token')
1853
+ * if (!token) return
1854
+ * try {
1855
+ * await client.auth.confirmPasswordReset({ token, new_password: 'newpass123' })
1856
+ * location.href = '/login'
1857
+ * } catch (e) {
1858
+ * // 만료, 이미 사용됨, 잘못된 링크가 모두 여기로 온다
1859
+ * alert('링크가 만료되었거나 이미 사용되었습니다. 재설정을 다시 요청해 주세요.')
1860
+ * }
1861
+ * ```
1862
+ */
1863
+ async confirmPasswordReset(data) {
1864
+ const response = await this.http.post(
1865
+ "/v1/public/app-members/password-reset/confirm",
1866
+ data,
1867
+ { skipAuth: true }
1868
+ );
1869
+ this.http.clearTokens();
1870
+ this.notifyVisitorTracker(null);
1871
+ return response;
1872
+ }
1873
+ /**
1874
+ * 로그인한 멤버가 현재 비밀번호를 확인하고 새 비밀번호로 바꿉니다. (AppMember 토큰 필요)
1875
+ *
1876
+ * `confirmPasswordReset` 과 마찬가지로 성공 시 **전 세션이 폐기**되고 로컬 토큰도
1877
+ * 정리됩니다. 호출한 클라이언트도 재로그인이 필요합니다.
1878
+ *
1879
+ * @example
1880
+ * ```typescript
1881
+ * await client.auth.changePassword({
1882
+ * current_password: 'oldpass123',
1883
+ * new_password: 'newpass123'
1884
+ * })
1885
+ * // 세션이 끊겼으므로 로그인 화면으로
1886
+ * location.href = '/login'
1887
+ * ```
1888
+ */
1889
+ async changePassword(data) {
1890
+ const response = await this.http.post(
1891
+ "/v1/public/app-members/me/password",
1892
+ data
1893
+ );
1894
+ this.http.clearTokens();
1895
+ this.notifyVisitorTracker(null);
1896
+ return response;
1897
+ }
1898
+ /**
1899
+ * 로그인한 멤버에게 이메일 인증 메일을 보냅니다. (AppMember 토큰 필요)
1900
+ *
1901
+ * 가입 시 자동 발송되지 않으므로, 인증이 필요한 앱은 이 메서드를 **직접** 불러야 합니다
1902
+ * (예: 가입 직후, 또는 설정 화면의 "인증 메일 다시 보내기" 버튼).
1903
+ *
1904
+ * 메일 링크의 토큰은 **24시간** 만료이고 **1회만** 쓸 수 있습니다.
1905
+ *
1906
+ * 이메일 인증은 **로그인의 전제 조건이 아닙니다** — 인증하지 않아도 로그인은 됩니다.
1907
+ * 인증 여부로 기능을 제한할지는 앱이 `getMe()` 등으로 확인해 스스로 정합니다.
1908
+ *
1909
+ * @example
1910
+ * ```typescript
1911
+ * await client.auth.sendEmailVerification({
1912
+ * redirect_url: 'https://myapp.com/verify-email'
1913
+ * })
1914
+ *
1915
+ * // redirect_url 없이 — 플랫폼 기본 페이지 링크로 발송된다
1916
+ * await client.auth.sendEmailVerification()
1917
+ * ```
1918
+ */
1919
+ async sendEmailVerification(data) {
1920
+ return this.http.post(
1921
+ "/v1/public/app-members/me/email-verification/request",
1922
+ data ?? {}
1923
+ );
1924
+ }
1925
+ /**
1926
+ * 인증 토큰으로 이메일 인증을 확정합니다. (로그인 불필요)
1927
+ *
1928
+ * 메일 링크가 연 페이지에서 `?token=` 을 꺼내 그대로 넘깁니다. 링크를 누른 사람이 그
1929
+ * 앱에 로그인해 있지 않아도(다른 기기의 메일 앱에서 열어도) 동작합니다.
1930
+ *
1931
+ * 만료(24시간), 이미 사용됨, 위조 토큰은 모두 400 이며 사유가 구분되지 않습니다.
1932
+ *
1933
+ * @example
1934
+ * ```typescript
1935
+ * const token = new URLSearchParams(location.search).get('token')
1936
+ * if (!token) return
1937
+ * try {
1938
+ * const r = await client.auth.confirmEmailVerification({ token })
1939
+ * console.log(r.email, r.is_email_verified) // true
1940
+ * } catch {
1941
+ * alert('인증 링크가 만료되었거나 이미 사용되었습니다.')
1942
+ * }
1943
+ * ```
1944
+ */
1945
+ async confirmEmailVerification(data) {
1946
+ const response = await this.http.post(
1947
+ "/v1/public/app-members/email-verification/confirm",
1948
+ data,
1949
+ { skipAuth: true }
1950
+ );
1951
+ assertShape(
1952
+ response,
1953
+ {
1954
+ member_id: { type: "string-or-number" }
1955
+ },
1956
+ "auth.confirmEmailVerification"
1957
+ );
1958
+ return response;
1959
+ }
1753
1960
  /**
1754
1961
  * 현재 로그인한 멤버 정보 조회
1755
1962
  * custom_data를 포함한 멤버 정보를 반환합니다.
@@ -1913,6 +2120,109 @@ var AuthAPI = class {
1913
2120
  this.notifyVisitorTracker(null);
1914
2121
  }
1915
2122
  }
2123
+ // ── 활성 세션(로그인된 기기) 관리 ────────────────────────────────────────
2124
+ //
2125
+ // 대상은 언제나 **로그인한 본인**이다. 세 메서드 모두 사용자 식별자를 받지 않는다 —
2126
+ // 누구의 세션인지는 서버가 검증된 토큰에서 정하고, 요청은 정할 수 없다.
2127
+ //
2128
+ // 폐기는 되돌릴 수 없다. 다만 이미 발급된 액세스 토큰은 만료(최대 1시간)까지 유효하므로,
2129
+ // 끊긴 기기가 곧바로 모든 호출에 실패하지는 않는다. 즉시 차단이 필요하면 앱이 서버
2130
+ // 측에서 회원을 정지시켜야 한다.
2131
+ /**
2132
+ * 로그인된 기기(활성 세션) 목록을 조회합니다.
2133
+ *
2134
+ * 현재 세션이 맨 앞에 오고, 나머지는 최근 사용 순입니다. 응답에는 토큰도 평문 IP 도
2135
+ * 담기지 않습니다.
2136
+ *
2137
+ * **`current` 를 화면에 반드시 표시하세요.** 구분이 없으면 사용자가 자기가 쓰고 있는
2138
+ * 기기를 끊고 영문도 모른 채 로그아웃됩니다. refresh 쿠키가 없는 환경에서는 서버가
2139
+ * 현재 세션을 특정하지 못해 `current` 가 어느 항목에도 붙지 않을 수 있습니다.
2140
+ *
2141
+ * @example
2142
+ * ```typescript
2143
+ * const { sessions } = await cb.auth.listSessions()
2144
+ * for (const s of sessions) {
2145
+ * console.log(s.browser, s.os, s.location, s.current ? '(이 기기)' : '')
2146
+ * }
2147
+ * ```
2148
+ */
2149
+ async listSessions() {
2150
+ const response = await this.http.get(
2151
+ "/v1/public/app-members/me/sessions"
2152
+ );
2153
+ assertShape(
2154
+ response,
2155
+ {
2156
+ sessions: { type: "array" }
2157
+ },
2158
+ "auth.listSessions"
2159
+ );
2160
+ return response;
2161
+ }
2162
+ /**
2163
+ * 기기 하나를 로그아웃시킵니다. `sessionId` 는 `listSessions()` 가 돌려준 `session_id`.
2164
+ *
2165
+ * 남의 세션 식별자를 넣으면 404 입니다 — 존재 여부조차 구분해 알려주지 않습니다.
2166
+ * 이미 끝난 세션도 같은 404 입니다.
2167
+ *
2168
+ * **현재 세션을 지목하면 그 자리에서 로그아웃됩니다.** 서버가 refresh 쿠키를 함께
2169
+ * 만료시키므로 로컬 토큰도 정리합니다 — 이 경우 앱은 로그인 화면으로 보내야 합니다.
2170
+ * 그렇지 않은 기기를 끊는 것은 현재 세션에 영향을 주지 않습니다.
2171
+ *
2172
+ * @example
2173
+ * ```typescript
2174
+ * const target = sessions.find((s) => !s.current)
2175
+ * if (target) await cb.auth.revokeSession(target.session_id)
2176
+ * ```
2177
+ */
2178
+ async revokeSession(sessionId) {
2179
+ const current = await this.isCurrentSession(sessionId);
2180
+ const response = await this.http.delete(
2181
+ `/v1/public/app-members/me/sessions/${encodeURIComponent(sessionId)}`
2182
+ );
2183
+ if (current) {
2184
+ this.http.clearTokens();
2185
+ this.notifyVisitorTracker(null);
2186
+ }
2187
+ return response;
2188
+ }
2189
+ /**
2190
+ * **현재 기기만 남기고** 나머지 기기에서 모두 로그아웃합니다.
2191
+ *
2192
+ * 자기 세션까지 포함한 전체 로그아웃은 `signOut()` 입니다 — 자격증명 유출 대응처럼
2193
+ * 모든 기기를 내보내야 하는 상황에서는 그쪽을 쓰세요.
2194
+ *
2195
+ * refresh 쿠키가 없어 서버가 현재 세션을 특정하지 못하면 이 호출은 **실패합니다**
2196
+ * (400). 기준점 없이 진행하면 사용자가 의도하지 않은 자기 세션 종료가 함께 일어나기
2197
+ * 때문입니다. 그 경우 `signOut()` 후 재로그인을 안내하세요.
2198
+ *
2199
+ * 반환되는 `revoked` 는 토큰 수가 아니라 **끊긴 기기 수** 입니다.
2200
+ *
2201
+ * @example
2202
+ * ```typescript
2203
+ * const { revoked } = await cb.auth.revokeOtherSessions()
2204
+ * toast(`${revoked}개 기기에서 로그아웃했습니다`)
2205
+ * ```
2206
+ */
2207
+ async revokeOtherSessions() {
2208
+ return this.http.delete(
2209
+ "/v1/public/app-members/me/sessions"
2210
+ );
2211
+ }
2212
+ /**
2213
+ * 폐기 대상이 현재 세션인지 미리 확인한다. 목록 조회가 실패하면 false 로 떨어져
2214
+ * 폐기 자체는 진행한다 — 판정 실패로 사용자의 로그아웃 요청을 막을 이유가 없다.
2215
+ */
2216
+ async isCurrentSession(sessionId) {
2217
+ try {
2218
+ const { sessions } = await this.listSessions();
2219
+ return sessions.some(
2220
+ (s) => s.session_id === sessionId && s.current === true
2221
+ );
2222
+ } catch {
2223
+ return false;
2224
+ }
2225
+ }
1916
2226
  };
1917
2227
 
1918
2228
  // src/api/database.ts
@@ -6631,6 +6941,199 @@ var OAuthAPI = class {
6631
6941
  }
6632
6942
  };
6633
6943
 
6944
+ // src/api/organizations.ts
6945
+ var OrganizationsAPI = class {
6946
+ constructor(http) {
6947
+ this.http = http;
6948
+ /** 조직 라우트는 `/v1/public` 아래에만 있습니다. */
6949
+ this.prefix = "/v1/public/organizations";
6950
+ }
6951
+ // =====================
6952
+ // 조직
6953
+ // =====================
6954
+ /**
6955
+ * 조직을 만듭니다. **만든 사람이 자동으로 `owner`** 가 됩니다.
6956
+ *
6957
+ * @example
6958
+ * ```typescript
6959
+ * const org = await cb.organizations.create({ name: '우리 팀' })
6960
+ * // slug 를 생략하면 name 에서 파생되고, 충돌하면 짧은 접미사가 붙는다
6961
+ * ```
6962
+ */
6963
+ async create(data) {
6964
+ return this.http.post(this.prefix, data);
6965
+ }
6966
+ /**
6967
+ * 내가 속한 조직 목록. 조직 전환 UI 를 그릴 때 씁니다.
6968
+ *
6969
+ * 각 항목의 `my_role` 이 채워집니다. `member_count` 는 목록에서는 실리지 않습니다
6970
+ * (조직마다 COUNT 를 돌면 N+1 이라 상세 조회에서만 채워집니다).
6971
+ */
6972
+ async listMine() {
6973
+ const res = await this.http.get(
6974
+ this.prefix
6975
+ );
6976
+ return res.organizations ?? [];
6977
+ }
6978
+ /**
6979
+ * 조직 상세. `my_role` 과 `member_count` 가 함께 실립니다.
6980
+ *
6981
+ * 내가 속하지 않은 조직은 **403 이 아니라 404** 입니다 — 403 을 주면 그것만으로 그 조직이
6982
+ * 실재한다는 사실이 새어 조직 ID 열거가 가능해지기 때문입니다.
6983
+ */
6984
+ async get(organizationId) {
6985
+ return this.http.get(`${this.prefix}/${organizationId}`);
6986
+ }
6987
+ /**
6988
+ * 조직 정보 수정.
6989
+ *
6990
+ * **`slug` 는 바꿀 수 없습니다** — 변경을 허용하면 조직 URL 과 외부 참조가 조용히 깨지고,
6991
+ * 놓아준 slug 를 다른 조직이 선점해 다른 조직의 페이지가 열립니다.
6992
+ */
6993
+ async update(organizationId, data) {
6994
+ return this.http.patch(
6995
+ `${this.prefix}/${organizationId}`,
6996
+ data
6997
+ );
6998
+ }
6999
+ /** 조직 삭제. */
7000
+ async delete(organizationId) {
7001
+ return this.http.delete(
7002
+ `${this.prefix}/${organizationId}`
7003
+ );
7004
+ }
7005
+ // =====================
7006
+ // 멤버
7007
+ // =====================
7008
+ /**
7009
+ * 조직 멤버 목록.
7010
+ *
7011
+ * 닉네임/이메일 같은 **프로필은 실리지 않습니다** — 조직에 초대되기만 하면 다른 구성원의
7012
+ * 이메일을 전부 수집할 수 있게 되기 때문입니다. 프로필이 필요하면 `member_id` 로
7013
+ * `cb.appMembers.*` 를 조회하세요.
7014
+ */
7015
+ async listMembers(organizationId, params) {
7016
+ const query = new URLSearchParams();
7017
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
7018
+ if (params?.offset !== void 0)
7019
+ query.set("offset", String(params.offset));
7020
+ const qs = query.toString();
7021
+ return this.http.get(
7022
+ `${this.prefix}/${organizationId}/members${qs ? `?${qs}` : ""}`
7023
+ );
7024
+ }
7025
+ /**
7026
+ * 멤버의 조직 역할 변경.
7027
+ *
7028
+ * 소유자는 별도 컬럼이 아니라 `role === 'owner'` 인 행에서 파생되므로, 마지막 owner 의
7029
+ * 역할을 내리는 것은 거부됩니다 (403).
7030
+ */
7031
+ async updateMemberRole(organizationId, memberId, role) {
7032
+ return this.http.patch(
7033
+ `${this.prefix}/${organizationId}/members/${memberId}`,
7034
+ { role }
7035
+ );
7036
+ }
7037
+ /**
7038
+ * 멤버 제거. **본인 ID 를 넘기면 조직 탈퇴**입니다.
7039
+ *
7040
+ * 마지막 owner 는 제거할 수 없습니다 (403) — 소유자 없는 조직은 아무도 삭제할 수 없게
7041
+ * 되기 때문입니다.
7042
+ */
7043
+ async removeMember(organizationId, memberId) {
7044
+ return this.http.delete(
7045
+ `${this.prefix}/${organizationId}/members/${memberId}`
7046
+ );
7047
+ }
7048
+ // =====================
7049
+ // 초대
7050
+ // =====================
7051
+ /**
7052
+ * 초대 생성.
7053
+ *
7054
+ * **평문 토큰은 이 응답에서만 볼 수 있습니다** (DB 에는 SHA-256 만 남습니다).
7055
+ * **초대 메일 발송은 플랫폼이 대신하지 않습니다** — 앱마다 문안과 발송 채널이 다르므로,
7056
+ * 받은 토큰을 초대 대상에게 전달하는 것은 앱의 몫입니다.
7057
+ *
7058
+ * @example
7059
+ * ```typescript
7060
+ * const { invitation, token } = await cb.organizations.createInvitation(orgId, {
7061
+ * email: 'teammate@example.com',
7062
+ * role: 'member',
7063
+ * })
7064
+ * // token 을 담은 초대 링크를 앱이 직접 발송한다
7065
+ * await sendMyInviteMail(invitation.email, `https://myapp.com/join?token=${token}`)
7066
+ * ```
7067
+ */
7068
+ async createInvitation(organizationId, data) {
7069
+ return this.http.post(
7070
+ `${this.prefix}/${organizationId}/invitations`,
7071
+ data
7072
+ );
7073
+ }
7074
+ /** 조직의 초대 목록. 평문 토큰은 실리지 않습니다. */
7075
+ async listInvitations(organizationId) {
7076
+ const res = await this.http.get(`${this.prefix}/${organizationId}/invitations`);
7077
+ return res.invitations ?? [];
7078
+ }
7079
+ /** 초대 취소. */
7080
+ async revokeInvitation(organizationId, invitationId) {
7081
+ return this.http.delete(
7082
+ `${this.prefix}/${organizationId}/invitations/${invitationId}`
7083
+ );
7084
+ }
7085
+ /**
7086
+ * 초대 토큰으로 조직에 합류합니다. 조직 ID 를 몰라도 됩니다.
7087
+ *
7088
+ * 수락자의 이메일은 **로그인한 회원 토큰의 클레임**에서 가져오므로, 초대 대상과 다른
7089
+ * 계정으로 로그인한 상태면 거부됩니다 (403).
7090
+ * 없는 토큰/만료/이미 사용됨/취소됨은 **구분하지 않고 404** 입니다 (토큰 추측 방지).
7091
+ */
7092
+ async acceptInvitation(token) {
7093
+ return this.http.post(
7094
+ `${this.prefix}/invitations/accept`,
7095
+ { token }
7096
+ );
7097
+ }
7098
+ // =====================
7099
+ // 조직 컨텍스트 (조직 전환)
7100
+ // =====================
7101
+ /**
7102
+ * 조직을 전환합니다 — 활성 조직이 실린 새 액세스 토큰을 받아 **SDK 에 자동 적용**합니다.
7103
+ *
7104
+ * 데이터베이스 보안 규칙(RLS)의 `auth.org_id` / `auth.org_role` 을 채우는 **유일한
7105
+ * 경로**이며, 호출할 때마다 소속과 역할을 DB 에서 다시 확인합니다.
7106
+ *
7107
+ * ## 반드시 주기적으로 다시 호출하세요
7108
+ *
7109
+ * 조직 컨텍스트의 수명은 반환값의 `org_context_expires_in`(기본 600초) 입니다. 액세스
7110
+ * 토큰(1시간)과 독립적이라, 만료되어도 401 이 나지 않고 조직 규칙만 조용히 거부됩니다.
7111
+ *
7112
+ * ## 세션을 연장하지 않습니다
7113
+ *
7114
+ * 새 토큰의 수명은 `min(1시간, 지금 토큰의 남은 수명)` 입니다. 전환으로 세션을 무한
7115
+ * 연장할 수 없게 하기 위한 제약이며, refresh token 은 건드리지 않습니다. 토큰 회전이
7116
+ * 일어나면 조직 컨텍스트는 사라지므로 다시 호출해야 합니다.
7117
+ *
7118
+ * @example
7119
+ * ```typescript
7120
+ * const ctx = await cb.organizations.switchTo(orgId)
7121
+ * console.log(ctx.role, ctx.org_context_expires_in)
7122
+ * // 이 시점부터 DB 요청은 auth.org_id 가 채워진 상태로 나간다
7123
+ * ```
7124
+ */
7125
+ async switchTo(organizationId) {
7126
+ const ctx = await this.http.post(
7127
+ `${this.prefix}/${organizationId}/context`,
7128
+ {}
7129
+ );
7130
+ if (ctx?.access_token) {
7131
+ this.http.setAccessToken(ctx.access_token);
7132
+ }
7133
+ return ctx;
7134
+ }
7135
+ };
7136
+
6634
7137
  // src/api/payment.ts
6635
7138
  var PaymentAPI = class {
6636
7139
  constructor(http) {
@@ -6936,6 +7439,35 @@ var PublicKeyAPI = class {
6936
7439
  this.ensureServerAuth("deletePublicKey");
6937
7440
  await this.http.delete(`/v1/apps/${appId}/public-keys/${keyId}`);
6938
7441
  }
7442
+ /**
7443
+ * Public Key 를 **무중단 회전**합니다 (management_scope: `publickey:manage`)
7444
+ *
7445
+ * 새 키를 발급하고 옛 키는 유예 기간 동안 함께 유효하게 둔 뒤 만료시킵니다. 옛 키의
7446
+ * 스코프와 `payment_mode` 는 새 키가 물려받습니다 — 리셋되면 읽기 전용 키가 회전 한 번에
7447
+ * 전권이 되기 때문입니다.
7448
+ *
7449
+ * **반환되는 `new_key.key` 는 이 응답에서만 볼 수 있습니다.**
7450
+ *
7451
+ * @param appId 앱 ID
7452
+ * @param keyId 회전할 Public Key ID
7453
+ * @param data 유예 기간(기본 24시간, 상한 720시간)과 새 키 이름
7454
+ *
7455
+ * @example
7456
+ * ```typescript
7457
+ * const rotated = await cb.publicKey.rotatePublicKey('app-id', 'key-id', {
7458
+ * grace_period_hours: 72,
7459
+ * })
7460
+ * // 1. rotated.new_key.key 를 배포 파이프라인에 반영
7461
+ * // 2. rotated.previous_key_expires_at 전에 모든 클라이언트 배포를 끝낸다
7462
+ * ```
7463
+ */
7464
+ async rotatePublicKey(appId, keyId, data) {
7465
+ this.ensureServerAuth("rotatePublicKey");
7466
+ return this.http.post(
7467
+ `/v1/apps/${appId}/public-keys/${keyId}/rotate`,
7468
+ data ?? {}
7469
+ );
7470
+ }
6939
7471
  };
6940
7472
 
6941
7473
  // src/api/push.ts
@@ -7314,13 +7846,13 @@ var QueueAPI = class {
7314
7846
  this.http = http;
7315
7847
  }
7316
7848
  /**
7317
- * 메시지 발행
7849
+ * 메시지 발행 — 엔드유저 동작. 퍼블릭 키만으로 동작합니다.
7318
7850
  */
7319
7851
  async publish(queueID, data) {
7320
7852
  return this.http.post(`/v1/public/queues/${queueID}/messages`, data);
7321
7853
  }
7322
7854
  /**
7323
- * 배치 메시지 발행 (최대 100개)
7855
+ * 배치 메시지 발행 (최대 100개) — 엔드유저 동작. 퍼블릭 키만으로 동작합니다.
7324
7856
  */
7325
7857
  async publishBatch(queueID, data) {
7326
7858
  return this.http.post(`/v1/public/queues/${queueID}/messages/batch`, data);
@@ -7328,6 +7860,9 @@ var QueueAPI = class {
7328
7860
  /**
7329
7861
  * 메시지 소비 (Pull 방식)
7330
7862
  *
7863
+ * **워커 동작 — Secret Key 필수.** `new ConnectBase({ publicKey, secretKey })` 로 만든
7864
+ * 서버사이드 클라이언트로 호출하세요. 퍼블릭 키만이면 401 `SECRET_KEY_REQUIRED`.
7865
+ *
7331
7866
  * auto_ack=false(기본값): 명시적 Ack 필요. 응답의 ack_token을 ack() 호출 시 전달.
7332
7867
  * auto_ack=true: 즉시 자동 Ack (at-most-once, 유실 가능)
7333
7868
  */
@@ -7355,6 +7890,8 @@ var QueueAPI = class {
7355
7890
  /**
7356
7891
  * 메시지 처리 완료 확인 (Ack)
7357
7892
  *
7893
+ * **워커 동작 — Secret Key 필수.** 퍼블릭 키만이면 401 `SECRET_KEY_REQUIRED`.
7894
+ *
7358
7895
  * ackToken: auto_ack=false 소비 시 반환된 ack_token. 생략 시 message_ids로만 확인.
7359
7896
  */
7360
7897
  async ack(queueID, messageIds, ackToken) {
@@ -7366,6 +7903,8 @@ var QueueAPI = class {
7366
7903
  }
7367
7904
  /**
7368
7905
  * 메시지 재시도 요청 (Nack)
7906
+ *
7907
+ * **워커 동작 — Secret Key 필수.** 퍼블릭 키만이면 401 `SECRET_KEY_REQUIRED`.
7369
7908
  */
7370
7909
  async nack(queueID, messageId, options) {
7371
7910
  return this.http.post(
@@ -7374,7 +7913,9 @@ var QueueAPI = class {
7374
7913
  );
7375
7914
  }
7376
7915
  /**
7377
- * 큐 정보 조회
7916
+ * 큐 정보 조회 (큐 목록/깊이 등 운영 정보)
7917
+ *
7918
+ * **워커 동작 — Secret Key 필수.** 퍼블릭 키만이면 401 `SECRET_KEY_REQUIRED`.
7378
7919
  */
7379
7920
  async getInfo(queueID) {
7380
7921
  return this.http.get(`/v1/public/queues/${queueID}`);
@@ -9139,6 +9680,10 @@ var StorageAPI = class {
9139
9680
  /**
9140
9681
  * 파일 목록 조회
9141
9682
  *
9683
+ * 접근 수준이 `private` 인 스토리지에서는 **자기가 올린 파일만** 반환하며, 멤버 토큰 없이
9684
+ * 호출하면 401 이다 (빈 배열이 아니라 에러다 — 빈 목록은 "스토리지가 비었다" 와 구분되지
9685
+ * 않아 원인을 찾을 수 없기 때문). `shared` / `public_read` 는 종전대로 전체를 반환한다.
9686
+ *
9142
9687
  * @param storageId - 파일 스토리지 ID
9143
9688
  * @param parentId - 부모 폴더 ID. 지정 시 해당 폴더의 **직계 자식만** 반환한다.
9144
9689
  * 미지정 시 스토리지 **전체 파일 트리**를 flat 배열로 반환한다.
@@ -9165,6 +9710,12 @@ var StorageAPI = class {
9165
9710
  *
9166
9711
  * 서버를 거치지 않고 Object Storage에 직접 업로드합니다.
9167
9712
  *
9713
+ * `Authorization: Bearer <AppMember 토큰>` 을 함께 보내면 업로드한 파일이 **그 멤버에게
9714
+ * 귀속**되어, 이후 그 멤버(와 앱 관리자)만 지울 수 있다. 토큰 없이 올린 파일은 귀속이 없어
9715
+ * `shared` 스토리지에서는 누구나 지울 수 있으므로, 사용자 업로드를 받는 앱이라면 로그인
9716
+ * 세션과 함께 호출하는 것을 권장한다. 접근 수준이 `private` 인 스토리지는 멤버 토큰이 없으면
9717
+ * 401 이다.
9718
+ *
9168
9719
  * 3번째 인자는 부모 폴더 ID(문자열) 또는 옵션 객체를 받습니다. 옵션으로
9169
9720
  * `onProgress` 콜백을 넘기면 업로드 진행률(%)을 실시간으로 받을 수 있습니다
9170
9721
  * (브라우저 환경). 기존 `uploadFile(id, file, 'folder-id')` 호출과 하위 호환됩니다.
@@ -9234,6 +9785,10 @@ var StorageAPI = class {
9234
9785
  }
9235
9786
  /**
9236
9787
  * 폴더 생성
9788
+ *
9789
+ * 업로드와 같은 귀속 규칙이 적용된다 — 멤버 토큰과 함께 만들면 그 멤버 소유가 된다.
9790
+ * 폴더 삭제는 CASCADE 라, 폴더 안에 **남이 올린 파일이 하나라도 있으면** 폴더 소유자라도
9791
+ * 지울 수 없다(403).
9237
9792
  */
9238
9793
  async createFolder(storageId, data) {
9239
9794
  const prefix = this.getPublicPrefix();
@@ -9244,6 +9799,13 @@ var StorageAPI = class {
9244
9799
  }
9245
9800
  /**
9246
9801
  * 파일/폴더 삭제
9802
+ *
9803
+ * **업로더 본인 또는 앱 관리자만** 지울 수 있다. 남의 파일이면 403
9804
+ * (`이 파일의 업로더만 수정/삭제할 수 있습니다`), 애초에 볼 수 없는 파일이면 404 다.
9805
+ *
9806
+ * 운영 도구처럼 앱의 모든 파일을 지워야 하는 코드라면 `Authorization: Bearer cb_sk_...`
9807
+ * (Secret Key)를 함께 보낸다. Secret Key 는 브라우저 번들에 넣으면 안 되므로 서버 쪽에서만
9808
+ * 사용한다.
9247
9809
  */
9248
9810
  async deleteFile(storageId, fileId) {
9249
9811
  const prefix = this.getPublicPrefix();
@@ -9301,6 +9863,10 @@ var StorageAPI = class {
9301
9863
  * 같은 경로에 파일이 이미 존재하면 덮어쓰기합니다.
9302
9864
  * URL이 변경되지 않아 고정 URL이 필요한 경우에 유용합니다.
9303
9865
  *
9866
+ * **덮어쓰기는 삭제와 같은 권한을 요구한다.** URL 은 그대로 둔 채 내용만 바뀌므로, 소유권
9867
+ * 검사가 없으면 사이트가 참조 중인 이미지를 임의 콘텐츠로 갈아치울 수 있기 때문이다.
9868
+ * 기존 파일의 업로더 본인이나 앱 관리자가 아니면 403 이다.
9869
+ *
9304
9870
  * @example
9305
9871
  * ```ts
9306
9872
  * // 프로필 이미지 업로드 (항상 같은 URL 유지)
@@ -9613,7 +10179,29 @@ var SubscriptionAPI = class {
9613
10179
  /**
9614
10180
  * 빌링키 삭제
9615
10181
  *
10182
+ * **PG 쪽 등록까지 지워졌는지는 프로바이더마다 다릅니다.** 반환값의 `provider_revoked` 를
10183
+ * 반드시 확인하세요 — `false` 면 Connect Base 기록만 지워졌고 PG 등록은 남아 있습니다.
10184
+ *
10185
+ * | 프로바이더 | PG 해지 | 동작 |
10186
+ * |-----------|---------|------|
10187
+ * | `toss` | O | 토스 빌링키 삭제 API 호출. PG 삭제가 실패하면 DB 행도 지우지 않고 에러 |
10188
+ * | `payapp` / `paypal` / `paddle` / `stripe` | X | "결제수단 삭제" API 가 없어 기록만 삭제. `provider_revoked: false` + `provider_revoke_note` |
10189
+ *
10190
+ * 살아 있는 구독(`active` / `trial` / `paused` / `past_due`)이 그 빌링키를 물고 있으면
10191
+ * PG 해지 경로가 없는 프로바이더에서는 `409 billing_key_bound_to_subscription` 으로
10192
+ * 거절됩니다 (구독을 먼저 해지하세요). **toss 는 이 검사를 하지 않고 그대로 진행합니다** —
10193
+ * PG 등록이 실제로 사라지므로 청구도 함께 멈추기 때문입니다.
10194
+ *
9616
10195
  * @param billingKeyId - 빌링키 ID
10196
+ * @returns 삭제 결과 (PG 해지 여부 포함)
10197
+ *
10198
+ * @example
10199
+ * ```typescript
10200
+ * const result = await client.subscription.deleteBillingKey(billingKeyId)
10201
+ * if (!result.provider_revoked) {
10202
+ * alert(result.provider_revoke_note) // PG 등록이 남아 있음을 사용자에게 알린다
10203
+ * }
10204
+ * ```
9617
10205
  */
9618
10206
  async deleteBillingKey(billingKeyId) {
9619
10207
  const prefix = this.getPublicPrefix();
@@ -10628,6 +11216,19 @@ var VideoAPI = class {
10628
11216
  }
10629
11217
  /**
10630
11218
  * Update video details
11219
+ *
11220
+ * ## 소유권 (2026-09 도입)
11221
+ *
11222
+ * **영상을 올린 멤버 본인 또는 앱 관리자만** 수정할 수 있다. 퍼블릭 키(`cb_pk_*`)는
11223
+ * 브라우저 번들에 실려 배포되는 공개값이라 그것만으로는 남의 영상을 다룰 수 없다.
11224
+ *
11225
+ * - `Authorization: Bearer <AppMember 토큰>` — 그 멤버가 올린 영상만
11226
+ * - `Authorization: Bearer cb_sk_...` (Secret Key, 서버 전용) — 앱의 모든 영상
11227
+ *
11228
+ * 남의 영상이면 403 (`NOT_RESOURCE_OWNER`) 이다.
11229
+ *
11230
+ * 업로더 정보가 없는 **기존 영상**(멤버 토큰 없이 업로드된 것)은 스토리지 접근 수준이
11231
+ * `shared` 일 때 종전대로 수정할 수 있다 — 운영 중인 앱의 동작을 바꾸지 않기 위한 하위호환.
10631
11232
  */
10632
11233
  async update(videoId, data) {
10633
11234
  const prefix = this.getPublicPrefix();
@@ -10635,6 +11236,8 @@ var VideoAPI = class {
10635
11236
  }
10636
11237
  /**
10637
11238
  * Delete a video
11239
+ *
11240
+ * `update()` 와 동일한 소유권 규칙이 적용된다 — 업로더 본인 또는 앱 관리자만 삭제할 수 있다.
10638
11241
  */
10639
11242
  async delete(videoId) {
10640
11243
  const prefix = this.getPublicPrefix();
@@ -11778,6 +12381,7 @@ function fetchCredentialsForPath(url) {
11778
12381
  return path.startsWith("/v1/public/") || path.startsWith("/v1/proxy/") ? "omit" : "include";
11779
12382
  }
11780
12383
  var TOKEN_STORAGE_KEY = "cb_auth_tokens";
12384
+ var NO_COOKIE_SESSION_COOLDOWN_MS = 6e4;
11781
12385
  function gatewayCodeFromStatus(status) {
11782
12386
  switch (status) {
11783
12387
  case 502:
@@ -11922,6 +12526,7 @@ var HttpClient = class {
11922
12526
  this.config.refreshToken = refreshToken;
11923
12527
  this.persistTokens();
11924
12528
  this.markSessionHint();
12529
+ this.clearNoCookieSessionMark();
11925
12530
  }
11926
12531
  clearTokens() {
11927
12532
  this.config.accessToken = void 0;
@@ -11929,6 +12534,20 @@ var HttpClient = class {
11929
12534
  this.removePersistedTokens();
11930
12535
  this.clearSessionHint();
11931
12536
  }
12537
+ /**
12538
+ * Access Token 만 교체한다 (refresh token 은 건드리지 않는다).
12539
+ *
12540
+ * 조직 컨텍스트 발급(`POST /v1/public/organizations/:orgID/context`)처럼 **새 액세스
12541
+ * 토큰만 돌려주는** 엔드포인트를 위한 것이다. 그 엔드포인트는 refresh token 을 회전시키지
12542
+ * 않으므로 `setTokens()` 로 빈 refresh token 을 덮어쓰면 세션 복구가 깨진다.
12543
+ *
12544
+ * 세션이 새로 생기는 것이 아니므로 세션 힌트/쿠키 없음 마커는 건드리지 않는다 — 그 두
12545
+ * 마커는 로그인/재발급이 소유한다.
12546
+ */
12547
+ setAccessToken(accessToken) {
12548
+ this.config.accessToken = accessToken;
12549
+ this.persistTokens();
12550
+ }
11932
12551
  // ===== 세션 힌트 =====
11933
12552
  //
11934
12553
  // "이 origin 에서 이 앱으로 로그인한 적이 있다"는 boolean 마커 (토큰 아님 — 민감정보 없음).
@@ -11965,6 +12584,45 @@ var HttpClient = class {
11965
12584
  return true;
11966
12585
  }
11967
12586
  }
12587
+ // ===== cookie 세션 없음 마커 (platform-issue 01a0237f) =====
12588
+ //
12589
+ // `refreshLockedUntil` 백오프는 인스턴스 안에서만 산다. 앱이 클라이언트를 새로 만들 때마다
12590
+ // 초기화되므로, 쿠키가 없는 브라우저에서는 인증 호출마다 re-issue 401 이 새로 나간다.
12591
+ // 이 마커는 같은 사실을 localStorage 에 짧게 남겨 인스턴스 경계를 넘어 공유한다.
12592
+ buildNoCookieSessionKey() {
12593
+ return `${this.storageKey}:no_cookie_session`;
12594
+ }
12595
+ markNoCookieSession() {
12596
+ if (typeof window === "undefined") return;
12597
+ try {
12598
+ localStorage.setItem(this.buildNoCookieSessionKey(), String(Date.now()));
12599
+ } catch {
12600
+ }
12601
+ }
12602
+ clearNoCookieSessionMark() {
12603
+ if (typeof window === "undefined") return;
12604
+ try {
12605
+ localStorage.removeItem(this.buildNoCookieSessionKey());
12606
+ } catch {
12607
+ }
12608
+ }
12609
+ /** 쿠키 전용 refresh 를 지금 건너뛰어야 하는지 (최근에 401/403 을 받았는지). */
12610
+ isNoCookieSessionMarkFresh() {
12611
+ if (typeof window === "undefined") return false;
12612
+ try {
12613
+ const raw = localStorage.getItem(this.buildNoCookieSessionKey());
12614
+ if (!raw) return false;
12615
+ const at = Number.parseInt(raw, 10);
12616
+ if (!Number.isFinite(at)) return false;
12617
+ if (Date.now() - at >= NO_COOKIE_SESSION_COOLDOWN_MS) {
12618
+ this.clearNoCookieSessionMark();
12619
+ return false;
12620
+ }
12621
+ return true;
12622
+ } catch {
12623
+ return false;
12624
+ }
12625
+ }
11968
12626
  /**
11969
12627
  * OAuth redirect callback 직후 호출되어 HttpOnly cookie 를 부트스트랩한다.
11970
12628
  *
@@ -12150,6 +12808,11 @@ var HttpClient = class {
12150
12808
  this.config.onAuthError?.(error);
12151
12809
  throw error;
12152
12810
  }
12811
+ if (!this.config.refreshToken && this.isNoCookieSessionMarkFresh()) {
12812
+ throw new AuthError(
12813
+ "No cookie session (recently rejected). Skipping token refresh."
12814
+ );
12815
+ }
12153
12816
  this.isRefreshing = true;
12154
12817
  if (!this.config.refreshToken && typeof window === "undefined") {
12155
12818
  this.isRefreshing = false;
@@ -12241,6 +12904,7 @@ var HttpClient = class {
12241
12904
  });
12242
12905
  this.refreshFailureCount = 0;
12243
12906
  this.refreshLockedUntil = 0;
12907
+ this.clearNoCookieSessionMark();
12244
12908
  return data.access_token;
12245
12909
  } catch (e) {
12246
12910
  const baseMsg = e instanceof Error ? e.message : "Token refresh failed";
@@ -12250,6 +12914,9 @@ var HttpClient = class {
12250
12914
  3e4
12251
12915
  );
12252
12916
  this.refreshLockedUntil = Date.now() + backoffMs;
12917
+ if (failureKind === "permanent" && !this.config.refreshToken) {
12918
+ this.markNoCookieSession();
12919
+ }
12253
12920
  if (failureKind === "permanent") {
12254
12921
  if (!silent) {
12255
12922
  this.clearTokens();
@@ -12350,6 +13017,9 @@ var HttpClient = class {
12350
13017
  } else if (!config?.skipAuth && this.config.publicKey && this.config.secretKey && !headers.has("Authorization")) {
12351
13018
  headers.set("Authorization", `Bearer ${this.config.secretKey}`);
12352
13019
  }
13020
+ if (!config?.skipAuth && this.config.secretKey) {
13021
+ headers.set("X-User-Secret-Key", this.config.secretKey);
13022
+ }
12353
13023
  if (config?.headers) {
12354
13024
  Object.entries(config.headers).forEach(([key, value]) => {
12355
13025
  headers.set(key, value);
@@ -13440,6 +14110,7 @@ var ConnectBase = class {
13440
14110
  this.oauth = new OAuthAPI(this.http);
13441
14111
  this.payment = new PaymentAPI(this.http);
13442
14112
  this.subscription = new SubscriptionAPI(this.http);
14113
+ this.organizations = new OrganizationsAPI(this.http);
13443
14114
  this.push = new PushAPI(this.http);
13444
14115
  this.roles = new RolesAPI(this.http);
13445
14116
  this.appMembers = new AppMembersAPI(this.http);
@@ -13552,6 +14223,7 @@ var index_default = ConnectBase;
13552
14223
  GameRoom,
13553
14224
  GameRoomTransport,
13554
14225
  NativeAPI,
14226
+ OrganizationsAPI,
13555
14227
  RolesAPI,
13556
14228
  SessionManager,
13557
14229
  VideoProcessingError,