connectbase-client 5.9.0 → 5.11.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
@@ -1467,8 +1467,9 @@ var AppMembersAPI = class {
1467
1467
  }
1468
1468
  ensureServerAuth(method) {
1469
1469
  if (this.http.hasPublicKey() && !this.http.hasJWT()) {
1470
+ const scope = method === "remove" ? "app_member:write" : "app_member:read";
1470
1471
  throw new Error(
1471
- `cb.appMembers.${method}() \uB294 \uCF58\uC194 JWT \uB610\uB294 service_role(ctx.cbAdmin, management_scopes: ["app_member:read"]) \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 \uC790\uAE30 \uC815\uBCF4\uB294 cb.auth.getMe() \uB97C \uC4F0\uC138\uC694.`
1472
+ `cb.appMembers.${method}() \uB294 \uCF58\uC194 JWT \uB610\uB294 service_role(ctx.cbAdmin, management_scopes: ["${scope}"]) \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 \uC790\uAE30 \uC815\uBCF4\uB294 cb.auth.getMe() \uB97C \uC4F0\uACE0, \uBCF8\uC778 \uD0C8\uD1F4\uB294 cb.auth.deleteMyAccount() \uB97C \uC4F0\uC138\uC694.`
1472
1473
  );
1473
1474
  }
1474
1475
  }
@@ -1503,6 +1504,36 @@ var AppMembersAPI = class {
1503
1504
  `/v1/apps/${appId}/app-members/${memberId}`
1504
1505
  );
1505
1506
  }
1507
+ /**
1508
+ * 앱 멤버를 삭제(탈퇴 처리)한다. **되돌릴 수 없다.** (management_scope: `app_member:write`)
1509
+ *
1510
+ * 멤버 행과 함께 로그인 수단(identities)이 지워지므로, 같은 소셜 계정으로 다시 로그인하면
1511
+ * 기존 member_id 로 되살아나지 않고 새 member_id 를 받는다. 결제·구독·푸시 디바이스·방문
1512
+ * 기록 등 멤버에 매달린 행도 함께 사라진다.
1513
+ *
1514
+ * 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않는다 — 같은 함수 안에서
1515
+ * `ctx.cbAdmin.database` 로 먼저 정리하고 마지막에 이 메서드를 부르는 순서를 권한다.
1516
+ *
1517
+ * `app_member:read` 로는 열리지 않는다 — 이메일 조회용으로 준 스코프가 회원을 지울 수 있게
1518
+ * 되면 안 되기 때문이다. 정지/수정/생성은 여전히 콘솔 전용이다.
1519
+ *
1520
+ * @param appId 앱 ID
1521
+ * @param memberId 삭제할 멤버 ID
1522
+ *
1523
+ * @example
1524
+ * ```typescript
1525
+ * // 함수(service_role, management_scopes: ["app_member:read", "app_member:write"]) 안에서
1526
+ * // 개인정보 삭제 요청 처리: 앱 데이터 정리 → 멤버 삭제
1527
+ * await ctx.cbAdmin.database.delete('posts', { author_id: memberId })
1528
+ * await ctx.cbAdmin.appMembers.remove(ctx.appId, memberId)
1529
+ * ```
1530
+ */
1531
+ async remove(appId, memberId) {
1532
+ this.ensureServerAuth("remove");
1533
+ return this.http.delete(
1534
+ `/v1/apps/${appId}/app-members/${memberId}`
1535
+ );
1536
+ }
1506
1537
  };
1507
1538
 
1508
1539
  // src/core/validate.ts
@@ -1755,6 +1786,73 @@ var AuthAPI = class {
1755
1786
  fields
1756
1787
  );
1757
1788
  }
