@antzsoft/chat-core 1.4.6 → 1.4.8
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/README.md +86 -7
- package/dist/chat.store-TA6G7PD6.js +7 -0
- package/dist/{chunk-U637W5MD.js → chunk-L537XWRD.js} +12 -4
- package/dist/chunk-L537XWRD.js.map +1 -0
- package/dist/chunk-QHELYVNT.js +109 -0
- package/dist/chunk-QHELYVNT.js.map +1 -0
- package/dist/index.cjs +145 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +82 -9
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +1 -1
- package/docs/integration-guide.html +19 -5
- package/package.json +1 -1
- package/dist/chat.store-UVTDBPEC.js +0 -7
- package/dist/chunk-U637W5MD.js.map +0 -1
- package/dist/chunk-UIYJAOGL.js +0 -62
- package/dist/chunk-UIYJAOGL.js.map +0 -1
- /package/dist/{chat.store-UVTDBPEC.js.map → chat.store-TA6G7PD6.js.map} +0 -0
package/dist/internal.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal.ts","../src/compression/compress.ts","../src/api/client.ts","../src/errors.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["// Internal exports for use by the web and RN SDKs only.\n// Not part of the public API — do not use in application code.\nexport { uploadBatchWithSlots } from './api/storage.js';\nexport { generateUUID } from './crypto/uuid.js';\n","import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, awaitTransitReadyOr, configureTransit, setTransitSession, getTransitSession } from '../crypto/session.js';\nimport { createRestTransitSession, fetchServerKeys, TransitRateLimitedError } from '../crypto/handshake.js';\nimport { detectTransitAlgo } from '../crypto/detect.js';\nimport { normalizeAxiosError, AntzChatNetworkError } from '../errors.js';\n\n// Hard ceiling on how long a single request will block waiting for the transit\n// handshake. A legitimate cold-start handshake resolves in well under this even\n// on a slow link (TransitGate already spent ~6s, establishTransit keeps\n// retrying). Past this we FAIL the request with a retryable error rather than\n// leave it pending forever — react-query cannot retry / refetch-on-focus a\n// request that never settles, so an unbounded wait here is an unrecoverable\n// silent hang. Each failed+retried request also re-arms ensureRestTransitHandshake.\nconst TRANSIT_GATE_MAX_WAIT_MS = 30_000;\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\nlet _avatarSent = false;\n// In-flight transit handshake promise — shared between initApiClient and connectSocket\n// so they never fire two concurrent HTTPS handshakes for the same session.\nlet _transitHandshakePromise: Promise<void> | null = null;\n// Gate the request interceptor until the SDK has resolved a token (async\n// authProvider) and, where wired, the transit handshake. Set by the SDK\n// provider; the interceptor awaits it before attaching the Authorization\n// header so early requests (e.g. useConversations' initial fetch) don't race\n// ahead unauthenticated.\nlet _authReadyPromise: Promise<unknown> | null = null;\n\nexport function getTransitHandshakePromise(): Promise<void> | null {\n return _transitHandshakePromise;\n}\n\nexport function setAuthReadyPromise(promise: Promise<unknown> | null): void {\n _authReadyPromise = promise;\n}\n\n// True once initApiClient() has run and its config has not been torn down by a\n// subsequent disconnectSocket(). Lets the SDK provider detect the case where a\n// React remount skipped re-init (its key was unchanged) but disconnectSocket()\n// had nulled _config in between — leaving the request interceptor unable to see\n// transitEncryption and firing every request as unencrypted plaintext.\nexport function isApiClientConfigured(): boolean {\n return _config !== null;\n}\n\n// (Re-)kick the HTTPS transit handshake. Idempotent: no-ops when transit is\n// disabled, a session already exists, or an attempt is already in flight.\n// Called both at init and from the request interceptor when a request is about\n// to block on waitForTransitReady() with no session — e.g. after a socket\n// disconnect cleared the session and nothing else re-established it.\n//\n// It only calls configureTransit(false) — which un-gates the interceptor and\n// lets requests go out as PLAINTEXT — when the server itself reports transit\n// disabled (GET /crypto/pubkey → enabled:false, i.e. an old server). A transient\n// failure of POST /crypto/session (network blip, rate limit, 5xx) must NOT\n// disable transit: the server still requires it, so plaintext would just 403.\n// Instead we retry with backoff; waitForTransitReady() keeps requests pending\n// and they dispatch the moment a retry sets the session.\nexport function ensureRestTransitHandshake(): void {\n if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;\n const apiUrl = _config.apiUrl;\n _transitHandshakePromise = (async () => {\n try {\n // A 429 is \"wait\", not \"broken\", so it must NOT consume the attempt\n // budget — otherwise a rate-limited client exhausts 5 attempts in a few\n // seconds and gives up on an endpoint that was working fine. Failures are\n // counted separately from rate-limit hits, and the loop is additionally\n // bounded by wall-clock time so a persistently limited server cannot keep\n // it running forever.\n const MAX_FAILURES = 5;\n const BACKSTOP_MS = 2 * 60_000;\n const deadline = Date.now() + BACKSTOP_MS;\n let failures = 0;\n let rateLimitHits = 0;\n\n while (failures < MAX_FAILURES && Date.now() < deadline) {\n if (getTransitSession()) return;\n let waitMs: number;\n try {\n // Identity is sent so the server can rate-limit these pre-auth routes\n // per user rather than per IP (see TransitIdentity in handshake.ts).\n const identity = { userId: _config?.userId, tenantId: _config?.tenantId };\n const keys = await fetchServerKeys(apiUrl, identity);\n if (!keys?.enabled) {\n configureTransit(false); // server genuinely doesn't want transit\n return;\n }\n const session = await createRestTransitSession(apiUrl, identity);\n if (session && !getTransitSession()) {\n const algo = typeof globalThis.crypto?.subtle !== 'undefined'\n ? await detectTransitAlgo()\n : 'x25519';\n setTransitSession({ sessionKey: session.sessionKey as CryptoKey, algo, sessionId: session.sessionId, enabled: true });\n return;\n }\n // Reachable when the server returned no session but did not throw\n // (e.g. an old server without the endpoint) — treat as a failure.\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n } catch (err) {\n if (err instanceof TransitRateLimitedError) {\n // Honour the server's own figure when it sent one (clamped to a\n // sane 1-60s), else escalate blind since the window is unknown.\n const blind = Math.min(15_000 * 2 ** rateLimitHits, 60_000);\n waitMs = err.retryAfterMs != null\n ? Math.min(Math.max(err.retryAfterMs, 1_000), 60_000)\n : blind;\n rateLimitHits++;\n console.warn(\n `[AntzChat] transit handshake rate-limited (429) — retrying in ${Math.round(waitMs / 1000)}s` +\n `${err.retryAfterMs != null ? ' (per Retry-After)' : ''}.`,\n );\n } else {\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n }\n }\n await new Promise((r) => setTimeout(r, waitMs));\n }\n console.error(\n '[AntzChat] transit handshake could not establish a session — ' +\n \"chat requests stay gated until one succeeds (server requires transit).\",\n );\n } finally {\n _transitHandshakePromise = null;\n }\n })();\n}\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // Kick off the HTTPS transit handshake immediately so REST calls that fire\n // before connectSocket (e.g. getMe() right after initApiClient) are not\n // blocked indefinitely. Store the promise so connectSocket can await it\n // instead of firing a duplicate handshake.\n ensureRestTransitHandshake();\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n // Wait for the SDK's auth (and, where wired, transit) gate before reading\n // the token — otherwise a request fired during boot goes out with no\n // Authorization header.\n if (_authReadyPromise) await _authReadyPromise;\n\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key before sending any request. The server\n // enforces transit encryption independent of auth (e.g. GET /app/config\n // fires before the async authProvider token resolves) — gating this on\n // `token` let those pre-auth requests race ahead of the handshake and get\n // rejected with 403 \"Transit encryption required\".\n if (_config?.transitEncryption) {\n // If the session is gone (socket disconnect cleared it, first boot still\n // pending), make sure a handshake is running before we block — otherwise\n // the wait could hang with nothing to resolve it.\n if (!getTransitSession()) ensureRestTransitHandshake();\n const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);\n if (!ready) {\n // Handshake still hasn't produced a session. Do NOT send plaintext\n // (server requires transit); fail loudly instead so the error surfaces\n // in the UI and react-query's retry re-drives the handshake.\n throw new AntzChatNetworkError(\n 'Secure channel to chat server not established — request not sent. It will retry automatically.',\n 'TRANSIT_NOT_READY',\n { url: req.url },\n );\n }\n }\n\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(normalizeAxiosError(error));\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","import { isAxiosError } from 'axios';\nimport { isTransitEnvelope } from './crypto/transit.js';\n\n// ─── Base error class ─────────────────────────────────────────────────────────\n\nexport class AntzChatError extends Error {\n readonly code: string;\n readonly retryable: boolean;\n readonly context?: Record<string, unknown>;\n\n constructor(\n code: string,\n message: string,\n retryable = false,\n context?: Record<string, unknown>,\n ) {\n super(message);\n this.name = 'AntzChatError';\n this.code = code;\n this.retryable = retryable;\n this.context = context;\n // Maintain proper prototype chain in transpiled environments\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Semantic subclasses ──────────────────────────────────────────────────────\n\n/** Thrown on 401 (after refresh also fails) or when no refresh token exists. */\nexport class AntzChatAuthError extends AntzChatError {\n constructor(message: string, code = 'AUTH_FAILED', context?: Record<string, unknown>) {\n super(code, message, false, context);\n this.name = 'AntzChatAuthError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 400 / 422 — bad input, validation failure. */\nexport class AntzChatValidationError extends AntzChatError {\n /** Server-returned field error array (when the server sends message as string[]). */\n readonly fields?: string[];\n\n constructor(message: string | string[], context?: Record<string, unknown>) {\n const msg = Array.isArray(message) ? message.join('; ') : message;\n super('VALIDATION_ERROR', msg, false, context);\n this.name = 'AntzChatValidationError';\n this.fields = Array.isArray(message) ? message : undefined;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on network failures, timeouts, socket disconnections, and queue overflow. retryable = true. */\nexport class AntzChatNetworkError extends AntzChatError {\n constructor(message: string, code = 'NETWORK_ERROR', context?: Record<string, unknown>) {\n super(code, message, true, context);\n this.name = 'AntzChatNetworkError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 403 — insufficient permissions. */\nexport class AntzChatPermissionError extends AntzChatError {\n constructor(message: string, context?: Record<string, unknown>) {\n super('PERMISSION_DENIED', message, false, context);\n this.name = 'AntzChatPermissionError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 5xx or other unexpected server errors. retryable = true. */\nexport class AntzChatServerError extends AntzChatError {\n readonly httpStatus?: number;\n\n constructor(message: string, httpStatus?: number, context?: Record<string, unknown>) {\n super('SERVER_ERROR', message, true, context);\n this.name = 'AntzChatServerError';\n this.httpStatus = httpStatus;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Error code reference ─────────────────────────────────────────────────────\n//\n// Code Class Source\n// ───────────────────── ──────────────────────── ─────────────────────────\n// AUTH_FAILED AntzChatAuthError 401 after refresh fails\n// SESSION_EXPIRED AntzChatAuthError 401, no refresh token\n// PERMISSION_DENIED AntzChatPermissionError 403\n// VALIDATION_ERROR AntzChatValidationError 400 / 422\n// NOT_FOUND AntzChatServerError 404\n// RATE_LIMITED AntzChatNetworkError 429\n// NETWORK_ERROR AntzChatNetworkError No response / conn failure\n// SOCKET_TIMEOUT AntzChatNetworkError ACK timeout / reconnect timeout\n// SOCKET_NOT_CONNECTED AntzChatNetworkError withAck when socket is down\n// SEND_QUEUE_FULL AntzChatNetworkError Queue overflow (>100 msgs)\n// MESSAGE_DROPPED AntzChatNetworkError Queue TTL expired (30s)\n// TRANSIT_MISMATCH AntzChatError SDK/server encryption config mismatch\n// SERVER_ERROR AntzChatServerError 5xx or unknown HTTP error\n\n// ─── REST error normaliser ────────────────────────────────────────────────────\n\n/**\n * Converts a raw axios error (or any unknown throw) into a typed AntzChatError.\n *\n * Call site: client.ts response interceptor — runs AFTER transit decryption,\n * so error.response.data is always plaintext by the time this function sees it.\n * If decryption itself failed, error.response.data remains the raw encrypted\n * envelope — detected via isTransitEnvelope() and noted in context.\n */\nexport function normalizeAxiosError(error: unknown): AntzChatError {\n if (error instanceof AntzChatError) return error;\n\n if (isAxiosError(error)) {\n const status = error.response?.status;\n const body = error.response?.data;\n\n // Detect if decryption failed — body is still an encrypted envelope\n const decryptionFailed = body != null && isTransitEnvelope(body);\n\n const rawMessage: string | string[] | undefined = decryptionFailed\n ? undefined\n : (body?.message ?? undefined);\n\n const message: string =\n (Array.isArray(rawMessage) ? rawMessage.join('; ') : rawMessage) ||\n error.message ||\n 'Request failed';\n\n const ctx: Record<string, unknown> = {\n ...(status != null && { httpStatus: status }),\n ...(body?.path != null && { path: body.path }),\n ...(body?.error != null && { serverError: body.error }),\n ...(error.code != null && { axiosCode: error.code }),\n ...(decryptionFailed && { decryptionFailed: true, note: 'Transit decryption failed — server error body is an encrypted envelope' }),\n };\n\n // No response at all — network/timeout failure\n if (!error.response) {\n return new AntzChatNetworkError(message || 'Network error', 'NETWORK_ERROR', ctx);\n }\n\n if (status === 401) {\n // AUTH_FAILED is used when a refresh was attempted but failed (set by interceptor).\n // SESSION_EXPIRED is the default: 401 with no prior retry = token simply expired.\n const code = (error.config as any)?._retry ? 'AUTH_FAILED' : 'SESSION_EXPIRED';\n return new AntzChatAuthError(message, code, ctx);\n }\n if (status === 403) return new AntzChatPermissionError(message, ctx);\n if (status === 400 || status === 422) {\n return new AntzChatValidationError(\n Array.isArray(rawMessage) ? rawMessage : message,\n ctx,\n );\n }\n if (status === 404) return new AntzChatServerError(message, 404, ctx);\n if (status === 429) return new AntzChatNetworkError(message, 'RATE_LIMITED', ctx);\n if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);\n\n return new AntzChatServerError(message, status, ctx);\n }\n\n const msg = error instanceof Error ? error.message : String(error);\n return new AntzChatError('UNKNOWN_ERROR', msg, false);\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n CompletedPart,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, PlatformUploadPartFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async completeMultipartUpload(\n fileId: string,\n uploadId: string,\n parts: CompletedPart[],\n ): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(\n `/storage/multipart/complete/${fileId}`,\n { uploadId, parts },\n );\n return data;\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\nasync function runMultipartUpload(\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n platformUploadPartFn: PlatformUploadPartFn,\n onProgress?: (pct: number) => void,\n): Promise<FileResponse> {\n const { multipart } = presigned;\n if (!multipart) throw new Error('No multipart info on presigned response');\n\n const CONCURRENCY = 3;\n const completedParts: CompletedPart[] = [];\n const partProgress: Record<number, number> = {};\n\n multipart.partUrls.forEach(({ partNumber }) => { partProgress[partNumber] = 0; });\n\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(partProgress);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg * 0.95));\n };\n\n const uploadPart = async (partNumber: number, uploadUrl: string, method: 'PUT' | 'POST'): Promise<void> => {\n const offset = (partNumber - 1) * multipart.chunkSize;\n const end = Math.min(offset + multipart.chunkSize, file.size);\n const blob = await fetch(file.uri).then((r) => r.blob());\n const slice = blob.slice(offset, end);\n\n const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {\n partProgress[partNumber] = pct;\n reportProgress();\n }, method);\n\n completedParts.push({ partNumber, etag });\n partProgress[partNumber] = 100;\n reportProgress();\n };\n\n for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {\n const batch = multipart.partUrls.slice(i, i + CONCURRENCY);\n const results = await Promise.allSettled(\n batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? 'PUT')),\n );\n const failed = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined;\n if (failed) throw failed.reason;\n }\n\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n const fileResponse = await storageApi.completeMultipartUpload(\n presigned.fileId,\n multipart.uploadId,\n completedParts,\n );\n onProgress?.(100);\n return fileResponse;\n}\n\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n let fileResponse: FileResponse;\n if (presigned.multipart && platformUploadPartFn) {\n fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {\n progressMap[originalIdx] = pct;\n reportProgress();\n });\n } else {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n fileResponse = await storageApi.confirmUpload(presigned.fileId);\n }\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n}\n"],"mappings":"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","names":["import_axios"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal.ts","../src/compression/compress.ts","../src/api/client.ts","../src/errors.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["// Internal exports for use by the web and RN SDKs only.\n// Not part of the public API — do not use in application code.\nexport { uploadBatchWithSlots } from './api/storage.js';\nexport { generateUUID } from './crypto/uuid.js';\n","import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, awaitTransitReadyOr, configureTransit, setTransitSession, getTransitSession } from '../crypto/session.js';\nimport { createRestTransitSession, fetchServerKeys, TransitRateLimitedError } from '../crypto/handshake.js';\nimport { detectTransitAlgo } from '../crypto/detect.js';\nimport { normalizeAxiosError, AntzChatNetworkError } from '../errors.js';\n\n// Hard ceiling on how long a single request will block waiting for the transit\n// handshake. A legitimate cold-start handshake resolves in well under this even\n// on a slow link (TransitGate already spent ~6s, establishTransit keeps\n// retrying). Past this we FAIL the request with a retryable error rather than\n// leave it pending forever — react-query cannot retry / refetch-on-focus a\n// request that never settles, so an unbounded wait here is an unrecoverable\n// silent hang. Each failed+retried request also re-arms ensureRestTransitHandshake.\nconst TRANSIT_GATE_MAX_WAIT_MS = 30_000;\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\nlet _avatarSent = false;\n// In-flight transit handshake promise — shared between initApiClient and connectSocket\n// so they never fire two concurrent HTTPS handshakes for the same session.\nlet _transitHandshakePromise: Promise<void> | null = null;\n// Gate the request interceptor until the SDK has resolved a token (async\n// authProvider) and, where wired, the transit handshake. Set by the SDK\n// provider; the interceptor awaits it before attaching the Authorization\n// header so early requests (e.g. useConversations' initial fetch) don't race\n// ahead unauthenticated.\nlet _authReadyPromise: Promise<unknown> | null = null;\n\nexport function getTransitHandshakePromise(): Promise<void> | null {\n return _transitHandshakePromise;\n}\n\nexport function setAuthReadyPromise(promise: Promise<unknown> | null): void {\n _authReadyPromise = promise;\n}\n\n// True once initApiClient() has run and its config has not been torn down by a\n// subsequent disconnectSocket(). Lets the SDK provider detect the case where a\n// React remount skipped re-init (its key was unchanged) but disconnectSocket()\n// had nulled _config in between — leaving the request interceptor unable to see\n// transitEncryption and firing every request as unencrypted plaintext.\nexport function isApiClientConfigured(): boolean {\n return _config !== null;\n}\n\n// (Re-)kick the HTTPS transit handshake. Idempotent: no-ops when transit is\n// disabled, a session already exists, or an attempt is already in flight.\n// Called both at init and from the request interceptor when a request is about\n// to block on waitForTransitReady() with no session — e.g. after a socket\n// disconnect cleared the session and nothing else re-established it.\n//\n// It only calls configureTransit(false) — which un-gates the interceptor and\n// lets requests go out as PLAINTEXT — when the server itself reports transit\n// disabled (GET /crypto/pubkey → enabled:false, i.e. an old server). A transient\n// failure of POST /crypto/session (network blip, rate limit, 5xx) must NOT\n// disable transit: the server still requires it, so plaintext would just 403.\n// Instead we retry with backoff; waitForTransitReady() keeps requests pending\n// and they dispatch the moment a retry sets the session.\nexport function ensureRestTransitHandshake(): void {\n if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;\n const apiUrl = _config.apiUrl;\n _transitHandshakePromise = (async () => {\n try {\n // A 429 is \"wait\", not \"broken\", so it must NOT consume the attempt\n // budget — otherwise a rate-limited client exhausts 5 attempts in a few\n // seconds and gives up on an endpoint that was working fine. Failures are\n // counted separately from rate-limit hits, and the loop is additionally\n // bounded by wall-clock time so a persistently limited server cannot keep\n // it running forever.\n const MAX_FAILURES = 5;\n const BACKSTOP_MS = 2 * 60_000;\n const deadline = Date.now() + BACKSTOP_MS;\n let failures = 0;\n let rateLimitHits = 0;\n\n while (failures < MAX_FAILURES && Date.now() < deadline) {\n if (getTransitSession()) return;\n let waitMs: number;\n try {\n // Identity is sent so the server can rate-limit these pre-auth routes\n // per user rather than per IP (see TransitIdentity in handshake.ts).\n const identity = { userId: _config?.userId, tenantId: _config?.tenantId };\n const keys = await fetchServerKeys(apiUrl, identity);\n if (!keys?.enabled) {\n configureTransit(false); // server genuinely doesn't want transit\n return;\n }\n const session = await createRestTransitSession(apiUrl, identity);\n if (session && !getTransitSession()) {\n const algo = typeof globalThis.crypto?.subtle !== 'undefined'\n ? await detectTransitAlgo()\n : 'x25519';\n setTransitSession({ sessionKey: session.sessionKey as CryptoKey, algo, sessionId: session.sessionId, enabled: true });\n return;\n }\n // Reachable when the server returned no session but did not throw\n // (e.g. an old server without the endpoint) — treat as a failure.\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n } catch (err) {\n if (err instanceof TransitRateLimitedError) {\n // Honour the server's own figure when it sent one, else escalate\n // blind since the window is unknown. Either way the wait is clamped\n // to [1s, ceiling]. Never sleep past the request gate. A wait longer than\n // TRANSIT_GATE_MAX_WAIT_MS is guaranteed to lose: the request that\n // triggered this handshake gives up at the gate and fails with\n // TRANSIT_NOT_READY while we are still politely waiting out the\n // server's window, so the retry can only ever help a LATER request.\n // Retrying at the gate boundary instead costs the server at most one\n // extra attempt per gate period and gives the in-flight request a\n // real chance to be rescued.\n const ceiling = Math.min(60_000, TRANSIT_GATE_MAX_WAIT_MS);\n const blind = Math.min(15_000 * 2 ** rateLimitHits, ceiling);\n waitMs = err.retryAfterMs != null\n ? Math.min(Math.max(err.retryAfterMs, 1_000), ceiling)\n : blind;\n rateLimitHits++;\n console.warn(\n `[AntzChat] transit handshake rate-limited (429) — retrying in ${Math.round(waitMs / 1000)}s` +\n `${err.retryAfterMs != null ? ' (per Retry-After)' : ''}.`,\n );\n } else {\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n }\n }\n await new Promise((r) => setTimeout(r, waitMs));\n }\n console.error(\n '[AntzChat] transit handshake could not establish a session — ' +\n \"chat requests stay gated until one succeeds (server requires transit).\",\n );\n } finally {\n _transitHandshakePromise = null;\n }\n })();\n}\n\n/**\n * Per-request opt-out of the transit GATE (not of transit itself).\n *\n * Set via `{ headers: { [TRANSIT_OPTIONAL_HEADER]: '1' } }` on the axios call.\n * A request marked this way still encrypts normally whenever a session happens\n * to be ready — it simply refuses to BLOCK waiting for one, and goes out\n * unencrypted rather than failing when the channel is down.\n *\n * This exists for teardown calls (logout, logout-all, device de-registration).\n * Those must succeed precisely when the secure channel is degraded: a control\n * that prevents a user from ending their own session is inverted. Their server\n * routes are marked @PreTransit() so they accept an unencrypted request, and\n * JwtAuthGuard still authenticates them — transit is a confidentiality layer,\n * never an authenticity one (POST /crypto/session is itself unauthenticated, so\n * holding a transit session proves nothing about the caller).\n *\n * Do NOT add this to ordinary routes: they are not @PreTransit() server-side and\n * would 403 with \"Transit encryption required\" the moment the gate is skipped.\n *\n * The marker header is stripped before the request leaves the client.\n */\nexport const TRANSIT_OPTIONAL_HEADER = 'x-antz-transit-optional';\n\nfunction isTransitOptional(req: InternalAxiosRequestConfig): boolean {\n const present = req.headers?.[TRANSIT_OPTIONAL_HEADER] != null;\n if (present) delete req.headers[TRANSIT_OPTIONAL_HEADER]; // client-side only\n return present;\n}\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // Kick off the HTTPS transit handshake immediately so REST calls that fire\n // before connectSocket (e.g. getMe() right after initApiClient) are not\n // blocked indefinitely. Store the promise so connectSocket can await it\n // instead of firing a duplicate handshake.\n ensureRestTransitHandshake();\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n // Wait for the SDK's auth (and, where wired, transit) gate before reading\n // the token — otherwise a request fired during boot goes out with no\n // Authorization header.\n if (_authReadyPromise) await _authReadyPromise;\n\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key before sending any request. The server\n // enforces transit encryption independent of auth (e.g. GET /app/config\n // fires before the async authProvider token resolves) — gating this on\n // `token` let those pre-auth requests race ahead of the handshake and get\n // rejected with 403 \"Transit encryption required\".\n if (_config?.transitEncryption && !isTransitOptional(req)) {\n // If the session is gone (socket disconnect cleared it, first boot still\n // pending), make sure a handshake is running before we block — otherwise\n // the wait could hang with nothing to resolve it.\n if (!getTransitSession()) ensureRestTransitHandshake();\n const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);\n if (!ready) {\n // Handshake still hasn't produced a session. Do NOT send plaintext\n // (server requires transit); fail loudly instead so the error surfaces\n // in the UI and react-query's retry re-drives the handshake.\n throw new AntzChatNetworkError(\n 'Secure channel to chat server not established — request not sent. It will retry automatically.',\n 'TRANSIT_NOT_READY',\n { url: req.url },\n );\n }\n }\n\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(normalizeAxiosError(error));\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","import { isAxiosError } from 'axios';\nimport { isTransitEnvelope } from './crypto/transit.js';\n\n// ─── Base error class ─────────────────────────────────────────────────────────\n\nexport class AntzChatError extends Error {\n readonly code: string;\n readonly retryable: boolean;\n readonly context?: Record<string, unknown>;\n\n constructor(\n code: string,\n message: string,\n retryable = false,\n context?: Record<string, unknown>,\n ) {\n super(message);\n this.name = 'AntzChatError';\n this.code = code;\n this.retryable = retryable;\n this.context = context;\n // Maintain proper prototype chain in transpiled environments\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Semantic subclasses ──────────────────────────────────────────────────────\n\n/** Thrown on 401 (after refresh also fails) or when no refresh token exists. */\nexport class AntzChatAuthError extends AntzChatError {\n constructor(message: string, code = 'AUTH_FAILED', context?: Record<string, unknown>) {\n super(code, message, false, context);\n this.name = 'AntzChatAuthError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 400 / 422 — bad input, validation failure. */\nexport class AntzChatValidationError extends AntzChatError {\n /** Server-returned field error array (when the server sends message as string[]). */\n readonly fields?: string[];\n\n constructor(message: string | string[], context?: Record<string, unknown>) {\n const msg = Array.isArray(message) ? message.join('; ') : message;\n super('VALIDATION_ERROR', msg, false, context);\n this.name = 'AntzChatValidationError';\n this.fields = Array.isArray(message) ? message : undefined;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on network failures, timeouts, socket disconnections, and queue overflow. retryable = true. */\nexport class AntzChatNetworkError extends AntzChatError {\n constructor(message: string, code = 'NETWORK_ERROR', context?: Record<string, unknown>) {\n super(code, message, true, context);\n this.name = 'AntzChatNetworkError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 403 — insufficient permissions. */\nexport class AntzChatPermissionError extends AntzChatError {\n constructor(message: string, context?: Record<string, unknown>) {\n super('PERMISSION_DENIED', message, false, context);\n this.name = 'AntzChatPermissionError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 5xx or other unexpected server errors. retryable = true. */\nexport class AntzChatServerError extends AntzChatError {\n readonly httpStatus?: number;\n\n constructor(message: string, httpStatus?: number, context?: Record<string, unknown>) {\n super('SERVER_ERROR', message, true, context);\n this.name = 'AntzChatServerError';\n this.httpStatus = httpStatus;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Error code reference ─────────────────────────────────────────────────────\n//\n// Code Class Source\n// ───────────────────── ──────────────────────── ─────────────────────────\n// AUTH_FAILED AntzChatAuthError 401 after refresh fails\n// SESSION_EXPIRED AntzChatAuthError 401, no refresh token\n// PERMISSION_DENIED AntzChatPermissionError 403\n// VALIDATION_ERROR AntzChatValidationError 400 / 422\n// NOT_FOUND AntzChatServerError 404\n// RATE_LIMITED AntzChatNetworkError 429\n// NETWORK_ERROR AntzChatNetworkError No response / conn failure\n// SOCKET_TIMEOUT AntzChatNetworkError ACK timeout / reconnect timeout\n// SOCKET_NOT_CONNECTED AntzChatNetworkError withAck when socket is down\n// SEND_QUEUE_FULL AntzChatNetworkError Queue overflow (>100 msgs)\n// MESSAGE_DROPPED AntzChatNetworkError Queue TTL expired (30s)\n// TRANSIT_MISMATCH AntzChatError SDK/server encryption config mismatch\n// SERVER_ERROR AntzChatServerError 5xx or unknown HTTP error\n\n// ─── REST error normaliser ────────────────────────────────────────────────────\n\n/**\n * Converts a raw axios error (or any unknown throw) into a typed AntzChatError.\n *\n * Call site: client.ts response interceptor — runs AFTER transit decryption,\n * so error.response.data is always plaintext by the time this function sees it.\n * If decryption itself failed, error.response.data remains the raw encrypted\n * envelope — detected via isTransitEnvelope() and noted in context.\n */\nexport function normalizeAxiosError(error: unknown): AntzChatError {\n if (error instanceof AntzChatError) return error;\n\n if (isAxiosError(error)) {\n const status = error.response?.status;\n const body = error.response?.data;\n\n // Detect if decryption failed — body is still an encrypted envelope\n const decryptionFailed = body != null && isTransitEnvelope(body);\n\n const rawMessage: string | string[] | undefined = decryptionFailed\n ? undefined\n : (body?.message ?? undefined);\n\n const message: string =\n (Array.isArray(rawMessage) ? rawMessage.join('; ') : rawMessage) ||\n error.message ||\n 'Request failed';\n\n const ctx: Record<string, unknown> = {\n ...(status != null && { httpStatus: status }),\n ...(body?.path != null && { path: body.path }),\n ...(body?.error != null && { serverError: body.error }),\n ...(error.code != null && { axiosCode: error.code }),\n ...(decryptionFailed && { decryptionFailed: true, note: 'Transit decryption failed — server error body is an encrypted envelope' }),\n };\n\n // No response at all — network/timeout failure\n if (!error.response) {\n return new AntzChatNetworkError(message || 'Network error', 'NETWORK_ERROR', ctx);\n }\n\n if (status === 401) {\n // AUTH_FAILED is used when a refresh was attempted but failed (set by interceptor).\n // SESSION_EXPIRED is the default: 401 with no prior retry = token simply expired.\n const code = (error.config as any)?._retry ? 'AUTH_FAILED' : 'SESSION_EXPIRED';\n return new AntzChatAuthError(message, code, ctx);\n }\n if (status === 403) return new AntzChatPermissionError(message, ctx);\n if (status === 400 || status === 422) {\n return new AntzChatValidationError(\n Array.isArray(rawMessage) ? rawMessage : message,\n ctx,\n );\n }\n if (status === 404) return new AntzChatServerError(message, 404, ctx);\n if (status === 429) return new AntzChatNetworkError(message, 'RATE_LIMITED', ctx);\n if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);\n\n return new AntzChatServerError(message, status, ctx);\n }\n\n const msg = error instanceof Error ? error.message : String(error);\n return new AntzChatError('UNKNOWN_ERROR', msg, false);\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n CompletedPart,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, PlatformUploadPartFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async completeMultipartUpload(\n fileId: string,\n uploadId: string,\n parts: CompletedPart[],\n ): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(\n `/storage/multipart/complete/${fileId}`,\n { uploadId, parts },\n );\n return data;\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\nasync function runMultipartUpload(\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n platformUploadPartFn: PlatformUploadPartFn,\n onProgress?: (pct: number) => void,\n): Promise<FileResponse> {\n const { multipart } = presigned;\n if (!multipart) throw new Error('No multipart info on presigned response');\n\n const CONCURRENCY = 3;\n const completedParts: CompletedPart[] = [];\n const partProgress: Record<number, number> = {};\n\n multipart.partUrls.forEach(({ partNumber }) => { partProgress[partNumber] = 0; });\n\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(partProgress);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg * 0.95));\n };\n\n const uploadPart = async (partNumber: number, uploadUrl: string, method: 'PUT' | 'POST'): Promise<void> => {\n const offset = (partNumber - 1) * multipart.chunkSize;\n const end = Math.min(offset + multipart.chunkSize, file.size);\n const blob = await fetch(file.uri).then((r) => r.blob());\n const slice = blob.slice(offset, end);\n\n const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {\n partProgress[partNumber] = pct;\n reportProgress();\n }, method);\n\n completedParts.push({ partNumber, etag });\n partProgress[partNumber] = 100;\n reportProgress();\n };\n\n for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {\n const batch = multipart.partUrls.slice(i, i + CONCURRENCY);\n const results = await Promise.allSettled(\n batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? 'PUT')),\n );\n const failed = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined;\n if (failed) throw failed.reason;\n }\n\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n const fileResponse = await storageApi.completeMultipartUpload(\n presigned.fileId,\n multipart.uploadId,\n completedParts,\n );\n onProgress?.(100);\n return fileResponse;\n}\n\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n let fileResponse: FileResponse;\n if (presigned.multipart && platformUploadPartFn) {\n fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {\n progressMap[originalIdx] = pct;\n reportProgress();\n });\n } else {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n fileResponse = await storageApi.confirmUpload(presigned.fileId);\n }\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n}\n"],"mappings":"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","names":["import_axios"]}
|
package/dist/internal.js
CHANGED
|
@@ -4270,26 +4270,39 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
|
|
|
4270
4270
|
<!-- ─── STEP 11: TYPING ───────────────────────────────────────────────── -->
|
|
4271
4271
|
<section id="step-typing">
|
|
4272
4272
|
<h2><span class="step">STEP 11</span> Typing Indicators</h2>
|
|
4273
|
+
<p><strong>Call <code>socketEmit.typing(id, true)</code> on every keystroke — do not throttle it yourself.</strong> Since core v1.4.7 the SDK leading-throttles it internally to one emit per 3 s per conversation, while <code>isTyping: false</code> always goes out and resets the window. Adding a second throttle on top makes the two windows interact unpredictably. You still want the trailing timer below, because that is what sends the <em>stop</em> edge.</p>
|
|
4274
|
+
<p>Incoming events arrive as <code>'typing_indicator'</code> (not <code>'typing'</code>). The server broadcasts to everyone in the room including the sender, so filter your own echo by <code>userId</code>. Each store entry self-expires after 6 s without a refreshing event, so a lost stop edge can no longer leave an indicator stuck.</p>
|
|
4273
4275
|
<div data-p="rn web">
|
|
4274
4276
|
<pre><code><span class="kw">const</span> timer = <span class="fn">useRef</span><<span class="tp">ReturnType</span><<span class="kw">typeof</span> setTimeout>>();
|
|
4275
4277
|
|
|
4276
4278
|
<span class="kw">function</span> <span class="fn">onChangeText</span>(val: <span class="tp">string</span>) {
|
|
4277
4279
|
<span class="fn">setValue</span>(val);
|
|
4280
|
+
<span class="cm">// Fine to call per keystroke — core throttles this internally.</span>
|
|
4278
4281
|
socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
|
|
4279
4282
|
<span class="fn">clearTimeout</span>(timer.current);
|
|
4280
|
-
timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">
|
|
4283
|
+
timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);
|
|
4281
4284
|
}
|
|
4282
4285
|
|
|
4283
|
-
<span class="cm">//
|
|
4284
|
-
|
|
4286
|
+
<span class="cm">// Also send the stop edge when the composer unmounts or the</span>
|
|
4287
|
+
<span class="cm">// conversation changes — otherwise a pending debounce is discarded</span>
|
|
4288
|
+
<span class="cm">// and the peer's indicator lingers until it expires.</span>
|
|
4289
|
+
<span class="fn">useEffect</span>(() => () => {
|
|
4290
|
+
<span class="fn">clearTimeout</span>(timer.current);
|
|
4291
|
+
socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>);
|
|
4292
|
+
}, [conversationId]);
|
|
4293
|
+
|
|
4294
|
+
<span class="cm">// Listen for others typing — event name is 'typing_indicator'</span>
|
|
4295
|
+
socket.<span class="fn">on</span>(<span class="str">'typing_indicator'</span>, ({ conversationId, userId, displayName, isTyping }) => {
|
|
4296
|
+
<span class="kw">if</span> (userId === currentUserId) <span class="kw">return</span>; <span class="cm">// ignore our own echo</span>
|
|
4285
4297
|
isTyping ? <span class="fn">addTypingUser</span>(conversationId, { userId, displayName })
|
|
4286
4298
|
: <span class="fn">removeTypingUser</span>(conversationId, userId);
|
|
4287
4299
|
});</code></pre>
|
|
4288
4300
|
</div>
|
|
4289
4301
|
<div data-p="node">
|
|
4290
4302
|
<pre><code>socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
|
|
4291
|
-
<span class="kw">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">
|
|
4303
|
+
<span class="kw">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);</code></pre>
|
|
4292
4304
|
</div>
|
|
4305
|
+
<div class="callout info"><strong>Server-side suppression.</strong> chat-server skips typing broadcasts entirely in rooms with more than <code>TYPING_MAX_ROOM_SOCKETS</code> connected sockets (default 30, <code>0</code> disables). In a large, busy group nobody sees an indicator — by design, since the UI can only render “Several people are typing…” there anyway. The count is sockets, not members, so one user on phone + laptop counts twice.</div>
|
|
4293
4306
|
</section>
|
|
4294
4307
|
|
|
4295
4308
|
<!-- ─── STEP 12: READ RECEIPTS & LAST SEEN ────────────────────────────── -->
|
|
@@ -5476,9 +5489,10 @@ Notifications.<span class="fn">addNotificationResponseReceivedListener</span>((r
|
|
|
5476
5489
|
};
|
|
5477
5490
|
|
|
5478
5491
|
<span class="kw">const</span> <span class="fn">onChangeText</span> = (val: <span class="tp">string</span>) => {
|
|
5492
|
+
<span class="cm">// Per-keystroke is fine: core throttles typing:true internally.</span>
|
|
5479
5493
|
<span class="fn">setText</span>(val); socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
|
|
5480
5494
|
<span class="fn">clearTimeout</span>(timer.current);
|
|
5481
|
-
timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">
|
|
5495
|
+
timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);
|
|
5482
5496
|
};
|
|
5483
5497
|
|
|
5484
5498
|
<span class="kw">const</span> <span class="fn">loadMore</span> = <span class="kw">async</span> () => {
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/compression/compress.ts","../src/crypto/transit.ts","../src/crypto/session.ts","../src/crypto/detect.ts","../src/crypto/handshake.ts","../src/errors.ts","../src/api/client.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","export interface TransitEnvelope {\n v: 1;\n iv: string; // base64, 12 bytes\n tag: string; // base64, 16 bytes\n ct: string; // base64, ciphertext\n}\n\n// sessionKey is CryptoKey on Web Crypto path, Uint8Array on noble/RN path.\ntype AnySessionKey = CryptoKey | Uint8Array;\n\nfunction hasWebCrypto(): boolean {\n return typeof globalThis.crypto?.subtle !== 'undefined';\n}\n\n// ─── Encrypt ─────────────────────────────────────────────────────────────────\n\nexport async function encryptPayload(\n data: unknown,\n sessionKey: AnySessionKey,\n): Promise<TransitEnvelope> {\n const plaintext = new TextEncoder().encode(JSON.stringify(data));\n\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return encryptNoble(plaintext, sessionKey as Uint8Array);\n }\n return encryptWebCrypto(plaintext, sessionKey as CryptoKey);\n}\n\nasync function encryptWebCrypto(plaintext: Uint8Array, sessionKey: CryptoKey): Promise<TransitEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv as Uint8Array<ArrayBuffer> }, sessionKey, plaintext as Uint8Array<ArrayBuffer>);\n const ct = encrypted.slice(0, encrypted.byteLength - 16);\n const tag = encrypted.slice(encrypted.byteLength - 16);\n return { v: 1, iv: bufToB64(iv), tag: bufToB64(tag), ct: bufToB64(ct) };\n}\n\nasync function encryptNoble(plaintext: Uint8Array, sessionKey: Uint8Array): Promise<TransitEnvelope> {\n const { gcm } = await import('@noble/ciphers/aes');\n const { randomBytes } = await import('@noble/hashes/utils');\n const iv = randomBytes(12);\n const cipher = gcm(sessionKey, iv);\n const encrypted = cipher.encrypt(plaintext); // ct + 16-byte tag appended\n const ct = encrypted.slice(0, encrypted.length - 16);\n const tag = encrypted.slice(encrypted.length - 16);\n return { v: 1, iv: uint8ToB64(iv), tag: uint8ToB64(tag), ct: uint8ToB64(ct) };\n}\n\n// ─── Decrypt ─────────────────────────────────────────────────────────────────\n\nexport async function decryptPayload(\n envelope: TransitEnvelope,\n sessionKey: AnySessionKey,\n): Promise<unknown> {\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return decryptNoble(envelope, sessionKey as Uint8Array);\n }\n return decryptWebCrypto(envelope, sessionKey as CryptoKey);\n}\n\nasync function decryptWebCrypto(envelope: TransitEnvelope, sessionKey: CryptoKey): Promise<unknown> {\n const iv = b64ToBuf(envelope.iv);\n const tag = b64ToBuf(envelope.tag);\n const ct = b64ToBuf(envelope.ct);\n const combined = new Uint8Array(ct.byteLength + tag.byteLength);\n combined.set(new Uint8Array(ct), 0);\n combined.set(new Uint8Array(tag), ct.byteLength);\n const decrypted = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: new Uint8Array(iv) },\n sessionKey,\n combined,\n );\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nasync function decryptNoble(envelope: TransitEnvelope, sessionKey: Uint8Array): Promise<unknown> {\n const { gcm } = await import('@noble/ciphers/aes');\n const iv = base64ToUint8(envelope.iv);\n const tag = base64ToUint8(envelope.tag);\n const ct = base64ToUint8(envelope.ct);\n const combined = new Uint8Array(ct.length + tag.length);\n combined.set(ct, 0);\n combined.set(tag, ct.length);\n const cipher = gcm(sessionKey, iv);\n const decrypted = cipher.decrypt(combined);\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nexport function isTransitEnvelope(v: unknown): v is TransitEnvelope {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as any).v === 1 &&\n typeof (v as any).iv === 'string' &&\n typeof (v as any).tag === 'string' &&\n typeof (v as any).ct === 'string'\n );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction uint8ToB64(bytes: Uint8Array): string {\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf;\n}\n","import type { TransitAlgo } from './detect.js';\n\ninterface TransitSession {\n sessionKey: CryptoKey | Uint8Array; // CryptoKey on Web Crypto, Uint8Array on noble/RN\n algo: TransitAlgo;\n sessionId: string;\n enabled: boolean;\n}\n\n// All state lives on globalThis so Turbopack <locals> module splits and any\n// other bundler that creates multiple instances of this module still share a\n// single source of truth. The symbol key prevents accidental collisions.\nconst _KEY = Symbol.for('__antz_chat_transit__');\n\ninterface TransitState {\n session: TransitSession | null;\n sessionEverEstablished: boolean;\n readyResolve: (() => void) | null;\n readyPromise: Promise<void> | null;\n transitConfigured: boolean | null;\n /** Edge-triggered listeners fired each time a session (re-)establishes. Used\n * by the socket emitters to re-send fire-and-forget state (join_room) that\n * was dropped while the handshake was settling. On globalThis like the rest\n * of transit state so bundler module-duplication can't split the set. */\n readyListeners: Set<() => void>;\n}\n\nfunction getState(): TransitState {\n const g = globalThis as any;\n if (!g[_KEY]) {\n g[_KEY] = {\n session: null,\n sessionEverEstablished: false,\n readyResolve: null,\n readyPromise: null,\n transitConfigured: null,\n readyListeners: new Set<() => void>(),\n } satisfies TransitState;\n }\n return g[_KEY] as TransitState;\n}\n\nexport function configureTransit(enabled: boolean): void {\n const s = getState();\n s.transitConfigured = enabled;\n if (!enabled) {\n // Transit disabled — resolve immediately so HTTP requests don't block\n s.readyResolve?.();\n s.readyResolve = null;\n }\n}\n\n/**\n * Set the transit-required flag ONLY if nothing has configured it yet. Used by\n * connectSocket() for socket-only consumers that never call initApiClient().\n * Must not override an explicit configureTransit(false) — that flag can carry\n * the authoritative \"server reported transit disabled\" signal, and re-gating\n * after it would wedge every request.\n */\nexport function configureTransitIfUnset(enabled: boolean): void {\n if (getState().transitConfigured === null) configureTransit(enabled);\n}\n\nexport function waitForTransitReady(): Promise<void> {\n const s = getState();\n // Not configured yet or disabled — resolve immediately\n if (!s.transitConfigured) return Promise.resolve();\n // Already have an active session — resolve immediately\n if (s.session) return Promise.resolve();\n // Transit is required but there is no session (first startup, or a socket\n // disconnect cleared it). Block until the handshake (re-)establishes one.\n // Resolving early here would let the request go out as plaintext and the\n // server rejects it with 403 \"Transit encryption required\". sessionEverEstablished\n // is deliberately NOT consulted — a stale `true` from a prior session must not\n // unblock a now-sessionless request. The request interceptor kicks a fresh\n // handshake before awaiting this, so the promise is guaranteed a resolver.\n if (!s.readyPromise) {\n s.readyPromise = new Promise<void>((resolve) => {\n s.readyResolve = resolve;\n });\n }\n return s.readyPromise;\n}\n\nexport function setTransitSession(session: TransitSession): void {\n const s = getState();\n s.session = session;\n s.sessionEverEstablished = true;\n // Resolve any pending HTTP requests waiting for the session key\n s.readyResolve?.();\n s.readyResolve = null;\n // Notify edge-triggered listeners (join_room re-flush, etc.)\n s.readyListeners.forEach((fn) => { try { fn(); } catch { /* listener must not break transit */ } });\n}\n\n/**\n * Register a listener fired every time a transit session (re-)establishes via\n * setTransitSession(). For re-sending fire-and-forget socket state that\n * secureEmit dropped during the handshake gap. Returns an unsubscribe fn.\n */\nexport function onTransitReady(listener: () => void): () => void {\n const s = getState();\n s.readyListeners.add(listener);\n return () => { s.readyListeners.delete(listener); };\n}\n\n/**\n * Wait until a transit session is available OR `timeoutMs` elapses, whichever\n * comes first. Never rejects. Returns true if it is now safe to proceed\n * (session present, or transit not required), false if it timed out with\n * transit still required and no session — the caller decides what a false\n * means (REST: fail the request loudly; socket emit: throw), so that a\n * handshake that never completes surfaces as a retryable error instead of an\n * infinite pending request that react-query can never recover.\n */\nexport async function awaitTransitReadyOr(timeoutMs: number): Promise<boolean> {\n const s = getState();\n if (!s.transitConfigured || s.session) return true;\n await Promise.race([\n waitForTransitReady(),\n new Promise<void>((r) => setTimeout(r, timeoutMs)),\n ]);\n const now = getState();\n return Boolean(now.session) || now.transitConfigured !== true;\n}\n\nexport function getTransitSession(): TransitSession | null {\n return getState().session;\n}\n\nexport function clearTransitSession(): void {\n const s = getState();\n s.session = null;\n // Reset sessionEverEstablished too. Leaving it `true` made waitForTransitReady()\n // (which trusted the flag) and isTransitEnabled() (which checks the live session)\n // permanently disagree after any clear-following-success: requests then went out\n // unencrypted and 403'd (\"Transit encryption required\") forever, with no path to\n // recovery. The ready promise is recreated lazily on the next waitForTransitReady().\n s.sessionEverEstablished = false;\n s.readyPromise = null;\n s.readyResolve = null;\n}\n\nexport function isTransitEnabled(): boolean {\n return getState().session?.enabled === true;\n}\n\n/**\n * True when transit encryption is *required* — i.e. the SDK was configured with\n * transitEncryption and the server has NOT authoritatively told us it is off\n * (which is the only thing that sets transitConfigured back to false).\n *\n * This is the correct signal for \"must this payload be encrypted?\". It is\n * deliberately distinct from isTransitEnabled() (which is \"is a live session\n * key available right now?\"): the gap between the two — required but no key —\n * is a handshake-in-progress / reconnect window where callers must WAIT or\n * FAIL, never fall through to plaintext.\n */\nexport function isTransitRequired(): boolean {\n return getState().transitConfigured === true;\n}\n\nexport function getSessionKey(): CryptoKey | Uint8Array | null {\n return getState().session?.sessionKey ?? null;\n}\n\n// Returns sessionId for the x-transit-session header sent with HTTP requests.\nexport function getSessionId(): string | null {\n return getState().session?.sessionId ?? null;\n}\n","export type TransitAlgo = 'x25519' | 'p256';\n\nlet _cached: TransitAlgo | null = null;\n\n// Probes Web Crypto API for X25519 support once; caches result for the session.\n// RN and Node callers never call this — they always get X25519 via @noble.\nexport async function detectTransitAlgo(): Promise<TransitAlgo> {\n if (_cached) return _cached;\n\n try {\n await globalThis.crypto.subtle.generateKey(\n { name: 'X25519' } as any,\n false,\n ['deriveKey'],\n );\n _cached = 'x25519';\n } catch {\n _cached = 'p256';\n }\n\n return _cached;\n}\n\nexport function getCachedAlgo(): TransitAlgo | null {\n return _cached;\n}\n\nexport function resetAlgoCache(): void {\n _cached = null;\n}\n","import type { TransitAlgo } from './detect.js';\nimport { detectTransitAlgo } from './detect.js';\n\n/**\n * Caller identity attached to the pre-auth transit handshake requests.\n *\n * GET /crypto/pubkey and POST /crypto/session run BEFORE a transit session (and\n * therefore before the authenticated axios client) exists, so they bypass the\n * request interceptor that normally adds these headers. Without them the server\n * can only rate-limit these two routes by client IP — which means every user\n * behind one NAT, office proxy or ALB shares a single bucket, and one client's\n * reload loop 429s everyone else.\n *\n * These values are NOT used for authentication: the endpoints are unauthenticated\n * by design and the server treats the headers as a fairness hint only, with a\n * per-IP ceiling underneath as the real abuse limit. Sending them is therefore\n * safe, optional, and backward compatible — an older SDK that omits them simply\n * falls back to the shared per-IP bucket.\n */\nexport interface TransitIdentity {\n /** External user id — same value sent as x-user-id on authenticated requests. */\n userId?: string;\n /** Tenant id — same value sent as X-Tenant-ID on authenticated requests. */\n tenantId?: string;\n}\n\n/**\n * How long the server asked us to wait, in ms, from a 429's Retry-After.\n *\n * The chat server runs TWO named rate-limit layers, and @nestjs/throttler\n * suffixes its headers with the throttler name unless that name is literally\n * \"default\". So a 429 carries `Retry-After-identity` or `Retry-After-ip`, not a\n * bare `Retry-After`. Older servers and intermediary proxies may still send the\n * bare name, so all three are read; when more than one is present the LONGER\n * wait wins, since retrying before the slower bucket drains just earns another\n * 429.\n *\n * Returns undefined when no variant is readable — in a browser that also\n * happens when the server omits these from Access-Control-Expose-Headers, in\n * which case callers must fall back to their own backoff.\n */\nexport function readRetryAfterMs(headers: Headers): number | undefined {\n const parse = (raw: string | null): number | undefined => {\n if (!raw) return undefined;\n const secs = Number(raw);\n if (Number.isFinite(secs)) return Math.max(0, secs * 1000);\n const when = Date.parse(raw);\n return Number.isNaN(when) ? undefined : Math.max(0, when - Date.now());\n };\n const found = ['Retry-After-identity', 'Retry-After-ip', 'Retry-After']\n .map((name) => parse(headers.get(name)))\n .filter((ms): ms is number => ms != null);\n return found.length > 0 ? Math.max(...found) : undefined;\n}\n\n/**\n * Thrown when the transit handshake is rate-limited (HTTP 429).\n *\n * Distinct from a generic failure because the correct response differs: a 429\n * means \"wait\", not \"this is broken\", so callers must not burn their retry\n * budget on it and should honour `retryAfterMs` when the server supplied it.\n */\nexport class TransitRateLimitedError extends Error {\n readonly retryAfterMs?: number;\n constructor(retryAfterMs?: number) {\n super('[AntzChat] transit handshake rate-limited (429)');\n this.name = 'TransitRateLimitedError';\n this.retryAfterMs = retryAfterMs;\n }\n}\n\n/** Build the identity headers, omitting whichever values the host app did not configure. */\nfunction identityHeaders(identity?: TransitIdentity): Record<string, string> {\n const headers: Record<string, string> = {};\n if (identity?.userId) headers['x-user-id'] = identity.userId;\n if (identity?.tenantId) headers['X-Tenant-ID'] = identity.tenantId;\n return headers;\n}\n\nexport interface ServerPublicKeys {\n x25519: string; // base64\n p256: string; // base64\n enabled: boolean;\n}\n\n// Returns true when the Web Crypto API is available (browser, Node 18+).\n// Hermes (React Native) does not expose crypto.subtle — use noble fallback.\nfunction hasWebCrypto(): boolean {\n return typeof globalThis.crypto?.subtle !== 'undefined';\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────────────────\n\n// Fetches server public keys + enabled flag. Called once per init.\n// Unwraps the server's standard { success, data } envelope if present.\n// `identity` is optional and only affects server-side rate-limit bucketing —\n// see TransitIdentity. Omitting it preserves the previous (per-IP) behaviour.\nexport async function fetchServerKeys(\n apiUrl: string,\n identity?: TransitIdentity,\n): Promise<ServerPublicKeys> {\n const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });\n if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));\n if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);\n const body = await res.json() as any;\n return (body?.data ?? body) as ServerPublicKeys;\n}\n\n// ─── Core key generation ──────────────────────────────────────────────────────\n\n// Generates an ephemeral key pair and returns the public key (base64) plus a\n// bound deriveSessionKey closure that captures the private key.\n// Used by both the HTTPS handshake path and the socket handshake path.\nexport async function generateEphemeralKey(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<unknown> }> {\n if (hasWebCrypto()) {\n return generateWebCryptoEphemeralKey(algo, serverKeys);\n }\n return generateNobleEphemeralKey(serverKeys);\n}\n\n// ─── New HTTPS handshake (browser, Node 18+, React Native via noble) ─────────\n\n// Self-contained REST key exchange — no socket required.\n// 1. Fetch server public keys\n// 2. Generate ephemeral key pair\n// 3. POST /crypto/session { ephemeralPub, algo } → { sessionId }\n// 4. Derive session key locally via HKDF\n// Returns null when the server doesn't support the endpoint (old server) so\n// callers can fall back to the socket handshake path gracefully.\nexport async function createRestTransitSession(\n apiUrl: string,\n identity?: TransitIdentity,\n): Promise<{ sessionId: string; sessionKey: CryptoKey | Uint8Array } | null> {\n try {\n const serverKeys = await fetchServerKeys(apiUrl, identity);\n if (!serverKeys.enabled) return null;\n\n const algo = hasWebCrypto() ? await detectTransitAlgo() : 'x25519';\n const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);\n\n const res = await fetch(`${apiUrl}/crypto/session`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...identityHeaders(identity) },\n body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo }),\n });\n // A 429 must NOT collapse into `null`: null means \"old server, fall back to\n // the socket handshake\", whereas a rate limit means \"this endpoint is fine,\n // wait and retry\". Conflating them makes the caller abandon a working path.\n if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));\n if (!res.ok) return null; // old server without the endpoint — caller falls back\n\n const body = await res.json() as any;\n const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;\n if (!sessionId) return null;\n\n const sessionKey = await deriveSessionKey(sessionId) as CryptoKey | Uint8Array;\n return { sessionId, sessionKey };\n } catch (err) {\n // Rate limiting is a distinct, actionable condition — rethrow so the caller\n // can wait the requested interval. Everything else stays a soft null.\n if (err instanceof TransitRateLimitedError) throw err;\n return null;\n }\n}\n\n// ─── Socket handshake entry point (backward compat) ──────────────────────────\n\n// Performs the client side of the ECDH handshake via socket auth.\n// Injects ephemeralPub + algo into socketHandshakeAuth (mutates in place).\n// Returns a bound deriveSessionKey closure called after transit_session arrives.\n// Kept for old-server compatibility — new path uses createRestTransitSession.\nexport async function performHandshake(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n socketHandshakeAuth: Record<string, unknown>,\n): Promise<(sessionId: string) => Promise<unknown>> {\n const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);\n socketHandshakeAuth['transitEphemeralPub'] = ephemeralPubB64;\n socketHandshakeAuth['transitAlgo'] = algo;\n return deriveSessionKey;\n}\n\n// ─── Web Crypto path (browser, Node 18+) ─────────────────────────────────────\n\nasync function generateWebCryptoEphemeralKey(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<CryptoKey> }> {\n const ephemeral = await globalThis.crypto.subtle.generateKey(\n algo === 'x25519'\n ? { name: 'X25519' }\n : { name: 'ECDH', namedCurve: 'P-256' } as any,\n false,\n ['deriveBits'],\n );\n\n const pubRaw = await globalThis.crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey);\n const ephemeralPriv = (ephemeral as CryptoKeyPair).privateKey;\n\n return {\n ephemeralPubB64: bufToB64(pubRaw),\n deriveSessionKey: (sessionId: string) =>\n deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId),\n };\n}\n\nasync function deriveWebCryptoSessionKey(\n ephemeralPriv: CryptoKey,\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n sessionId: string,\n): Promise<CryptoKey> {\n const serverPubRaw = b64ToBuf(algo === 'x25519' ? serverKeys.x25519 : serverKeys.p256);\n const keyAlgoParams = algo === 'x25519' ? { name: 'X25519' } : { name: 'ECDH', namedCurve: 'P-256' };\n\n const serverPubKey = await globalThis.crypto.subtle.importKey('raw', serverPubRaw, keyAlgoParams as any, false, []);\n const sharedBits = await globalThis.crypto.subtle.deriveBits(\n { name: algo === 'x25519' ? 'X25519' : 'ECDH', public: serverPubKey } as any,\n ephemeralPriv,\n 256,\n );\n const hkdfKey = await globalThis.crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);\n const salt = new TextEncoder().encode(sessionId);\n const info = new TextEncoder().encode('antz-transit-v1');\n\n return globalThis.crypto.subtle.deriveKey(\n { name: 'HKDF', hash: 'SHA-256', salt, info },\n hkdfKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\n// ─── Noble path (React Native / Hermes) ──────────────────────────────────────\n\nasync function generateNobleEphemeralKey(\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<Uint8Array> }> {\n const { x25519 } = await import('@noble/curves/ed25519');\n const { hkdf } = await import('@noble/hashes/hkdf');\n const { sha256 } = await import('@noble/hashes/sha256');\n const { randomBytes } = await import('@noble/hashes/utils');\n\n const ephemeralPriv = randomBytes(32);\n const ephemeralPub = x25519.getPublicKey(ephemeralPriv);\n const serverPubBytes = base64ToUint8(serverKeys.x25519);\n\n return {\n ephemeralPubB64: uint8ToBase64(ephemeralPub),\n deriveSessionKey: (sessionId: string): Promise<Uint8Array> => {\n const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);\n const salt = new TextEncoder().encode(sessionId);\n const info = new TextEncoder().encode('antz-transit-v1');\n return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32) as Uint8Array);\n },\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf;\n}\n","import { isAxiosError } from 'axios';\nimport { isTransitEnvelope } from './crypto/transit.js';\n\n// ─── Base error class ─────────────────────────────────────────────────────────\n\nexport class AntzChatError extends Error {\n readonly code: string;\n readonly retryable: boolean;\n readonly context?: Record<string, unknown>;\n\n constructor(\n code: string,\n message: string,\n retryable = false,\n context?: Record<string, unknown>,\n ) {\n super(message);\n this.name = 'AntzChatError';\n this.code = code;\n this.retryable = retryable;\n this.context = context;\n // Maintain proper prototype chain in transpiled environments\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Semantic subclasses ──────────────────────────────────────────────────────\n\n/** Thrown on 401 (after refresh also fails) or when no refresh token exists. */\nexport class AntzChatAuthError extends AntzChatError {\n constructor(message: string, code = 'AUTH_FAILED', context?: Record<string, unknown>) {\n super(code, message, false, context);\n this.name = 'AntzChatAuthError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 400 / 422 — bad input, validation failure. */\nexport class AntzChatValidationError extends AntzChatError {\n /** Server-returned field error array (when the server sends message as string[]). */\n readonly fields?: string[];\n\n constructor(message: string | string[], context?: Record<string, unknown>) {\n const msg = Array.isArray(message) ? message.join('; ') : message;\n super('VALIDATION_ERROR', msg, false, context);\n this.name = 'AntzChatValidationError';\n this.fields = Array.isArray(message) ? message : undefined;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on network failures, timeouts, socket disconnections, and queue overflow. retryable = true. */\nexport class AntzChatNetworkError extends AntzChatError {\n constructor(message: string, code = 'NETWORK_ERROR', context?: Record<string, unknown>) {\n super(code, message, true, context);\n this.name = 'AntzChatNetworkError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 403 — insufficient permissions. */\nexport class AntzChatPermissionError extends AntzChatError {\n constructor(message: string, context?: Record<string, unknown>) {\n super('PERMISSION_DENIED', message, false, context);\n this.name = 'AntzChatPermissionError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 5xx or other unexpected server errors. retryable = true. */\nexport class AntzChatServerError extends AntzChatError {\n readonly httpStatus?: number;\n\n constructor(message: string, httpStatus?: number, context?: Record<string, unknown>) {\n super('SERVER_ERROR', message, true, context);\n this.name = 'AntzChatServerError';\n this.httpStatus = httpStatus;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Error code reference ─────────────────────────────────────────────────────\n//\n// Code Class Source\n// ───────────────────── ──────────────────────── ─────────────────────────\n// AUTH_FAILED AntzChatAuthError 401 after refresh fails\n// SESSION_EXPIRED AntzChatAuthError 401, no refresh token\n// PERMISSION_DENIED AntzChatPermissionError 403\n// VALIDATION_ERROR AntzChatValidationError 400 / 422\n// NOT_FOUND AntzChatServerError 404\n// RATE_LIMITED AntzChatNetworkError 429\n// NETWORK_ERROR AntzChatNetworkError No response / conn failure\n// SOCKET_TIMEOUT AntzChatNetworkError ACK timeout / reconnect timeout\n// SOCKET_NOT_CONNECTED AntzChatNetworkError withAck when socket is down\n// SEND_QUEUE_FULL AntzChatNetworkError Queue overflow (>100 msgs)\n// MESSAGE_DROPPED AntzChatNetworkError Queue TTL expired (30s)\n// TRANSIT_MISMATCH AntzChatError SDK/server encryption config mismatch\n// SERVER_ERROR AntzChatServerError 5xx or unknown HTTP error\n\n// ─── REST error normaliser ────────────────────────────────────────────────────\n\n/**\n * Converts a raw axios error (or any unknown throw) into a typed AntzChatError.\n *\n * Call site: client.ts response interceptor — runs AFTER transit decryption,\n * so error.response.data is always plaintext by the time this function sees it.\n * If decryption itself failed, error.response.data remains the raw encrypted\n * envelope — detected via isTransitEnvelope() and noted in context.\n */\nexport function normalizeAxiosError(error: unknown): AntzChatError {\n if (error instanceof AntzChatError) return error;\n\n if (isAxiosError(error)) {\n const status = error.response?.status;\n const body = error.response?.data;\n\n // Detect if decryption failed — body is still an encrypted envelope\n const decryptionFailed = body != null && isTransitEnvelope(body);\n\n const rawMessage: string | string[] | undefined = decryptionFailed\n ? undefined\n : (body?.message ?? undefined);\n\n const message: string =\n (Array.isArray(rawMessage) ? rawMessage.join('; ') : rawMessage) ||\n error.message ||\n 'Request failed';\n\n const ctx: Record<string, unknown> = {\n ...(status != null && { httpStatus: status }),\n ...(body?.path != null && { path: body.path }),\n ...(body?.error != null && { serverError: body.error }),\n ...(error.code != null && { axiosCode: error.code }),\n ...(decryptionFailed && { decryptionFailed: true, note: 'Transit decryption failed — server error body is an encrypted envelope' }),\n };\n\n // No response at all — network/timeout failure\n if (!error.response) {\n return new AntzChatNetworkError(message || 'Network error', 'NETWORK_ERROR', ctx);\n }\n\n if (status === 401) {\n // AUTH_FAILED is used when a refresh was attempted but failed (set by interceptor).\n // SESSION_EXPIRED is the default: 401 with no prior retry = token simply expired.\n const code = (error.config as any)?._retry ? 'AUTH_FAILED' : 'SESSION_EXPIRED';\n return new AntzChatAuthError(message, code, ctx);\n }\n if (status === 403) return new AntzChatPermissionError(message, ctx);\n if (status === 400 || status === 422) {\n return new AntzChatValidationError(\n Array.isArray(rawMessage) ? rawMessage : message,\n ctx,\n );\n }\n if (status === 404) return new AntzChatServerError(message, 404, ctx);\n if (status === 429) return new AntzChatNetworkError(message, 'RATE_LIMITED', ctx);\n if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);\n\n return new AntzChatServerError(message, status, ctx);\n }\n\n const msg = error instanceof Error ? error.message : String(error);\n return new AntzChatError('UNKNOWN_ERROR', msg, false);\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, awaitTransitReadyOr, configureTransit, setTransitSession, getTransitSession } from '../crypto/session.js';\nimport { createRestTransitSession, fetchServerKeys, TransitRateLimitedError } from '../crypto/handshake.js';\nimport { detectTransitAlgo } from '../crypto/detect.js';\nimport { normalizeAxiosError, AntzChatNetworkError } from '../errors.js';\n\n// Hard ceiling on how long a single request will block waiting for the transit\n// handshake. A legitimate cold-start handshake resolves in well under this even\n// on a slow link (TransitGate already spent ~6s, establishTransit keeps\n// retrying). Past this we FAIL the request with a retryable error rather than\n// leave it pending forever — react-query cannot retry / refetch-on-focus a\n// request that never settles, so an unbounded wait here is an unrecoverable\n// silent hang. Each failed+retried request also re-arms ensureRestTransitHandshake.\nconst TRANSIT_GATE_MAX_WAIT_MS = 30_000;\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\nlet _avatarSent = false;\n// In-flight transit handshake promise — shared between initApiClient and connectSocket\n// so they never fire two concurrent HTTPS handshakes for the same session.\nlet _transitHandshakePromise: Promise<void> | null = null;\n// Gate the request interceptor until the SDK has resolved a token (async\n// authProvider) and, where wired, the transit handshake. Set by the SDK\n// provider; the interceptor awaits it before attaching the Authorization\n// header so early requests (e.g. useConversations' initial fetch) don't race\n// ahead unauthenticated.\nlet _authReadyPromise: Promise<unknown> | null = null;\n\nexport function getTransitHandshakePromise(): Promise<void> | null {\n return _transitHandshakePromise;\n}\n\nexport function setAuthReadyPromise(promise: Promise<unknown> | null): void {\n _authReadyPromise = promise;\n}\n\n// True once initApiClient() has run and its config has not been torn down by a\n// subsequent disconnectSocket(). Lets the SDK provider detect the case where a\n// React remount skipped re-init (its key was unchanged) but disconnectSocket()\n// had nulled _config in between — leaving the request interceptor unable to see\n// transitEncryption and firing every request as unencrypted plaintext.\nexport function isApiClientConfigured(): boolean {\n return _config !== null;\n}\n\n// (Re-)kick the HTTPS transit handshake. Idempotent: no-ops when transit is\n// disabled, a session already exists, or an attempt is already in flight.\n// Called both at init and from the request interceptor when a request is about\n// to block on waitForTransitReady() with no session — e.g. after a socket\n// disconnect cleared the session and nothing else re-established it.\n//\n// It only calls configureTransit(false) — which un-gates the interceptor and\n// lets requests go out as PLAINTEXT — when the server itself reports transit\n// disabled (GET /crypto/pubkey → enabled:false, i.e. an old server). A transient\n// failure of POST /crypto/session (network blip, rate limit, 5xx) must NOT\n// disable transit: the server still requires it, so plaintext would just 403.\n// Instead we retry with backoff; waitForTransitReady() keeps requests pending\n// and they dispatch the moment a retry sets the session.\nexport function ensureRestTransitHandshake(): void {\n if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;\n const apiUrl = _config.apiUrl;\n _transitHandshakePromise = (async () => {\n try {\n // A 429 is \"wait\", not \"broken\", so it must NOT consume the attempt\n // budget — otherwise a rate-limited client exhausts 5 attempts in a few\n // seconds and gives up on an endpoint that was working fine. Failures are\n // counted separately from rate-limit hits, and the loop is additionally\n // bounded by wall-clock time so a persistently limited server cannot keep\n // it running forever.\n const MAX_FAILURES = 5;\n const BACKSTOP_MS = 2 * 60_000;\n const deadline = Date.now() + BACKSTOP_MS;\n let failures = 0;\n let rateLimitHits = 0;\n\n while (failures < MAX_FAILURES && Date.now() < deadline) {\n if (getTransitSession()) return;\n let waitMs: number;\n try {\n // Identity is sent so the server can rate-limit these pre-auth routes\n // per user rather than per IP (see TransitIdentity in handshake.ts).\n const identity = { userId: _config?.userId, tenantId: _config?.tenantId };\n const keys = await fetchServerKeys(apiUrl, identity);\n if (!keys?.enabled) {\n configureTransit(false); // server genuinely doesn't want transit\n return;\n }\n const session = await createRestTransitSession(apiUrl, identity);\n if (session && !getTransitSession()) {\n const algo = typeof globalThis.crypto?.subtle !== 'undefined'\n ? await detectTransitAlgo()\n : 'x25519';\n setTransitSession({ sessionKey: session.sessionKey as CryptoKey, algo, sessionId: session.sessionId, enabled: true });\n return;\n }\n // Reachable when the server returned no session but did not throw\n // (e.g. an old server without the endpoint) — treat as a failure.\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n } catch (err) {\n if (err instanceof TransitRateLimitedError) {\n // Honour the server's own figure when it sent one (clamped to a\n // sane 1-60s), else escalate blind since the window is unknown.\n const blind = Math.min(15_000 * 2 ** rateLimitHits, 60_000);\n waitMs = err.retryAfterMs != null\n ? Math.min(Math.max(err.retryAfterMs, 1_000), 60_000)\n : blind;\n rateLimitHits++;\n console.warn(\n `[AntzChat] transit handshake rate-limited (429) — retrying in ${Math.round(waitMs / 1000)}s` +\n `${err.retryAfterMs != null ? ' (per Retry-After)' : ''}.`,\n );\n } else {\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n }\n }\n await new Promise((r) => setTimeout(r, waitMs));\n }\n console.error(\n '[AntzChat] transit handshake could not establish a session — ' +\n \"chat requests stay gated until one succeeds (server requires transit).\",\n );\n } finally {\n _transitHandshakePromise = null;\n }\n })();\n}\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // Kick off the HTTPS transit handshake immediately so REST calls that fire\n // before connectSocket (e.g. getMe() right after initApiClient) are not\n // blocked indefinitely. Store the promise so connectSocket can await it\n // instead of firing a duplicate handshake.\n ensureRestTransitHandshake();\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n // Wait for the SDK's auth (and, where wired, transit) gate before reading\n // the token — otherwise a request fired during boot goes out with no\n // Authorization header.\n if (_authReadyPromise) await _authReadyPromise;\n\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key before sending any request. The server\n // enforces transit encryption independent of auth (e.g. GET /app/config\n // fires before the async authProvider token resolves) — gating this on\n // `token` let those pre-auth requests race ahead of the handshake and get\n // rejected with 403 \"Transit encryption required\".\n if (_config?.transitEncryption) {\n // If the session is gone (socket disconnect cleared it, first boot still\n // pending), make sure a handshake is running before we block — otherwise\n // the wait could hang with nothing to resolve it.\n if (!getTransitSession()) ensureRestTransitHandshake();\n const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);\n if (!ready) {\n // Handshake still hasn't produced a session. Do NOT send plaintext\n // (server requires transit); fail loudly instead so the error surfaces\n // in the UI and react-query's retry re-drives the handshake.\n throw new AntzChatNetworkError(\n 'Secure channel to chat server not established — request not sent. It will retry automatically.',\n 'TRANSIT_NOT_READY',\n { url: req.url },\n );\n }\n }\n\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(normalizeAxiosError(error));\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n CompletedPart,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, PlatformUploadPartFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async completeMultipartUpload(\n fileId: string,\n uploadId: string,\n parts: CompletedPart[],\n ): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(\n `/storage/multipart/complete/${fileId}`,\n { uploadId, parts },\n );\n return data;\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\nasync function runMultipartUpload(\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n platformUploadPartFn: PlatformUploadPartFn,\n onProgress?: (pct: number) => void,\n): Promise<FileResponse> {\n const { multipart } = presigned;\n if (!multipart) throw new Error('No multipart info on presigned response');\n\n const CONCURRENCY = 3;\n const completedParts: CompletedPart[] = [];\n const partProgress: Record<number, number> = {};\n\n multipart.partUrls.forEach(({ partNumber }) => { partProgress[partNumber] = 0; });\n\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(partProgress);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg * 0.95));\n };\n\n const uploadPart = async (partNumber: number, uploadUrl: string, method: 'PUT' | 'POST'): Promise<void> => {\n const offset = (partNumber - 1) * multipart.chunkSize;\n const end = Math.min(offset + multipart.chunkSize, file.size);\n const blob = await fetch(file.uri).then((r) => r.blob());\n const slice = blob.slice(offset, end);\n\n const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {\n partProgress[partNumber] = pct;\n reportProgress();\n }, method);\n\n completedParts.push({ partNumber, etag });\n partProgress[partNumber] = 100;\n reportProgress();\n };\n\n for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {\n const batch = multipart.partUrls.slice(i, i + CONCURRENCY);\n const results = await Promise.allSettled(\n batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? 'PUT')),\n );\n const failed = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined;\n if (failed) throw failed.reason;\n }\n\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n const fileResponse = await storageApi.completeMultipartUpload(\n presigned.fileId,\n multipart.uploadId,\n completedParts,\n );\n onProgress?.(100);\n return fileResponse;\n}\n\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n let fileResponse: FileResponse;\n if (presigned.multipart && platformUploadPartFn) {\n fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {\n progressMap[originalIdx] = pct;\n reportProgress();\n });\n } else {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n fileResponse = await storageApi.confirmUpload(presigned.fileId);\n }\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n}\n"],"mappings":"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","names":["hasWebCrypto","bufToB64","b64ToBuf","base64ToUint8"]}
|
package/dist/chunk-UIYJAOGL.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/stores/chat.store.ts
|
|
2
|
-
import { create } from "zustand";
|
|
3
|
-
var useChatStore = create((set) => ({
|
|
4
|
-
activeConversationId: null,
|
|
5
|
-
pendingTarget: null,
|
|
6
|
-
typingUsers: {},
|
|
7
|
-
onlineUsers: [],
|
|
8
|
-
lastRead: {},
|
|
9
|
-
lastSeen: {},
|
|
10
|
-
replyingTo: null,
|
|
11
|
-
editingMessage: null,
|
|
12
|
-
forwardingMessage: null,
|
|
13
|
-
isSidebarOpen: true,
|
|
14
|
-
isGroupInfoOpen: false,
|
|
15
|
-
isStarredPanelOpen: false,
|
|
16
|
-
messageInfoId: null,
|
|
17
|
-
setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
|
|
18
|
-
setPendingTarget: (target) => set({ pendingTarget: target }),
|
|
19
|
-
addTypingUser: (conversationId, user) => set((state) => {
|
|
20
|
-
const existing = state.typingUsers[conversationId] ?? [];
|
|
21
|
-
const deduped = existing.filter((u) => u.userId !== user.userId);
|
|
22
|
-
return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
|
|
23
|
-
}),
|
|
24
|
-
removeTypingUser: (conversationId, userId) => set((state) => ({
|
|
25
|
-
typingUsers: {
|
|
26
|
-
...state.typingUsers,
|
|
27
|
-
[conversationId]: (state.typingUsers[conversationId] ?? []).filter(
|
|
28
|
-
(u) => u.userId !== userId
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
})),
|
|
32
|
-
setUserOnline: (userId) => set((state) => ({
|
|
33
|
-
onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
|
|
34
|
-
})),
|
|
35
|
-
setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
|
|
36
|
-
setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
|
|
37
|
-
setLastRead: (conversationId, messageId, readAt) => set((state) => ({
|
|
38
|
-
lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } }
|
|
39
|
-
})),
|
|
40
|
-
setLastSeen: (userId, lastSeenAt) => set((state) => {
|
|
41
|
-
if (lastSeenAt === null) {
|
|
42
|
-
const { [userId]: _, ...rest } = state.lastSeen;
|
|
43
|
-
return { lastSeen: rest };
|
|
44
|
-
}
|
|
45
|
-
return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };
|
|
46
|
-
}),
|
|
47
|
-
setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
|
|
48
|
-
setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
|
|
49
|
-
setForwardingMessage: (message) => set({ forwardingMessage: message }),
|
|
50
|
-
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
|
|
51
|
-
setSidebarOpen: (open) => set({ isSidebarOpen: open }),
|
|
52
|
-
toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
|
|
53
|
-
setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
|
|
54
|
-
toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
|
|
55
|
-
setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),
|
|
56
|
-
setMessageInfoId: (id) => set({ messageInfoId: id })
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
export {
|
|
60
|
-
useChatStore
|
|
61
|
-
};
|
|
62
|
-
//# sourceMappingURL=chunk-UIYJAOGL.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stores/chat.store.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { Message } from '../types/index.js';\n\ninterface TypingUser {\n userId: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface LastReadEntry {\n messageId: string;\n readAt: string;\n}\n\ninterface ChatState {\n activeConversationId: string | null;\n pendingTarget: { conversationId: string; messageId: string } | null;\n typingUsers: Record<string, TypingUser[]>;\n onlineUsers: string[];\n /** keyed by conversationId — current user's last read pointer per conversation */\n lastRead: Record<string, LastReadEntry>;\n /** keyed by userId — last seen timestamp for each user */\n lastSeen: Record<string, string>;\n replyingTo: Message | null;\n editingMessage: Message | null;\n /** Message staged in the forward picker, null = picker closed. Unlike replyingTo,\n * this does not fold into the next composer send — forwarding targets a different\n * conversation and needs its own conversation-picker UI. */\n forwardingMessage: Message | null;\n isSidebarOpen: boolean;\n isGroupInfoOpen: boolean;\n isStarredPanelOpen: boolean;\n /** messageId currently shown in the Message Info panel, null = closed */\n messageInfoId: string | null;\n\n setActiveConversation: (id: string | null) => void;\n setPendingTarget: (target: { conversationId: string; messageId: string } | null) => void;\n addTypingUser: (conversationId: string, user: TypingUser) => void;\n removeTypingUser: (conversationId: string, userId: string) => void;\n setUserOnline: (userId: string) => void;\n setUserOffline: (userId: string) => void;\n setOnlineUsers: (userIds: string[]) => void;\n setLastRead: (conversationId: string, messageId: string, readAt: string) => void;\n setLastSeen: (userId: string, lastSeenAt: string | null) => void;\n setReplyingTo: (message: Message | null) => void;\n setEditingMessage: (message: Message | null) => void;\n setForwardingMessage: (message: Message | null) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (open: boolean) => void;\n toggleGroupInfo: () => void;\n setGroupInfoOpen: (open: boolean) => void;\n toggleStarredPanel: () => void;\n setStarredPanelOpen: (open: boolean) => void;\n setMessageInfoId: (id: string | null) => void;\n}\n\nexport const useChatStore = create<ChatState>((set) => ({\n activeConversationId: null,\n pendingTarget: null,\n typingUsers: {},\n onlineUsers: [],\n lastRead: {},\n lastSeen: {},\n replyingTo: null,\n editingMessage: null,\n forwardingMessage: null,\n isSidebarOpen: true,\n isGroupInfoOpen: false,\n isStarredPanelOpen: false,\n messageInfoId: null,\n\n setActiveConversation: (id) =>\n set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),\n\n setPendingTarget: (target) => set({ pendingTarget: target }),\n\n addTypingUser: (conversationId, user) =>\n set((state) => {\n const existing = state.typingUsers[conversationId] ?? [];\n const deduped = existing.filter((u) => u.userId !== user.userId);\n return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };\n }),\n\n removeTypingUser: (conversationId, userId) =>\n set((state) => ({\n typingUsers: {\n ...state.typingUsers,\n [conversationId]: (state.typingUsers[conversationId] ?? []).filter(\n (u) => u.userId !== userId,\n ),\n },\n })),\n\n setUserOnline: (userId) =>\n set((state) => ({\n onlineUsers: state.onlineUsers.includes(userId)\n ? state.onlineUsers\n : [...state.onlineUsers, userId],\n })),\n\n setUserOffline: (userId) =>\n set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),\n\n setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),\n\n setLastRead: (conversationId, messageId, readAt) =>\n set((state) => ({\n lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } },\n })),\n\n setLastSeen: (userId, lastSeenAt) =>\n set((state) => {\n if (lastSeenAt === null) {\n const { [userId]: _, ...rest } = state.lastSeen;\n return { lastSeen: rest };\n }\n return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };\n }),\n\n setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),\n\n setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),\n\n setForwardingMessage: (message) => set({ forwardingMessage: message }),\n\n toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),\n setSidebarOpen: (open) => set({ isSidebarOpen: open }),\n\n toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),\n setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),\n\n toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),\n setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),\n\n setMessageInfoId: (id: string | null) => set({ messageInfoId: id }),\n}));\n"],"mappings":"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","names":[]}
|