@antzsoft/chat-core 1.4.5 → 1.4.7

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.
@@ -0,0 +1 @@
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/index.cjs CHANGED
@@ -35,11 +35,37 @@ var chat_store_exports = {};
35
35
  __export(chat_store_exports, {
36
36
  useChatStore: () => useChatStore
37
37
  });
38
- var import_zustand2, useChatStore;
38
+ function clearTypingExpiry(conversationId, userId) {
39
+ const key = typingKey(conversationId, userId);
40
+ const timer = _typingExpiryTimers.get(key);
41
+ if (timer) {
42
+ clearTimeout(timer);
43
+ _typingExpiryTimers.delete(key);
44
+ }
45
+ }
46
+ function clearAllTypingExpiry() {
47
+ _typingExpiryTimers.forEach((timer) => clearTimeout(timer));
48
+ _typingExpiryTimers.clear();
49
+ }
50
+ function scheduleTypingExpiry(conversationId, userId) {
51
+ const key = typingKey(conversationId, userId);
52
+ const existing = _typingExpiryTimers.get(key);
53
+ if (existing) clearTimeout(existing);
54
+ const timer = setTimeout(() => {
55
+ _typingExpiryTimers.delete(key);
56
+ useChatStore.getState().removeTypingUser(conversationId, userId);
57
+ }, TYPING_EXPIRY_MS);
58
+ timer.unref?.();
59
+ _typingExpiryTimers.set(key, timer);
60
+ }
61
+ var import_zustand2, TYPING_EXPIRY_MS, _typingExpiryTimers, typingKey, useChatStore;
39
62
  var init_chat_store = __esm({
40
63
  "src/stores/chat.store.ts"() {
41
64
  "use strict";
42
65
  import_zustand2 = require("zustand");
66
+ TYPING_EXPIRY_MS = 6e3;
67
+ _typingExpiryTimers = /* @__PURE__ */ new Map();
68
+ typingKey = (conversationId, userId) => `${conversationId}\0${userId}`;
43
69
  useChatStore = (0, import_zustand2.create)((set) => ({
44
70
  activeConversationId: null,
45
71
  pendingTarget: null,
@@ -56,19 +82,40 @@ var init_chat_store = __esm({
56
82
  messageInfoId: null,
57
83
  setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
58
84
  setPendingTarget: (target) => set({ pendingTarget: target }),
85
+ // Each typing user carries a self-expiring timer, so an indicator can never
86
+ // outlive the evidence for it. Previously the ONLY things that cleared an
87
+ // indicator were an explicit isTyping:false from the sender and a local
88
+ // socket disconnect — so any lost false edge left "is typing…" on screen
89
+ // indefinitely. That is reachable in normal use: a sender whose tab is killed
90
+ // while their other devices stay connected never triggers the server's
91
+ // disconnect cleanup (it only fires when the user's LAST socket goes), and
92
+ // the false edge is fire-and-forget so it is never retried.
93
+ //
94
+ // The timer is the authority on liveness; the sender's false edge is now just
95
+ // a fast path. TYPING_EXPIRY_MS must exceed chat-core's outbound throttle
96
+ // window (3s) by enough that a still-typing peer always re-asserts before it
97
+ // fires, otherwise indicators would visibly flicker mid-typing.
59
98
  addTypingUser: (conversationId, user) => set((state) => {
99
+ scheduleTypingExpiry(conversationId, user.userId);
60
100
  const existing = state.typingUsers[conversationId] ?? [];
61
101
  const deduped = existing.filter((u) => u.userId !== user.userId);
62
102
  return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
63
103
  }),
64
- removeTypingUser: (conversationId, userId) => set((state) => ({
65
- typingUsers: {
66
- ...state.typingUsers,
67
- [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
68
- (u) => u.userId !== userId
69
- )
70
- }
71
- })),
104
+ removeTypingUser: (conversationId, userId) => set((state) => {
105
+ clearTypingExpiry(conversationId, userId);
106
+ const existing = state.typingUsers[conversationId];
107
+ if (!existing || !existing.some((u) => u.userId === userId)) return state;
108
+ return {
109
+ typingUsers: {
110
+ ...state.typingUsers,
111
+ [conversationId]: existing.filter((u) => u.userId !== userId)
112
+ }
113
+ };
114
+ }),
115
+ clearTypingUsers: () => {
116
+ clearAllTypingExpiry();
117
+ set({ typingUsers: {} });
118
+ },
72
119
  setUserOnline: (userId) => set((state) => ({
73
120
  onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
74
121
  })),
@@ -111,6 +158,7 @@ __export(src_exports, {
111
158
  HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
112
159
  MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
113
160
  MENTION_ALL_ID: () => MENTION_ALL_ID,
161
+ TransitRateLimitedError: () => TransitRateLimitedError,
114
162
  appConfigApi: () => appConfigApi,
115
163
  authApi: () => authApi,
116
164
  buildMentionText: () => buildMentionText,
@@ -143,11 +191,14 @@ __export(src_exports, {
143
191
  onSocketStatus: () => onSocketStatus,
144
192
  parseMentions: () => parseMentions,
145
193
  performHandshake: () => performHandshake,
194
+ readRetryAfterMs: () => readRetryAfterMs,
146
195
  reconnectSocket: () => reconnectSocket,
147
196
  refreshSocketAuth: () => refreshSocketAuth,
197
+ registerTeardownHook: () => registerTeardownHook,
148
198
  renderMentionParts: () => renderMentionParts,
149
199
  resetAuthStore: () => resetAuthStore,
150
200
  resetTrackedRooms: () => resetTrackedRooms,
201
+ resetTypingThrottle: () => resetTypingThrottle,
151
202
  resolveConfig: () => resolveConfig,
152
203
  resolveSystemMessageText: () => resolveSystemMessageText,
153
204
  setApiClientInstance: () => setApiClientInstance,
@@ -492,11 +543,36 @@ function resetAlgoCache() {
492
543
  }
493
544
 
494
545
  // src/crypto/handshake.ts
546
+ function readRetryAfterMs(headers) {
547
+ const parse = (raw) => {
548
+ if (!raw) return void 0;
549
+ const secs = Number(raw);
550
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
551
+ const when = Date.parse(raw);
552
+ return Number.isNaN(when) ? void 0 : Math.max(0, when - Date.now());
553
+ };
554
+ const found = ["Retry-After-identity", "Retry-After-ip", "Retry-After"].map((name) => parse(headers.get(name))).filter((ms) => ms != null);
555
+ return found.length > 0 ? Math.max(...found) : void 0;
556
+ }
557
+ var TransitRateLimitedError = class extends Error {
558
+ constructor(retryAfterMs) {
559
+ super("[AntzChat] transit handshake rate-limited (429)");
560
+ this.name = "TransitRateLimitedError";
561
+ this.retryAfterMs = retryAfterMs;
562
+ }
563
+ };
564
+ function identityHeaders(identity) {
565
+ const headers = {};
566
+ if (identity?.userId) headers["x-user-id"] = identity.userId;
567
+ if (identity?.tenantId) headers["X-Tenant-ID"] = identity.tenantId;
568
+ return headers;
569
+ }
495
570
  function hasWebCrypto2() {
496
571
  return typeof globalThis.crypto?.subtle !== "undefined";
497
572
  }
498
- async function fetchServerKeys(apiUrl) {
499
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
573
+ async function fetchServerKeys(apiUrl, identity) {
574
+ const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });
575
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
500
576
  if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
501
577
  const body = await res.json();
502
578
  return body?.data ?? body;
@@ -507,24 +583,26 @@ async function generateEphemeralKey(algo, serverKeys) {
507
583
  }
508
584
  return generateNobleEphemeralKey(serverKeys);
509
585
  }
510
- async function createRestTransitSession(apiUrl) {
586
+ async function createRestTransitSession(apiUrl, identity) {
511
587
  try {
512
- const serverKeys = await fetchServerKeys(apiUrl);
588
+ const serverKeys = await fetchServerKeys(apiUrl, identity);
513
589
  if (!serverKeys.enabled) return null;
514
590
  const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
515
591
  const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
516
592
  const res = await fetch(`${apiUrl}/crypto/session`, {
517
593
  method: "POST",
518
- headers: { "Content-Type": "application/json" },
594
+ headers: { "Content-Type": "application/json", ...identityHeaders(identity) },
519
595
  body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
520
596
  });
597
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
521
598
  if (!res.ok) return null;
522
599
  const body = await res.json();
523
600
  const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
524
601
  if (!sessionId) return null;
525
602
  const sessionKey = await deriveSessionKey(sessionId);
526
603
  return { sessionId, sessionKey };
527
- } catch {
604
+ } catch (err) {
605
+ if (err instanceof TransitRateLimitedError) throw err;
528
606
  return null;
529
607
  }
530
608
  }
@@ -722,26 +800,46 @@ function ensureRestTransitHandshake() {
722
800
  const apiUrl = _config.apiUrl;
723
801
  _transitHandshakePromise = (async () => {
724
802
  try {
725
- for (let attempt = 0; attempt < 5; attempt++) {
803
+ const MAX_FAILURES = 5;
804
+ const BACKSTOP_MS = 2 * 6e4;
805
+ const deadline = Date.now() + BACKSTOP_MS;
806
+ let failures = 0;
807
+ let rateLimitHits = 0;
808
+ while (failures < MAX_FAILURES && Date.now() < deadline) {
726
809
  if (getTransitSession()) return;
810
+ let waitMs;
727
811
  try {
728
- const keys = await fetchServerKeys(apiUrl);
812
+ const identity = { userId: _config?.userId, tenantId: _config?.tenantId };
813
+ const keys = await fetchServerKeys(apiUrl, identity);
729
814
  if (!keys?.enabled) {
730
815
  configureTransit(false);
731
816
  return;
732
817
  }
733
- const session = await createRestTransitSession(apiUrl);
818
+ const session = await createRestTransitSession(apiUrl, identity);
734
819
  if (session && !getTransitSession()) {
735
820
  const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
736
821
  setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
737
822
  return;
738
823
  }
739
- } catch {
824
+ failures++;
825
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
826
+ } catch (err) {
827
+ if (err instanceof TransitRateLimitedError) {
828
+ const blind = Math.min(15e3 * 2 ** rateLimitHits, 6e4);
829
+ waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), 6e4) : blind;
830
+ rateLimitHits++;
831
+ console.warn(
832
+ `[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
833
+ );
834
+ } else {
835
+ failures++;
836
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
837
+ }
740
838
  }
741
- await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** attempt, 8e3)));
839
+ await new Promise((r) => setTimeout(r, waitMs));
742
840
  }
743
841
  console.error(
744
- "[AntzChat] transit handshake could not establish a session after 5 attempts \u2014 chat requests stay gated until one succeeds (server requires transit)."
842
+ "[AntzChat] transit handshake could not establish a session \u2014 chat requests stay gated until one succeeds (server requires transit)."
745
843
  );
746
844
  } finally {
747
845
  _transitHandshakePromise = null;
@@ -1188,6 +1286,19 @@ function secureOn(socket, event, handler) {
1188
1286
  handler(raw);
1189
1287
  });
1190
1288
  }
1289
+ var _teardownHooks = /* @__PURE__ */ new Set();
1290
+ function registerTeardownHook(hook) {
1291
+ _teardownHooks.add(hook);
1292
+ return () => _teardownHooks.delete(hook);
1293
+ }
1294
+ function runTeardownHooks() {
1295
+ _teardownHooks.forEach((hook) => {
1296
+ try {
1297
+ hook();
1298
+ } catch {
1299
+ }
1300
+ });
1301
+ }
1191
1302
  var _joinedRooms = /* @__PURE__ */ new Set();
1192
1303
  function trackRoomJoin(conversationId) {
1193
1304
  _joinedRooms.add(conversationId);
@@ -1241,7 +1352,7 @@ async function _doConnect(config, getToken) {
1241
1352
  if (existingSession?.enabled && existingSession.sessionId) {
1242
1353
  httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
1243
1354
  } else {
1244
- httpsSession = await createRestTransitSession(config.apiUrl);
1355
+ httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1245
1356
  if (httpsSession) {
1246
1357
  const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1247
1358
  setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
@@ -1252,7 +1363,7 @@ async function _doConnect(config, getToken) {
1252
1363
  boundDeriveSessionKey = null;
1253
1364
  } else {
1254
1365
  try {
1255
- const serverKeys = await fetchServerKeys(config.apiUrl);
1366
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1256
1367
  if (!serverKeys.enabled) {
1257
1368
  throw new AntzChatError(
1258
1369
  "TRANSIT_MISMATCH",
@@ -1269,7 +1380,7 @@ async function _doConnect(config, getToken) {
1269
1380
  }
1270
1381
  } else {
1271
1382
  try {
1272
- const serverKeys = await fetchServerKeys(config.apiUrl);
1383
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1273
1384
  if (serverKeys.enabled) {
1274
1385
  throw new AntzChatError(
1275
1386
  "TRANSIT_MISMATCH",
@@ -1383,6 +1494,10 @@ function disconnectSocket() {
1383
1494
  }
1384
1495
  clearTransitSession();
1385
1496
  resetAlgoCache();
1497
+ runTeardownHooks();
1498
+ Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
1499
+ useChatStore2.getState().clearTypingUsers();
1500
+ });
1386
1501
  _getToken = null;
1387
1502
  _userId = void 0;
1388
1503
  _tenantId = void 0;
@@ -1434,6 +1549,12 @@ var RECONNECT_WAIT_TIMEOUT = 15e3;
1434
1549
  var QUEUE_MAX_SIZE = 100;
1435
1550
  var QUEUE_ENTRY_TTL = 3e4;
1436
1551
  var sendQueues = /* @__PURE__ */ new Map();
1552
+ var TYPING_THROTTLE_MS = 3e3;
1553
+ var _lastTypingSentAt = /* @__PURE__ */ new Map();
1554
+ function resetTypingThrottle() {
1555
+ _lastTypingSentAt.clear();
1556
+ }
1557
+ registerTeardownHook(resetTypingThrottle);
1437
1558
  var sendQueueRunning = /* @__PURE__ */ new Map();
1438
1559
  async function drainSendQueue(conversationId) {
1439
1560
  if (sendQueueRunning.get(conversationId)) return;
@@ -1564,8 +1685,40 @@ var socketEmit = {
1564
1685
  return withAck("unpin_message", { messageId });
1565
1686
  },
1566
1687
  // markRead and typing are best-effort — silently dropped if socket not ready
1688
+ //
1689
+ // Typing is additionally LEADING-THROTTLED here rather than in the UI layer,
1690
+ // so every consumer (both UI SDKs and any host calling socketEmit directly)
1691
+ // gets the same emit budget and none can bypass it.
1692
+ //
1693
+ // The composer calls this on EVERY keystroke. A 40-word message is ~200 calls
1694
+ // of which exactly one carries information: "this person started typing".
1695
+ // Each one previously cost a round trip plus real server work, making typing
1696
+ // the single most expensive event in the system per unit of user value.
1697
+ //
1698
+ // Shape of the throttle:
1699
+ // isTyping:true — passed through at most once per TYPING_THROTTLE_MS per
1700
+ // conversation. A continuous typist emits ~20/min instead
1701
+ // of ~200/min.
1702
+ // isTyping:false — ALWAYS passed through, and resets the window. It is the
1703
+ // edge that clears the indicator on every peer, it is
1704
+ // already debounced by the composer, and dropping it is
1705
+ // exactly the failure that leaves "is typing…" stuck.
1706
+ //
1707
+ // The server refreshes its typing key on each true edge (10s TTL) and
1708
+ // receivers expire their own indicators after TYPING_EXPIRY_MS, both of which
1709
+ // are comfortably longer than TYPING_THROTTLE_MS — so a throttled-away event
1710
+ // never lets an indicator lapse mid-typing.
1567
1711
  typing(conversationId, isTyping) {
1568
- fireAndForget("typing", { conversationId, isTyping });
1712
+ if (!isTyping) {
1713
+ _lastTypingSentAt.delete(conversationId);
1714
+ fireAndForget("typing", { conversationId, isTyping: false });
1715
+ return;
1716
+ }
1717
+ const now = Date.now();
1718
+ const last = _lastTypingSentAt.get(conversationId) ?? 0;
1719
+ if (now - last < TYPING_THROTTLE_MS) return;
1720
+ _lastTypingSentAt.set(conversationId, now);
1721
+ fireAndForget("typing", { conversationId, isTyping: true });
1569
1722
  },
1570
1723
  markRead(conversationId, messageId) {
1571
1724
  fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
@@ -2283,6 +2436,7 @@ var AntzChatClient = class {
2283
2436
  HIGHLY_FORWARDED_DEPTH_THRESHOLD,
2284
2437
  MAX_FORWARD_TARGETS,
2285
2438
  MENTION_ALL_ID,
2439
+ TransitRateLimitedError,
2286
2440
  appConfigApi,
2287
2441
  authApi,
2288
2442
  buildMentionText,
@@ -2315,11 +2469,14 @@ var AntzChatClient = class {
2315
2469
  onSocketStatus,
2316
2470
  parseMentions,
2317
2471
  performHandshake,
2472
+ readRetryAfterMs,
2318
2473
  reconnectSocket,
2319
2474
  refreshSocketAuth,
2475
+ registerTeardownHook,
2320
2476
  renderMentionParts,
2321
2477
  resetAuthStore,
2322
2478
  resetTrackedRooms,
2479
+ resetTypingThrottle,
2323
2480
  resolveConfig,
2324
2481
  resolveSystemMessageText,
2325
2482
  setApiClientInstance,