1789
+ /**
1790
+ * Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
1791
+ *
1792
+ * `adminUpdateMember` 와 같은 권한 컨텍스트입니다 — `cb_pk_` 로 앱을 식별하고 함께 보낸
1793
+ * `cb_sk_` 의 사용자가 그 앱에 `app_member:write` 권한을 가져야 합니다. 브라우저 번들에
1794
+ * 시크릿 키를 넣지 마세요. 서버(함수·백엔드)에서만 호출합니다.
1795
+ *
1796
+ * 멤버 행과 함께 **로그인 수단(identities)** 이 지워집니다. 따라서 같은 소셜 계정으로 다시
1797
+ * 로그인하면 기존 member_id 로 되살아나지 않고 **새 member_id** 를 받습니다. 결제·구독·
1798
+ * 푸시 디바이스·방문 기록 등 멤버에 매달린 행도 함께 사라집니다.
1799
+ *
1800
+ * 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않습니다 — 그건 `service_role`
1801
+ * 로 앱이 직접 정리하세요.
1802
+ *
1803
+ * 이미 발급된 그 멤버의 access token 은 만료(최대 1시간)까지 서명이 유효합니다. 즉시
1804
+ * 차단이 필요하면 삭제 전에 정지(콘솔 `멤버 정지`)를 함께 쓰세요.
1805
+ *
1806
+ * @example
1807
+ * ```typescript
1808
+ * const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
1809
+ * await cb.auth.adminDeleteMember('member-uuid')
1810
+ * ```
1811
+ */
1812
+ async adminDeleteMember(memberID) {
1813
+ if (!this.http.getSecretKey()) {
1814
+ throw new Error(
1815
+ "adminDeleteMember requires a secretKey. Initialize ConnectBase with both publicKey and secretKey."
1816
+ );
1817
+ }
1818
+ return this.http.delete(
1819
+ `/v1/public/app-members/${encodeURIComponent(memberID)}`
1820
+ );
1821
+ }
1822
+ /**
1823
+ * 로그인한 본인이 스스로 탈퇴합니다. **되돌릴 수 없습니다.**
1824
+ *
1825
+ * App Store 심사 가이드라인 5.1.1(v)는 "앱 안에서 가입이 되면 앱 안에서 탈퇴도 가능해야
1826
+ * 한다"를 요구합니다 — 고객센터 안내로는 통과되지 않습니다. 이 메서드가 그 요구를 만족하는
1827
+ * 경로입니다.
1828
+ *
1829
+ * 확인 UI(“정말 탈퇴하시겠습니까?”)는 앱이 책임집니다. 서버는 확인 없이 즉시 삭제합니다.
1830
+ *
1831
+ * 성공하면 로컬 토큰이 정리되고 refresh 쿠키도 서버가 만료시킵니다 — `signOut()` 을 따로
1832
+ * 부를 필요가 없습니다.
1833
+ *
1834
+ * `adminDeleteMember` 와 동일하게 로그인 수단(identities)까지 지우므로, 같은 소셜 계정으로
1835
+ * 재가입하면 새 member_id 를 받습니다.
1836
+ *
1837
+ * @example
1838
+ * ```typescript
1839
+ * // 설정 > 회원 탈퇴 버튼
1840
+ * if (confirm('탈퇴하면 되돌릴 수 없습니다. 계속할까요?')) {
1841
+ * await cb.auth.deleteMyAccount()
1842
+ * location.href = '/'
1843
+ * }
1844
+ * ```
1845
+ */
1846
+ async deleteMyAccount() {
1847
+ try {
1848
+ return await this.http.delete(
1849
+ "/v1/public/app-members/me"
1850
+ );
1851
+ } finally {
1852
+ this.http.clearTokens();
1853
+ this.notifyVisitorTracker(null);
1854
+ }
1855
+ }
1758
1856
  /**
1759
1857
  * 로그아웃
1760
1858
  */
@@ -11294,6 +11392,9 @@ var COOKIE_BEARING_PUBLIC_PATHS = /* @__PURE__ */ new Set([
11294
11392
  "/v1/public/app-members/signin",
11295
11393
  "/v1/public/app-members/signout"
11296
11394
  ]);
11395
+ function isAbortError(err) {
11396
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
11397
+ }
11297
11398
  function fetchCredentialsForPath(url) {
11298
11399
  const path = url.split("?")[0];
11299
11400
  if (COOKIE_BEARING_PUBLIC_PATHS.has(path)) {
@@ -11365,6 +11466,18 @@ var HttpClient = class {
11365
11466
  // OAuth 콜백 부트스트랩처럼 세션 힌트 유무와 무관하게 첫 인증 호출이 반드시 기다려야
11366
11467
  // 하는 boot promise 인지 여부 (setBootRestorePromise 의 alwaysAwait 옵션).
11367
11468
  this.bootRestoreAlwaysAwait = false;
11469
+ /**
11470
+ * 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
11471
+ * 을 쓴다. {@link resolveOrigin} 참고.
11472
+ */
11473
+ this.directBaseUrl = null;
11474
+ /**
11475
+ * direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
11476
+ * 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
11477
+ * 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
11478
+ * 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
11479
+ */
11480
+ this.directDisabled = false;
11368
11481
  this.config = { ...config };
11369
11482
  this.storageKey = this.buildStorageKey();
11370
11483
  this.warnIfUnsafePersistence();
@@ -11955,18 +12068,73 @@ var HttpClient = class {
11955
12068
  });
11956
12069
  }
11957
12070
  }
