declare class PalbaseError extends Error { readonly code: string; readonly status: number; readonly details?: unknown; constructor(code: string, message: string, status: number, details?: unknown); } interface PalbaseResponse { data: T | null; error: PalbaseError | null; count?: number; status: number; /** * The response's `ETag`, when it carried one. * * Lifted out of the header bag for the same reason `count` is (it comes from * `Content-Range`): a caller doing conditional requests needs exactly this * one value, and the validator it must echo is the SERVER's — not something * the caller can reconstruct. Offering a validator of one's own invention is * how a conditional request quietly stops being conditional: measured on * centauri 2026-09-11, where the flags poll sent `If-None-Match: 0` against * an `etag: "15-e0e4f431743eb0c5"` and so could never once be answered 304. */ etag?: string; } interface HttpClientOptions { /** * Auth requests are sealed automatically. Self-hosted stacks can pin their * own base64 Ed25519 root. `false` is an explicit opt-out for test servers or * older self-hosted stacks; it cannot satisfy a server requiring sealing. */ sealed?: { root?: string; stackRef?: string; } | false; /** * The host application's own version, sent as `X-Palbase-Client-Version` and * readable in flag targeting as `client.app_version`. * * Opt-in because the SDK cannot discover it on the web — there is no bundle * to read a version out of. iOS fills the same header automatically from * CFBundleShortVersionString. */ appVersion?: string; url?: string; headers?: Record; } interface RequestOptions { headers?: Record; body?: unknown; signal?: AbortSignal; /** Disable network/429 replay for one-time exchanges. Protocol retries remain bounded. */ retry?: boolean; /** Credential/token exchanges must not refresh an unrelated or expired bearer first. */ skipSessionRefresh?: boolean; } interface Session { accessToken: string; refreshToken: string; expiresAt: number; } type AuthStateEvent = 'SESSION_SET' | 'SESSION_CLEARED'; type AuthStateCallback = (event: AuthStateEvent, session: Session | null) => void; type Unsubscribe = () => void; declare class TokenManager { private session; private listeners; private refreshPromise; private refreshing; refreshFunction: ((refreshToken: string) => Promise) | null; setSession(session: Session): void; getAccessToken(): string | null; getRefreshToken(): string | null; clearSession(): void; isExpired(): boolean; refreshSession(): Promise; onAuthStateChange(callback: AuthStateCallback): Unsubscribe; private executeRefresh; private notify; } /** * Request interceptor. Runs before every HTTP request. * Can modify headers, body, or reject the request. */ type RequestInterceptor = (request: { headers: Record; method: string; path: string; }) => void | Promise; declare class HttpClient { protected readonly apiKey: string; protected readonly options?: HttpClientOptions; tokenManager: TokenManager | null; /** * Admin JWT used for platform admin endpoints (/admin/*). * When set, takes precedence over tokenManager access token in the * Authorization header. */ adminToken: string | null; private readonly interceptors; private readonly forbiddenListeners; private sealedKeys?; constructor(apiKey: string, options?: HttpClientOptions); /** Set (or clear) the admin JWT used on admin endpoints. */ setAdminToken(token: string | null): void; /** * Called with the RAW body of every 403 this client receives (FR-010). Core * does not know the envelope's fields — the layer that does (palbe) reads * `required` out of it. Nothing is retried or swallowed: the error still * returns to the caller. */ onForbidden(handler: (body: Record) => void): () => void; /** * Create a scoped HttpClient that adds the given extra headers to every * request. The returned client shares the admin token and token manager * with the parent at runtime — later changes on the parent propagate to * the scope and vice versa. * * Typical use: adding an Environment-routing header for an admin call. */ withHeaders(extra: Record): HttpClient; /** Add a request interceptor. Runs before every request. */ addInterceptor(interceptor: RequestInterceptor): void; request(method: string, path: string, options?: RequestOptions): Promise>; /** * A response read as it arrives, for `text/event-stream` routes. * * Deliberately NOT `executeWithRetry`: a retry replays the request, and a * stream the caller has already begun reading cannot be replayed — the frames * it handed over would arrive a second time. A stream that fails to open fails * to the caller, once, with its status. * * The buffered path's headers, base URL and interceptors are reused verbatim, * so a streaming call is authenticated exactly like every other call; only the * body handling differs. `Accept` says what the caller wants, and the status * is returned beside the body because the CALLER decides what a non-2xx means * (an error envelope arrives as an ordinary buffered body). */ requestStream(method: string, path: string, options?: RequestOptions): Promise<{ status: number; body: ReadableStream | null; contentType: string; }>; private prepareSealed; private getBaseUrl; private buildHeaders; private executeWithRetry; private delay; } export { type HttpClientOptions as H, type Session as S, TokenManager as T, HttpClient as a };