@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.
- package/README.md +82 -5
- package/dist/chat.store-TA6G7PD6.js +7 -0
- package/dist/chunk-QHELYVNT.js +109 -0
- package/dist/chunk-QHELYVNT.js.map +1 -0
- package/dist/{chunk-WUNH3UTE.js → chunk-U637W5MD.js} +62 -13
- package/dist/chunk-U637W5MD.js.map +1 -0
- package/dist/index.cjs +182 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -3
- package/dist/index.d.ts +60 -3
- package/dist/index.js +70 -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 +170 -9
- package/package.json +1 -1
- package/dist/chat.store-UVTDBPEC.js +0 -7
- package/dist/chunk-UIYJAOGL.js +0 -62
- package/dist/chunk-UIYJAOGL.js.map +0 -1
- package/dist/chunk-WUNH3UTE.js.map +0 -1
- /package/dist/{chat.store-UVTDBPEC.js.map → chat.store-TA6G7PD6.js.map} +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
839
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
742
840
|
}
|
|
743
841
|
console.error(
|
|
744
|
-
"[AntzChat] transit handshake could not establish a session
|
|
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
|
-
|
|
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,
|