interface ApiErrorDetail { code: string; message: string; details?: unknown; } declare class ApiError extends Error { statusCode: number; code: string | undefined; details: unknown | undefined; constructor(statusCode: number, message: string, code?: string, details?: unknown); } declare class AuthError extends Error { constructor(message: string); } /** * GameError 는 game-server 가 surface 한 `error` 메시지 (createRoom/joinRoom/sendAction * 응답 또는 broadcastScriptError) 를 client 측에서 일관된 형태로 다루기 위한 예외 클래스. * * 기존엔 모든 reject 가 `new Error(message)` 였어서 `code`/`phase`/`feature` 같은 * 분류 메타가 손실되어 SDK 사용자가 onError UI 분기를 만들 수 없었다 (NJB 사례, * platform-issue 019e21dd / 2026-05-13). * * 사용 패턴: * ```ts * try { * await room.createRoom({ scriptName: 'my-script' }) * } catch (e) { * if (e instanceof GameError && e.code === 'SCRIPT_NOT_FOUND') { * console.error('script missing — available:', e.available) * } * } * ``` * * onError 콜백도 동일하게 GameError 인스턴스로 surface 된다. */ declare class GameError extends Error { /** server 가 분류한 에러 코드. literal 비교용은 GameErrorCode 참조. */ code: string; /** Lua hook 단계 — 'onJoin'|'onLeave'|'onTick'|'onAction'. 비-lua 에러는 undefined. */ phase?: string; /** FEATURE_DISABLED 시 비활성 feature 이름. */ feature?: string; /** 영향받은 room id. */ roomId?: string; /** `:` 형식의 attached script id. */ scriptId?: string; /** broadcast 형태의 onAction 에러일 때, 액션을 일으킨 client (본인 아님 확인용). */ originClientId?: string; /** SCRIPT_NOT_FOUND 응답 시 요청했던 scriptName. */ requested?: string; /** SCRIPT_NOT_FOUND 응답 시 active 상태인 script 이름 목록. */ available?: string[]; constructor(init: { code?: string; message?: string; phase?: string; feature?: string; roomId?: string; scriptId?: string; originClientId?: string; requested?: string; available?: string[]; }); } interface AbortOptions { timeout?: number; signal?: AbortSignal; } /** * Recent API calls breadcrumb buffer — SDK 디버깅 / platform issue 발행 시 자동 첨부. * * **저장 정책 (PII 보호):** * - method, path (query string 전체 strip), status, duration_ms, timestamp 만 저장 * - body / response body / 인증 토큰 미저장 * - 쿼리스트링은 키 단위 redact 가 아니라 통째로 제거 (토큰/PII 누출 가능성 0) * - 따라서 `/v1/auth/*`, `/v1/oauth/token` 등 민감 endpoint 도 path 만 보존 */ interface RecentApiCall { method: string; path: string; status: number; duration_ms: number; timestamp: string; } type TokenPersistence = "localStorage" | "sessionStorage" | "none"; interface HttpClientConfig { baseUrl: string; /** * Public Key (cb_pk_* 형식). 콘솔 → 설정 → API 에서 발급. * 브라우저/클라이언트에서 사용 (Row Level Security 적용). */ publicKey?: string; /** * Secret Key (cb_sk_* 형식). 사용자 프로필에서 발급. * 서버 환경에서만 사용 (전체 권한, 절대 노출 금지). */ secretKey?: string; /** * 이 클라이언트가 속한 앱 ID (선택). 설정 시, refresh 응답이 **다른 앱**의 access token 을 * 돌려주면(예: 같은 브라우저의 다른 앱 세션 쿠키로 복구된 토큰) 해당 토큰을 채택하지 않고 * 세션 없음으로 처리한다. 백엔드 re-issue 가 app-scoped 가드로 1차 차단하지만, SDK 측에서도 * 방어적으로 한 번 더 막는다 (platform-issue 019e86d1). appId 미설정 시 이 가드는 비활성. */ appId?: string; accessToken?: string; refreshToken?: string; /** * 토큰 저장 방식. **기본값은 'none' (메모리 저장)**. * XSS 취약점이 하나라도 있을 경우 영구 저장은 즉시 전 세션 탈취로 이어지므로, * 영구 저장 옵션은 위험을 이해하고 명시적으로 선택한 경우에만 사용한다. * * - 'none' (권장·기본값): access token 만 메모리 저장. refresh token 은 서버가 발급한 * HttpOnly cookie 로만 보관되어 JS 가 접근할 수 없다 (XSS 시 탈취 불가). 새로고침 후에는 * `autoRestoreSession`(기본 true) 으로 cookie 만으로 자동 복구된다. * - 'sessionStorage': 탭 종료 시 삭제. JS 접근 가능 → XSS 로 탭 세션 탈취 가능 * - 'localStorage': 브라우저 종료 후에도 유지. JS 접근 가능 → XSS 로 영구 탈취 가능 */ persistence?: TokenPersistence; /** * 새로고침/탭 재개 시 HttpOnly cookie 로부터 access token 을 자동 복구할지 여부. * 기본값은 브라우저 환경에서 true. 비-브라우저(Node.js, RN) 에서는 무시된다. * * cookie 가 없는 경우(미로그인) 조용히 실패하며 콘솔 에러를 발생시키지 않는다. */ autoRestoreSession?: boolean; /** * 요청별 기본 타임아웃(ms). 개별 호출의 `timeout` 이 우선. * 기본값 30000ms. 0 또는 음수 지정 시 타임아웃 비활성화. */ requestTimeoutMs?: number; /** * 전역 에러 관찰자. 모든 ApiError/AuthError 발생 시 호출된다. * 운영 관측성(Sentry/Datadog/자체 엔드포인트)과 연결하기 위한 훅. */ onError?: (error: ApiError | AuthError) => void; onTokenRefresh?: (tokens: { accessToken: string; refreshToken: string; }) => void; onAuthError?: (error: AuthError) => void; onTokenExpired?: () => void; /** * `/v1/auth/re-issue` 의 일시적 실패(5xx, 네트워크 오류, abort) 발생 시 호출. * 이 경우 토큰은 폐기되지 않으며 `onTokenExpired` 도 호출되지 않는다 — 다음 호출에서 * backoff 만료 후 자동 재시도. 사용자에게 "연결이 잠시 불안정합니다" 같은 비파괴적 알림을 * 표시할 때 사용. `onAuthError` 도 함께 호출되므로 둘을 동시에 wiring 하면 중복 처리에 주의. */ onTransientRefreshFailure?: (error: AuthError) => void; } interface RequestConfig extends AbortOptions { skipAuth?: boolean; headers?: Record; } declare class HttpClient { private config; private isRefreshing; private refreshPromise; private storageKey; private refreshFailureCount; private refreshLockedUntil; /** * 최근 API 호출 breadcrumb (PII strip 후 저장). platform_issue 발행 시 자동 첨부 가능. * `client.support.getRecentApiCalls()` 로 외부 노출. */ private recentCalls; /** * 부팅 시 fire-and-forget 으로 시작한 cookie 기반 복구 promise. `prepareHeaders` 가 * 인증 호출을 보내기 직전에 이 promise 를 await 해, 페이지 진입 직후 첫 API 호출이 * 메모리 토큰 빈 상태로 401 받는 race 를 막는다 (platform-issue 019e638d, 2026-05-26). * 한 번 settle 되면 그 결과(메모리 적재 또는 미로그인)가 항상 반영되어 있다. */ private bootRestorePromise; private bootRestoreAlwaysAwait; constructor(config: HttpClientConfig); /** * 페이지 진입 시 fire-and-forget 으로 시작된 cookie 복구 promise 를 SDK 가 등록한다. * 같은 promise 가 `prepareHeaders` 에서 await 되어, 첫 인증 호출이 cookie 복구 * 완료 후 발화한다. * * options.alwaysAwait=true 면 세션 힌트가 없어도 첫 인증 호출이 이 promise 를 * 기다린다 — OAuth 리다이렉트 콜백처럼 "이 부트 promise 가 곧 세션을 만든다"는 것이 * 확실한 흐름용 (일반 cookie 복구는 힌트 기반 fast-path 적용). */ setBootRestorePromise(p: Promise, options?: { alwaysAwait?: boolean; }): void; /** 최근 호출 ring buffer 스냅샷 (시간순). */ getRecentCalls(): RecentApiCall[]; /** 최근 호출 buffer clear (테스트/프라이버시 처리). */ clearRecentCalls(): void; private warnIfUnsafePersistence; updateConfig(config: Partial): void; setTokens(accessToken: string, refreshToken: string): void; clearTokens(): void; private buildSessionHintKey; private markSessionHint; private clearSessionHint; private hasSessionHint; /** * OAuth redirect callback 직후 호출되어 HttpOnly cookie 를 부트스트랩한다. * * 배경: 콜백 흐름에서 서버가 redirect 응답에 Set-Cookie 를 함께 발급하지만, 일부 deployment / * 브라우저 정책 / 사용자 설정 (3rd-party cookie 차단 등) 환경에서 이 Set-Cookie 가 브라우저에 * 저장되지 않는다. `persistence='none'` 모드에서는 토큰이 메모리에만 있고 cookie 가 없는 상태로 * 페이지 새로고침이 발생하면 `/v1/auth/re-issue` 가 401 으로 떨어져 강제 로그아웃되는 회귀가 * 있다 (platform-issue 019e3960, 2026-05-18). * * 이 메서드는 메모리의 refresh token 으로 한 번 `/v1/auth/re-issue` 를 호출해 서버가 cookie 를 * 명시적으로 발급하도록 유도한다. 백엔드는 Bearer + `X-Public-Key` 조합을 SDK 호출 신호로 * 인식해 응답에 `cb_member_refresh_token` cookie 를 함께 내려준다. * * persistence='localStorage' / 'sessionStorage' 모드는 새로고침 후에도 메모리 복구가 가능하므로 * 호출하지 않는다 (rotation 만 일어나 비용만 증가). 비-브라우저 환경에서도 cookie 자체가 없으므로 * 호출하지 않는다. */ bootstrapRefreshCookie(): Promise; private get persistence(); private getStorage; /** * AuthAPI 등 내부 사용자가 "현재 persistence 설정된 스토리지"를 확인할 수 있도록 노출. * persistence='none' 이면 null. XSS 완화 기본값을 공유하기 위함. */ getPersistenceStorage(): Storage | null; private buildStorageKey; private persistTokens; private restoreTokens; private removePersistedTokens; /** * Public Key 가 설정되어 있는지 확인 */ hasPublicKey(): boolean; /** * Public Key 반환 */ getPublicKey(): string | undefined; /** * Secret Key 가 설정되어 있는지 확인 */ hasSecretKey(): boolean; /** * Secret Key 반환 */ getSecretKey(): string | undefined; /** * 현재 설정된 자격증명 반환. * * publicKey 가 있으면 그걸 반환한다 (X-Public-Key 헤더 = 앱 식별용 = cb_pk_*). * publicKey 가 없고 secretKey 만 있는 경우는 secretKey 로 폴백 — 단, 서버는 * cb_sk_ 가 X-Public-Key 에 실리면 거부하므로 의도된 401 이 나간다(앱 식별 불가). * * secretKey 의 admin 권한은 별도로 Authorization: Bearer 로 보낸다. 그 처리는 * prepareHeaders 안에서 publicKey + secretKey 가 함께 있을 때 수행한다. */ getCredential(): string | undefined; /** * Access Token 반환 */ getAccessToken(): string | undefined; /** * JWT(Access Token) 가 설정되어 있는지 확인 */ hasJWT(): boolean; /** * Base URL 반환 */ getBaseUrl(): string; /** * 이 클라이언트에 설정된 앱 ID 반환 (미설정 시 undefined). * * 콘솔(앱 소유자 JWT) 경로를 호출하기 전에 appId 유무를 확인하기 위한 public 접근자. * private `config` 필드를 access-modifier 관통 cast 없이 노출한다. */ getAppId(): string | undefined; private refreshAccessToken; /** * 새로고침/탭 재개 시 HttpOnly cookie 만으로 access token 을 복구한다. * * 동작: * - 메모리에 access token 이 이미 있으면 그대로 반환 (true). * - 없으면 `/v1/auth/re-issue` 를 cookie 만으로 호출. cookie 가 있으면 access token 회복. * - cookie 가 없거나(미로그인) 만료된 경우 조용히 false 반환 (콘솔 에러 없음). * * 비-브라우저 환경에서는 cookie 흐름이 없으므로 즉시 false 반환. */ tryRestoreSessionFromCookie(): Promise; private emitError; private isTokenExpired; private prepareHeaders; private handleResponse; /** * AbortController 를 관리하며 fetch 호출을 실행. 타임아웃/외부 signal 병합. * * 401 자동 복구: 인증 호출이 401 을 받으면 cookie 기반 복구를 *한 번* 시도하고 retry 한다. * 메모리 토큰이 만료/누락 + cookie 는 살아 있는 경우를 자동으로 회복시켜, * 페이지 진입 직후 race 로 401 을 받은 첫 호출이 사용자 흐름을 차단하지 않게 한다 * (platform-issue 019e638d, 2026-05-26). retry 는 1회 한정 — 무한 루프 차단. */ private doFetch; private tryFetchOnce; get(url: string, config?: RequestConfig): Promise; post(url: string, data?: unknown, config?: RequestConfig): Promise; put(url: string, data: unknown, config?: RequestConfig): Promise; patch(url: string, data: unknown, config?: RequestConfig): Promise; delete(url: string, config?: RequestConfig): Promise; /** * Raw fetch 요청 (SSE 스트리밍 등에 사용) * 인증 헤더가 자동으로 추가됩니다. timeout 은 호출자가 직접 signal 로 관리해야 합니다 * (스트리밍 특성상 전역 timeout 을 강제하지 않음). */ fetchRaw(url: string, init?: RequestInit): Promise; } interface AdsenseConnectionInfo { is_connected: boolean; email?: string; account_id?: string; } interface AdmobConnectionInfo { is_connected: boolean; email?: string; account_id?: string; publisher_id?: string; } interface GoogleConnectionStatus { adsense: AdsenseConnectionInfo; admob: AdmobConnectionInfo; } interface AdReportSummary { total_earnings: number; total_impressions: number; total_clicks: number; ctr: number; cpc: number; rpm: number; } interface DailyReport { date: string; earnings: number; impressions: number; clicks: number; ctr: number; } interface AdReportResponse { summary: AdReportSummary; daily: DailyReport[]; } interface AdMobReportSummary { total_earnings: number; total_impressions: number; total_clicks: number; total_ad_requests: number; total_matched_requests: number; impression_ctr: number; impression_rpm: number; match_rate: number; } interface AdMobDailyReport { date: string; earnings: number; impressions: number; clicks: number; ad_requests: number; matched_requests: number; impression_ctr: number; } interface AdMobReportResponse { summary: AdMobReportSummary; daily: AdMobDailyReport[]; } declare class AdsAPI { private http; constructor(http: HttpClient); /** * API Key 인증 시 /v1/public 접두사 반환 */ private getPublicPrefix; /** * AdSense / AdMob 연결 상태 확인 (중첩 구조) * * @returns `{ adsense, admob }` 각각 연결 상태·이메일·계정 ID * * @example * ```typescript * const status = await cb.ads.getConnectionStatus() * if (status.adsense.is_connected) { * console.log('AdSense 연결됨:', status.adsense.email, status.adsense.account_id) * } * if (status.admob.is_connected) { * console.log('AdMob 연결됨:', status.admob.account_id, status.admob.publisher_id) * } * ``` */ getConnectionStatus(): Promise; /** * AdSense 리포트 조회 (일별 데이터 + 요약) * * @param startDate - 시작일 (YYYY-MM-DD), 미지정 시 최근 30일 * @param endDate - 종료일 (YYYY-MM-DD), 미지정 시 최근 30일 * @returns 요약 + 일별 데이터 * * @example * ```typescript * const report = await cb.ads.getReport('2025-01-01', '2025-01-31') * console.log('총 수익:', report.summary.total_earnings) * report.daily.forEach(day => { * console.log(`${day.date}: $${day.earnings}`) * }) * ``` */ getReport(startDate?: string, endDate?: string): Promise; /** * 최근 30일 AdSense 요약 조회 * * @returns 30일 합산 수익, 노출, 클릭, CTR, CPC, RPM * * @example * ```typescript * const summary = await cb.ads.getReportSummary() * console.log(`총 수익: $${summary.total_earnings}`) * console.log(`RPM: $${summary.rpm}`) * ``` */ getReportSummary(): Promise; /** * AdMob 리포트 조회 (일별 데이터 + 요약) * * @param startDate - 시작일 (YYYY-MM-DD), 미지정 시 최근 30일 * @param endDate - 종료일 (YYYY-MM-DD), 미지정 시 최근 30일 * @returns 요약 + 일별 데이터 * * @example * ```typescript * const report = await cb.ads.getAdMobReport('2025-01-01', '2025-01-31') * console.log('총 수익:', report.summary.total_earnings) * console.log('매치율:', report.summary.match_rate + '%') * ``` */ getAdMobReport(startDate?: string, endDate?: string): Promise; /** * 최근 30일 AdMob 요약 조회 * * @returns 30일 합산 수익, 노출, 클릭, 광고 요청, 매치율, RPM * * @example * ```typescript * const summary = await cb.ads.getAdMobReportSummary() * console.log(`총 수익: $${summary.total_earnings}`) * console.log(`매치율: ${summary.match_rate}%`) * ``` */ getAdMobReportSummary(): Promise; } interface AIMessage { role: "system" | "user" | "assistant" | "tool"; content: string; toolCalls?: AIToolCall[]; toolCallId?: string; } interface AIToolCall { id: string; name: string; arguments: Record; } interface AITool { name: string; description: string; inputSchema: Record; } interface AIChatRequest { messages: AIMessage[]; maxTokens?: number; temperature?: number; topP?: number; provider?: string; model?: string; tools?: AITool[]; appId?: string; knowledgeBaseId?: string; topK?: number; agentic?: boolean; /** * KB 검색 hybrid(BM25 + 벡터 시맨틱, RRF 융합) 제어. 미설정(기본)이면 콘솔 > AI 에서 * embedding 을 켠 앱은 자동 적용, `false` 면 키워드 전용 검색 강제. */ hybrid?: boolean; toolGroupId?: string; /** * 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에 * **재투입하기 전** N 자(rune 기준)로 잘라낸다(말미에 잘림 표시). SSE `onToolEvent` 의 * `tool_end.result` 로 표시되는 값은 원문을 유지한다. * * 거대한 도구 결과(예: 복잡한 페이지의 `browser_snapshot`)가 누적 컨텍스트를 부풀려 * 매 턴 prefill 을 무겁게 만들 때, 표시는 원문으로 두고 모델 재투입분만 압축해 * 지연을 줄이는 용도. 미지정/0 이면 자르지 않는다. */ toolResultMaxChars?: number; /** * 요청에 포함한 MCP 도구(`tools`)를 호출할 외부 MCP 서버의 Public Key (cb_pk_* 형식). * MCP 도구를 직접 넘기지 않으면 불필요하다. */ mcpPublicKey?: string; } /** * `chatStream` 콜백. * * 대부분 콜백은 SSE 이벤트 종류별로 매핑되며, `onAbort` 는 호출자가 전달한 * `AbortSignal` 로 스트림을 취소했을 때 호출된다(에러가 아니므로 `onError` 와 구분). */ interface AIChatStreamCallbacks { onSources?: (sources: AISource[]) => void; /** * 추론 모델의 사고 과정 델타. 추론 모델(Qwen3 reasoning, o-series 등)을 * 사용할 때만 호출되며, 최종 답변은 `onToken`으로 별도 전달됩니다. */ onReasoning?: (reasoning: string) => void; onToken?: (content: string) => void; onToolEvent?: (event: AIToolEvent) => void; /** * Agentic 검색(`agentic: true`) 진행 상황. agentic 검색이 검색어를 * 생성하고 다중 라운드로 검색하는 각 단계마다 호출되어, "검색 중…" * 진행 UI 를 구성할 수 있다. agentic 미사용 시 호출되지 않는다. */ onSearching?: (progress: AgenticSearchProgress) => void; onDone?: () => void; onError?: (error: string) => void; /** * `options.signal` 로 스트림을 취소(abort)했을 때 호출된다. 정상적인 * 사용자 취소이므로 `onError` 대신 본 콜백이 호출되며, `onDone` 은 호출되지 * 않는다. 미지정 시 abort 는 조용히(예외 없이) 종료된다. */ onAbort?: () => void; } /** * `chatStream` 호출 옵션. `signal` 로 진행 중인 SSE 스트림을 취소할 수 있다. */ interface AIChatStreamOptions { /** * 진행 중인 스트림을 취소하기 위한 `AbortSignal`. abort 하면 SSE 연결이 * 닫히고, 서버는 요청 컨텍스트 취소를 통해 server-side agent tool loop * (`toolGroupId` 사용 시)까지 중단한다. */ signal?: AbortSignal; } interface AIToolEvent { type: "tool_start" | "tool_end" | "heartbeat"; name?: string; toolCallId?: string; arguments?: Record; result?: string; success?: boolean; durationMs?: number; } interface AISource { chunkId: string; documentId: string; documentName: string; content: string; score: number; } interface AIChatResponse { content: string; /** * 추론 모델(Qwen3 reasoning, OpenAI o-series, DeepSeek-R1 등)의 사고 과정. * 최종 답변(`content`)과 분리되어 제공됩니다. 추론 모델이 아니면 비어 있습니다. */ reasoning?: string; finishReason?: string; usage?: { promptTokens: number; completionTokens: number; totalTokens: number; }; provider: string; model: string; toolCalls?: AIToolCall[]; sources?: AISource[]; } /** * Agentic 검색의 한 단계 진행 상황. `chatStream({ agentic: true })` 시 * `onSearching` 콜백으로 실시간 전달되어 "검색 중…" 진행 UI 를 구성할 수 있다. */ interface AgenticSearchProgress { /** 'query_generation' = 검색어 생성 중, 'searching' = 검색 실행 중, 'complete' = 검색 종료 */ phase: "query_generation" | "searching" | "complete"; /** 검색 라운드 (1~2). agentic 은 결과가 부족하면 2라운드까지 수행. */ round?: number; /** 이 라운드에서 생성된 검색 쿼리들 (phase='searching'). */ queries?: string[]; /** phase='complete': 누적 결과 청크 수. */ results?: number; /** phase='complete': 총 검색 라운드 수. 0 은 폴백(단일 키워드 검색)을 의미. */ rounds?: number; } interface AIStreamChunk { type?: "sources" | "token" | "searching" | "tool_start" | "tool_end" | "heartbeat"; content: string; /** * 추론 모델의 사고 과정 델타. 추론 구간의 청크는 `content`가 비어 있고 * `reasoning`만 채워져 전달됩니다. */ reasoning?: string; finishReason?: string; done: boolean; toolCalls?: AIToolCall[]; sources?: AISource[]; name?: string; toolCallId?: string; arguments?: Record; result?: string; success?: boolean; durationMs?: number; searching?: AgenticSearchProgress; error?: string; message?: string; } /** * AI API * * AI 채팅 및 AI 데이터베이스 연동 API. * AI 데이터베이스 ID를 지정하면 문서 검색 후 컨텍스트를 포함하여 응답합니다. * * @example * ```typescript * const cb = new ConnectBase({ publicKey: 'your-public-key' }) * * // 일반 AI 채팅 * const response = await cb.ai.chat({ * messages: [{ role: 'user', content: '안녕하세요' }], * provider: 'gemini' * }) * * // AI 데이터베이스 연동 채팅 * const ragResponse = await cb.ai.chat({ * messages: [{ role: 'user', content: '환불 정책이 어떻게 되나요?' }], * knowledgeBaseId: 'kb-id', * topK: 5 * }) * ``` */ /** * # 🔒 보안 — LLM API key 는 절대 클라이언트에 hardcode 금지 * * 본 SDK 의 `chat` / `chatStream` 은 **ConnectBase 서버 프록시 경유** — provider (OpenAI/Claude/ * Gemini/Ollama/vLLM/...) API key 는 `AppAIConfig` 에 암호화 저장된 상태로 ConnectBase 가 server-side * 에서만 복호화하여 사용합니다. 클라이언트는 user public key (또는 secret key) 만 알면 됩니다. * * **❌ 안티 패턴 — 즉시 다른 운영자에게 cross_app_issue 로 알려지고 차단됨**: * ```ts * // 브라우저 정적 자산에 raw provider URL + key — 절대 금지 * fetch("https://api.openai.com/v1/chat/completions", { * headers: { Authorization: "Bearer sk-..." } * }) * // 또는 * fetch("https://tunnel.connectbase.world//v1/chat/completions", { * headers: { Authorization: "Bearer " } * }) * ``` * DevTools → Network 한 번이면 누구나 key 추출 → 추론 비용 폭주 / GPU 자원 고갈. * * **✅ 베스트프랙티스 — 본 SDK 호출만 사용**: * ```ts * await cb.ai.chatStream({ messages: [...], provider: "openai_compatible" }, callbacks) * ``` */ declare class AIAPI { private http; constructor(http: HttpClient); /** * AI 채팅 (동기). ConnectBase 서버 프록시 경유 — provider API key 는 server-side `AppAIConfig` * 에서만 사용되어 클라이언트에 노출되지 않습니다. raw provider URL 직접 호출 금지 (안티 패턴). */ chat(request: AIChatRequest): Promise; /** * AI 채팅 스트리밍 (SSE) * * @param request AI 채팅 요청 * @param callbacks 스트림 이벤트 콜백 (토큰/추론/도구/검색/완료/에러/취소) * @param options 선택적 옵션. `options.signal` 에 `AbortSignal` 을 전달하면 * 호출자가 진행 중인 스트림을 취소할 수 있다. abort 시 SSE 연결이 닫히고, * 서버는 요청 컨텍스트(`c.Request.Context()`) 취소를 통해 server-side agent * tool loop (`toolGroupId` 사용 시)까지 함께 중단한다. abort 는 정상적인 * 사용자 취소이므로 `onError` 가 아니라 `callbacks.onAbort?.()` 가 호출되며 * (지정 시), `onDone` 은 호출되지 않는다. * * @example * ```typescript * await cb.ai.chatStream({ * messages: [{ role: 'user', content: '안녕하세요' }], * knowledgeBaseId: 'kb-id', * }, { * onSources: (sources) => console.log('참조:', sources), * onReasoning: (reasoning) => process.stdout.write(reasoning), * onToken: (content) => process.stdout.write(content), * onDone: () => console.log('\\n완료'), * onError: (error) => console.error('에러:', error) * }) * ``` * * @example 취소(stop 버튼) * ```typescript * const controller = new AbortController() * // stop 버튼: controller.abort() * await cb.ai.chatStream( * { messages: [{ role: 'user', content: '...' }], toolGroupId: 'tg-id' }, * { onToken: (c) => process.stdout.write(c), onAbort: () => console.log('취소됨') }, * { signal: controller.signal }, * ) * ``` */ chatStream(request: AIChatRequest, callbacks: AIChatStreamCallbacks, options?: AIChatStreamOptions): Promise; } interface AnalyticsConfig { /** 자동 페이지뷰 추적 @default true */ trackPageViews?: boolean; /** 커스텀 이벤트 활성화 @default true */ trackEvents?: boolean; /** 세션 + heartbeat @default true */ trackSessions?: boolean; /** 히트맵 (opt-in) @default false */ heatmap?: boolean; /** 세션 녹화 (opt-in) @default false */ recording?: boolean; /** 배치 크기 @default 10 */ batchSize?: number; /** 배치 전송 간격 (ms) @default 5000 */ flushInterval?: number; /** DNT 헤더 존중 @default true */ respectDoNotTrack?: boolean; /** 디버그 모드 @default false */ debug?: boolean; } interface ConsentOptions { analytics?: boolean; heatmap?: boolean; recording?: boolean; } interface AnalyticsEvent { name: string; properties?: Record; timestamp?: number; } /** * 인기 페이지 목록 응답. * * 백엔드 `dto.PopularPagesResponse` 와 1:1 매핑. */ interface PopularPagesResponse { pages: Array<{ page_path: string; page_views: number; }>; start_date: number; end_date: number; } /** * 방문자 목록 응답. * * 백엔드 `dto.VisitorListResponse` 와 1:1 매핑. `app_member_id` 는 게스트 방문자에는 * 없을 수 있다. */ interface VisitorListResponse { visitors: Array<{ id: string; visitor_uid: string; app_member_id?: string; total_visits: number; total_page_views: number; referrer?: string; last_ip?: string; country?: string; is_bot: boolean; first_visit_at: string; last_visit_at: string; }>; total: number; limit: number; offset: number; has_more: boolean; } /** * 페이지 전환 플로우(Sankey) 응답. * * 백엔드 `dto.NavigationFlowResponse` 와 1:1 매핑. `nodes` 는 페이지, `links` 는 전환. */ interface NavigationFlowResponse { nodes: Array<{ id: string; label: string; value: number; }>; links: Array<{ source: string; target: string; value: number; }>; } /** * 조회 메서드 공통 옵션. * * `start_date` / `end_date` 는 백엔드가 정수형 epoch-day 또는 yyyymmdd 로 다루는 값 * (서비스 메서드 시그니처 기준 int). 0 또는 미지정 시 기본 기간을 사용. */ interface AnalyticsRangeOptions { start_date?: number; end_date?: number; limit?: number; } interface VisitorListOptions { limit?: number; offset?: number; /** * 백엔드가 인식하는 정렬 키 — 외 값은 silent 로 default(`last_visit`) 처리됩니다. * * 1.12.0 에서 백엔드 실제 동작에 맞춰 enum 정정 (1.10/1.11 의 `total_visits`/ * `total_page_views` 는 백엔드에서 인식되지 않아 항상 default 분기였음). */ sort_by?: "last_visit" | "visits" | "page_views" | "first_visit"; } /** * 멤버별 합산 방문자 그룹 항목. * * 한 명의 회원이 여러 디바이스/브라우저로 접속했을 때 visitor row 들을 합쳐 단일 row. * 익명 visitor 는 `app_member_id == undefined` 로 단일 row 그대로 노출되며 `visitor_count == 1`. * * `visitor_count` 는 "디바이스 수" 가 아닌 **"추적 브라우저 인스턴스 수"** 를 의미합니다. * 같은 디바이스에서 시크릿모드 + 일반모드는 visitor 2 = visitor_count 2 로 카운트됩니다. */ interface VisitorGroupItem { app_member_id?: string; /** 익명 visitor row 의 경우 단일 visitor_uid. 회원 그룹은 visitor_uids 참조. */ visitor_uid?: string; /** 회원 그룹에 속한 visitor_uid 목록. 익명 row 는 undefined. */ visitor_uids?: string[]; visitor_count: number; total_visits: number; total_page_views: number; first_visit_at: string; last_visit_at: string; country?: string; is_bot: boolean; } interface VisitorGroupListResponse { groups: VisitorGroupItem[]; total: number; limit: number; offset: number; has_more: boolean; } interface VisitorByMemberResponse { app_member_id: string; visitor_uids: string[]; visitor_count: number; total_visits: number; total_page_views: number; first_visit_at: string; last_visit_at: string; country?: string; is_bot: boolean; } interface MergeVisitorsRequest { source_visitor_uid: string; /** target_visitor_uid 또는 target_member_id 중 하나는 필수. */ target_visitor_uid?: string; target_member_id?: string; } interface MergeVisitorsResponse { success: boolean; target_visitor_id: string; moved_records: number; message?: string; } declare class SessionManager { private _sessionId; private _visitorUid; private _lastActivity; private _isNewSession; get sessionId(): string; get visitorUid(): string; get isNewSession(): boolean; /** 활동 기록 — 세션 타임아웃 리셋 */ touch(): void; /** 세션 강제 리셋 */ reset(): void; /** * visitor_uid 를 새로 발급하고 세션도 함께 초기화. * * 사용 시점: * - 사용자 로그아웃 (이전 사용자 활동이 다음 사용자에 attribution 되는 것 방지) * - 다른 사용자 로그인 감지 (link-member 가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답) * * localStorage 의 `__cb_visitor_uid` 가 새 UUID 로 교체되며 sessionStorage 의 * 세션 키도 같이 비워진다. 이후의 모든 batch 는 새 visitor 로 기록되어 멤버 간 * 데이터 오염이 차단된다. */ regenerateVisitorUid(): string; private ensureSession; private loadOrCreateVisitorUid; } declare class AnalyticsAPI { private http; private config; private consent; private session; private storageWebId; private memberId; private eventQueue; private batchTimer; private isInitialized; private heartbeatTimer; private visibilityHandler; private unloadHeartbeatHandler; private popstateHandler; private beforeUnloadHandler; private origPushState; private origReplaceState; private heatmapClickHandler; private heatmapScrollHandler; private utm; constructor(http: HttpClient); /** * Analytics 초기화 * @param storageWebId 웹 스토리지 ID * @param config 설정 (선택) */ init(storageWebId: string, config?: AnalyticsConfig): void; /** Analytics 정리 */ destroy(): void; /** * 동의 설정 변경 */ setConsent(consent: ConsentOptions): void; /** 현재 동의 상태 조회 */ getConsent(): ConsentOptions; /** * 페이지뷰 수동 추적 */ trackPageView(path?: string): void; /** * 커스텀 이벤트 추적 */ trackEvent(name: string, properties?: Record): void; /** * 사용자 식별 (로그인 직후 호출). * * 이후 모든 방문 배치에 `app_member_id` 가 첨부되어 새 활동은 회원으로 기록됩니다. * 추가로 **현재 visitor_uid 의 기존 익명 활동을 즉시 회원에게 backfill** 하기 위해 * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는 * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구. * * **사용자 전환 자동 처리 (1.13.0+)**: 동일 브라우저에서 다른 사용자가 로그인해 * 백엔드가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 보내면, 큐를 비우고 * `visitor_uid` 를 새로 발급한 뒤 link-member 를 한 번 더 호출해 새 visitor 가 * 즉시 회원으로 기록되도록 자가 복구한다. * * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합. * * @example * ```ts * // 로그인 성공 직후 * const member = await cb.auth.signInMember({ login_id, password }) * cb.analytics.identify(member.member_id) * ``` */ identify(memberId: string): void; /** * 로그아웃 / 사용자 전환 시 호출. 익명 상태로 복귀하면서 visitor_uid 를 새로 * 발급해 다음 사용자의 활동이 이전 사용자에게 attribution 되는 데이터 오염을 차단. * * 동작: * 1. memberId 를 null 로 설정 (이후 batch 는 익명으로 기록) * 2. 큐에 쌓인 미전송 이벤트 폐기 (이전 visitor 로 가는 것을 막기 위함) * 3. localStorage `__cb_visitor_uid` 새 UUID 로 교체 + sessionStorage 세션 키 정리 * 4. heatmap 큐도 함께 비움 * * @example * ```ts * // 로그아웃 핸들러 * await cb.auth.signOut() * cb.analytics.reset() * ``` */ reset(): void; /** * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출). * * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의 * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃 시에는 * 데이터 오염 방지를 위해 `reset()` 를 권장). */ setMemberId(memberId: string | null): void; /** * 백엔드 link-member 엔드포인트 한 번 호출 — 즉시 backfill 트리거. * * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가 * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출. * - 다른 멤버에 이미 연결된 visitor 인 경우 (`VISITOR_LINKED_TO_OTHER_MEMBER`) * visitor_uid 를 자동 재발급하고 link-member 를 한 번 더 호출 — 한 단계의 자가 * 복구로 끝나며 무한 재귀를 막기 위해 두 번째 시도는 응답을 더 보지 않는다. * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail. */ private linkMemberSilent; /** 현재 설정된 회원 ID 조회 (미설정 시 null) */ getMemberId(): string | null; /** * 히트맵 수집 활성화 (opt-in) */ enableHeatmap(options?: { click?: boolean; scroll?: boolean; }): void; /** * 세션 heartbeat 자동 전송 시작 (30초 간격) */ enableHeartbeat(): void; /** * 큐에 있는 이벤트 즉시 전송. * * 기본적으로 이벤트는 배치(10개) 또는 주기(5초)로 flush 되지만, 페이지 이탈 직전이나 * 결정적 이벤트(결제 완료 등)는 수동으로 `flush()` 를 호출해 전송 지연을 줄일 수 있다. * * @returns 서버 응답 완료 시까지 대기하는 Promise * * @example * ```ts * // 결제 완료 직후 이탈에 대비해 즉시 flush * await cb.analytics.trackEvent('purchase_completed', { order_id: '123' }) * await cb.analytics.flush() * window.location.href = '/thank-you' * ``` */ flush(): Promise; /** * 인기 페이지 조회 (콘솔 JWT 또는 User Secret Key `cb_sk_` 인증 필요). * * 브라우저 환경에서 Public Key(`cb_pk_`) 만 가진 SDK 인스턴스로는 호출이 차단된다. * Functions / 어드민 앱 등 cb_sk_ 환경에서 사용하라. * * @param storageWebId 조회할 웹스토리지 ID. 미지정 시 `init()` 에 전달된 ID 사용. * @param options 기간/limit 옵션 * @example * ```ts * const { pages } = await cb.analytics.getPopularPages('019d8...', { limit: 20 }) * ``` */ getPopularPages(storageWebId?: string, options?: AnalyticsRangeOptions): Promise; /** * 페이지 전환 플로우(Sankey) 조회 — JWT/cb_sk_ 인증 필요. */ getNavigationFlow(storageWebId?: string, options?: AnalyticsRangeOptions): Promise; /** * 방문자 목록 조회 — JWT/cb_sk_ 인증 필요. */ getVisitors(storageWebId?: string, options?: VisitorListOptions): Promise; /** * 멤버별 합산 방문자 그룹 조회 — JWT/cb_sk_ 인증 필요. (1.11+) * * `getVisitors` 와 달리 visitor row 들을 `app_member_id` 로 합쳐 단일 row 로 반환. * 같은 사람이 PC + 모바일 + 태블릿으로 접속한 경우 visitor 3개 → 그룹 1개. * 익명 visitor 는 단일 row 로 그대로 포함되어 페이지네이션이 일관됨. * * @example * ```ts * const { groups } = await cb.analytics.getVisitorGroups('019d8...', { limit: 50 }) * for (const g of groups) { * if (g.app_member_id) console.log(`${g.app_member_id}: ${g.visitor_count} 디바이스`) * } * ``` */ getVisitorGroups(storageWebId?: string, options?: VisitorListOptions): Promise; /** * 단건 멤버 합산 방문자 조회 — JWT/cb_sk_ 인증 필요. (1.11+) * * 어드민 회원 상세 페이지처럼 **한 명만 필요**할 때. 페이지네이션 풀 다운 없이 * 한 번의 호출로 합산 결과 반환. * * @example * ```ts * const v = await cb.analytics.getVisitorByMember('019d8...', memberId) * console.log(`${v.visitor_count} 디바이스, 총 ${v.total_page_views} pv`) * ``` */ getVisitorByMember(storageWebId: string | undefined, memberId: string): Promise; /** * 두 visitor 를 한 사람으로 통합하는 admin 작업 — JWT/cb_sk_ 인증 필요. (1.11+) * * 외부 인증 시스템에서 두 visitor 가 동일인임을 알았을 때 사용. source 의 자식 레코드 * (page_views, daily, custom_events, experiment_assignments, heatmap_events, * session_recordings) 를 target 으로 옮기고 source visitor 는 삭제됨. * * @param request 둘 중 하나 필수: `target_visitor_uid` 또는 `target_member_id`. * @example * ```ts * await cb.analytics.mergeVisitors('019d8...', { * source_visitor_uid: 'old-uid', * target_member_id: '01a...', * }) * ``` */ mergeVisitors(storageWebId: string | undefined, request: MergeVisitorsRequest): Promise; /** * 조회 메서드 공통 가드 — Public Key 인증 SDK 인스턴스에서는 명확한 에러를 던진다. * 백엔드 라우트는 cb_pk_ 를 거부하므로 호출 자체를 막는 것이 디버깅에 유리. */ private requireServerSideStorageId; /** * 세션 매니저 접근 (고급 사용자용). * * 외부에서 세션 ID 를 읽어 자체 로깅에 합치거나 강제 세션 종료/재시작이 필요한 경우 * 사용. 일반적으로는 AnalyticsAPI 가 내부적으로 세션을 관리하므로 호출할 필요가 없다. * * @returns 내부 SessionManager 인스턴스 */ getSession(): SessionManager; private canTrack; private isDNT; private createBaseEvent; private enqueue; private flushQueue; /** * sendBeacon 으로 동기 flush (beforeunload 용). * * sendBeacon 은 일반 fetch 와 달리 User-Agent 헤더가 신뢰할 수 있지만, 백엔드 봇 * 탐지가 body 의 `user_agent` 필드 첫 번째 값만 보므로 여기서도 동일하게 채워 * 첫 방문이 unload 타이밍에 도달했을 때 봇으로 오판되는 것을 방지한다. */ private flushSync; private trackSessionStart; private startBatchTimer; private stopBatchTimer; private startHeartbeat; private stopHeartbeat; /** 하트비트를 큐에 넣어 배치 전송 */ private sendHeartbeat; /** sendBeacon으로 하트비트 전송 (unload/visibility hidden용) */ private sendHeartbeatBeacon; private setupAutoPageView; private removeAutoPageView; private removeHeatmapListeners; private handleHeatmapClick; private recordHeatmapEvent; private heatmapQueue; private log; } /** 앱 멤버 회원가입 요청 */ interface MemberSignUpRequest { login_id: string; password: string; nickname?: string; } /** 앱 멤버 회원가입 응답 */ interface MemberSignUpResponse { member_id: string; nickname: string; access_token: string; refresh_token: string; } /** 앱 멤버 로그인 요청 */ interface MemberSignInRequest { login_id: string; password: string; } /** 앱 멤버 로그인 응답 */ interface MemberSignInResponse { member_id: string; nickname: string; access_token: string; refresh_token: string; } /** 앱 멤버 내 정보 응답 */ interface MemberInfoResponse { member_id: string; nickname: string; is_active: boolean; custom_data: Record; /** 멤버 이메일 (EMAIL identity 에서 추출, 없으면 빈 문자열) */ email?: string; /** 멤버 역할 — RLS 규칙에서 `auth.role` 로 참조 (예: "admin", "moderator") */ role?: string; /** * 현재 세션이 게스트(identity 가 전혀 없는 멤버 또는 레거시 GUEST identity)인지 여부. * 신규 게스트 로그인은 제거됐으나, 기존 게스트 멤버 식별을 위해 유지된다. */ is_guest?: boolean; /** * 현재 세션의 인증 방식 식별자. * "guest" | "email" | "username" | "oauth_google" | "oauth_kakao" | * "oauth_naver" | "oauth_apple" | "oauth_github" | "oauth_discord" * OAuth 외 미래 provider 가 추가되면 동일한 `oauth_` 컨벤션을 따른다. * identity 가 전혀 없는 경우 빈 문자열. */ auth_provider?: string; } /** 앱 멤버 custom_data 수정 요청 */ interface UpdateCustomDataRequest { custom_data: Record; } /** 앱 멤버 custom_data 수정 응답 */ interface UpdateCustomDataResponse { member_id: string; custom_data: Record; } /** 앱 인증 설정 응답 */ interface AuthSettingsResponse { /** 아이디/비밀번호 로그인 허용 여부 */ allow_id_password_login: boolean; /** 활성화된 OAuth 프로바이더 목록 (GOOGLE, KAKAO, NAVER, APPLE, GITHUB, DISCORD) */ enabled_oauth_providers: string[]; } declare class AuthAPI { private http; private analytics; constructor(http: HttpClient); /** * AnalyticsAPI 주입 — 로그인/가입/로그아웃 시 방문자 트래커에 member_id 를 전달하여 * 게스트 방문자를 회원과 연결한다. ConnectBase 컨스트럭터에서 내부적으로 호출된다. */ _attachAnalytics(analytics: AnalyticsAPI): void; private notifyVisitorTracker; /** * 앱의 인증 설정 조회 * 어떤 로그인 방식이 허용되는지 확인합니다. * * @example * ```typescript * const settings = await client.auth.getAuthSettings() * if (settings.allow_id_password_login) { * // 로그인 폼 표시 * } else if (settings.enabled_oauth_providers.includes('GOOGLE')) { * // 구글 소셜 로그인 버튼 표시 * } * ``` */ getAuthSettings(): Promise; /** * 앱 멤버 회원가입 (아이디/비밀번호 기반) * 앱에 새로운 멤버를 등록합니다. * * @example * ```typescript * const result = await client.auth.signUpMember({ * login_id: 'myuser123', * password: 'password123', * nickname: 'John' * }) * console.log('가입 완료:', result.member_id) * ``` */ signUpMember(data: MemberSignUpRequest): Promise; /** * 앱 멤버 로그인 (아이디/비밀번호 기반) * 기존 멤버로 로그인합니다. * * @example * ```typescript * const result = await client.auth.signInMember({ * login_id: 'myuser123', * password: 'password123' * }) * console.log('로그인 성공:', result.member_id) * ``` */ signInMember(data: MemberSignInRequest): Promise; /** * 현재 로그인한 멤버 정보 조회 * custom_data를 포함한 멤버 정보를 반환합니다. * * @example * ```typescript * const me = await client.auth.getMe() * console.log('내 정보:', me.nickname, me.custom_data) * ``` */ getMe(): Promise; /** * 현재 로그인한 멤버의 custom_data 수정 * PATCH 방식으로 기존 데이터에 머지됩니다. 값을 null로 설정하면 해당 키가 삭제됩니다. * * @example * ```typescript * // 키 추가/수정 * await client.auth.updateCustomData({ * custom_data: { role: 'admin', level: 5 } * }) * * // 키 삭제 (null 전달) * await client.auth.updateCustomData({ * custom_data: { role: null } * }) * ``` */ updateCustomData(data: UpdateCustomDataRequest): Promise; /** * Admin: 다른 멤버의 정보를 수정합니다 (role / nickname / custom_data). * * 이 메서드는 RLS 의 `auth.role` 평가에 사용되는 `member.role` 필드를 set 할 수 있는 * 유일한 SDK 경로입니다. 보안상 AppMember 자신의 self-update 는 허용되지 않습니다 * (권한 상승 우회 차단) — 이 메서드는 cb_sk_ admin secret key 권한 컨텍스트에서만 * 호출 가능합니다. * * 사전 조건: * - ConnectBase 인스턴스가 `publicKey` 와 `secretKey` 를 모두 가지고 있어야 함 * - secretKey 의 user 가 해당 앱에 권한을 가지고 있어야 함 (서버에서 검증) * * @example * ```typescript * const cb = new ConnectBase({ publicKey: 'cb_pk_...', secretKey: 'cb_sk_...' }) * await cb.auth.adminUpdateMember('member-uuid', { role: 'admin' }) * * // role 해제 * await cb.auth.adminUpdateMember('member-uuid', { role: '' }) * * // 다중 필드 * await cb.auth.adminUpdateMember('member-uuid', { * nickname: 'Alice', * role: 'editor', * custom_data: { level: 5 } * }) * ``` */ adminUpdateMember(memberID: string, fields: { nickname?: string; role?: string; custom_data?: Record; }): Promise<{ member_id: string; nickname: string; custom_data: Record; role: string; }>; /** * 로그아웃 */ signOut(): Promise; } /** * 서버(data-server `ent/schema/table.go` — `ValidSchemaTypes`) 가 허용하는 컬럼 타입 목록. * - `string` / `number` / `int` : 문자열 / 숫자(float) / 정수 * - `bool` : 참/거짓 (JS 의 `boolean` 이 아니라 `bool` 입니다) * - `uuid` : UUID 문자열 * - `date` : ISO-8601 날짜/시간 * - `object` / `array` : 중첩 JSON */ type DataType = "string" | "int" | "number" | "bool" | "uuid" | "date" | "object" | "array"; /** * 컬럼 정보 (Table.schema 맵에서 합성된 객체). * * 서버는 컬럼을 별도 레코드로 저장하지 않으므로 `id` 는 컬럼 이름과 동일하며, * `order` 는 SDK 가 맵 순서대로 부여한 인덱스입니다. `created_at` 은 테이블의 * `created_at` 으로 대체됩니다. */ interface ColumnSchema { /** 컬럼 이름과 동일 (서버 별도 ID 없음) */ id: string; name: string; data_type: DataType; is_required: boolean; default_value?: unknown; description?: string; encrypted?: boolean; /** SDK 가 맵 순서대로 부여한 인덱스 */ order: number; /** 테이블의 `created_at` (컬럼 개별 타임스탬프는 없음) */ created_at: string; /** * @deprecated 서버가 지원하지 않습니다. */ validation_rule?: string; /** * @deprecated 컬럼 개별 타임스탬프가 없습니다. */ updated_at?: string; } interface CreateColumnRequest { name: string; data_type: DataType; /** 필수 필드 여부 (기본 `false`) */ is_required?: boolean; /** 기본값 (서버는 임의 타입을 허용하므로 `unknown`) */ default_value?: unknown; description?: string; /** * 필드 레벨 암호화 활성화 여부. * true로 설정 시 AES-256-GCM으로 DB에 암호화 저장되며, 조회 시 자동 복호화됩니다. * 암호화된 필드는 DB 레벨 필터/정렬이 불가하며, 앱 레벨 검색은 정상 동작합니다. * 서버에 SECRET_ENCRYPTION_KEY가 설정되어 있어야 합니다. */ encrypted?: boolean; /** * @deprecated 서버가 무시합니다. 컬럼 순서는 서버가 자동 관리합니다. */ order?: number; /** * @deprecated 서버가 해당 필드를 저장하지 않습니다. */ validation_rule?: string; } interface UpdateColumnRequest { data_type?: DataType; is_required?: boolean; default_value?: unknown; description?: string; encrypted?: boolean; /** * @deprecated 서버가 컬럼 이름 변경을 지원하지 않습니다. 변경하려면 * 삭제 후 재생성하거나 data-server 의 rename 엔드포인트를 사용하세요. */ name?: string; /** * @deprecated 서버가 무시합니다. */ order?: number; /** * @deprecated 서버가 해당 필드를 저장하지 않습니다. */ validation_rule?: string; } /** * 서버(data-server `ent/schema/table.go` 의 `Table`) 가 반환하는 테이블 레코드. * 필드 이름은 Go ent 모델과 일치합니다. */ interface TableSchema { id: string; app_id: string; /** 테이블 이름 (ent 필드: `title`) */ title: string; /** 평면 스키마 맵. 컬럼 객체 배열로 변환하려면 `getColumns()` 사용. */ schema: TableSchemaDefinition; /** 검증 스키마 (table-level). 설정되어 있으면 데이터 insert/update 시 평가됨. */ validation_schema?: ValidationSchema; access_level: TableAccessLevel; is_active: boolean; created_at: string; updated_at: string; } /** * 컬럼 정의. 서버는 두 가지 형식 중 하나로 저장합니다: * - 플랫: 타입 문자열만 (`'string'`, `'int'` 등) * - 중첩: 타입 + 추가 속성 객체 (`{type, required, default, description, encrypted}`) * * 클라이언트에서 `createTable` 시에는 플랫 형태가 권장되며, 추가 속성은 * `createColumn` 을 사용하거나 중첩 객체로 직접 지정할 수 있습니다. */ type TableColumnDef = DataType | { type: DataType; required?: boolean; default?: unknown; description?: string; encrypted?: boolean; }; /** * 테이블 스키마 정의. * 키는 컬럼 이름, 값은 타입 문자열 또는 중첩 컬럼 객체입니다. * `$required` 는 특수 키로, 필수 컬럼 이름의 배열을 받습니다. * * @example * ```ts * // 플랫 형태 * { email: 'string', age: 'int', $required: ['email'] } * * // 중첩 형태 (필드별 옵션) * { email: { type: 'string', required: true, encrypted: true } } * ``` */ interface TableSchemaDefinition { /** 필수 컬럼 이름 목록 (서버 hook 이 `$required` 키로 해석) */ $required?: string[]; /** 컬럼명 → 컬럼 정의. `$required` 키는 예외적으로 `string[]` */ [columnName: string]: TableColumnDef | string[] | undefined; } /** 테이블 접근 수준 */ type TableAccessLevel = "Creator" | "Public" | "AppMember"; /** 단일 필드의 검증 규칙 */ interface ValidationSchemaField { /** 데이터 타입 */ type: DataType; /** 필수 여부 */ required?: boolean; /** 기본값 */ default?: unknown; /** 숫자 최소값 */ min?: number; /** 숫자 최대값 */ max?: number; /** 문자열 최소 길이 */ minLength?: number; /** 문자열 최대 길이 */ maxLength?: number; /** 정규식 패턴 (Go regexp 호환) */ pattern?: string; /** 허용 값 목록 */ enum?: unknown[]; /** 배열 아이템 스키마 */ items?: ValidationSchemaField; /** object 의 속성 스키마 */ properties?: Record; /** 수정 불가 (immutable) */ immutable?: boolean; /** 유니크 제약 */ unique?: boolean; /** 새니타이징 ('html', 'xss', 'trim') */ sanitize?: string; /** 자동 계산 표현식 */ computed?: string; /** 상태 전이 규칙: 현재상태 → 허용되는 다음 상태들 */ transitions?: Record; } /** 상태 전이 규칙 (객체 단위 — 필드 + 전이 맵) */ interface ValidationStateTransitions { field: string; transitions: Record; } /** * 테이블 검증 스키마 (table-level). * 데이터 insert/update 시 자동으로 평가됩니다. * * @example * ```ts * { * fields: { * email: { type: 'string', pattern: '^.+@.+$', required: true }, * age: { type: 'int', min: 0, max: 150 } * }, * required: ['email'], * immutable: ['email'] * } * ``` */ interface ValidationSchema { fields: Record; required?: string[]; unique?: string[]; immutable?: string[]; computed?: Record; transitions?: Record; } interface CreateTableRequest { /** 테이블 이름 (서버는 내부적으로 `title` 로 저장) */ name: string; /** * 초기 컬럼 스키마 (선택). * 생략하면 컬럼이 없는 빈 테이블로 생성되며, 이후 `addColumn()` 으로 추가할 수 있습니다. */ schema?: TableSchemaDefinition; /** * 접근 수준 (선택, 기본값 `'Creator'`). * - `Creator`: 테이블 생성자만 전체 권한 * - `Public`: 누구나 읽기/쓰기 * - `AppMember`: 앱 멤버만 읽기/쓰기 */ accessLevel?: TableAccessLevel; /** * @deprecated 서버에 저장되지 않습니다. 필드는 backward-compat 을 위해 남아있으며 SDK 가 서버로 전송하지 않습니다. */ description?: string; } interface DataItem { id: string; data: Record; created_at: string; updated_at: string; } interface CreateDataRequest { data: Record; } interface UpdateDataRequest { data: Record; } /** * 테이블 데이터 조회 응답. * * 서버(data-server `internal_data_controller.go` 의 `FetchDataByTableGET`, * `QueryDataByTable`) 가 반환하는 JSON 과 1:1 매핑합니다. * * 참고: `1.3.0` 이전 버전(`0.16.1` 기준)에서는 `datas` / `total_size` 로 * 선언돼 있었으나, 실제 서버 wire 포맷은 `data` / `total_count` 이므로 런타임에 * 속성 접근이 `undefined` 를 반환하는 결함이 있었습니다. `1.4.0` 에서 바로잡습니다. */ interface FetchDataResponse { /** 조회된 문서 배열 */ data: DataItem[]; /** * 총 매칭 문서 수. * * `count: false` 로 조회한 경우: 마지막 페이지(반환 건수 < limit)면 서버가 정확한 * 총계를 파생해 돌려주고, 아니면 `-1` (미상) 이 반환됩니다. * `cursor` 를 사용한 조회는 기본적으로 `-1` (잔여 카운트가 필요하면 `count: true` 명시). */ total_count: number; /** * 다음 페이지 커서 (keyset 페이지네이션). * * 값이 있으면 다음 페이지가 있을 수 있으며, 다음 요청의 `cursor` 옵션에 그대로 * 전달하면 됩니다. 없으면 마지막 페이지입니다. 사용자 정렬(orderBy)과 함께는 * 제공되지 않습니다. */ next_cursor?: string; } interface QueryOptions { limit?: number; offset?: number; orderBy?: string; orderDirection?: "asc" | "desc"; where?: WhereCondition; /** 반환할 필드 목록 (Projection) - 지정된 필드만 반환 */ select?: string[]; /** 제외할 필드 목록 - 지정된 필드를 제외하고 반환 */ exclude?: string[]; /** * `false` 면 `total_count` 계산(서버측 COUNT 쿼리)을 생략합니다. * * 필터가 붙은 COUNT 는 본 조회와 같은 비용의 스캔을 한 번 더 도는 것이므로, * 총계가 필요 없는 목록/무한스크롤 조회는 `count: false` 로 눈에 띄게 빨라집니다. * 생략 시 기본값은 `true` (기존 동작 유지). `cursor` 사용 시 기본값은 `false`. */ count?: boolean; /** * Keyset 페이지네이션 커서 — 이전 응답의 `next_cursor` 값. * * 설정하면 `offset` 은 무시되고 생성순(id) 정렬로 커서 이후 문서를 반환합니다. * `offset` 방식은 깊은 페이지일수록 느려지지만 커서 방식은 페이지 깊이와 무관하게 * 일정합니다 (무한스크롤 권장). `orderBy` 와 함께 쓸 수 없습니다. */ cursor?: string; } type WhereCondition = { /** OR 조건: 배열 내 조건들 중 하나 이상 만족 */ $or?: WhereCondition[]; } & Record; interface WhereOperator { $eq?: unknown; $ne?: unknown; $gt?: number; $gte?: number; $lt?: number; $lte?: number; $in?: unknown[]; $nin?: unknown[]; $contains?: string; $startsWith?: string; $endsWith?: string; /** 정규식 매칭 (MySQL REGEXP) */ $matches?: string; /** 범위 쿼리 [min, max] */ $between?: [number, number]; /** NULL 체크 (true: null인 문서, false: null이 아닌 문서) */ $isNull?: boolean; /** JSON 배열에 특정 값이 포함되어 있는지 확인 */ $arrayContains?: unknown; /** JSON 배열에 주어진 값들 중 하나라도 포함되어 있는지 확인 */ $arrayContainsAny?: unknown[]; } interface BulkCreateResponse { created: DataItem[]; failed?: BulkError[]; total: number; success: number; } interface BulkError { index: number; error: string; } interface DeleteWhereResponse { deleted_count: number; failed_count?: number; } interface SecurityRule { id: string; app_id: string; table_name: string; rules: Record; is_active: boolean; priority: number; created_at: string; updated_at: string; } interface CreateSecurityRuleRequest { table_name: string; rules: Record; is_active?: boolean; priority?: number; } interface UpdateSecurityRuleRequest { rules?: Record; is_active?: boolean; priority?: number; } interface TableIndex { id: string; name: string; fields: string[]; unique: boolean; sparse: boolean; created_at: string; } interface CreateIndexRequest { name: string; fields: string[]; unique?: boolean; sparse?: boolean; } interface IndexAnalysis { recommendations: IndexRecommendation[]; slow_queries: SlowQueryInfo[]; existing_indexes: TableIndex[]; summary: { total_queries: number; slow_query_count: number; avg_response_time: number; index_utilization: number; recommended_count: number; }; } interface IndexRecommendation { fields: string[]; reason: string; estimated_improvement: string; priority: string; query_count: number; avg_query_time: number; } interface SlowQueryInfo { pattern: string; count: number; avg_time: number; } type RelationType = "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many"; interface TableRelation { id: string; app_id: string; source_table: string; source_field: string; target_table: string; target_field: string; relation_type: RelationType; alias: string; cascade_delete: boolean; created_at: string; updated_at: string; } interface CreateRelationRequest { source_table: string; source_field: string; target_table: string; target_field?: string; relation_type: RelationType; alias?: string; cascade_delete?: boolean; } interface PopulateOption { field: string; from?: string; as?: string; select?: string[]; limit?: number; orderBy?: string; order?: "asc" | "desc"; populate?: PopulateOption[]; } type TriggerEvent = "create" | "update" | "delete" | "all"; type TriggerHandlerType = "function" | "workflow" | "webhook" | "inline"; interface Trigger { id: string; app_id: string; name: string; table_name: string; event: TriggerEvent; condition?: Record; handler_type: TriggerHandlerType; handler_id?: string; handler_url?: string; handler_action?: Record; is_active: boolean; order: number; created_at: string; updated_at: string; } interface CreateTriggerRequest { name: string; table_name: string; event: TriggerEvent; handler_type: TriggerHandlerType; handler_id?: string; handler_url?: string; handler_action?: Record; condition?: Record; is_active?: boolean; order?: number; } interface UpdateTriggerRequest { name?: string; event?: TriggerEvent; handler_type?: TriggerHandlerType; handler_id?: string; handler_url?: string; handler_action?: Record; condition?: Record; is_active?: boolean; order?: number; } interface AggregateStage { $match?: Record; $group?: { _id: string | Record; [key: string]: unknown; }; $sort?: Record; $limit?: number; $skip?: number; $project?: Record; $unwind?: string; $count?: string; $lookup?: { from: string; localField: string; foreignField: string; as: string; }; $bucket?: { groupBy: string; boundaries: unknown[]; default?: unknown; output?: Record; }; $bucketAuto?: { groupBy: string; buckets: number; output?: Record; granularity?: string; }; $addFields?: Record; } interface AggregateResult { results: Record[]; count: number; } interface SearchOptions { case_sensitive?: boolean; whole_word?: boolean; fuzzy?: boolean; fuzzy_distance?: number; highlight?: boolean; limit?: number; offset?: number; min_score?: number; } interface SearchResult { id: string; data: Record; score: number; highlights?: Record; } interface SearchResponse { results: SearchResult[]; total_count: number; query: string; } /** * 지리 좌표. 서버는 `{ lat, lng }`, `{ latitude, longitude }`, `[lng, lat]` 세 형식을 모두 받지만 * SDK 는 `{ lat, lng }` 를 표준으로 사용한다. */ interface GeoPoint { lat: number; lng: number; } interface GeoNear { center: GeoPoint; /** 최대 거리 (미터) */ max_distance: number; /** 최소 거리 (미터, 도넛 검색) */ min_distance?: number; } interface GeoBoundingBox { bottom_left: GeoPoint; top_right: GeoPoint; } interface GeoPolygon { /** 폴리곤 꼭지점 (최소 3개) */ points: GeoPoint[]; } /** * near / box / polygon 중 정확히 하나를 지정한다. */ interface GeoQuery { near?: GeoNear; box?: GeoBoundingBox; polygon?: GeoPolygon; } interface GeoResult { id: string; data: Record; distance: number; } interface GeoResponse { results: GeoResult[]; total_count: number; } type AtomicOperatorType = "increment" | "serverTimestamp" | "deleteField" | "arrayUnion" | "arrayRemove" | "arrayPush" | "min" | "max" | "objectMerge" | "objectSet" | "geoPoint"; interface AtomicOperator { type: AtomicOperatorType; value?: unknown; } interface BatchOperation { type: "create" | "update" | "delete"; table_id: string; doc_id?: string; data?: Record; operators?: Record; } interface TransactionRead { table_id: string; doc_id: string; alias?: string; } interface TransactionWrite { type: "create" | "update" | "delete"; table_id: string; doc_id?: string; data?: Record; operators?: Record; precondition?: { version?: number; exists?: boolean; }; } interface BatchOperationResult { index: number; success: boolean; doc_id?: string; error?: string; } interface BatchWriteResult { success: boolean; results: BatchOperationResult[]; total_count?: number; success_count?: number; failed_count?: number; } interface TransactionWriteResult { index: number; doc_id?: string; success: boolean; } interface TransactionResult { success: boolean; transaction_id?: string; reads?: Record; results?: TransactionWriteResult[]; error?: string; } interface TTLConfig { table_name: string; field: string; enabled: boolean; } interface RetentionPolicy { table_name: string; retention_days: number; date_field?: string; action: "delete" | "archive"; archive_table?: string; schedule?: string; enabled: boolean; } /** 데이터베이스 실시간 구독 변경 타입 */ type DatabaseChangeType = "added" | "modified" | "removed"; /** 데이터베이스 실시간 변경 이벤트 */ interface DatabaseChange { type: DatabaseChangeType; doc_id: string; data?: Record; old_data?: Record; metadata?: { has_pending_writes: boolean; updated_at: string; }; } /** 데이터베이스 실시간 스냅샷 */ interface DatabaseSnapshot { id: string; data: Record | null; exists: boolean; metadata?: { updated_at: string; }; } /** 데이터베이스 실시간 구독 옵션 */ interface DatabaseSubscribeOptions { /** 특정 문서 ID만 구독 */ docId?: string; /** 쿼리 필터 조건 */ where?: DatabaseRealtimeFilter[]; /** true면 자기 변경도 수신 (기본: false) */ includeSelf?: boolean; /** 메타데이터 변경도 수신 (기본: false) */ includeMetadataChanges?: boolean; } /** 데이터베이스 실시간 필터 조건 */ interface DatabaseRealtimeFilter { field: string; operator: "==" | "!=" | ">" | ">=" | "<" | "<=" | "in"; value: unknown; } /** 데이터베이스 실시간 스냅샷 메시지 */ interface DatabaseSnapshotMessage { subscription_id: string; docs: DatabaseSnapshot[]; total_count: number; has_more: boolean; /** 다음 페이지 offset (has_more가 true일 때만 존재) */ next_offset?: number; } /** 데이터베이스 실시간 변경 메시지 */ interface DatabaseChangeMessage { subscription_id: string; changes: DatabaseChange[]; } /** 데이터베이스 실시간 구독 핸들러 */ interface DatabaseRealtimeHandlers { /** 초기 스냅샷 수신 시 */ onSnapshot?: (docs: DatabaseSnapshot[], info: { totalCount: number; hasMore: boolean; nextOffset?: number; }) => void; /** 변경 이벤트 수신 시 */ onChange?: (changes: DatabaseChange[]) => void; /** 에러 발생 시 */ onError?: (error: Error) => void; } /** 데이터베이스 실시간 구독 객체 */ interface DatabaseRealtimeSubscription { /** 구독 ID */ subscriptionId: string; /** 구독 해제 */ unsubscribe(): void; /** 다음 페이지 스냅샷 로드 (has_more가 true일 때 사용) */ loadMore(offset: number, limit?: number): void; } /** 데이터베이스 실시간 연결 옵션 */ interface DatabaseRealtimeConnectOptions { /** * 액세스 토큰 (필수). 두 종류를 모두 지원: * - 앱 멤버(AppMember) JWT — 자기 앱의 테이블 구독 * - cross-app OAuth access token — Provider 앱의 테이블 구독 (`database:read` scope 필요) */ accessToken: string; /** data-server URL (기본: baseUrl) */ dataServerUrl?: string; /** 자동 재연결 최대 시도 (기본: 5) */ maxRetries?: number; /** 재연결 간격 밀리초 (기본: 1000) */ retryInterval?: number; /** 디버그 로깅 (기본: false) */ debug?: boolean; } interface CreateBackupRequest { /** 백업 이름 */ name: string; /** 특정 테이블만 백업 (비어있으면 전체) */ tables?: string[]; /** gzip 압축 여부 */ compress?: boolean; /** AES-256-GCM 암호화 여부 (SECRET_ENCRYPTION_KEY 필요) */ encrypt?: boolean; } interface BackupInfo { id: string; app_id: string; name: string; type: string; status: "pending" | "completed" | "failed" | "in_progress"; tables: string[]; storage_path?: string; size_bytes?: number; compressed: boolean; encrypted: boolean; completed_at?: string; error_message?: string; metadata?: Record; created_at: string; updated_at: string; } interface RestoreBackupRequest { /** 백업 ID */ backup_id: string; /** 특정 테이블만 복원 (비어있으면 전체) */ tables?: string[]; /** 복원 모드: replace (기존 삭제 후 삽입, 기본) 또는 merge (기존 유지) */ mode?: "replace" | "merge"; } interface RestoreBackupResponse { success: boolean; tables_restored: number; rows_restored: number; message?: string; errors?: string[]; } interface ExportDataRequest { /** 내보낼 테이블 이름 (비어있으면 전체) */ tables?: string[]; /** 내보내기 형식 */ format?: "json" | "csv" | "ndjson"; /** 스키마 포함 여부 */ include_schema?: boolean; } interface ExportDataResponse { data?: Record; schema?: Record; } /** * 데이터 가져오기 요청 * @example * { * data: { * users: { rows: [{ name: 'Alice', age: 30 }] }, * orders: { rows: [{ user: 'Alice', amount: 1000 }] } * }, * mode: 'merge' * } */ interface ImportDataRequest { /** 가져올 데이터: { "테이블이름": { "rows": [{...}, ...] }, ... } */ data: Record[]; }>; /** 가져오기 모드: merge (기본), replace, skip */ mode?: "merge" | "replace" | "skip"; /** 시뮬레이션만 수행 */ dry_run?: boolean; } interface ImportDataResponse { success: boolean; rows_imported: number; rows_skipped: number; rows_failed: number; errors?: string[]; } interface CopyTableRequest { /** 원본 테이블 이름 */ source_table: string; /** 대상 테이블 이름 */ target_table: string; /** 데이터 복사 여부 (기본: true) */ copy_data?: boolean; /** 스키마 복사 여부 (기본: true) */ copy_schema?: boolean; } interface CopyTableResponse { success: boolean; source_table: string; target_table: string; table_id: string; rows_copied: number; message?: string; } interface MigrateDataRequest { /** 원본 테이블 이름 */ source_table: string; /** 대상 테이블 이름 */ target_table: string; /** 필드 매핑: { "old_field": "new_field", ... } */ transform?: Record; /** 필터 조건: { "field": "value", ... } */ filter?: Record; /** 마이그레이션 후 원본 삭제 */ delete_source?: boolean; /** 시뮬레이션만 수행 */ dry_run?: boolean; } interface MigrateDataResponse { success: boolean; rows_migrated: number; rows_deleted?: number; rows_failed: number; errors?: string[]; } interface SearchIndex { id: string; table_id: string; name: string; fields: string[]; weights?: Record; synonyms?: string[][]; language?: string; min_length?: number; created_at: string; updated_at?: string; } interface CreateSearchIndexRequest { name: string; fields: string[]; weights?: Record; synonyms?: string[][]; language?: string; min_length?: number; } interface GeoIndex { id: string; table_id: string; field: string; precision?: number; created_at: string; updated_at?: string; } interface CreateGeoIndexRequest { field: string; precision?: number; } interface ArchivePolicy { table_name: string; condition: Record; destination_table: string; schedule?: string; delete_after_archive?: boolean; enabled: boolean; } interface LifecyclePolicy { id: string; type: "ttl" | "retention" | "archive"; table_name: string; enabled: boolean; config: Record; created_at: string; updated_at?: string; } declare class DatabaseAPI { private http; private realtimeWs; private realtimeState; private realtimeHandlers; private realtimeRetryCount; private realtimeOptions; private pendingRequests; private pingInterval; private realtimeOnStateChange; private realtimeOnError; private activeSubscriptions; constructor(http: HttpClient); /** * Database 공개 API 접두사. * * 서버의 테이블/컬럼/데이터 CRUD 는 모두 `/v1/public/*` 경로에서만 제공되며 * (core-server → data-server 프록시), JWT 기반 `/v1/*` 경로는 존재하지 않습니다. * 따라서 항상 `/v1/public` 을 사용합니다. 인증은 `X-Public-Key` 헤더에 * `publicKey` (`cb_pk_*`) 를 실어 서버가 검증합니다. */ private getPublicPrefix; /** * 테이블 목록 조회 */ getTables(): Promise; /** * 테이블 상세 조회 */ getTable(tableId: string): Promise; /** * 테이블 생성. * * 서버 DTO (`{title, schema, access_level}`) 로 변환하여 전송합니다. * `schema` 가 비어있으면 요청 본문에서 키 자체를 생략하여 빈 테이블을 * 생성합니다. (서버는 explicit 빈 맵을 거부) * * 서버는 `{message}` 만 돌려주므로 반환 타입은 `void` 입니다. * 생성된 테이블 메타데이터가 필요하면 이어서 `getTables()` 로 조회하세요. */ createTable(data: CreateTableRequest): Promise; /** * 테이블 수정. * * `name` 은 서버의 `title` 로, `accessLevel` 은 `access_level` 로 매핑됩니다. */ updateTable(tableId: string, data: Partial): Promise; /** * 테이블 삭제 */ deleteTable(tableId: string): Promise; /** * 테이블 검증 스키마 조회. * * 검증 스키마가 설정되어 있지 않으면 `null` 을 반환합니다. * Public Key 인증으로 호출 가능 (조회 권한). */ getValidationSchema(tableId: string): Promise; /** * 테이블 검증 스키마 설정/교체. * * **권한**: 콘솔 (앱 소유자 JWT) 전용. Public Key 로는 설정 불가. * SDK 에서는 `accessToken` 이 설정되어 있어야 하며, 콘솔에서 사용하는 패턴입니다. * * 빈 fields 는 거부됩니다. 검증 스키마를 제거하려면 `deleteValidationSchema` 사용. * * @example * ```ts * await cb.database.setValidationSchema(tableId, { * fields: { * email: { type: 'string', pattern: '^.+@.+$', required: true }, * age: { type: 'int', min: 0, max: 150 } * }, * required: ['email'], * immutable: ['email'] * }) * ``` */ setValidationSchema(tableId: string, schema: ValidationSchema): Promise; /** * 테이블 검증 스키마 제거 (schemaless 모드로 복귀). * * **권한**: 콘솔 (앱 소유자 JWT) 전용. */ deleteValidationSchema(tableId: string): Promise; /** * appId 가 설정되어 있어야 콘솔 경로를 호출할 수 있습니다 (validation_schema set/delete). */ private requireAppId; /** * 컬럼 목록 조회 (`Table.schema` 맵을 컬럼 객체 배열로 변환). * * 서버는 컬럼을 다음 두 가지 중 하나로 저장합니다: * - 플랫: `"email": "string"` (추가 속성이 없는 경우) * - 중첩: `"email": { "type": "string", "required": true, ... }` (추가 속성이 있는 경우) * * `$required` 키는 별도 배열로 필수 컬럼을 지정할 수 있으며, 중첩 객체의 * `required: true` 와 병합되어 판정됩니다. */ getColumns(tableId: string): Promise; /** * 컬럼 생성. * * 서버는 `{message}` 만 돌려주므로 반환 타입은 `void` 입니다. * 생성 결과 컬럼을 얻으려면 이어서 `getColumns(tableId)` 로 조회하세요. */ createColumn(tableId: string, data: CreateColumnRequest): Promise; /** * 컬럼 수정. * * 서버는 `{message}` 만 돌려주므로 반환 타입은 `void` 입니다. */ updateColumn(tableId: string, columnName: string, data: UpdateColumnRequest): Promise; /** * 컬럼 삭제 */ deleteColumn(tableId: string, columnName: string): Promise; /** * 데이터 조회 (페이지네이션) */ getData(tableId: string, options?: QueryOptions): Promise; /** * 조건부 데이터 조회 (Where, OrderBy, Select, Exclude) */ queryData(tableId: string, options: QueryOptions): Promise; /** * 단일 데이터 조회 */ getDataById(tableId: string, dataId: string): Promise; /** * 데이터 생성. * * `tableIdOrName` 은 테이블 UUID 또는 이름. UUID 형식이면 기존 경로 * (`/tables/:tableId/data`), 그렇지 않으면 이름 기반 경로 * (`/tables/name/:tableName/data`) 를 사용합니다. * * `options.autoCreate: true` 시 테이블이 없으면 빈 schemaless 테이블을 * 자동으로 생성한 뒤 insert. opt-in 이며 프로토타입/AI 자동화 워크플로에 * 권장합니다. production 앱은 명시적 `createTable` 호출 권장. */ createData(tableIdOrName: string, data: CreateDataRequest, options?: { autoCreate?: boolean; }): Promise; /** * 데이터 수정 (부분 업데이트) * 제공된 필드만 수정되고, 기존 필드는 유지됩니다. */ updateData(tableId: string, dataId: string, data: UpdateDataRequest): Promise; /** * 데이터 삭제 */ deleteData(tableId: string, dataId: string): Promise; /** * 여러 데이터 한번에 생성 (Bulk Create) */ createMany(tableId: string, items: CreateDataRequest[]): Promise; /** * 조건에 맞는 데이터 삭제 */ deleteWhere(tableId: string, where: WhereCondition): Promise; /** * 집계 파이프라인 실행 (MongoDB 스타일) */ aggregate(tableId: string, pipeline: AggregateStage[]): Promise; /** * 전문 검색 */ search(tableId: string, query: string, fields: string[], options?: SearchOptions): Promise; /** * 자동완성 검색 */ autocomplete(tableId: string, query: string, field: string, options?: { limit?: number; }): Promise; /** * 지리 쿼리 (반경, 박스, 폴리곤) */ geoQuery(tableId: string, field: string, query: GeoQuery, options?: { limit?: number; offset?: number; }): Promise; /** * 배치 쓰기 (여러 테이블 다중 문서 원자적 처리) * * 서버는 HTTP 200 + `success:false` + 개별 op `error` 로 부분 실패를 표현한다. * SDK 는 호출자가 silent success 로 오해하지 않도록 첫 실패 op 의 메시지로 throw 한다. */ batch(operations: BatchOperation[]): Promise; /** * 트랜잭션 실행 (읽기 → 쓰기 ACID) * * 서버는 부분 실패가 없는 ACID 트랜잭션을 보장하지만, 검증/RLS/충돌은 * `success:false` + `error` 로 알린다. SDK 는 silent success 회귀 방지 차원에서 throw. */ transaction(reads: TransactionRead[], writes: TransactionWrite[]): Promise; /** * 데이터 조회 시 릴레이션 로딩 (JOIN) */ getDataWithPopulate(tableId: string, options: QueryOptions & { populate: PopulateOption[]; }): Promise; /** * 보안 규칙 목록 조회 */ listSecurityRules(appId: string): Promise; /** * 보안 규칙 생성 */ createSecurityRule(appId: string, data: CreateSecurityRuleRequest): Promise; /** * 보안 규칙 수정 */ updateSecurityRule(appId: string, ruleId: string, data: UpdateSecurityRuleRequest): Promise; /** * 보안 규칙 삭제 */ deleteSecurityRule(appId: string, ruleId: string): Promise; /** * 테이블 인덱스 목록 조회 */ listIndexes(appId: string, tableId: string): Promise; /** * 인덱스 생성 */ createIndex(appId: string, tableId: string, data: CreateIndexRequest): Promise; /** * 인덱스 삭제 */ deleteIndex(appId: string, tableId: string, indexId: string): Promise; /** * 인덱스 분석 및 추천 */ analyzeIndexes(appId: string, tableId: string): Promise; listSearchIndexes(appId: string, tableId: string): Promise; createSearchIndex(appId: string, tableId: string, data: CreateSearchIndexRequest): Promise; deleteSearchIndex(appId: string, tableId: string, indexId: string): Promise; listGeoIndexes(appId: string, tableId: string): Promise; createGeoIndex(appId: string, tableId: string, data: CreateGeoIndexRequest): Promise; deleteGeoIndex(appId: string, tableId: string, indexId: string): Promise; /** * 테이블 릴레이션 목록 조회. `sourceTable` 생략 시 앱 전체의 릴레이션을 반환. * * 서버 경로: `GET /v1/apps/:appID/databases/relations[?source_table=...]` */ listRelations(appId: string, sourceTable?: string): Promise; /** * 릴레이션 생성. * * 서버 경로: `POST /v1/apps/:appID/databases/relations` — body 내 `source_table`/`target_table` 로 테이블 지정. */ createRelation(appId: string, data: CreateRelationRequest): Promise; /** * 릴레이션 삭제. `relationId` 는 서버에서 발급한 UUID (listRelations 로 조회해 얻음). * * 서버 경로: `DELETE /v1/apps/:appID/databases/relations/:relationID` */ deleteRelation(appId: string, relationId: string): Promise; /** * 트리거 목록 조회 */ listTriggers(appId: string): Promise; /** * 트리거 생성 */ createTrigger(appId: string, data: CreateTriggerRequest): Promise; /** * 트리거 수정 */ updateTrigger(appId: string, triggerId: string, data: UpdateTriggerRequest): Promise; /** * 트리거 삭제 */ deleteTrigger(appId: string, triggerId: string): Promise; /** * TTL 정책 설정 */ setTTL(appId: string, config: TTLConfig): Promise; /** * TTL 정책 조회 */ getTTL(appId: string, tableName: string): Promise; /** * 보관 정책 설정 */ setRetentionPolicy(appId: string, policy: RetentionPolicy): Promise; /** * 보관 정책 조회 */ getRetentionPolicy(appId: string, tableName: string): Promise; setArchivePolicy(appId: string, policy: ArchivePolicy): Promise; getArchivePolicy(appId: string, tableName: string): Promise; executeTTL(appId: string, tableName: string): Promise<{ deleted: number; }>; executeArchive(appId: string, tableName: string): Promise<{ archived: number; }>; executeRetention(appId: string, tableName: string): Promise<{ deleted: number; }>; listPolicies(appId: string): Promise; deletePolicy(appId: string, policyId: string): Promise; generateTypes(appId: string): Promise; listBackups(appId: string): Promise<{ backups: BackupInfo[]; total_count: number; }>; createBackup(appId: string, data: CreateBackupRequest): Promise; getBackup(appId: string, backupId: string): Promise; deleteBackup(appId: string, backupId: string): Promise; restoreBackup(appId: string, backupId: string, options?: RestoreBackupRequest): Promise; exportData(appId: string, options?: ExportDataRequest): Promise; importData(appId: string, data: ImportDataRequest): Promise; copyTable(appId: string, data: CopyTableRequest): Promise; migrateData(appId: string, data: MigrateDataRequest): Promise; /** * 데이터베이스 실시간 연결 * data-server의 WebSocket에 연결하여 데이터 변경을 실시간으로 수신합니다. * * `accessToken` 은 두 종류를 모두 받습니다: * - 앱 멤버(AppMember) JWT — 자기 앱의 테이블을 구독 * - cross-app OAuth access token — Provider 앱이 `database:read` scope 를 노출했을 때, * Consumer 가 Provider 앱의 테이블을 구독 (구독 대상 앱은 토큰 `aud` 로 결정되므로 * Provider 의 publicKey 는 불필요). RLS 는 토큰 `end_user_id` 를 subject 로 평가합니다. * * @example * ```typescript * // 자기 앱 테이블 구독 (AppMember JWT) * cb.database.connectRealtime({ * accessToken: 'your-jwt-token', * dataServerUrl: 'https://data.connectbase.world' * }) * * // cross-app: Provider 앱의 테이블 구독 (OAuth access token) * cb.database.connectRealtime({ * accessToken: oauthAccessToken, * dataServerUrl: 'https://data.connectbase.world' * }) * * // 테이블 구독 * const sub = cb.database.subscribe('users', { * onSnapshot: (docs, info) => { * console.log('Initial data:', docs, 'total:', info.totalCount) * }, * onChange: (changes) => { * changes.forEach(change => { * console.log(change.type, change.doc_id, change.data) * }) * }, * onError: (error) => console.error(error) * }) * * // 구독 해제 * sub.unsubscribe() * * // 연결 해제 * cb.database.disconnectRealtime() * ``` */ connectRealtime(options: DatabaseRealtimeConnectOptions): Promise; /** * 데이터베이스 실시간 연결 해제 */ disconnectRealtime(): void; /** * 테이블 또는 문서의 실시간 변경 구독 * * @param tableId 구독할 테이블 이름 * @param handlers 이벤트 핸들러 (onSnapshot, onChange, onError) * @param options 구독 옵션 (docId, where, includeSelf 등) * @returns 구독 해제 가능한 객체 */ subscribe(tableId: string, handlers: DatabaseRealtimeHandlers, options?: DatabaseSubscribeOptions): DatabaseRealtimeSubscription; /** * 실시간 연결 상태 확인 */ isRealtimeConnected(): boolean; /** * 실시간 연결 상태 반환 */ getRealtimeState(): "disconnected" | "connecting" | "connected"; /** * 상태 변경 콜백 등록 */ onRealtimeStateChange(handler: (state: "disconnected" | "connecting" | "connected") => void): () => void; /** * 에러 콜백 등록 */ onRealtimeError(handler: (error: Error) => void): () => void; private setRealtimeState; private doRealtimeConnect; /** * 서버에 subscribe 메시지를 보내고 서버 subscription_id를 반환하는 Promise */ private sendSubscribeRequest; /** * 재연결 후 모든 활성 구독을 서버에 다시 등록 */ private resubscribeAll; private handleRealtimeMessage; private attemptRealtimeReconnect; private startRealtimePing; private stopRealtimePing; private sendRealtimeMessage; private generateRequestId; private debugLog; } /** * EndpointAPI — 사용자 PC GPU 모델을 `cb_pk_*` 한 키로 호출하는 dumb pipe. * * 핵심 비전: ConnectBase 는 **모델·API·워크플로우를 알지 않는다**. 사용자가 자기 PC 에서 * 자기 모델을 띄우고 자기 API 를 정한다. SDK 는 라벨 → tunnel 매핑만 알고 페이로드 * 그대로 forward. * * @example ComfyUI 호출 * ```typescript * const cb = new ConnectBase({ publicKey: "cb_pk_..." }) * const res = await cb.endpoint.call("comfyui-main", { * method: "POST", * path: "/prompt", * body: JSON.stringify({ * prompt: { * // ComfyUI 노드 그래프 * }, * }), * headers: { "Content-Type": "application/json" }, * }) * const data = await res.json() * ``` * * @example 스트리밍 응답 (SSE / chunked) * ```typescript * const res = await cb.endpoint.call("vllm-local", { * method: "POST", * path: "/v1/chat/completions", * body: JSON.stringify({ * stream: true, * messages: [ * // { role, content } * ], * }), * headers: { "Content-Type": "application/json" }, * }) * if (!res.body) throw new Error("no stream") * const reader = res.body.getReader() * while (true) { * const { done, value } = await reader.read() * if (done) break * // value 는 Uint8Array — 디코드 후 처리 * } * ``` * * @example AbortSignal 으로 취소 * ```typescript * const ctrl = new AbortController() * setTimeout(() => ctrl.abort(), 30_000) // 30초 후 취소 * const res = await cb.endpoint.call("hunyuan-laptop", { * method: "POST", * path: "/generate", * signal: ctrl.signal, * body: JSON.stringify({ * // 모델 입력 * }), * }) * ``` */ declare class EndpointAPI { private http; constructor(http: HttpClient); /** * 라벨 + path 로 사용자 PC 모델 호출. fetch() 시그니처 호환. * * 동작: * - URL 조립: `${baseUrl}/v1/proxy/${label}${path}` * - X-Public-Key 헤더 자동 주입 (호출자가 명시하면 그 값 우선) * - body / method / 추가 헤더 / signal 그대로 전달 * - 응답 그대로 반환 (Response 객체) — 스트리밍은 res.body 로 read * * @param label - 콘솔에서 등록한 endpoint 라벨 (예: "comfyui-main") * @param init - fetch() 의 RequestInit + path. path 는 사용자 모델 서버의 엔드포인트 경로 (예: "/prompt", "/v1/chat/completions"). */ call(label: string, init: EndpointCallInit): Promise; /** * 라벨 + path 의 최종 호출 URL `${baseUrl}/v1/proxy/${label}${path}` 을 조립해서 * 반환. URL 을 다른 시스템 (Service Worker, 백엔드 워커, 로깅) 에 넘기거나 * 디버깅 용도일 때 사용. * * ⚠️ **`` / 네이티브 `WebSocket` / `