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/CHANGELOG.md +99 -0
- package/dist/connect-base.umd.js +4 -4
- package/dist/index.d.mts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +185 -7
- package/dist/index.mjs +185 -7
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -264,6 +264,18 @@ declare class HttpClient {
|
|
|
264
264
|
*/
|
|
265
265
|
private bootRestorePromise;
|
|
266
266
|
private bootRestoreAlwaysAwait;
|
|
267
|
+
/**
|
|
268
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
269
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
270
|
+
*/
|
|
271
|
+
private directBaseUrl;
|
|
272
|
+
/**
|
|
273
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
274
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
275
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
276
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
277
|
+
*/
|
|
278
|
+
private directDisabled;
|
|
267
279
|
/**
|
|
268
280
|
* 클라이언트 생성 시 명시된 요청 타임아웃(ms). 미지정이면 `undefined`.
|
|
269
281
|
*
|
|
@@ -406,6 +418,30 @@ declare class HttpClient {
|
|
|
406
418
|
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
407
419
|
*/
|
|
408
420
|
private doFetch;
|
|
421
|
+
/**
|
|
422
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
423
|
+
*
|
|
424
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
425
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
426
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
427
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
428
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
429
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
430
|
+
*/
|
|
431
|
+
private resolveOrigin;
|
|
432
|
+
/**
|
|
433
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
434
|
+
*
|
|
435
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
436
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
437
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
438
|
+
* 즉시 회수할 수도 있다.
|
|
439
|
+
*
|
|
440
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
441
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
442
|
+
* 일은 없어야 한다.
|
|
443
|
+
*/
|
|
444
|
+
private learnDirectOrigin;
|
|
409
445
|
private tryFetchOnce;
|
|
410
446
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
411
447
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -1458,6 +1494,33 @@ declare class AppMembersAPI {
|
|
|
1458
1494
|
* @param memberId 멤버 ID
|
|
1459
1495
|
*/
|
|
1460
1496
|
get(appId: string, memberId: string): Promise<AppMemberDetail>;
|
|
1497
|
+
/**
|
|
1498
|
+
* 앱 멤버를 삭제(탈퇴 처리)한다. **되돌릴 수 없다.** (management_scope: `app_member:write`)
|
|
1499
|
+
*
|
|
1500
|
+
* 멤버 행과 함께 로그인 수단(identities)이 지워지므로, 같은 소셜 계정으로 다시 로그인하면
|
|
1501
|
+
* 기존 member_id 로 되살아나지 않고 새 member_id 를 받는다. 결제·구독·푸시 디바이스·방문
|
|
1502
|
+
* 기록 등 멤버에 매달린 행도 함께 사라진다.
|
|
1503
|
+
*
|
|
1504
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않는다 — 같은 함수 안에서
|
|
1505
|
+
* `ctx.cbAdmin.database` 로 먼저 정리하고 마지막에 이 메서드를 부르는 순서를 권한다.
|
|
1506
|
+
*
|
|
1507
|
+
* `app_member:read` 로는 열리지 않는다 — 이메일 조회용으로 준 스코프가 회원을 지울 수 있게
|
|
1508
|
+
* 되면 안 되기 때문이다. 정지/수정/생성은 여전히 콘솔 전용이다.
|
|
1509
|
+
*
|
|
1510
|
+
* @param appId 앱 ID
|
|
1511
|
+
* @param memberId 삭제할 멤버 ID
|
|
1512
|
+
*
|
|
1513
|
+
* @example
|
|
1514
|
+
* ```typescript
|
|
1515
|
+
* // 함수(service_role, management_scopes: ["app_member:read", "app_member:write"]) 안에서
|
|
1516
|
+
* // 개인정보 삭제 요청 처리: 앱 데이터 정리 → 멤버 삭제
|
|
1517
|
+
* await ctx.cbAdmin.database.delete('posts', { author_id: memberId })
|
|
1518
|
+
* await ctx.cbAdmin.appMembers.remove(ctx.appId, memberId)
|
|
1519
|
+
* ```
|
|
1520
|
+
*/
|
|
1521
|
+
remove(appId: string, memberId: string): Promise<{
|
|
1522
|
+
message: string;
|
|
1523
|
+
}>;
|
|
1461
1524
|
}
|
|
1462
1525
|
|
|
1463
1526
|
/** 앱 멤버 회원가입 요청 */
|
|
@@ -1647,6 +1710,61 @@ declare class AuthAPI {
|
|
|
1647
1710
|
custom_data: Record<string, unknown>;
|
|
1648
1711
|
role: string;
|
|
1649
1712
|
}>;
|
|
1713
|
+
/**
|
|
1714
|
+
* Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
|
|
1715
|
+
*
|
|
1716
|
+
* `adminUpdateMember` 와 같은 권한 컨텍스트입니다 — `cb_pk_` 로 앱을 식별하고 함께 보낸
|
|
1717
|
+
* `cb_sk_` 의 사용자가 그 앱에 `app_member:write` 권한을 가져야 합니다. 브라우저 번들에
|
|
1718
|
+
* 시크릿 키를 넣지 마세요. 서버(함수·백엔드)에서만 호출합니다.
|
|
1719
|
+
*
|
|
1720
|
+
* 멤버 행과 함께 **로그인 수단(identities)** 이 지워집니다. 따라서 같은 소셜 계정으로 다시
|
|
1721
|
+
* 로그인하면 기존 member_id 로 되살아나지 않고 **새 member_id** 를 받습니다. 결제·구독·
|
|
1722
|
+
* 푸시 디바이스·방문 기록 등 멤버에 매달린 행도 함께 사라집니다.
|
|
1723
|
+
*
|
|
1724
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않습니다 — 그건 `service_role`
|
|
1725
|
+
* 로 앱이 직접 정리하세요.
|
|
1726
|
+
*
|
|
1727
|
+
* 이미 발급된 그 멤버의 access token 은 만료(최대 1시간)까지 서명이 유효합니다. 즉시
|
|
1728
|
+
* 차단이 필요하면 삭제 전에 정지(콘솔 `멤버 정지`)를 함께 쓰세요.
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* ```typescript
|
|
1732
|
+
* const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
|
|
1733
|
+
* await cb.auth.adminDeleteMember('member-uuid')
|
|
1734
|
+
* ```
|
|
1735
|
+
*/
|
|
1736
|
+
adminDeleteMember(memberID: string): Promise<{
|
|
1737
|
+
message: string;
|
|
1738
|
+
member_id: string;
|
|
1739
|
+
}>;
|
|
1740
|
+
/**
|
|
1741
|
+
* 로그인한 본인이 스스로 탈퇴합니다. **되돌릴 수 없습니다.**
|
|
1742
|
+
*
|
|
1743
|
+
* App Store 심사 가이드라인 5.1.1(v)는 "앱 안에서 가입이 되면 앱 안에서 탈퇴도 가능해야
|
|
1744
|
+
* 한다"를 요구합니다 — 고객센터 안내로는 통과되지 않습니다. 이 메서드가 그 요구를 만족하는
|
|
1745
|
+
* 경로입니다.
|
|
1746
|
+
*
|
|
1747
|
+
* 확인 UI(“정말 탈퇴하시겠습니까?”)는 앱이 책임집니다. 서버는 확인 없이 즉시 삭제합니다.
|
|
1748
|
+
*
|
|
1749
|
+
* 성공하면 로컬 토큰이 정리되고 refresh 쿠키도 서버가 만료시킵니다 — `signOut()` 을 따로
|
|
1750
|
+
* 부를 필요가 없습니다.
|
|
1751
|
+
*
|
|
1752
|
+
* `adminDeleteMember` 와 동일하게 로그인 수단(identities)까지 지우므로, 같은 소셜 계정으로
|
|
1753
|
+
* 재가입하면 새 member_id 를 받습니다.
|
|
1754
|
+
*
|
|
1755
|
+
* @example
|
|
1756
|
+
* ```typescript
|
|
1757
|
+
* // 설정 > 회원 탈퇴 버튼
|
|
1758
|
+
* if (confirm('탈퇴하면 되돌릴 수 없습니다. 계속할까요?')) {
|
|
1759
|
+
* await cb.auth.deleteMyAccount()
|
|
1760
|
+
* location.href = '/'
|
|
1761
|
+
* }
|
|
1762
|
+
* ```
|
|
1763
|
+
*/
|
|
1764
|
+
deleteMyAccount(): Promise<{
|
|
1765
|
+
message: string;
|
|
1766
|
+
deleted: boolean;
|
|
1767
|
+
}>;
|
|
1650
1768
|
/**
|
|
1651
1769
|
* 로그아웃
|
|
1652
1770
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -264,6 +264,18 @@ declare class HttpClient {
|
|
|
264
264
|
*/
|
|
265
265
|
private bootRestorePromise;
|
|
266
266
|
private bootRestoreAlwaysAwait;
|
|
267
|
+
/**
|
|
268
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
269
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
270
|
+
*/
|
|
271
|
+
private directBaseUrl;
|
|
272
|
+
/**
|
|
273
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
274
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
275
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
276
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
277
|
+
*/
|
|
278
|
+
private directDisabled;
|
|
267
279
|
/**
|
|
268
280
|
* 클라이언트 생성 시 명시된 요청 타임아웃(ms). 미지정이면 `undefined`.
|
|
269
281
|
*
|
|
@@ -406,6 +418,30 @@ declare class HttpClient {
|
|
|
406
418
|
* (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단.
|
|
407
419
|
*/
|
|
408
420
|
private doFetch;
|
|
421
|
+
/**
|
|
422
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
423
|
+
*
|
|
424
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
425
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
426
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
427
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
428
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
429
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
430
|
+
*/
|
|
431
|
+
private resolveOrigin;
|
|
432
|
+
/**
|
|
433
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
434
|
+
*
|
|
435
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
436
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
437
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
438
|
+
* 즉시 회수할 수도 있다.
|
|
439
|
+
*
|
|
440
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
441
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
442
|
+
* 일은 없어야 한다.
|
|
443
|
+
*/
|
|
444
|
+
private learnDirectOrigin;
|
|
409
445
|
private tryFetchOnce;
|
|
410
446
|
get<T>(url: string, config?: RequestConfig): Promise<T>;
|
|
411
447
|
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
|
|
@@ -1458,6 +1494,33 @@ declare class AppMembersAPI {
|
|
|
1458
1494
|
* @param memberId 멤버 ID
|
|
1459
1495
|
*/
|
|
1460
1496
|
get(appId: string, memberId: string): Promise<AppMemberDetail>;
|
|
1497
|
+
/**
|
|
1498
|
+
* 앱 멤버를 삭제(탈퇴 처리)한다. **되돌릴 수 없다.** (management_scope: `app_member:write`)
|
|
1499
|
+
*
|
|
1500
|
+
* 멤버 행과 함께 로그인 수단(identities)이 지워지므로, 같은 소셜 계정으로 다시 로그인하면
|
|
1501
|
+
* 기존 member_id 로 되살아나지 않고 새 member_id 를 받는다. 결제·구독·푸시 디바이스·방문
|
|
1502
|
+
* 기록 등 멤버에 매달린 행도 함께 사라진다.
|
|
1503
|
+
*
|
|
1504
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않는다 — 같은 함수 안에서
|
|
1505
|
+
* `ctx.cbAdmin.database` 로 먼저 정리하고 마지막에 이 메서드를 부르는 순서를 권한다.
|
|
1506
|
+
*
|
|
1507
|
+
* `app_member:read` 로는 열리지 않는다 — 이메일 조회용으로 준 스코프가 회원을 지울 수 있게
|
|
1508
|
+
* 되면 안 되기 때문이다. 정지/수정/생성은 여전히 콘솔 전용이다.
|
|
1509
|
+
*
|
|
1510
|
+
* @param appId 앱 ID
|
|
1511
|
+
* @param memberId 삭제할 멤버 ID
|
|
1512
|
+
*
|
|
1513
|
+
* @example
|
|
1514
|
+
* ```typescript
|
|
1515
|
+
* // 함수(service_role, management_scopes: ["app_member:read", "app_member:write"]) 안에서
|
|
1516
|
+
* // 개인정보 삭제 요청 처리: 앱 데이터 정리 → 멤버 삭제
|
|
1517
|
+
* await ctx.cbAdmin.database.delete('posts', { author_id: memberId })
|
|
1518
|
+
* await ctx.cbAdmin.appMembers.remove(ctx.appId, memberId)
|
|
1519
|
+
* ```
|
|
1520
|
+
*/
|
|
1521
|
+
remove(appId: string, memberId: string): Promise<{
|
|
1522
|
+
message: string;
|
|
1523
|
+
}>;
|
|
1461
1524
|
}
|
|
1462
1525
|
|
|
1463
1526
|
/** 앱 멤버 회원가입 요청 */
|
|
@@ -1647,6 +1710,61 @@ declare class AuthAPI {
|
|
|
1647
1710
|
custom_data: Record<string, unknown>;
|
|
1648
1711
|
role: string;
|
|
1649
1712
|
}>;
|
|
1713
|
+
/**
|
|
1714
|
+
* Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
|
|
1715
|
+
*
|
|
1716
|
+
* `adminUpdateMember` 와 같은 권한 컨텍스트입니다 — `cb_pk_` 로 앱을 식별하고 함께 보낸
|
|
1717
|
+
* `cb_sk_` 의 사용자가 그 앱에 `app_member:write` 권한을 가져야 합니다. 브라우저 번들에
|
|
1718
|
+
* 시크릿 키를 넣지 마세요. 서버(함수·백엔드)에서만 호출합니다.
|
|
1719
|
+
*
|
|
1720
|
+
* 멤버 행과 함께 **로그인 수단(identities)** 이 지워집니다. 따라서 같은 소셜 계정으로 다시
|
|
1721
|
+
* 로그인하면 기존 member_id 로 되살아나지 않고 **새 member_id** 를 받습니다. 결제·구독·
|
|
1722
|
+
* 푸시 디바이스·방문 기록 등 멤버에 매달린 행도 함께 사라집니다.
|
|
1723
|
+
*
|
|
1724
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않습니다 — 그건 `service_role`
|
|
1725
|
+
* 로 앱이 직접 정리하세요.
|
|
1726
|
+
*
|
|
1727
|
+
* 이미 발급된 그 멤버의 access token 은 만료(최대 1시간)까지 서명이 유효합니다. 즉시
|
|
1728
|
+
* 차단이 필요하면 삭제 전에 정지(콘솔 `멤버 정지`)를 함께 쓰세요.
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* ```typescript
|
|
1732
|
+
* const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
|
|
1733
|
+
* await cb.auth.adminDeleteMember('member-uuid')
|
|
1734
|
+
* ```
|
|
1735
|
+
*/
|
|
1736
|
+
adminDeleteMember(memberID: string): Promise<{
|
|
1737
|
+
message: string;
|
|
1738
|
+
member_id: string;
|
|
1739
|
+
}>;
|
|
1740
|
+
/**
|
|
1741
|
+
* 로그인한 본인이 스스로 탈퇴합니다. **되돌릴 수 없습니다.**
|
|
1742
|
+
*
|
|
1743
|
+
* App Store 심사 가이드라인 5.1.1(v)는 "앱 안에서 가입이 되면 앱 안에서 탈퇴도 가능해야
|
|
1744
|
+
* 한다"를 요구합니다 — 고객센터 안내로는 통과되지 않습니다. 이 메서드가 그 요구를 만족하는
|
|
1745
|
+
* 경로입니다.
|
|
1746
|
+
*
|
|
1747
|
+
* 확인 UI(“정말 탈퇴하시겠습니까?”)는 앱이 책임집니다. 서버는 확인 없이 즉시 삭제합니다.
|
|
1748
|
+
*
|
|
1749
|
+
* 성공하면 로컬 토큰이 정리되고 refresh 쿠키도 서버가 만료시킵니다 — `signOut()` 을 따로
|
|
1750
|
+
* 부를 필요가 없습니다.
|
|
1751
|
+
*
|
|
1752
|
+
* `adminDeleteMember` 와 동일하게 로그인 수단(identities)까지 지우므로, 같은 소셜 계정으로
|
|
1753
|
+
* 재가입하면 새 member_id 를 받습니다.
|
|
1754
|
+
*
|
|
1755
|
+
* @example
|
|
1756
|
+
* ```typescript
|
|
1757
|
+
* // 설정 > 회원 탈퇴 버튼
|
|
1758
|
+
* if (confirm('탈퇴하면 되돌릴 수 없습니다. 계속할까요?')) {
|
|
1759
|
+
* await cb.auth.deleteMyAccount()
|
|
1760
|
+
* location.href = '/'
|
|
1761
|
+
* }
|
|
1762
|
+
* ```
|
|
1763
|
+
*/
|
|
1764
|
+
deleteMyAccount(): Promise<{
|
|
1765
|
+
message: string;
|
|
1766
|
+
deleted: boolean;
|
|
1767
|
+
}>;
|
|
1650
1768
|
/**
|
|
1651
1769
|
* 로그아웃
|
|
1652
1770
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1516,8 +1516,9 @@ var AppMembersAPI = class {
|
|
|
1516
1516
|
}
|
|
1517
1517
|
ensureServerAuth(method) {
|
|
1518
1518
|
if (this.http.hasPublicKey() && !this.http.hasJWT()) {
|
|
1519
|
+
const scope = method === "remove" ? "app_member:write" : "app_member:read";
|
|
1519
1520
|
throw new Error(
|
|
1520
|
-
`cb.appMembers.${method}() \uB294 \uCF58\uC194 JWT \uB610\uB294 service_role(ctx.cbAdmin, management_scopes: ["
|
|
1521
|
+
`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.`
|
|
1521
1522
|
);
|
|
1522
1523
|
}
|
|
1523
1524
|
}
|
|
@@ -1552,6 +1553,36 @@ var AppMembersAPI = class {
|
|
|
1552
1553
|
`/v1/apps/${appId}/app-members/${memberId}`
|
|
1553
1554
|
);
|
|
1554
1555
|
}
|
|
1556
|
+
/**
|
|
1557
|
+
* 앱 멤버를 삭제(탈퇴 처리)한다. **되돌릴 수 없다.** (management_scope: `app_member:write`)
|
|
1558
|
+
*
|
|
1559
|
+
* 멤버 행과 함께 로그인 수단(identities)이 지워지므로, 같은 소셜 계정으로 다시 로그인하면
|
|
1560
|
+
* 기존 member_id 로 되살아나지 않고 새 member_id 를 받는다. 결제·구독·푸시 디바이스·방문
|
|
1561
|
+
* 기록 등 멤버에 매달린 행도 함께 사라진다.
|
|
1562
|
+
*
|
|
1563
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않는다 — 같은 함수 안에서
|
|
1564
|
+
* `ctx.cbAdmin.database` 로 먼저 정리하고 마지막에 이 메서드를 부르는 순서를 권한다.
|
|
1565
|
+
*
|
|
1566
|
+
* `app_member:read` 로는 열리지 않는다 — 이메일 조회용으로 준 스코프가 회원을 지울 수 있게
|
|
1567
|
+
* 되면 안 되기 때문이다. 정지/수정/생성은 여전히 콘솔 전용이다.
|
|
1568
|
+
*
|
|
1569
|
+
* @param appId 앱 ID
|
|
1570
|
+
* @param memberId 삭제할 멤버 ID
|
|
1571
|
+
*
|
|
1572
|
+
* @example
|
|
1573
|
+
* ```typescript
|
|
1574
|
+
* // 함수(service_role, management_scopes: ["app_member:read", "app_member:write"]) 안에서
|
|
1575
|
+
* // 개인정보 삭제 요청 처리: 앱 데이터 정리 → 멤버 삭제
|
|
1576
|
+
* await ctx.cbAdmin.database.delete('posts', { author_id: memberId })
|
|
1577
|
+
* await ctx.cbAdmin.appMembers.remove(ctx.appId, memberId)
|
|
1578
|
+
* ```
|
|
1579
|
+
*/
|
|
1580
|
+
async remove(appId, memberId) {
|
|
1581
|
+
this.ensureServerAuth("remove");
|
|
1582
|
+
return this.http.delete(
|
|
1583
|
+
`/v1/apps/${appId}/app-members/${memberId}`
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1555
1586
|
};
|
|
1556
1587
|
|
|
1557
1588
|
// src/core/validate.ts
|
|
@@ -1804,6 +1835,73 @@ var AuthAPI = class {
|
|
|
1804
1835
|
fields
|
|
1805
1836
|
);
|
|
1806
1837
|
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Admin: 다른 멤버를 삭제(탈퇴 처리)합니다. **되돌릴 수 없습니다.**
|
|
1840
|
+
*
|
|
1841
|
+
* `adminUpdateMember` 와 같은 권한 컨텍스트입니다 — `cb_pk_` 로 앱을 식별하고 함께 보낸
|
|
1842
|
+
* `cb_sk_` 의 사용자가 그 앱에 `app_member:write` 권한을 가져야 합니다. 브라우저 번들에
|
|
1843
|
+
* 시크릿 키를 넣지 마세요. 서버(함수·백엔드)에서만 호출합니다.
|
|
1844
|
+
*
|
|
1845
|
+
* 멤버 행과 함께 **로그인 수단(identities)** 이 지워집니다. 따라서 같은 소셜 계정으로 다시
|
|
1846
|
+
* 로그인하면 기존 member_id 로 되살아나지 않고 **새 member_id** 를 받습니다. 결제·구독·
|
|
1847
|
+
* 푸시 디바이스·방문 기록 등 멤버에 매달린 행도 함께 사라집니다.
|
|
1848
|
+
*
|
|
1849
|
+
* 앱이 자체 테이블(`cb.database`)에 보관한 데이터는 지우지 않습니다 — 그건 `service_role`
|
|
1850
|
+
* 로 앱이 직접 정리하세요.
|
|
1851
|
+
*
|
|
1852
|
+
* 이미 발급된 그 멤버의 access token 은 만료(최대 1시간)까지 서명이 유효합니다. 즉시
|
|
1853
|
+
* 차단이 필요하면 삭제 전에 정지(콘솔 `멤버 정지`)를 함께 쓰세요.
|
|
1854
|
+
*
|
|
1855
|
+
* @example
|
|
1856
|
+
* ```typescript
|
|
1857
|
+
* const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' })
|
|
1858
|
+
* await cb.auth.adminDeleteMember('member-uuid')
|
|
1859
|
+
* ```
|
|
1860
|
+
*/
|
|
1861
|
+
async adminDeleteMember(memberID) {
|
|
1862
|
+
if (!this.http.getSecretKey()) {
|
|
1863
|
+
throw new Error(
|
|
1864
|
+
"adminDeleteMember requires a secretKey. Initialize ConnectBase with both publicKey and secretKey."
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
return this.http.delete(
|
|
1868
|
+
`/v1/public/app-members/${encodeURIComponent(memberID)}`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* 로그인한 본인이 스스로 탈퇴합니다. **되돌릴 수 없습니다.**
|
|
1873
|
+
*
|
|
1874
|
+
* App Store 심사 가이드라인 5.1.1(v)는 "앱 안에서 가입이 되면 앱 안에서 탈퇴도 가능해야
|
|
1875
|
+
* 한다"를 요구합니다 — 고객센터 안내로는 통과되지 않습니다. 이 메서드가 그 요구를 만족하는
|
|
1876
|
+
* 경로입니다.
|
|
1877
|
+
*
|
|
1878
|
+
* 확인 UI(“정말 탈퇴하시겠습니까?”)는 앱이 책임집니다. 서버는 확인 없이 즉시 삭제합니다.
|
|
1879
|
+
*
|
|
1880
|
+
* 성공하면 로컬 토큰이 정리되고 refresh 쿠키도 서버가 만료시킵니다 — `signOut()` 을 따로
|
|
1881
|
+
* 부를 필요가 없습니다.
|
|
1882
|
+
*
|
|
1883
|
+
* `adminDeleteMember` 와 동일하게 로그인 수단(identities)까지 지우므로, 같은 소셜 계정으로
|
|
1884
|
+
* 재가입하면 새 member_id 를 받습니다.
|
|
1885
|
+
*
|
|
1886
|
+
* @example
|
|
1887
|
+
* ```typescript
|
|
1888
|
+
* // 설정 > 회원 탈퇴 버튼
|
|
1889
|
+
* if (confirm('탈퇴하면 되돌릴 수 없습니다. 계속할까요?')) {
|
|
1890
|
+
* await cb.auth.deleteMyAccount()
|
|
1891
|
+
* location.href = '/'
|
|
1892
|
+
* }
|
|
1893
|
+
* ```
|
|
1894
|
+
*/
|
|
1895
|
+
async deleteMyAccount() {
|
|
1896
|
+
try {
|
|
1897
|
+
return await this.http.delete(
|
|
1898
|
+
"/v1/public/app-members/me"
|
|
1899
|
+
);
|
|
1900
|
+
} finally {
|
|
1901
|
+
this.http.clearTokens();
|
|
1902
|
+
this.notifyVisitorTracker(null);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1807
1905
|
/**
|
|
1808
1906
|
* 로그아웃
|
|
1809
1907
|
*/
|
|
@@ -11343,6 +11441,9 @@ var COOKIE_BEARING_PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
|
11343
11441
|
"/v1/public/app-members/signin",
|
|
11344
11442
|
"/v1/public/app-members/signout"
|
|
11345
11443
|
]);
|
|
11444
|
+
function isAbortError(err) {
|
|
11445
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
11446
|
+
}
|
|
11346
11447
|
function fetchCredentialsForPath(url) {
|
|
11347
11448
|
const path = url.split("?")[0];
|
|
11348
11449
|
if (COOKIE_BEARING_PUBLIC_PATHS.has(path)) {
|
|
@@ -11414,6 +11515,18 @@ var HttpClient = class {
|
|
|
11414
11515
|
// OAuth 콜백 부트스트랩처럼 세션 힌트 유무와 무관하게 첫 인증 호출이 반드시 기다려야
|
|
11415
11516
|
// 하는 boot promise 인지 여부 (setBootRestorePromise 의 alwaysAwait 옵션).
|
|
11416
11517
|
this.bootRestoreAlwaysAwait = false;
|
|
11518
|
+
/**
|
|
11519
|
+
* 서버가 `X-CB-Direct-Base` 로 알려준 저지연 원본. 학습 전에는 null 이라 항상 `baseUrl`
|
|
11520
|
+
* 을 쓴다. {@link resolveOrigin} 참고.
|
|
11521
|
+
*/
|
|
11522
|
+
this.directBaseUrl = null;
|
|
11523
|
+
/**
|
|
11524
|
+
* direct 원본을 이 세션에서 더는 쓰지 않는다는 표시. 한 번이라도 네트워크 레벨에서
|
|
11525
|
+
* 실패하면(CSP `connect-src` 차단, 사내 프록시, DNS 미전파, 호스트 회수 등) 즉시 세워
|
|
11526
|
+
* 이후 요청은 전부 `baseUrl` 로 간다. direct 는 최적화일 뿐 필수가 아니므로 재시도를
|
|
11527
|
+
* 반복하지 않고 한 번에 포기하는 쪽이 안전하다.
|
|
11528
|
+
*/
|
|
11529
|
+
this.directDisabled = false;
|
|
11417
11530
|
this.config = { ...config };
|
|
11418
11531
|
this.storageKey = this.buildStorageKey();
|
|
11419
11532
|
this.warnIfUnsafePersistence();
|
|
@@ -12004,18 +12117,73 @@ var HttpClient = class {
|
|
|
12004
12117
|
});
|
|
12005
12118
|
}
|
|
12006
12119
|
}
|
|
12120
|
+
/**
|
|
12121
|
+
* 이 요청을 보낼 원본을 고른다.
|
|
12122
|
+
*
|
|
12123
|
+
* `credentials:'omit'` 인 요청 — 즉 `X-Public-Key` 로만 인증하고 쿠키를 주고받지 않는
|
|
12124
|
+
* `/v1/public/*` 데이터 평면 — 만 direct 원본으로 보낸다. 이 조건이 곧 안전 조건이다:
|
|
12125
|
+
* 멤버 refresh 쿠키는 **host-only** 로 발급되므로(백엔드 auth_cookie.SetMemberRefreshCookie)
|
|
12126
|
+
* 로그인·가입·로그아웃·OAuth·탈퇴를 다른 호스트로 보내면 세션이 조용히 끊긴다.
|
|
12127
|
+
* `fetchCredentialsForPath` 가 이미 그 경계를 계산하고 있어, 나중에 쿠키 엔드포인트가
|
|
12128
|
+
* 추가돼도 자동으로 제외된다(서버도 같은 경로를 421 로 되돌려보낸다).
|
|
12129
|
+
*/
|
|
12130
|
+
resolveOrigin(url) {
|
|
12131
|
+
if (this.directBaseUrl && !this.directDisabled && fetchCredentialsForPath(url) === "omit") {
|
|
12132
|
+
return this.directBaseUrl;
|
|
12133
|
+
}
|
|
12134
|
+
return this.config.baseUrl;
|
|
12135
|
+
}
|
|
12136
|
+
/**
|
|
12137
|
+
* 응답에서 저지연 원본 광고를 학습한다.
|
|
12138
|
+
*
|
|
12139
|
+
* 왜 서버가 정하나: 클라이언트는 자기가 어느 나라에서 접속했는지 신뢰성 있게 모르고,
|
|
12140
|
+
* 두 호스트를 재보는 프로브는 요청을 한 번 더 쓴다. Cloudflare 엣지는 `CF-IPCountry` 로
|
|
12141
|
+
* 이미 알고 있으므로 서버가 판단해 알려주는 쪽이 정확하고 공짜다. 서버 env 하나로
|
|
12142
|
+
* 즉시 회수할 수도 있다.
|
|
12143
|
+
*
|
|
12144
|
+
* 값은 검증한다 — https 이고 `baseUrl` 과 같은 등록 도메인이어야 한다. 응답은 우리
|
|
12145
|
+
* 서버에서 TLS 로 오지만, 설정 실수 하나로 SDK 트래픽 전체가 엉뚱한 호스트로 향하는
|
|
12146
|
+
* 일은 없어야 한다.
|
|
12147
|
+
*/
|
|
12148
|
+
learnDirectOrigin(response) {
|
|
12149
|
+
if (this.directDisabled || this.directBaseUrl) return;
|
|
12150
|
+
const advertised = response.headers?.get("X-CB-Direct-Base");
|
|
12151
|
+
if (!advertised) return;
|
|
12152
|
+
try {
|
|
12153
|
+
const next = new URL(advertised);
|
|
12154
|
+
const base = new URL(this.config.baseUrl);
|
|
12155
|
+
if (next.protocol !== "https:") return;
|
|
12156
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
12157
|
+
if (tail(next.hostname) !== tail(base.hostname)) return;
|
|
12158
|
+
this.directBaseUrl = next.origin;
|
|
12159
|
+
} catch {
|
|
12160
|
+
}
|
|
12161
|
+
}
|
|
12007
12162
|
async tryFetchOnce(url, init, config) {
|
|
12008
12163
|
const { signal, cleanup } = createTimeoutController({
|
|
12009
12164
|
timeout: config?.timeout ?? this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
12010
12165
|
signal: config?.signal
|
|
12011
12166
|
});
|
|
12167
|
+
const origin = this.resolveOrigin(url);
|
|
12012
12168
|
try {
|
|
12013
|
-
const response = await fetch(`${
|
|
12169
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12014
12170
|
...init,
|
|
12015
12171
|
credentials: fetchCredentialsForPath(url),
|
|
12016
12172
|
signal
|
|
12017
12173
|
});
|
|
12174
|
+
this.learnDirectOrigin(response);
|
|
12018
12175
|
return { response, ok: response.ok, status: response.status };
|
|
12176
|
+
} catch (err) {
|
|
12177
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12178
|
+
this.directDisabled = true;
|
|
12179
|
+
const response = await fetch(`${this.config.baseUrl}${url}`, {
|
|
12180
|
+
...init,
|
|
12181
|
+
credentials: fetchCredentialsForPath(url),
|
|
12182
|
+
signal
|
|
12183
|
+
});
|
|
12184
|
+
return { response, ok: response.ok, status: response.status };
|
|
12185
|
+
}
|
|
12186
|
+
throw err;
|
|
12019
12187
|
} finally {
|
|
12020
12188
|
cleanup();
|
|
12021
12189
|
}
|
|
@@ -12082,11 +12250,21 @@ var HttpClient = class {
|
|
|
12082
12250
|
const extra = new Headers(init.headers);
|
|
12083
12251
|
extra.forEach((value, key) => mergedHeaders.set(key, value));
|
|
12084
12252
|
}
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12253
|
+
const origin = this.resolveOrigin(url);
|
|
12254
|
+
try {
|
|
12255
|
+
const response = await fetch(`${origin}${url}`, {
|
|
12256
|
+
...init,
|
|
12257
|
+
credentials: fetchCredentialsForPath(url),
|
|
12258
|
+
headers: mergedHeaders
|
|
12259
|
+
});
|
|
12260
|
+
this.learnDirectOrigin(response);
|
|
12261
|
+
return response;
|
|
12262
|
+
} catch (err) {
|
|
12263
|
+
if (origin !== this.config.baseUrl && !isAbortError(err)) {
|
|
12264
|
+
this.directDisabled = true;
|
|
12265
|
+
}
|
|
12266
|
+
throw err;
|
|
12267
|
+
}
|
|
12090
12268
|
}
|
|
12091
12269
|
};
|
|
12092
12270
|
|