12071
+ /**
12072
+ * 이 요청을 보낼 원본을 고른다.
12073
+ *
12074
+ * `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
12075
+ * `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
12076
+ * 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
12077
+ * 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
12078
+ * `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
12079
+ * 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
12080
+ */
12081
+ resolveOrigin(url) {
12082
+ if (this.directBaseUrl && !this.directDisabled && fetchCredentialsForPath(url) === "omit") {
12083
+ return this.directBaseUrl;
12084
+ }
12085
+ return this.config.baseUrl;
12086
+ }
12087
+ /**
12088
+ * 응답에서 저지연 원본 광고를 학습한다.
12089
+ *
12090
+ * 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
12091
+ * 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
12092
+ * 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
12093
+ * 즉시 회수할 수도 있다.
12094
+ *
12095
+ * 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
12096
+ * 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
12097
+ * 일은 없어야 한다.
12098
+ */
12099
+ learnDirectOrigin(response) {
12100
+ if (this.directDisabled || this.directBaseUrl) return;
12101
+ const advertised = response.headers?.get("X-CB-Direct-Base");
12102
+ if (!advertised) return;
12103
+ try {
12104
+ const next = new URL(advertised);
12105
+ const base = new URL(this.config.baseUrl);
12106
+ if (next.protocol !== "https:") return;
12107
+ const tail = (h) => h.split(".").slice(-2).join(".");
12108
+ if (tail(next.hostname) !== tail(base.hostname)) return;
12109
+ this.directBaseUrl = next.origin;
12110
+ } catch {
12111
+ }
12112
+ }
11958
12113
  async tryFetchOnce(url, init, config) {
11959
12114
  const { signal, cleanup } = createTimeoutController({
11960
12115
  timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
11961
12116
  signal: config?.signal
11962
12117
  });
12118
+ const origin = this.resolveOrigin(url);
11963
12119
  try {
11964
- const response = await fetch(`${this.config.baseUrl}${url}`, {
12120
+ const response = await fetch(`${origin}${url}`, {
11965
12121
  ...init,
11966
12122
  credentials: fetchCredentialsForPath(url),
11967
12123
  signal
11968
12124
  });
12125
+ this.learnDirectOrigin(response);
11969
12126
  return { response, ok: response.ok, status: response.status };
12127
+ } catch (err) {
12128
+ if (origin !== this.config.baseUrl && !isAbortError(err)) {
12129
+ this.directDisabled = true;
12130
+ const response = await fetch(`${this.config.baseUrl}${url}`, {
12131
+ ...init,
12132
+ credentials: fetchCredentialsForPath(url),
12133
+ signal
12134
+ });
12135
+ return { response, ok: response.ok, status: response.status };
12136
+ }
12137
+ throw err;
11970
12138
  } finally {
11971
12139
  cleanup();
11972
12140
  }
@@ -12033,11 +12201,21 @@ var HttpClient = class {
12033
12201
  const extra = new Headers(init.headers);
12034
12202
  extra.forEach((value, key) => mergedHeaders.set(key, value));
12035
12203
  }
12036
- return fetch(`${this.config.baseUrl}${url}`, {
12037
- ...init,
12038
- credentials: fetchCredentialsForPath(url),
12039
- headers: mergedHeaders
12040
- });
12204
+ const origin = this.resolveOrigin(url);
12205
+ try {
12206
+ const response = await fetch(`${origin}${url}`, {
12207
+ ...init,
12208
+ credentials: fetchCredentialsForPath(url),
12209
+ headers: mergedHeaders
12210
+ });
12211
+ this.learnDirectOrigin(response);
12212
+ return response;
12213
+ } catch (err) {
12214
+ if (origin !== this.config.baseUrl && !isAbortError(err)) {
12215
+ this.directDisabled = true;
12216
+ }
12217
+ throw err;
12218
+ }
12041
12219
  }
12042
12220
  };
12043
12221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",