@antzsoft/chat-core 1.4.6 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -7
- package/dist/chat.store-TA6G7PD6.js +7 -0
- package/dist/{chunk-U637W5MD.js → chunk-L537XWRD.js} +12 -4
- package/dist/chunk-L537XWRD.js.map +1 -0
- package/dist/chunk-QHELYVNT.js +109 -0
- package/dist/chunk-QHELYVNT.js.map +1 -0
- package/dist/index.cjs +145 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +82 -9
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.js +1 -1
- package/docs/integration-guide.html +19 -5
- package/package.json +1 -1
- package/dist/chat.store-UVTDBPEC.js +0 -7
- package/dist/chunk-U637W5MD.js.map +0 -1
- package/dist/chunk-UIYJAOGL.js +0 -62
- package/dist/chunk-UIYJAOGL.js.map +0 -1
- /package/dist/{chat.store-UVTDBPEC.js.map → chat.store-TA6G7PD6.js.map} +0 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/types.ts","../src/api/auth.ts","../src/api/sync.ts","../src/api/app-config.ts","../src/stores/auth.store.ts","../src/utils/resolveSystemMessageText.ts","../src/socket/socket.ts","../src/socket/emitters.ts","../src/api/messages.ts","../src/api/conversations.ts","../src/api/devices.ts","../src/api/users.ts","../src/types/index.ts","../src/utils/mentions.ts","../src/client-facade.ts"],"sourcesContent":["import type { FileType, UploadableFile, PresignedUrlResponse, CompressedFile } from '../types/index.js';\n\nexport interface FileSizeLimits {\n image?: number;\n video?: number;\n audio?: number;\n document?: number;\n default?: number;\n}\n\n/**\n * Platform-provided function that performs the actual binary upload to a presigned URL.\n * - Web implementation: XMLHttpRequest with progress events\n * - RN implementation: fetch with FormData or direct body\n *\n * The core calls this — it never does the upload itself.\n */\nexport type PlatformUploadFn = (\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n onProgress?: (pct: number) => void,\n) => Promise<void>;\n\n/**\n * Platform-provided persistent key-value storage for auth token persistence.\n * - Web: localStorage adapter\n * - RN: AsyncStorage adapter\n */\nexport interface PersistStorage {\n getItem(key: string): string | null | Promise<string | null>;\n setItem(key: string, value: string): void | Promise<void>;\n removeItem(key: string): void | Promise<void>;\n}\n\n/**\n * Platform-provided compression function. Optional — if omitted, files are uploaded as-is.\n * - Web: uses canvas (images) + CompressionStream (text/docs) — both browser-native\n * - RN: uses expo-image-manipulator (images) + pako (text/docs)\n * - Node: uses sharp (images) + zlib (text/docs)\n */\nexport type PlatformCompressFn = (\n file: UploadableFile,\n options: ResolvedCompressionConfig,\n) => Promise<CompressedFile>;\n\n/**\n * Platform-provided function for uploading a single multipart part.\n * Takes a presigned PUT URL and a raw blob/buffer slice, returns the ETag\n * from the S3 response (required for CompleteMultipartUpload).\n * Internal — each SDK (web, RN) provides its own; not exposed to consumers.\n */\nexport type PlatformUploadPartFn = (\n uploadUrl: string,\n blob: Blob | ArrayBuffer,\n onProgress?: (pct: number) => void,\n method?: 'PUT' | 'POST',\n) => Promise<string>; // returns ETag\n\nexport interface CompressionConfig {\n /** Master switch. Default: true when platformCompressFn is provided, false otherwise */\n enabled?: boolean;\n /** WebP quality for images, 0–1. Default: 0.85 */\n imageQuality?: number;\n /** Longest side cap in px before encoding. Default: 1920 */\n imageMaxDimension?: number;\n /** Gzip text-based documents (plain, csv, json, xml, yaml, svg). Default: true */\n compressDocuments?: boolean;\n}\n\nexport interface ResolvedCompressionConfig {\n enabled: boolean;\n imageQuality: number;\n imageMaxDimension: number;\n compressDocuments: boolean;\n}\n\nexport interface UploadConfig {\n /**\n * Per-type file size limits in MB. Can also pass a single number for all types.\n * Defaults: image 5MB, video 25MB, audio 10MB, document 10MB.\n */\n maxFileSizeMB?: number | FileSizeLimits;\n\n /** Max files per message. Default: 10 */\n maxFilesPerMessage?: number;\n\n /** Which attachment types users can send. Default: all */\n allowedTypes?: Array<FileType>;\n\n /** Called when a file fails validation or upload */\n onUploadError?: (file: UploadableFile, error: Error) => void;\n\n /** Called with 0–100 aggregate progress during a batch upload */\n onProgress?: (progress: number) => void;\n}\n\n/** Context passed to onSendError. */\nexport interface SendErrorContext {\n conversationId: string;\n /** tempId of the optimistic message the failure belongs to. */\n tempId: string;\n /**\n * True when resending the identical payload can't succeed (file size/type\n * limits, message too long, no longer a participant, …). The SDK has removed\n * the optimistic message from the timeline. False for transient failures\n * (network / socket / generic server error), where a \"! Retry\" bubble stays.\n */\n permanent: boolean;\n}\n\nexport interface AntzChatConfig {\n /** REST API base URL — e.g. \"https://api.yourapp.com/api/v1\" */\n apiUrl: string;\n\n /**\n * WebSocket server URL. Defaults to apiUrl with /api/vN path stripped.\n * SDK connects to {socketUrl}/chat\n */\n socketUrl?: string;\n\n /** Static JWT. Use this OR authProvider, not both. */\n authToken?: string;\n\n /**\n * Dynamic token getter — called before requests and on socket reconnect.\n * Preferred when the host app manages its own auth lifecycle.\n */\n authProvider?: () => Promise<string>;\n\n /**\n * External user ID — required for non-builtin modes (antz / external / wso2).\n * Sent as x-user-id header on every request so the chat server can forward it\n * to the user-service for token validation.\n * Not needed for builtin mode (chat server issues its own JWTs).\n */\n userId?: string;\n\n /**\n * Profile picture for non-builtin modes.\n * Server fetches/decodes, hashes for dedup, uploads to chat storage on first use.\n * Only sent once at init — not re-sent on every request.\n * Client never needs to compute hashes; the server owns all dedup logic.\n */\n avatar?: {\n /** Full URL the server can fetch (preferred — Antz/external/wso2 clients know their own file URLs) */\n url?: string;\n /** Raw base64 string (with or without data:... prefix) as fallback */\n base64?: string;\n };\n\n /** Required for multi-tenant backends. Sent as X-Tenant-ID header. */\n tenantId?: string;\n\n /**\n * Enable payload-level transit encryption (ECDH key exchange + AES-256-GCM).\n * Encrypts every HTTP request/response body and every socket event payload.\n * Server must have TRANSIT_ENCRYPTION_ENABLED=true to match.\n * Default: true — set false only for local development/debugging.\n * Safe to toggle anytime — no data migration needed (wire-only, never affects storage).\n */\n transitEncryption?: boolean;\n\n upload?: UploadConfig;\n\n /**\n * Optional compression config. Compression is disabled if platformCompressFn is not provided.\n */\n compression?: CompressionConfig;\n\n /**\n * Platform-specific compression implementation. Optional — omit to disable compression.\n * Each SDK (web, RN) provides its own default; Node.js users wire in their own.\n */\n platformCompressFn?: PlatformCompressFn;\n\n /**\n * Called when sending a message ultimately fails, after the SDK has updated\n * the timeline (removed the bubble for permanent failures, flagged it retryable\n * otherwise). Use it to surface a toast / error banner to the user.\n */\n onSendError?: (error: Error, context: SendErrorContext) => void;\n\n /**\n * Number of messages fetched per page when loading chat history.\n * Default: 40\n */\n messagePageSize?: number;\n\n /**\n * Number of starred messages fetched per page.\n * Default: 30\n */\n starredMessagePageSize?: number;\n\n /**\n * Number of search results fetched per request.\n * Default: 50\n */\n searchPageSize?: number;\n\n /**\n * Platform-specific binary upload implementation.\n * Required — each SDK (web, RN) provides its own.\n */\n platformUploadFn: PlatformUploadFn;\n\n /**\n * Platform-specific single-part upload for chunked multipart uploads.\n * Internal — web/RN SDKs set this automatically; not set by SDK consumers.\n */\n platformUploadPartFn?: PlatformUploadPartFn;\n\n /**\n * Platform-specific persistent storage for auth tokens.\n * Required — each SDK provides its own (localStorage / AsyncStorage).\n */\n persistStorage: PersistStorage;\n}\n\n// ─── Resolved (all defaults filled) ─────────────────────────────────────────\n\nexport interface ResolvedFileSizeLimits {\n image: number;\n video: number;\n audio: number;\n document: number;\n default: number;\n}\n\nexport interface ResolvedConfig {\n apiUrl: string;\n socketUrl: string;\n socketOrigin: string;\n socketPath: string;\n authToken?: string;\n authProvider?: () => Promise<string>;\n userId?: string;\n tenantId?: string;\n avatar?: { url?: string; base64?: string };\n transitEncryption: boolean;\n upload: {\n maxFileSizeMB: ResolvedFileSizeLimits;\n maxFilesPerMessage: number;\n allowedTypes: FileType[];\n onUploadError?: (file: UploadableFile, error: Error) => void;\n onProgress?: (progress: number) => void;\n };\n platformUploadFn: PlatformUploadFn;\n platformUploadPartFn?: PlatformUploadPartFn;\n platformCompressFn?: PlatformCompressFn;\n compression: ResolvedCompressionConfig;\n persistStorage: PersistStorage;\n onSendError?: (error: Error, context: SendErrorContext) => void;\n messagePageSize: number;\n starredMessagePageSize: number;\n searchPageSize: number;\n}\n\nexport function resolveConfig(config: AntzChatConfig): ResolvedConfig {\n const socketUrl =\n config.socketUrl ??\n config.apiUrl.replace(/\\/api\\/v\\d+\\/?$/, '').replace(/\\/$/, '');\n\n // Auto-derive Socket.IO engine path from the socketUrl pathname.\n // If the server is behind a reverse proxy with a path prefix (e.g. /chat-api),\n // that prefix is already embedded in socketUrl. We append /socket.io to it.\n // Examples:\n // socketUrl = https://host/chat-api → socketPath = /chat-api/socket.io\n // socketUrl = https://host → socketPath = /socket.io\n // No env var or manual config needed — SERVER_URL on the server and apiUrl\n // on the client carry the same prefix, so both sides derive the same path.\n // Derive Socket.IO engine path and origin separately.\n // Socket.IO uses the URL passed to io() as the namespace origin — anything\n // in the pathname of that URL becomes part of the namespace, not the engine path.\n // So we must pass only the bare origin (https://host) to io(), and use the\n // path option for the engine mount point.\n //\n // socketUrl = https://host/chat-api\n // → socketOrigin = https://host (passed to io())\n // → socketPath = /chat-api/socket.io (engine path option)\n // → namespace = /chat (hardcoded in connectSocket)\n //\n // socketUrl = https://host\n // → socketOrigin = https://host\n // → socketPath = /socket.io\n let socketOrigin = socketUrl;\n let socketPath = '/socket.io';\n try {\n const parsed = new URL(socketUrl);\n socketOrigin = parsed.origin; // strips pathname, search, hash\n const pathname = parsed.pathname.replace(/\\/$/, '');\n if (pathname && pathname !== '/') socketPath = `${pathname}/socket.io`;\n } catch {\n // invalid URL — fall back to defaults\n }\n\n const raw = config.upload?.maxFileSizeMB;\n let limits: ResolvedFileSizeLimits;\n\n if (typeof raw === 'number') {\n limits = { image: raw, video: raw, audio: raw, document: raw, default: raw };\n } else {\n const fallback = raw?.default ?? 25;\n limits = {\n image: raw?.image ?? 5,\n video: raw?.video ?? 25,\n audio: raw?.audio ?? 10,\n document: raw?.document ?? 10,\n default: fallback,\n };\n }\n\n return {\n apiUrl: config.apiUrl.replace(/\\/$/, ''),\n socketUrl,\n socketOrigin,\n socketPath,\n authToken: config.authToken,\n authProvider: config.authProvider,\n userId: config.userId,\n tenantId: config.tenantId,\n avatar: config.avatar,\n transitEncryption: config.transitEncryption ?? true,\n upload: {\n maxFileSizeMB: limits,\n maxFilesPerMessage: config.upload?.maxFilesPerMessage ?? 10,\n allowedTypes: config.upload?.allowedTypes ?? ['image', 'video', 'audio', 'document'],\n onUploadError: config.upload?.onUploadError,\n onProgress: config.upload?.onProgress,\n },\n platformUploadFn: config.platformUploadFn,\n platformUploadPartFn: config.platformUploadPartFn,\n platformCompressFn: config.platformCompressFn,\n compression: {\n enabled: config.compression?.enabled ?? (config.platformCompressFn != null),\n imageQuality: config.compression?.imageQuality ?? 0.85,\n imageMaxDimension: config.compression?.imageMaxDimension ?? 1920,\n compressDocuments: config.compression?.compressDocuments ?? true,\n },\n persistStorage: config.persistStorage,\n onSendError: config.onSendError,\n messagePageSize: config.messagePageSize ?? 40,\n starredMessagePageSize: config.starredMessagePageSize ?? 30,\n searchPageSize: config.searchPageSize ?? 50,\n };\n}\n","import type { AuthResponse, AuthTokens, LoginCredentials, RegisterData, User } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\nexport const authApi = {\n async login(credentials: LoginCredentials): Promise<AuthResponse> {\n const { data } = await getApiClient().post<AuthResponse>('/auth/login', credentials);\n return data;\n },\n\n async register(payload: RegisterData): Promise<AuthResponse> {\n const { data } = await getApiClient().post<AuthResponse>('/auth/register', payload);\n return data;\n },\n\n async refresh(refreshToken: string): Promise<AuthTokens> {\n const { data } = await getApiClient().post<AuthTokens>('/auth/refresh', { refreshToken });\n return data;\n },\n\n async logout(refreshToken?: string): Promise<void> {\n await getApiClient().post('/auth/logout', refreshToken ? { refreshToken } : {});\n },\n\n async logoutAll(): Promise<void> {\n await getApiClient().post('/auth/logout-all');\n },\n\n async getMe(): Promise<User> {\n const { data } = await getApiClient().get<User>('/users/me');\n return data;\n },\n\n // Upload a new avatar file (builtin mode — multipart)\n async uploadAvatar(file: File | Blob, mimeType?: string): Promise<{ avatarUrl: string }> {\n const form = new FormData();\n form.append('avatar', file instanceof File ? file : new File([file], 'avatar.jpg', { type: mimeType ?? 'image/jpeg' }));\n const { data } = await getApiClient().post<{ avatarUrl: string }>('/users/me/avatar', form, {\n headers: { 'Content-Type': 'multipart/form-data' },\n });\n return data;\n },\n\n // Sync avatar from URL or base64 (non-builtin modes — or post-init update)\n async syncAvatar(source: { url?: string; base64?: string }): Promise<{ avatarUrl: string }> {\n const headers: Record<string, string> = {};\n if (source.base64) headers['x-avatar-base64'] = source.base64;\n else if (source.url) headers['x-avatar-url'] = source.url;\n const { data } = await getApiClient().post<{ avatarUrl: string }>('/users/me/avatar/sync', {}, { headers });\n return data;\n },\n};\n","import type { CrossConversationSyncResponse, ConversationSyncResponse } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\nexport const syncApi = {\n /**\n * Cross-conversation delta sync — call on every socket reconnect.\n * `since` is the ISO timestamp returned as `syncedAt` from the previous call.\n * Returns `stale: true` (with empty arrays) when the gap exceeds 60 days —\n * the app should mark all conversations `needs_refresh` and lazy-sync on open.\n */\n pull(since: string): Promise<CrossConversationSyncResponse> {\n return getApiClient()\n .get<CrossConversationSyncResponse>('/sync', { params: { since } })\n .then(r => r.data);\n },\n\n /**\n * Per-conversation delta sync — call when a conversation has `needs_refresh = true`\n * or on fresh install (omit all opts to receive full history).\n * Covers messages, deletedForMe, full reaction state, stars, participant changes,\n * and read receipts — everything needed to make local SQLite authoritative.\n *\n * Direction:\n * afterSeq — forward (oldest→newest, catch-up/sync). Default when seq is known.\n * beforeSeq — backward (newest→oldest, scroll-back / load history).\n * since — timestamp fallback for old servers or first sync on a new install.\n *\n * When `hasMore` is true, call again with `nextSeq` as the same cursor type you sent.\n */\n pullConversation(\n conversationId: string,\n opts?: { since?: string; afterSeq?: number; beforeSeq?: number },\n ): Promise<ConversationSyncResponse> {\n const params: Record<string, string | number> = {};\n if (opts?.afterSeq !== undefined) {\n params.afterSeq = opts.afterSeq;\n } else if (opts?.beforeSeq !== undefined) {\n params.beforeSeq = opts.beforeSeq;\n } else if (opts?.since) {\n params.since = opts.since;\n }\n return getApiClient()\n .get<ConversationSyncResponse>(`/conversations/${conversationId}/sync`, { params })\n .then(r => r.data);\n },\n};\n","import { getApiClient } from './client.js';\nimport type { AppConfig } from '../types/index.js';\n\nexport const appConfigApi = {\n async get(): Promise<AppConfig> {\n const { data } = await getApiClient().get<AppConfig>('/app/config');\n return data;\n },\n};\n","import { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport type { AuthTokens, User } from '../types/index.js';\nimport type { PersistStorage } from '../config/types.js';\n\ninterface AuthState {\n user: User | null;\n tokens: AuthTokens | null;\n isAuthenticated: boolean;\n isLoading: boolean;\n isHydrated: boolean;\n\n setAuth: (user: User, tokens: AuthTokens) => void;\n setTokens: (tokens: AuthTokens) => void;\n setUser: (user: User) => void;\n logout: () => void;\n setLoading: (loading: boolean) => void;\n setHydrated: (hydrated: boolean) => void;\n}\n\n/**\n * Creates a platform-specific auth store.\n * Call once during SDK init with the platform's PersistStorage adapter.\n * - Web: pass a localStorage adapter\n * - RN: pass an AsyncStorage adapter\n */\nexport function createAuthStore(storage: PersistStorage) {\n if (!storage) throw new Error('[AntzChat] createAuthStore requires a valid PersistStorage — received undefined. Make sure the SDK config is fully resolved before initializing the store.');\n // Use a ref object so the onRehydrateStorage closure can call store.setState\n // without hitting a temporal dead zone. On web, localStorage is synchronous\n // and onRehydrateStorage fires during create() before `store` is assigned.\n const ref: { store: any } = { store: null };\n\n const store = create<AuthState>()(\n persist(\n (set) => ({\n user: null,\n tokens: null,\n isAuthenticated: false,\n isLoading: false,\n isHydrated: false,\n\n setAuth: (user, tokens) =>\n set({ user, tokens, isAuthenticated: true, isLoading: false }),\n\n setTokens: (tokens) => set({ tokens }),\n\n setUser: (user) => set({ user }),\n\n logout: () =>\n set({ user: null, tokens: null, isAuthenticated: false, isLoading: false }),\n\n setLoading: (isLoading) => set({ isLoading }),\n\n setHydrated: (isHydrated) => set({ isHydrated }),\n }),\n {\n name: 'antz-chat-auth',\n storage: {\n getItem: (name) => {\n const result = storage.getItem(name);\n if (result instanceof Promise) {\n return result.then((str) => (str ? JSON.parse(str) : null));\n }\n return result ? JSON.parse(result) : null;\n },\n setItem: (name, value) => {\n storage.setItem(name, JSON.stringify(value));\n },\n removeItem: (name) => storage.removeItem(name),\n },\n partialize: (state) => ({\n user: state.user,\n tokens: state.tokens,\n isAuthenticated: state.isAuthenticated,\n }) as AuthState,\n onRehydrateStorage: () => (state, error) => {\n if (error) {\n console.warn('[AntzChat] Auth store rehydration failed:', error);\n }\n // Access via ref — safe even when called synchronously during create()\n ref.store?.setState({ isHydrated: true });\n },\n },\n ),\n );\n\n // Assign ref immediately after create() completes\n ref.store = store;\n\n // If already hydrated synchronously (web/localStorage rehydrated during create),\n // set the flag now since the closure above may have called ref.store when it was null.\n if (!store.getState().isHydrated) {\n // Fallback: async storage (RN) — force hydrated after 3s max\n const hydrationTimeout = setTimeout(() => {\n if (!store.getState().isHydrated) {\n store.setState({ isHydrated: true });\n }\n }, 3000);\n\n // Clean up if it resolves naturally before timeout\n const unsub = store.subscribe((s) => {\n if (s.isHydrated) {\n clearTimeout(hydrationTimeout);\n unsub();\n }\n });\n }\n\n const tokenStore = {\n getAccessToken: () => store.getState().tokens?.accessToken,\n getRefreshToken: () => store.getState().tokens?.refreshToken,\n setTokens: (tokens: AuthTokens) => store.getState().setTokens(tokens),\n clearTokens: () => store.getState().logout(),\n };\n\n return { useAuthStore: store, authTokenStore: tokenStore };\n}\n\n// ─── Singleton instances (set during SDK init) ────────────────────────────────\n// Each SDK (web / RN) calls createAuthStore once and exports these.\n// Direct import of these will throw until initAuthStore is called.\n\nlet _authStore: ReturnType<typeof createAuthStore> | null = null;\n\n/**\n * Initialize the auth store singleton.\n * Idempotent — subsequent calls with the same storage return the existing instance.\n * This ensures tokens persisted in localStorage/AsyncStorage survive re-renders\n * and React StrictMode double-invocations.\n */\nexport function initAuthStore(storage: PersistStorage) {\n if (!_authStore) {\n _authStore = createAuthStore(storage);\n }\n return _authStore;\n}\n\nexport function getAuthStore() {\n if (!_authStore) throw new Error('[AntzChat] Auth store not initialized. Call initAuthStore first.');\n return _authStore;\n}\n\n/** Reset the singleton — only for tests / SDK teardown. */\nexport function resetAuthStore() {\n _authStore = null;\n}\n","import type { Message } from '../types/index.js';\n\n/**\n * Resolves viewer-aware display text for system messages.\n *\n * The server stores a neutral third-person text in content.text (used for push\n * notifications and exports) and structured actor/target fields in metadata.\n * This function uses those fields plus the current viewer's userId to produce\n * the correct first-person variant (\"You removed Ajay\", \"Anil removed you\", etc.).\n *\n * Backward compatible: if the server does not send actorUserId (old server), the\n * raw content.text is returned unchanged so old behaviour is preserved.\n */\nexport function resolveSystemMessageText(message: Message, currentUserId: string): string {\n const m = message.metadata;\n\n if (!m?.systemOperationType || !m.actorUserId) {\n return message.content.text ?? '';\n }\n\n const iActor = m.actorUserId === currentUserId;\n const iTarget = m.targetUserId === currentUserId;\n const actor = iActor ? 'You' : (m.actorUserName ?? 'Someone');\n const target = iTarget ? 'you' : (m.targetUserName ?? 'a member');\n\n switch (m.systemOperationType) {\n case 'group_created':\n return iActor ? 'You created the group' : `${actor} created the group`;\n case 'user_added':\n return iActor ? `You added ${target}` : `${actor} added ${target}`;\n case 'user_removed':\n return iActor ? `You removed ${target}` : `${actor} removed ${target}`;\n case 'user_left':\n return iActor ? 'You left the group' : `${actor} left the group`;\n case 'admin_promoted':\n return iActor ? `You made ${target} an admin` : `${actor} made ${target} an admin`;\n case 'admin_demoted':\n return iActor ? `You removed ${target} as admin` : `${actor} removed ${target} as admin`;\n case 'group_updated':\n return iActor ? 'You updated the group' : `${actor} updated the group`;\n default:\n return message.content.text ?? '';\n }\n}\n","import { io, Socket } from 'socket.io-client';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { ReadReceiptEvent, UserStatusEvent } from '../types/index.js';\nimport { AntzChatError, AntzChatNetworkError } from '../errors.js';\nimport { detectTransitAlgo, resetAlgoCache } from '../crypto/detect.js';\nimport { fetchServerKeys, performHandshake, createRestTransitSession } from '../crypto/handshake.js';\nimport { setTransitSession, clearTransitSession, configureTransitIfUnset, isTransitEnabled, isTransitRequired, awaitTransitReadyOr, onTransitReady, getSessionKey, getTransitSession } from '../crypto/session.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getTransitHandshakePromise, ensureRestTransitHandshake } from '../api/client.js';\n\nexport type SocketStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';\nexport type StatusListener = (status: SocketStatus) => void;\n\nlet _socket: Socket | null = null;\nlet _socketProxy: Socket | null = null; // proxy returned by connectSocket — all .on() calls auto-decrypt\nlet _connectingPromise: Promise<Socket> | null = null;\nlet _status: SocketStatus = 'disconnected';\nconst _statusListeners = new Set<StatusListener>();\n\n// Stored so SocketProvider can call refreshAndReconnect() without re-passing config\nlet _getToken: (() => string | null | undefined) | null = null;\nlet _userId: string | undefined;\nlet _tenantId: string | undefined;\n// Retained so reconnectSocket() can re-run the FULL connectSocket() handshake\n// (re-establishing the transit session) instead of a bare auth-swap that would\n// drop the transit fields. See reconnectSocket() for why this matters.\nlet _config: ResolvedConfig | null = null;\nfunction setStatus(s: SocketStatus) {\n _status = s;\n _statusListeners.forEach(l => l(s));\n}\n\nexport function getSocket(): Socket {\n if (!_socketProxy) throw new Error('[AntzChat] Socket not initialized. Call connectSocket first.');\n return _socketProxy;\n}\n\n/** Returns the socket if connected, null otherwise. Use this for fire-and-forget\n * operations (markRead, typing) that should silently no-op when not yet connected. */\nexport function tryGetSocket(): Socket | null {\n return _socket?.connected ? _socketProxy : null;\n}\n\nexport function getSocketStatus(): SocketStatus {\n return _status;\n}\n\nexport function onSocketStatus(listener: StatusListener): () => void {\n _statusListeners.add(listener);\n return () => _statusListeners.delete(listener);\n}\n\n// ─── Transit helpers ──────────────────────────────────────────────────────────\n\n// How long secureEmit will wait for an in-progress handshake to (re-)establish\n// a session key before giving up. Kept under emitters.ts's ACK_TIMEOUT (5s) so a\n// caller's own ack timeout still governs the total wait.\nconst SECURE_EMIT_WAIT_MS = 4000;\n\nfunction emitEncrypted(\n socket: Socket,\n event: string,\n envelope: unknown,\n key: CryptoKey | Uint8Array,\n ack?: (response: unknown) => void,\n): void {\n if (ack) {\n socket.emit(event, envelope, async (encryptedResponse: unknown) => {\n if (isTransitEnvelope(encryptedResponse)) {\n try {\n ack(await decryptPayload(encryptedResponse, key));\n } catch {\n ack(encryptedResponse);\n }\n } else {\n ack(encryptedResponse);\n }\n });\n } else {\n socket.emit(event, envelope);\n }\n}\n\n// Wraps socket.emit. Contract:\n// • transit has a live session key → encrypt, then emit\n// • transit is REQUIRED but no key → wait for the handshake (bounded); if it\n// still fails, throw. NEVER emit plaintext.\n// • transit is genuinely disabled → emit plaintext (server doesn't want it)\nexport async function secureEmit(\n socket: Socket,\n event: string,\n payload: unknown,\n ack?: (response: unknown) => void,\n): Promise<void> {\n let key = getSessionKey();\n if (key && isTransitEnabled()) {\n emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);\n return;\n }\n\n // No usable session key. If transit is required, a plaintext emit here would\n // be a silent security downgrade (the whole point of payload encryption).\n // Give an in-flight / re-established handshake a bounded chance to land.\n if (isTransitRequired()) {\n try { ensureRestTransitHandshake(); } catch { /* REST client not wired — socket-only usage */ }\n const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);\n\n key = getSessionKey();\n if (ok && key && isTransitEnabled()) {\n emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);\n return;\n }\n\n if (isTransitRequired()) {\n // Still required, still no key — refuse rather than leak plaintext.\n throw new AntzChatNetworkError(\n `Transit encryption required but no session established — \"${event}\" not sent`,\n 'TRANSIT_NOT_READY',\n { event },\n );\n }\n // fell through: the wait returned ok because the server told us transit is\n // disabled (configureTransit(false)). Plaintext below is now correct.\n }\n\n // Transit genuinely disabled — send plaintext.\n if (ack) {\n socket.emit(event, payload, ack);\n } else {\n socket.emit(event, payload);\n }\n}\n\n// Registers an incoming event handler that auto-decrypts when transit is enabled.\nexport function secureOn(\n socket: Socket,\n event: string,\n handler: (data: unknown) => void,\n): void {\n socket.on(event, async (raw: unknown) => {\n if (isTransitEnabled() && isTransitEnvelope(raw)) {\n const key = getSessionKey();\n if (key) {\n try {\n const data = await decryptPayload(raw, key);\n handler(data);\n return;\n } catch {\n console.error(`[AntzChat] Transit decryption failed for event: ${event}`);\n return;\n }\n }\n }\n handler(raw);\n });\n}\n\n// ─── Room-membership tracking ────────────────────────────────────────────────\n// join_room is fire-and-forget and the UI emits it only once per conversation\n// open (re-emitting only on an isConnected transition). A join_room dropped by\n// secureEmit during the transit-handshake gap — or lost on a plain socket\n// reconnect — would leave this socket out of the server-side room with no error\n// and no retry: new_message broadcasts for that room then silently never arrive.\n//\n// So track the rooms the client believes it is in and re-emit join for all of\n// them whenever a transit session (re-)establishes or the socket (re)connects.\n// Server-side join is idempotent; a stale room (user no longer a member) is\n// rejected server-side and the rejection is swallowed.\nconst _joinedRooms = new Set<string>();\n\nexport function trackRoomJoin(conversationId: string): void {\n _joinedRooms.add(conversationId);\n}\nexport function trackRoomLeave(conversationId: string): void {\n _joinedRooms.delete(conversationId);\n}\n/** Drop all tracked rooms. Call on a genuine identity change (user/tenant\n * switch) so the reconnect re-flush can't re-join the previous user's rooms.\n * NOT called from disconnectSocket() — a token refresh is not an intent change. */\nexport function resetTrackedRooms(): void {\n _joinedRooms.clear();\n}\n\nfunction flushJoinedRooms(): void {\n const socket = tryGetSocket();\n if (!socket || _joinedRooms.size === 0) return;\n for (const conversationId of _joinedRooms) {\n void secureEmit(socket, 'join_room', { conversationId }).catch(() => { /* retried on next ready/connect */ });\n }\n}\n\nonTransitReady(flushJoinedRooms);\nonSocketStatus((status) => { if (status === 'connected') flushJoinedRooms(); });\n\n// ─── Connect ──────────────────────────────────────────────────────────────────\n\nexport async function connectSocket(\n config: ResolvedConfig,\n getToken: () => string | null | undefined,\n): Promise<Socket> {\n if (_socket && !_socket.disconnected) return _socket;\n if (_connectingPromise) return _connectingPromise;\n\n _connectingPromise = _doConnect(config, getToken).finally(() => {\n _connectingPromise = null;\n });\n return _connectingPromise;\n}\n\nasync function _doConnect(\n config: ResolvedConfig,\n getToken: () => string | null | undefined,\n): Promise<Socket> {\n _getToken = getToken;\n _userId = config.userId;\n _tenantId = config.tenantId;\n _config = config;\n\n const token = getToken();\n\n const socketHandshakeAuth: Record<string, unknown> = {\n token: token ? `Bearer ${token}` : '',\n ...(config.userId && { userId: config.userId }),\n ...(config.tenantId && { tenantId: config.tenantId }),\n ...(config.avatar?.url && { avatarUrl: config.avatar.url }),\n ...(config.avatar?.base64 && { avatarBase64: config.avatar.base64 }),\n };\n\n // Mirror the SDK's transit intent into the shared transit state up front, so\n // isTransitRequired() is authoritative even in socket-only usage where\n // initApiClient() (the other configureTransit caller) was never invoked.\n // *IfUnset* so we never clobber an authoritative configureTransit(false) that\n // the REST handshake set after GET /crypto/pubkey reported enabled:false.\n configureTransitIfUnset(config.transitEncryption);\n\n // ── Transit handshake setup ──────────────────────────────────────────────\n let boundDeriveSessionKey: ((sessionId: string) => Promise<unknown>) | null = null;\n\n if (config.transitEncryption) {\n // ── New path: HTTPS key exchange (independent of socket) ──────────────\n // initApiClient may have already established the session via HTTPS before\n // connectSocket was called. Reuse it so we don't create a second session.\n // If initApiClient's handshake is still in-flight, wait for it so we never\n // fire two concurrent HTTPS handshakes for the same session.\n const inFlight = getTransitHandshakePromise();\n if (inFlight) await inFlight;\n\n const existingSession = getTransitSession();\n let httpsSession: { sessionId: string; sessionKey: CryptoKey | Uint8Array } | null = null;\n\n if (existingSession?.enabled && existingSession.sessionId) {\n // Session already established by initApiClient — reuse it.\n httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };\n } else {\n // initApiClient hasn't resolved yet or session was cleared — do HTTPS handshake now.\n httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (httpsSession) {\n const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : 'x25519';\n setTransitSession({ sessionKey: httpsSession.sessionKey as CryptoKey, algo, sessionId: httpsSession.sessionId, enabled: true });\n }\n }\n\n if (httpsSession) {\n // Tell the socket middleware to reuse this session instead of doing ECDH.\n socketHandshakeAuth['transitSessionId'] = httpsSession.sessionId;\n boundDeriveSessionKey = null;\n } else {\n // ── Fallback: old socket ECDH path (old server without POST /crypto/session) ──\n try {\n const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (!serverKeys.enabled) {\n throw new AntzChatError(\n 'TRANSIT_MISMATCH',\n '[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server ' +\n 'has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides.',\n false,\n { sdkEnabled: true, serverEnabled: false },\n );\n }\n const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : 'x25519';\n boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);\n } catch (err: any) {\n throw err;\n }\n }\n } else {\n // Explicitly disabled — verify server also has it off to catch config mismatches early\n try {\n const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (serverKeys.enabled) {\n throw new AntzChatError(\n 'TRANSIT_MISMATCH',\n '[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server ' +\n 'has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides.',\n false,\n { sdkEnabled: false, serverEnabled: true },\n );\n }\n } catch (err: any) {\n if (err?.code === 'TRANSIT_MISMATCH') throw err;\n // /crypto/pubkey unavailable (old server without transit support) — proceed\n }\n }\n\n _socket = io(`${config.socketOrigin}/chat`, {\n auth: socketHandshakeAuth,\n path: config.socketPath,\n transports: ['websocket', 'polling'],\n reconnection: true,\n reconnectionAttempts: 10,\n reconnectionDelay: 1000,\n reconnectionDelayMax: 5000,\n autoConnect: true,\n });\n\n setStatus('connecting');\n\n _socket.on('connect', () => setStatus('connected'));\n _socket.on('disconnect', () => {\n setStatus('disconnected');\n // Do NOT clear the transit session on a transient websocket drop. The REST\n // transit session is independent of the socket transport — wiping it here\n // made every chat REST call 403 (\"Transit encryption required\") after any\n // network blip / tab backgrounding / server redeploy until a full page\n // reload. socket.io auto-reconnect re-runs the handshake auth and the server\n // re-emits `transit_session`, which replaces the key via the persistent\n // listener registered below. The session is only torn down on an explicit\n // disconnectSocket() (teardown) or an auth change.\n });\n _socket.on('connect_error', (err) => {\n console.error('[AntzChat] Socket connect_error:', err?.message, (err as any)?.data);\n setStatus('error');\n });\n _socket.on('reconnecting', () => setStatus('reconnecting'));\n _socket.on('reconnect', () => setStatus('connected'));\n\n // ── Transit session key derivation ──────────────────────────────────────\n // Block connectSocket from resolving until the session key is ready.\n // This ensures all HTTP requests and socket.on() registrations that happen\n // after await connectSocket() already have an active session key.\n if (config.transitEncryption && boundDeriveSessionKey) {\n await new Promise<void>((resolve) => {\n const done = () => {\n clearTimeout(timeout);\n resolve();\n };\n\n // Safety timeout — if transit_session never arrives, stop blocking THIS\n // connect attempt, but do NOT disable transit. A timeout means \"slow / not\n // responding right now\", not \"server doesn't want transit\" — the only\n // authoritative off signal is GET /crypto/pubkey returning enabled:false,\n // which is handled earlier and throws TRANSIT_MISMATCH. Disabling transit\n // here turned a transient stall into every subsequent HTTP request AND\n // socket emit going out as plaintext for the rest of the session.\n //\n // Instead: leave transit required (REST interceptor's ensureRestTransitHandshake\n // keeps retrying with backoff; socket.io auto-reconnect re-runs the handshake\n // auth and the server re-emits transit_session), and let secureEmit /\n // waitForTransitReady keep requests gated until a session lands.\n const timeout = setTimeout(() => {\n console.error(\n '[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED — ' +\n 'chat requests are gated (not downgraded to plaintext) until the handshake ' +\n 'succeeds via REST retry or socket reconnect. Persistent failure = check ' +\n 'POST /crypto/session (rate limit / 5xx) and the socket transit middleware.',\n );\n done();\n }, 5000);\n\n _socket!.on('transit_session', async ({ sessionId }: { sessionId: string }) => {\n try {\n if (boundDeriveSessionKey) {\n const sessionKey = await boundDeriveSessionKey(sessionId) as CryptoKey;\n const algo = (socketHandshakeAuth['transitAlgo'] as 'x25519' | 'p256') ?? 'p256';\n setTransitSession({ sessionKey, algo, sessionId, enabled: true });\n }\n } catch (err: any) {\n console.error('[AntzChat] Failed to derive transit session key:', err.message);\n }\n done();\n });\n\n // Also unblock if the socket fails to connect at all\n _socket!.on('connect_error', () => done());\n });\n }\n\n // ── Application event listeners ──────────────────────────────────────────\n // Use secureOn so incoming events are auto-decrypted when transit is enabled.\n secureOn(_socket, 'read_receipt', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const e = event as ReadReceiptEvent;\n useChatStore.getState().setLastRead(e.conversationId, e.messageId, e.readAt);\n });\n });\n\n secureOn(_socket, 'user_online', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const store = useChatStore.getState();\n const e = event as UserStatusEvent;\n store.setUserOnline(e.userId);\n store.setLastSeen(e.userId, null);\n });\n });\n\n secureOn(_socket, 'user_offline', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const e = event as UserStatusEvent;\n const store = useChatStore.getState();\n store.setUserOffline(e.userId);\n if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);\n });\n });\n\n // Store and return a proxy so ALL socket.on() calls — whether from\n // SocketProvider, getSocket(), or tryGetSocket() — auto-decrypt via secureOn.\n _socketProxy = createSecureSocketProxy(_socket);\n return _socketProxy;\n}\n\n// Wraps a Socket so every .on() call is transparently routed through secureOn.\n// Events that don't need decryption (transit disabled or no key) pass through unchanged.\nfunction createSecureSocketProxy(socket: Socket): Socket {\n return new Proxy(socket, {\n get(target, prop) {\n if (prop === 'on') {\n return (event: string, handler: (data: unknown) => void) => {\n // Low-level Socket.IO internal events pass through directly\n const internal = ['connect', 'disconnect', 'connect_error', 'reconnect', 'reconnecting', 'error'];\n if (internal.includes(event as string)) {\n return target.on(event as any, handler as any);\n }\n secureOn(target, event, handler);\n return socket; // for chaining\n };\n }\n const val = (target as any)[prop];\n return typeof val === 'function' ? val.bind(target) : val;\n },\n });\n}\n\nexport function disconnectSocket() {\n _connectingPromise = null;\n if (_socket) {\n _socket.disconnect();\n _socket = null;\n _socketProxy = null;\n setStatus('disconnected');\n }\n clearTransitSession();\n resetAlgoCache();\n _getToken = null;\n _userId = undefined;\n _tenantId = undefined;\n _config = null;\n}\n\nexport function reconnectSocket(token: string, userId?: string, tenantId?: string) {\n if (!_socket) return;\n\n // Transit encryption: reconnectSocket predates transit and was never migrated.\n // It must NOT (a) drop the transit fields from _socket.auth (server rejects the\n // handshake with \"Transit encryption required\"), NOR (b) tear the session down\n // and re-establish a NEW one (server-broadcast events like user_online are\n // encrypted with the session the server still has; a fresh client session key\n // can't decrypt them → continuous \"Transit decryption failed\").\n //\n // reconnectSocket is a TOKEN-only refresh: the transit session is still valid.\n // So preserve the existing session — carry its transitSessionId forward in the\n // handshake auth and re-cycle the SAME socket, keeping the session key intact.\n //\n // IMPORTANT: socket.connect() is a NO-OP on an already-connected socket\n // (socket.io-client: `if (this.connected) return this;`). Since a token\n // refresh typically fires while the user is on a chat screen — socket\n // connected — a bare connect() here would NOT re-send the new auth. The\n // socket keeps running on the now-stale token until the server drops it, and\n // the uncontrolled auto-reconnect that follows would run on the stale token.\n //\n // Fix: force a real transport re-cycle (disconnect → connect) so the fresh\n // auth (token + transitSessionId) is actually sent and the server re-links\n // this socket to the SAME session (same key). The 'disconnect' handler no\n // longer clears the session on a transient drop, so the key we carry forward\n // survives the re-cycle.\n if (_config?.transitEncryption) {\n const existing = getTransitSession();\n if (existing?.enabled && existing.sessionId) {\n _socket.auth = {\n token: `Bearer ${token}`,\n ...(userId && { userId }),\n ...(tenantId && { tenantId }),\n transitSessionId: existing.sessionId,\n };\n if (_socket.connected) _socket.disconnect();\n _socket.connect();\n return;\n }\n // No valid cached session to preserve (e.g. it was cleared by a prior\n // disconnect). Fall back to the full connectSocket() flow, which\n // re-establishes the session and rebuilds the handshake auth. Dedupes via\n // _connectingPromise so repeated calls collapse into one connect.\n if (_getToken) {\n if (!_socket.disconnected) _socket.disconnect();\n void connectSocket(_config, _getToken);\n return;\n }\n }\n\n // Non-transit deployments: keep the original fast bare auth-swap + reconnect.\n _socket.auth = {\n token: `Bearer ${token}`,\n ...(userId && { userId }),\n ...(tenantId && { tenantId }),\n };\n _socket.connect();\n}\n\n/**\n * Updates the socket auth token from the stored getToken function and reconnects.\n * Called by SocketProvider on connect_error to handle expired-token scenarios.\n * Returns true if the token was updated, false if no token source is available.\n */\nexport function refreshSocketAuth(): boolean {\n if (!_socket || !_getToken) return false;\n const fresh = _getToken();\n if (!fresh) return false;\n _socket.auth = {\n token: `Bearer ${fresh}`,\n ...(_userId && { userId: _userId }),\n ...(_tenantId && { tenantId: _tenantId }),\n };\n return true;\n}\n","import type { SendMessagePayload, ForwardMessagePayload, ForwardAckPayload } from '../types/index.js';\nimport { getSocket, tryGetSocket, onSocketStatus, secureEmit, trackRoomJoin, trackRoomLeave } from './socket.js';\nimport { AntzChatNetworkError } from '../errors.js';\n\ntype SendMessageWire = SendMessagePayload & { sentAt: number };\n\nconst ACK_TIMEOUT = 5000;\nconst RECONNECT_WAIT_TIMEOUT = 15000;\n\n// Per-conversation send queue for send_message — preserves ordering within a\n// conversation while allowing messages in different conversations to flow in parallel.\n// Emits are fired without awaiting the ack so a slow server response on one message\n// never blocks subsequent messages.\n//\n// Safety limits:\n// QUEUE_MAX_SIZE — hard cap on pending entries; overflow rejects immediately\n// QUEUE_ENTRY_TTL — max time an entry may wait in the queue before being\n// dropped (before it even reaches the socket), so stale\n// messages never arrive at the server minutes later\nconst QUEUE_MAX_SIZE = 100;\nconst QUEUE_ENTRY_TTL = 30_000; // 30 s\n\ntype QueueEntry = {\n payload: SendMessageWire;\n resolve: (v: unknown) => void;\n reject: (e: unknown) => void;\n enqueuedAt: number;\n};\n\nconst sendQueues = new Map<string, QueueEntry[]>();\nconst sendQueueRunning = new Map<string, boolean>();\n\nasync function drainSendQueue(conversationId: string) {\n if (sendQueueRunning.get(conversationId)) return;\n sendQueueRunning.set(conversationId, true);\n const queue = sendQueues.get(conversationId)!;\n while (queue.length > 0) {\n const entry = queue.shift()!;\n if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {\n entry.reject(new AntzChatNetworkError('Message dropped: queued too long', 'MESSAGE_DROPPED'));\n continue;\n }\n // Await only the EMIT step (transit wait + encryption + socket.emit), not the\n // server ack. This is the ordering guarantee the per-conversation queue\n // exists for: message N's packet is on the wire before message N+1's\n // encryption starts. The ack is awaited out-of-band (below) so a slow server\n // RTT on one message still never blocks the next one.\n let acked: Promise<unknown>;\n try {\n ({ acked } = await emitWithAck('send_message', entry.payload));\n } catch (err) {\n // Phase-1 failure (no socket / reconnect timed out / transit not ready).\n // These conditions apply equally to every still-queued message, so fail\n // the whole remaining batch now rather than make each one re-wait the\n // full reconnect/transit timeout in series. The user retries from the UI.\n entry.reject(err);\n while (queue.length > 0) queue.shift()!.reject(err);\n break;\n }\n acked.then(entry.resolve).catch(entry.reject);\n }\n sendQueues.delete(conversationId);\n sendQueueRunning.delete(conversationId);\n}\n\nfunction queueSendMessage(payload: SendMessageWire): Promise<unknown> {\n const conversationId = payload.conversationId;\n if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);\n const queue = sendQueues.get(conversationId)!;\n if (queue.length >= QUEUE_MAX_SIZE) {\n return Promise.reject(new AntzChatNetworkError('Send queue full: too many messages in flight', 'SEND_QUEUE_FULL', { conversationId }));\n }\n return new Promise((resolve, reject) => {\n queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });\n drainSendQueue(conversationId);\n });\n}\n\nfunction waitForReconnect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n unsubscribe();\n reject(new AntzChatNetworkError('Socket reconnect timeout', 'SOCKET_TIMEOUT'));\n }, RECONNECT_WAIT_TIMEOUT);\n\n const unsubscribe = onSocketStatus((status) => {\n if (status === 'connected') {\n clearTimeout(timer);\n unsubscribe();\n resolve();\n } else if (status === 'error') {\n clearTimeout(timer);\n unsubscribe();\n reject(new AntzChatNetworkError('Socket reconnect failed', 'SOCKET_NOT_CONNECTED'));\n }\n });\n });\n}\n\n// emitWithAck: the two phases of an ack'd emit, separated so callers that need\n// ordering (the send queue) can await phase 1 without holding up on phase 2.\n// Phase 1 (awaited return): reconnect wait → secureEmit (transit wait +\n// encryption + socket.emit). Resolves once the packet is on the wire.\n// Phase 2 (the returned promise): the server ack, bounded by ACK_TIMEOUT which\n// only starts now, so the full budget covers server RTT.\n// Returns a wrapper object (NOT a bare Promise — `await` would recursively\n// unwrap that and collapse the two phases back into one).\nasync function emitWithAck<T>(event: string, payload: unknown): Promise<{ acked: Promise<T> }> {\n let socket = tryGetSocket();\n if (!socket) {\n await waitForReconnect();\n socket = tryGetSocket();\n }\n if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, 'SOCKET_NOT_CONNECTED', { event });\n\n let resolveAck!: (v: T) => void;\n let rejectAck!: (e: unknown) => void;\n const acked = new Promise<T>((res, rej) => { resolveAck = res; rejectAck = rej; });\n\n // A throw here (e.g. TRANSIT_NOT_READY) propagates out of emitWithAck as a\n // phase-1 failure — the packet never went out.\n await secureEmit(socket, event, payload, (response) => resolveAck(response as T));\n\n const timer = setTimeout(\n () => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, 'SOCKET_TIMEOUT', { event })),\n ACK_TIMEOUT,\n );\n void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));\n return { acked };\n}\n\n// withAck: ack'd emit where the caller only wants the final result and does not\n// care about the phase split (reactions, pin, delete, forward, …).\nasync function withAck<T>(event: string, payload: unknown): Promise<T> {\n const { acked } = await emitWithAck<T>(event, payload);\n return acked;\n}\n\n// fireAndForget: best-effort — silently no-ops when not connected, and swallows\n// a transit-not-ready rejection from secureEmit. Safe for genuinely self-healing\n// events (typing is debounced and re-sent; mark_read is re-sent on every new\n// message and on reconnect). join_room additionally relies on socket.ts's\n// room-membership tracking (trackRoomJoin) to be re-sent after a transit gap or\n// reconnect — see there.\nfunction fireAndForget(event: string, payload: unknown): void {\n const socket = tryGetSocket();\n if (!socket) return;\n secureEmit(socket, event, payload).catch(() => { /* best-effort */ });\n}\n\nexport const socketEmit = {\n joinRoom(conversationId: string) {\n trackRoomJoin(conversationId);\n fireAndForget('join_room', { conversationId });\n },\n\n leaveRoom(conversationId: string) {\n trackRoomLeave(conversationId);\n fireAndForget('leave_room', { conversationId });\n },\n\n sendMessage(payload: SendMessagePayload): Promise<unknown> {\n return queueSendMessage({ ...payload, sentAt: Date.now() });\n },\n\n // Not queued like sendMessage — forward targets are independent conversations,\n // not the single conversation ordering that queueSendMessage protects, and each\n // target's own new_message/push already fires server-side via MessagesService.forward()'s\n // internal create() calls, so there's nothing here that needs in-order draining.\n forwardMessage(payload: ForwardMessagePayload): Promise<ForwardAckPayload> {\n return withAck('forward_message', payload);\n },\n\n updateMessage(messageId: string, text: string): Promise<unknown> {\n return withAck('update_message', { messageId, text });\n },\n\n deleteMessage(messageId: string): Promise<unknown> {\n return withAck('delete_message', { messageId });\n },\n\n deleteMessageForMe(messageId: string): Promise<unknown> {\n return withAck('delete_message_for_me', { messageId });\n },\n\n clearChat(conversationId: string): Promise<unknown> {\n return withAck('clear_chat_for_me', { conversationId });\n },\n\n addReaction(messageId: string, emoji: string): Promise<unknown> {\n return withAck('add_reaction', { messageId, emoji });\n },\n\n removeReaction(messageId: string, emoji: string): Promise<unknown> {\n return withAck('remove_reaction', { messageId, emoji });\n },\n\n pinMessage(messageId: string): Promise<unknown> {\n return withAck('pin_message', { messageId });\n },\n\n unpinMessage(messageId: string): Promise<unknown> {\n return withAck('unpin_message', { messageId });\n },\n\n // markRead and typing are best-effort — silently dropped if socket not ready\n typing(conversationId: string, isTyping: boolean) {\n fireAndForget('typing', { conversationId, isTyping });\n },\n\n markRead(conversationId: string, messageId?: string) {\n fireAndForget('mark_read', { conversationId, ...(messageId ? { messageId } : {}) });\n },\n\n getOnlineUsers(userIds: string[]): Promise<string[]> {\n const socket = tryGetSocket();\n if (!socket) return Promise.resolve([]);\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n secureEmit(socket, 'get_online_users', { userIds }, (response) => {\n clearTimeout(timer);\n if (response && typeof response === 'object' && 'onlineStatus' in (response as object)) {\n const status = (response as { onlineStatus: Record<string, boolean> }).onlineStatus;\n resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));\n } else if (Array.isArray(response)) {\n resolve(response as string[]);\n } else {\n resolve([]);\n }\n }).then(() => {\n timer = setTimeout(() => reject(new AntzChatNetworkError('Socket ack timeout: get_online_users', 'SOCKET_TIMEOUT', { event: 'get_online_users' })), ACK_TIMEOUT);\n }).catch(reject);\n });\n },\n\n getTypingUsers(conversationId: string): Promise<unknown> {\n return withAck('get_typing_users', { conversationId });\n },\n};\n","import type {\n Message,\n CursorPaginatedResponse,\n PaginatedResponse,\n SendMessagePayload,\n MessageReceiptsResponse,\n MessageReactionsResponse,\n} from '../types/index.js';\nimport { getApiClient } from './client.js';\nimport { getAuthStore } from '../stores/auth.store.js';\nimport { resolveSystemMessageText } from '../utils/resolveSystemMessageText.js';\nimport { generateUUID } from '../crypto/uuid.js';\nimport { socketEmit } from '../socket/emitters.js';\nimport { tryGetSocket } from '../socket/socket.js';\n\nexport interface ListMessagesParams {\n cursor?: string;\n limit?: number;\n direction?: 'before' | 'after';\n}\n\nexport interface SearchParams {\n query: string;\n conversationId?: string;\n page?: number;\n limit?: number;\n}\n\nexport interface SendData {\n text?: string;\n attachments?: SendMessagePayload['attachments'];\n replyTo?: string;\n /**\n * Client-generated idempotency key. Generate one per send attempt and reuse the\n * SAME value if you retry this exact send (e.g. after a client-side timeout) —\n * never mint a new one for a retry. The server recognizes a matching\n * (conversationId, senderId, tempId) and returns the already-created message\n * instead of creating a duplicate. Unlike `messagesApi.forward()`, this is NOT\n * auto-generated if omitted — omitting it (or generating a fresh one on every\n * retry) means no dedup protection.\n */\n tempId?: string;\n}\n\n/** Matches the server-enforced cap in messages/dto/create-message.dto.ts (MAX_FORWARD_TARGETS). */\nexport const MAX_FORWARD_TARGETS = 5;\n\n/**\n * Matches the server's HIGHLY_FORWARDED_DEPTH_THRESHOLD in messages.service.ts — the\n * same forwardDepth value that triggers the server's fan-out-to-1 cap. Also used as\n * the \"Forwarded many times\" label threshold so the label and the cap agree on what\n * \"highly forwarded\" means; do not use a different number for the label alone.\n */\nexport const HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;\n\nexport interface ForwardResult {\n conversationId: string;\n success: boolean;\n message?: Message;\n error?: string;\n}\n\nexport const messagesApi = {\n async list(conversationId: string, params: ListMessagesParams = {}): Promise<CursorPaginatedResponse<Message>> {\n const { cursor, direction, ...rest } = params;\n const serverParams: Record<string, unknown> = { ...rest };\n if (cursor) {\n serverParams[direction === 'after' ? 'after' : 'before'] = cursor;\n }\n const { data } = await getApiClient().get<CursorPaginatedResponse<Message>>(\n `/conversations/${conversationId}/messages`,\n { params: serverParams },\n );\n const currentUserId = getAuthStore().useAuthStore.getState().user?.id;\n if (!currentUserId) return data;\n return {\n ...data,\n data: data.data.map(m =>\n m.content.type === 'system'\n ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } }\n : m,\n ),\n };\n },\n\n async get(messageId: string): Promise<Message> {\n const { data } = await getApiClient().get<Message>(`/messages/${messageId}`);\n return data;\n },\n\n /**\n * Sends a message via REST. For real-time delivery use `socketEmit.sendMessage`\n * instead — this is a lower-level entry point (used e.g. by the socket path's\n * REST-mirror flows). Pass `payload.tempId` and reuse the SAME value on retry to\n * make a retry-after-timeout safe — see `SendData.tempId`.\n */\n async send(conversationId: string, payload: SendData): Promise<Message> {\n const { data } = await getApiClient().post<Message>(\n `/conversations/${conversationId}/messages`,\n payload,\n );\n return data;\n },\n\n async update(messageId: string, text: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/update`, { text });\n return data;\n },\n\n async delete(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/delete`);\n },\n\n async deleteForMe(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/delete-for-me`);\n },\n\n async addReaction(messageId: string, emoji: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/reactions`, { emoji });\n return data;\n },\n\n async removeReaction(messageId: string, emoji: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(\n `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`,\n );\n return data;\n },\n\n async getReactions(messageId: string): Promise<MessageReactionsResponse> {\n const { data } = await getApiClient().post<MessageReactionsResponse>(`/messages/${messageId}/reactions/list`);\n return data;\n },\n\n async star(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/star`);\n },\n\n async unstar(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/unstar`);\n },\n\n async getStarred(params: { page?: number; limit?: number; conversationId?: string } = {}): Promise<PaginatedResponse<Message>> {\n const { data } = await getApiClient().get<PaginatedResponse<Message>>('/messages/starred', { params });\n return data;\n },\n\n async search(params: SearchParams): Promise<PaginatedResponse<Message>> {\n const { data } = await getApiClient().get<PaginatedResponse<Message>>('/messages/search', { params });\n return data;\n },\n\n async getLastRead(conversationId: string): Promise<{ lastReadMessageId: string | null; lastReadAt: string | null }> {\n const { data } = await getApiClient().get<{ lastReadMessageId: string | null; lastReadAt: string | null }>(\n `/conversations/${conversationId}/read-receipt`,\n );\n return data;\n },\n\n async markAsRead(conversationId: string, messageId?: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});\n },\n\n async pin(messageId: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/pin`);\n return data;\n },\n\n async unpin(messageId: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/unpin`);\n return data;\n },\n\n async getPinned(conversationId: string): Promise<Message[]> {\n const { data } = await getApiClient().get<Message[]>(`/conversations/${conversationId}/pinned-messages`);\n return data;\n },\n\n async getReceipts(messageId: string): Promise<MessageReceiptsResponse> {\n const { data } = await getApiClient().get<MessageReceiptsResponse>(`/messages/${messageId}/receipts`);\n return data;\n },\n\n /**\n * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS\n * per call, also enforced server-side). Each target is independent — one failing\n * (e.g. no longer a participant) does not block the others; check `success`/`error`\n * per entry in the returned array.\n *\n * Pass `attachmentIds` to forward only a subset of the source message's attachments\n * (e.g. one image out of a multi-image message) — omit it to forward the whole\n * message, including all its attachments, unchanged. An ID not present on the\n * source message is ignored server-side.\n *\n * Idempotency: pass `tempId` — generate ONE value per forward action (e.g. via\n * `generateUUID` from '@antzsoft/chat-core/internal') and reuse that SAME value if\n * you retry this exact forward (e.g. after a client-side timeout). The server then\n * recognizes the retry per target and returns the already-created message instead\n * of creating a duplicate. Never mint a new tempId for a retry — only when the user\n * initiates a genuinely new forward action. If omitted, a fresh one is generated\n * per call, which means a retry without an explicit tempId gets NO dedup protection.\n *\n * Transport: uses the 'forward_message' socket event when a socket is connected\n * (lower latency, same server-side MessagesService.forward() path, so broadcast/\n * push notification behavior is identical either way) and transparently falls back\n * to the REST endpoint when no socket is available.\n */\n async forward(messageId: string, targetConversationIds: string[], attachmentIds?: string[], tempId?: string): Promise<ForwardResult[]> {\n const resolvedTempId = tempId ?? generateUUID();\n\n if (tryGetSocket()) {\n const ack = await socketEmit.forwardMessage({\n messageId,\n targetConversationIds,\n ...(attachmentIds ? { attachmentIds } : {}),\n tempId: resolvedTempId,\n });\n if (ack.error) throw new Error(ack.error);\n return ack.results;\n }\n\n const { data } = await getApiClient().post<ForwardResult[]>(\n `/messages/${messageId}/forward`,\n { targetConversationIds, ...(attachmentIds ? { attachmentIds } : {}), tempId: resolvedTempId },\n );\n return data;\n },\n};\n","import type { Conversation, ConversationListParams, ConversationUnreadCount, DeliveryStatus, Message, MessageStatus, Participant, PaginatedResponse, UnreadSummary, User } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\nfunction normalizeParticipant(p: any): Participant {\n const hasUserDetails = p.displayName || p.username || p.avatarUrl;\n return {\n userId: p.userId,\n externalId: p.externalId ?? p.user?.externalId,\n role: p.role,\n joinedAt: p.joinedAt,\n isActive: p.isActive,\n user: hasUserDetails\n ? {\n id: p.userId,\n externalId: p.externalId,\n tenantId: '',\n email: '',\n username: p.username ?? '',\n displayName: p.displayName ?? p.username ?? '',\n avatarUrl: p.avatarUrl,\n status: 'offline' as const,\n createdAt: p.joinedAt ?? '',\n updatedAt: p.joinedAt ?? '',\n }\n : (p.user as User | undefined),\n };\n}\n\nfunction normalizeLastMessage(lastMsg: any): Message | undefined {\n if (!lastMsg) return undefined;\n if (lastMsg.content !== undefined) return lastMsg as Message;\n return {\n id: lastMsg.messageId ?? '',\n tenantId: '',\n conversationId: '',\n senderId: lastMsg.senderId ?? '',\n content: {\n type: lastMsg.hasAttachments ? 'attachment' : 'text',\n text: lastMsg.contentPreview,\n },\n reactions: [],\n lastReaction: lastMsg.lastReaction ?? null,\n status: (lastMsg.status ?? 'active') as MessageStatus,\n deliveryStatus: (lastMsg.deliveryStatus ?? 'sent') as DeliveryStatus,\n isEdited: false,\n sentAt: lastMsg.sentAt ?? '',\n createdAt: lastMsg.sentAt ?? '',\n ...(lastMsg.senderName && { senderName: lastMsg.senderName }),\n ...(lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }),\n };\n}\n\nexport function normalizeConversation(conv: any): Conversation {\n return {\n ...conv,\n id: conv.id ?? conv.conversationId,\n participants: (conv.participants ?? []).map(normalizeParticipant),\n lastMessage: normalizeLastMessage(conv.lastMessage),\n };\n}\n\nexport interface CreateGroupData {\n name: string;\n description?: string;\n participantIds: string[];\n}\n\nexport interface CreateDirectData {\n userId: string;\n}\n\nexport interface UpdateConversationData {\n name?: string;\n description?: string;\n}\n\nexport const conversationsApi = {\n async list(params: ConversationListParams = {}): Promise<PaginatedResponse<Conversation>> {\n const { data } = await getApiClient().get<PaginatedResponse<Conversation>>('/conversations', { params });\n return { ...data, data: data.data.map(normalizeConversation) };\n },\n\n async get(conversationId: string): Promise<Conversation> {\n const { data } = await getApiClient().get<Conversation>(`/conversations/${conversationId}`);\n return normalizeConversation(data);\n },\n\n async createGroup(payload: CreateGroupData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>('/conversations', payload);\n return normalizeConversation(data);\n },\n\n async createDirect(payload: CreateDirectData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>('/conversations/direct', payload);\n return normalizeConversation(data);\n },\n\n async update(conversationId: string, payload: UpdateConversationData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(`/conversations/${conversationId}/update`, payload);\n return normalizeConversation(data);\n },\n\n async delete(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/delete`);\n },\n\n async addParticipants(conversationId: string, userIds: string[], role?: 'admin' | 'member'): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants`,\n { userIds, ...(role && { role }) },\n );\n return normalizeConversation(data);\n },\n\n async removeParticipant(conversationId: string, userId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants/${userId}/remove`,\n );\n return normalizeConversation(data);\n },\n\n async updateParticipantRole(conversationId: string, userId: string, role: 'admin' | 'member'): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants/${userId}/role`,\n { role },\n );\n return normalizeConversation(data);\n },\n\n async mute(conversationId: string, mutedUntil?: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});\n },\n\n async unmute(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unmute`);\n },\n\n async pin(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/pin`);\n },\n\n async unpin(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unpin`);\n },\n\n async markUnread(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unread`);\n },\n\n async markRead(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unread/clear`);\n },\n\n async leave(conversationId: string, andDelete?: boolean): Promise<void> {\n const url = andDelete\n ? `/conversations/${conversationId}/leave?delete=true`\n : `/conversations/${conversationId}/leave`;\n await getApiClient().post(url);\n },\n\n async getMembers(conversationId: string, filter?: 'deleted' | 'all'): Promise<Participant[]> {\n const { data } = await getApiClient().get<Participant[]>(\n `/conversations/${conversationId}/participants`,\n filter ? { params: { filter } } : undefined,\n );\n // Normalize so display-name fields arrive consistently under `user` — the raw\n // endpoint may return flat fields (displayName/username/avatarUrl). This makes\n // getMembers a drop-in name source for the @mention picker without each\n // consumer re-normalizing.\n return (data ?? []).map(normalizeParticipant);\n },\n\n /**\n * Get unread message count for a single conversation.\n * Use this after app foreground or socket reconnect to refresh a specific count.\n */\n async getUnreadCount(conversationId: string): Promise<ConversationUnreadCount> {\n const { data } = await getApiClient().get<ConversationUnreadCount>(\n `/conversations/${conversationId}/unread`,\n );\n return data;\n },\n\n /**\n * Get total unread count across all conversations + per-conversation breakdown.\n * Use on app cold start, foreground resume, or after socket reconnect.\n * The socket keeps counts live while connected — this is the source of truth\n * when the socket was down.\n */\n async getUnreadSummary(): Promise<UnreadSummary> {\n const { data } = await getApiClient().get<UnreadSummary>('/conversations/unread');\n return data;\n },\n\n /**\n * Set the group icon from an already-uploaded file (admin only).\n * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.\n * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.\n */\n async uploadIcon(conversationId: string, fileId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/icon`,\n { fileId },\n );\n return normalizeConversation(data);\n },\n\n async removeIcon(conversationId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(`/conversations/${conversationId}/icon/remove`);\n return normalizeConversation(data);\n },\n\n async clearChat(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);\n },\n\n};\n","import { getApiClient } from './client.js';\n\n// ─── Types ─────────────────────────────────────────────────────────────────\n\n/** Shared fields for every device registration. */\ninterface DeviceTokenBase {\n /**\n * Client-generated stable UUID for this device.\n * Store this in localStorage / SecureStore / Keychain and reuse it on every\n * app start so the same record is updated rather than duplicated.\n */\n deviceId: string;\n platform: 'ios' | 'android' | 'web';\n provider: 'expo' | 'fcm' | 'apns' | 'web-push';\n userAgent?: string;\n}\n\n/** Mobile push registration (expo | fcm | apns). */\nexport interface MobileDeviceToken extends DeviceTokenBase {\n provider: 'expo' | 'fcm' | 'apns';\n /** FCM registration token / APNs device token / Expo push token. */\n token: string;\n endpoint?: never;\n p256dh?: never;\n auth?: never;\n}\n\n/** Web push (VAPID) registration. */\nexport interface WebPushDeviceToken extends DeviceTokenBase {\n provider: 'web-push';\n /** PushSubscription.endpoint */\n endpoint: string;\n /** base64url-encoded PushSubscription key 'p256dh' */\n p256dh: string;\n /** base64url-encoded PushSubscription key 'auth' */\n auth: string;\n token?: never;\n}\n\nexport type RegisterDeviceTokenPayload = MobileDeviceToken | WebPushDeviceToken;\n\n// ─── API ───────────────────────────────────────────────────────────────────\n\nexport const devicesApi = {\n /**\n * Register or update a device push token with the chat server.\n *\n * Upserts by `deviceId` — calling this multiple times with the same deviceId\n * simply refreshes the token value (tokens can rotate silently on some platforms).\n *\n * The SDK never calls this automatically. The parent app or the `tokenProvider`\n * option in `pushNotifications` config is responsible for calling it after\n * obtaining the token from the OS / browser.\n */\n async register(payload: RegisterDeviceTokenPayload): Promise<void> {\n await getApiClient().post('/users/me/devices', payload);\n },\n\n /**\n * Remove a device token from the chat server.\n * Call this on logout so the user stops receiving push notifications on this device.\n */\n async remove(deviceId: string): Promise<void> {\n await getApiClient().post(`/users/me/devices/${deviceId}/remove`);\n },\n};\n","import type { PaginatedResponse, User, UserPreferences } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\n/**\n * Fields for updating the current user's profile.\n *\n * Works in both builtin and non-builtin (external/SSO/WSO2) modes.\n *\n * In non-builtin modes, firstName/lastName/email/displayName/phone are also\n * synced from the upstream user-service (on socket connect if stale, and every\n * 2 hours via cron). Calling updateProfile() is the way to push an immediate\n * update to the chat server without waiting for the next sync cycle — useful\n * when the host app knows a profile change just happened in the identity provider.\n *\n * Uniqueness constraints: email and phone must each be unique within the tenant.\n * A 409 is returned if another user already holds the same email or phone.\n *\n * username and status are intentionally excluded — username is a system\n * identifier and status is lifecycle-managed by user-service.\n */\nexport interface UpdateProfilePayload {\n firstName?: string;\n lastName?: string;\n email?: string;\n displayName?: string;\n phone?: string;\n}\n\nexport const usersApi = {\n async list(params: { query?: string; page?: number; limit?: number } = {}): Promise<PaginatedResponse<User>> {\n const { data } = await getApiClient().get<PaginatedResponse<User>>('/users', { params });\n return data;\n },\n\n async getById(userId: string): Promise<User> {\n const { data } = await getApiClient().get<User>(`/users/${userId}`);\n return data;\n },\n\n async getLastSeen(userId: string): Promise<{ lastSeenAt: string | null }> {\n const { data } = await getApiClient().get<User>(`/users/${userId}`);\n return { lastSeenAt: data.lastSeenAt ?? null };\n },\n\n /**\n * Update basic profile fields for the current user.\n * Works in both builtin and non-builtin modes. Use this to push an immediate\n * profile update to the chat server when the host app knows a change just\n * happened — without waiting for the next 2-hour sync cycle.\n */\n async updateProfile(payload: UpdateProfilePayload): Promise<User> {\n const { data } = await getApiClient().post<User>('/users/me/update', payload);\n return data;\n },\n\n /**\n * Update notification preferences for the current user.\n * Partial update — only send fields you want to change.\n * A prefs record is automatically created with defaults when a device\n * token is first registered, so this never fails with \"not found\".\n */\n async updatePreferences(prefs: UserPreferences): Promise<User> {\n const { data } = await getApiClient().post<User>('/users/me/preferences', prefs);\n return data;\n },\n\n /**\n * Fetch current notification preferences for the current user.\n * Returns null if no prefs record exists yet (all defaults apply).\n */\n async getPreferences(): Promise<UserPreferences | null> {\n try {\n const { data } = await getApiClient().get<UserPreferences>('/users/me/preferences');\n return data;\n } catch {\n return null;\n }\n },\n};\n","// ─── Primitives ───────────────────────────────────────────────────────────────\n\nexport type FileType = 'image' | 'video' | 'audio' | 'document';\nexport type ConversationType = 'direct' | 'group';\nexport type MessageStatus = 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';\nexport type DeliveryStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';\nexport type UserStatus = 'online' | 'offline' | 'away';\nexport type ParticipantRole = 'admin' | 'member';\n\n// ─── Upload ───────────────────────────────────────────────────────────────────\n\n/**\n * Platform-agnostic file descriptor.\n * - Web: built from a browser File object ({ uri: URL.createObjectURL(f), name: f.name, type: f.type, size: f.size })\n * - React Native: built from document/image picker result ({ uri: result.uri, name: result.name, type: result.mimeType, size: result.size })\n */\nexport interface UploadableFile {\n /** Local URI — blob URL on web, file:// URI on RN */\n uri: string;\n name: string;\n /** MIME type, e.g. \"image/jpeg\" */\n type: string;\n size: number;\n}\n\n// ─── User ─────────────────────────────────────────────────────────────────────\n\nexport interface User {\n id: string;\n externalId?: string; // external system user ID (non-builtin modes only)\n tenantId: string;\n email: string;\n username: string;\n firstName?: string;\n lastName?: string;\n displayName: string;\n avatarUrl?: string;\n phone?: string;\n status: UserStatus;\n lastSeenAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// ─── Auth ─────────────────────────────────────────────────────────────────────\n\nexport interface AuthTokens {\n accessToken: string;\n refreshToken: string;\n tokenType: string;\n expiresIn: number;\n}\n\nexport interface AuthResponse {\n user: User;\n tokens: AuthTokens;\n}\n\nexport interface LoginCredentials {\n email: string;\n password: string;\n}\n\nexport interface RegisterData {\n email: string;\n password: string;\n username: string;\n firstName: string;\n lastName: string;\n displayName?: string;\n phone?: string;\n tenantId?: string;\n}\n\n// ─── Attachment ───────────────────────────────────────────────────────────────\n\nexport interface Attachment {\n id: string;\n slotId?: string;\n type: FileType;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n dimensions?: { width: number; height: number };\n duration?: number;\n isUploading?: boolean;\n uploadProgress?: number;\n}\n\n\n// ─── Message ──────────────────────────────────────────────────────────────────\n\nexport interface MessageMetadata {\n type: string;\n hasAttachments: boolean;\n containsLink: boolean;\n systemOperationType?: string;\n actorUserId?: string;\n actorUserName?: string;\n targetUserId?: string;\n targetUserName?: string;\n}\n\nexport interface MessageReaction {\n emoji: string;\n userIds: string[];\n count: number;\n}\n\nexport interface MessageContent {\n text?: string;\n type: 'text' | 'attachment' | 'system';\n attachments?: Attachment[];\n}\n\n/**\n * A @mention of a user in a group message.\n *\n * The mention is stored inline in `content.text` as a self-describing token\n * `@[DisplayName](userId)` (and, for @all, `@[all](all)`). This flat array is a\n * denormalized copy of the mentioned userIds, used for notification fan-out and\n * \"who was mentioned\" lookups — it is NOT a positional/offset map.\n *\n * The reserved id `\"all\"` represents an @all mention (server resolves it to every\n * active participant at send time).\n */\nexport const MENTION_ALL_ID = 'all';\n\nexport interface ReplyAttachmentSnapshot {\n type: FileType;\n filename: string;\n mimeType: string;\n size: number;\n duration?: number;\n dimensions?: { width: number; height: number };\n url: string;\n}\n\nexport interface MessageReplyReference {\n messageId?: string;\n contentPreview?: string;\n senderName?: string;\n attachmentSnapshot?: ReplyAttachmentSnapshot;\n // Present on optimistic messages before server confirmation\n id?: string;\n content?: MessageContent;\n sender?: Pick<User, 'displayName' | 'avatarUrl'>;\n}\n\n/**\n * Present on a message that was created via forwarding. Unlike `MessageReplyReference`,\n * this does NOT carry a content preview — the forwarded message's own `content` already\n * holds the text/attachments, so the UI only needs this to render a \"Forwarded\" label.\n *\n * `originalMessageId`/`originalConversationId`/`originalSenderId` always point one hop\n * back (the message actually forwarded), never the root of a longer forward chain — the\n * true original author/conversation is intentionally not reconstructable after more than\n * one hop, matching WhatsApp's forwarding privacy model. `forwardDepth` still accumulates\n * across the whole chain, so \"Forwarded many times\" (depth >= HIGHLY_FORWARDED_DEPTH_THRESHOLD,\n * exported from '../api/messages.js', currently 5 — the same value that triggers the\n * server's fan-out-to-1 cap) keeps working.\n */\nexport interface MessageForwardReference {\n originalMessageId: string;\n originalConversationId: string;\n originalSenderId: string;\n forwardDepth: number;\n}\n\n/** Metadata attached to system messages by the server. */\nexport interface SystemMessageMetadata {\n type?: 'system';\n systemOperationType?: string;\n /** user_removed / user_left */\n removedUserId?: string;\n removedUserName?: string;\n /** user_added */\n addedUserId?: string;\n addedUserName?: string;\n /** admin_promoted / admin_demoted */\n targetUserId?: string;\n targetUserName?: string;\n hasAttachments?: boolean;\n containsLink?: boolean;\n [key: string]: unknown;\n}\n\nexport interface Message {\n id: string;\n tenantId: string;\n conversationId: string;\n senderId: string;\n content: MessageContent;\n replyTo?: MessageReplyReference;\n /** Present when this message was created via forwarding rather than composed directly. */\n forwardedFrom?: MessageForwardReference;\n /**\n * Flat list of mentioned userIds (denormalized from the `@[name](id)` tokens in\n * `content.text`). Contains `\"all\"` for an @all mention. Used for fan-out and\n * \"mentions me\" lookups; not a positional map.\n */\n mentions?: string[];\n reactions: MessageReaction[];\n lastReaction?: LastReaction | null;\n status: MessageStatus;\n deliveryStatus?: DeliveryStatus;\n isEdited: boolean;\n editedAt?: string;\n isStarred?: boolean;\n isPinned?: boolean;\n pinnedBy?: string;\n pinnedAt?: string;\n uploadProgress?: number;\n seq?: number;\n sentAt: string;\n createdAt: string;\n sender?: User;\n senderName?: string;\n /** Present on system messages (content.type === 'system'). */\n metadata?: SystemMessageMetadata;\n readBy?: Array<{ userId: string; readAt: string }>;\n deliveredTo?: Array<{ userId: string; deliveredAt: string }>;\n}\n\n// ─── Conversation ─────────────────────────────────────────────────────────────\n\nexport interface Participant {\n userId: string;\n /** External system user ID of the chat user (non-builtin modes). Mirrors `user.externalId`. */\n externalId?: string;\n role: ParticipantRole;\n joinedAt: string;\n isActive?: boolean;\n user?: User;\n}\n\nexport interface MessageConfig {\n editWindowSeconds?: number;\n deleteWindowSeconds?: number;\n}\n\nexport interface ConversationSettings {\n onlyAdminsCanMessage?: boolean;\n onlyAdminsCanAddMembers?: boolean;\n messageConfig?: MessageConfig;\n}\n\nexport interface Conversation {\n id: string;\n tenantId: string;\n conversationType: ConversationType;\n name?: string;\n description?: string;\n iconUrl?: string;\n participants: Participant[];\n participantCount?: number;\n settings?: ConversationSettings;\n lastMessage?: Message;\n createdBy?: string;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n unreadCount?: number;\n isPinned?: boolean;\n isMuted?: boolean;\n mutedUntil?: string;\n /** True if the current user manually marked this conversation unread (independent of unreadCount) */\n isManuallyUnread?: boolean;\n /** Last message sequence number in this conversation — use as afterSeq cursor for sync */\n lastSeq?: number;\n}\n\n// ─── App Config ───────────────────────────────────────────────────────────────\n\nexport interface AppConfig {\n maxPinnedConversations: number;\n}\n\n// ─── Unread Count ─────────────────────────────────────────────────────────────\n\nexport interface ConversationUnreadCount {\n conversationId: string;\n unreadCount: number;\n}\n\nexport interface UnreadSummary {\n /** Total unread messages across all conversations */\n totalUnread: number;\n /** Per-conversation breakdown — only includes conversations with unread > 0 */\n byConversation: ConversationUnreadCount[];\n}\n\n// ─── Conversation Filters ─────────────────────────────────────────────────────\n\nexport interface ConversationListParams {\n page?: number;\n limit?: number;\n /** Filter by conversation type */\n type?: ConversationType;\n /** Only pinned (true) or unpinned (false) conversations */\n isPinned?: boolean;\n /** Only muted (true) or unmuted (false) conversations */\n isMuted?: boolean;\n /** Only conversations with unread messages */\n hasUnread?: boolean;\n /** Search by group name / description (text index) */\n search?: string;\n /** Filter by current user's role in the conversation */\n role?: ParticipantRole;\n /** Filter by whether the last message has attachments */\n hasAttachments?: boolean;\n /** Filter by last message attachment type */\n attachmentType?: FileType;\n /** Filter by notification setting */\n notificationsEnabled?: boolean;\n}\n\n// ─── Pagination ───────────────────────────────────────────────────────────────\n\nexport interface PaginationMeta {\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n}\n\nexport interface PaginatedResponse<T> {\n data: T[];\n meta: PaginationMeta;\n}\n\nexport interface CursorPaginationMeta {\n hasMore: boolean;\n nextCursor?: string;\n prevCursor?: string;\n}\n\nexport interface CursorPaginatedResponse<T> {\n data: T[];\n meta: CursorPaginationMeta;\n}\n\n// ─── Compression ─────────────────────────────────────────────────────────────\n\nexport type CompressionAlgorithm = 'webp' | 'jpeg' | 'gzip' | 'none';\n\nexport interface CompressedFile extends UploadableFile {\n originalSize: number;\n compressed: boolean;\n compressionAlgorithm: CompressionAlgorithm;\n}\n\n// ─── Storage ──────────────────────────────────────────────────────────────────\n\nexport interface PresignedUrlRequest {\n filename: string;\n mimeType: string;\n size: number;\n conversationId?: string;\n folder?: string;\n metadata?: Record<string, unknown>;\n clientIndex?: number;\n}\n\nexport interface MultipartPartUrl {\n partNumber: number;\n uploadUrl: string;\n expiresAt: string;\n method: 'PUT' | 'POST';\n}\n\nexport interface MultipartUploadInfo {\n uploadId: string;\n storageKey: string;\n totalParts: number;\n chunkSize: number;\n partUrls: MultipartPartUrl[];\n}\n\nexport interface CompletedPart {\n partNumber: number;\n etag: string;\n}\n\nexport interface PresignedUrlResponse {\n fileId: string;\n uploadUrl: string;\n method: 'PUT' | 'POST';\n headers: Record<string, string>;\n fields?: Record<string, string>;\n expiresAt: string;\n maxSize: number;\n allowedMimeType: string;\n clientIndex?: number;\n multipart?: MultipartUploadInfo;\n}\n\nexport interface FileResponse {\n id: string;\n filename: string;\n originalFilename: string;\n mimeType: string;\n size: number;\n url: string;\n thumbnailUrl?: string;\n type: FileType;\n uploadedAt?: string;\n confirmedAt?: string;\n}\n\nexport interface UploadProgress {\n fileId: string;\n filename: string;\n progress: number;\n status: 'pending' | 'uploading' | 'confirming' | 'done' | 'error';\n error?: string;\n}\n\nexport interface BatchUploadResult {\n successful: FileResponse[];\n failed: Array<{ filename: string; error: string }>;\n}\n\n// ─── Socket Event Payloads ────────────────────────────────────────────────────\n\nexport interface NewMessageEvent {\n tempId?: string;\n senderName?: string;\n senderAvatarUrl?: string;\n message: Message;\n}\n\nexport interface ConversationUpdatedEvent {\n id: string;\n lastMessage?: Partial<Message> | null;\n unreadCount?: number;\n updatedAt?: string;\n /** Updated when a new message is sent — use to keep sync cursor up to date */\n lastSeq?: number;\n participants?: Participant[];\n}\n\nexport interface MessageUpdatedEvent {\n messageId: string;\n conversationId: string;\n text: string;\n editedAt: string;\n}\n\nexport interface MessageDeletedEvent {\n messageId: string;\n conversationId: string;\n}\n\nexport interface MessageDeletedForMeEvent {\n messageId: string;\n conversationId: string;\n}\n\nexport interface LastReaction {\n emoji: string;\n reactorId: string;\n reactorName: string;\n reactedAt: string;\n}\n\nexport interface ReactionUpdatedEvent {\n messageId: string;\n conversationId: string;\n reactions: MessageReaction[];\n lastReaction: LastReaction | null;\n}\n\nexport interface MessageStarUpdatedEvent {\n messageId: string;\n conversationId: string;\n isStarred: boolean;\n}\n\nexport interface TypingIndicatorEvent {\n conversationId: string;\n userId: string;\n /** External system user ID (non-builtin modes). Absent in builtin mode. */\n externalId?: string;\n username: string;\n displayName: string;\n avatarUrl?: string;\n isTyping: boolean;\n}\n\nexport interface UserStatusEvent {\n userId: string;\n status: UserStatus;\n lastSeenAt?: string | null;\n}\n\nexport interface ReadReceiptEvent {\n conversationId: string;\n messageId: string;\n userId: string;\n readAt: string;\n updatedMessageIds?: string[];\n fullyReadMessageIds?: string[];\n}\n\nexport interface MessageAckEvent {\n tempId: string;\n messageId: string;\n status: MessageStatus;\n /** Present when server supports seq — monotonic counter for this conversation. */\n seq?: number;\n}\n\nexport interface MessageDeliveredEvent {\n messageId: string;\n conversationId: string;\n deliveredTo: { userId: string; deliveredAt: Date };\n}\n\nexport interface MessageReceiptEntry {\n userId: string;\n /** External system user ID (non-builtin modes). */\n externalId?: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface MessageReceiptsResponse {\n messageId: string;\n readBy: Array<MessageReceiptEntry & { readAt: string }>;\n deliveredTo: Array<MessageReceiptEntry & { deliveredAt: string }>;\n}\n\nexport interface MessagesDeliveredEvent {\n conversationId: string;\n messageIds: string[];\n deliveredTo: string;\n deliveredAt: string;\n}\n\n// ─── Sync ─────────────────────────────────────────────────────────────────────\n\nexport interface SyncReadReceipt {\n messageId: string;\n conversationId: string;\n userId: string;\n readAt: string | null;\n}\n\nexport interface SyncDeliveredReceipt {\n messageId: string;\n conversationId: string;\n userId: string;\n deliveredAt: string | null;\n}\n\nexport interface SyncDeletedForMe {\n messageId: string;\n conversationId: string;\n deletedAt: string | null;\n}\n\nexport interface SyncParticipantChange {\n conversationId: string;\n userId: string;\n role: string;\n isActive: boolean;\n isMuted: boolean;\n mutedUntil: string | null;\n updatedAt: string | null;\n}\n\nexport interface SyncStarEntry {\n messageId: string;\n conversationId: string;\n /** true = starred, false = unstarred (soft-deleted) */\n isActive: boolean;\n updatedAt: string | null;\n}\n\nexport interface SyncReactionEntry {\n emoji: string;\n count: number;\n /** True when the current user has reacted with this emoji */\n didIReact: boolean;\n}\n\n/** Full current reaction state keyed by messageId. Only message IDs with activity since `since` are included. */\nexport type SyncReactions = Record<string, SyncReactionEntry[]>;\n\nexport interface CrossConversationSyncResponse {\n syncedAt: string;\n /** When true the gap exceeds 60 days — app should mark all conversations needs_refresh and lazy-sync on open */\n stale: boolean;\n messages: Message[];\n deletedForMe: SyncDeletedForMe[];\n reactions: SyncReactions;\n stars: SyncStarEntry[];\n participantChanges: SyncParticipantChange[];\n readReceipts: SyncReadReceipt[];\n deliveredReceipts: SyncDeliveredReceipt[];\n /**\n * Conversation IDs the client should remove from its local list.\n * Set when the user explicitly deleted their own copy of a conversation\n * (DM or group) via the delete action. Scoped to this user only — other\n * participants are not affected.\n */\n deletedConversationIds: string[];\n}\n\nexport interface ConversationSyncResponse {\n syncedAt: string;\n /** Cursor for the next page in the same direction — pass as afterSeq or beforeSeq matching what you sent. */\n nextSeq?: number;\n /** True when more pages are available — call again with nextSeq as the same cursor type. */\n hasMore?: boolean;\n messages: Message[];\n deletedForMe: SyncDeletedForMe[];\n reactions: SyncReactions;\n stars: SyncStarEntry[];\n participantChanges: SyncParticipantChange[];\n readReceipts: SyncReadReceipt[];\n deliveredReceipts: SyncDeliveredReceipt[];\n}\n\n// ─── Reactions ────────────────────────────────────────────────────────────────\n\nexport interface ReactionUser {\n userId: string;\n /** External system user ID (non-builtin modes). */\n externalId?: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface ReactionGroup {\n emoji: string;\n /** All user IDs who reacted with this emoji */\n userIds: string[];\n count: number;\n /** Resolved user profiles for display in \"who reacted\" sheet */\n users: ReactionUser[];\n}\n\nexport interface MessageReactionsResponse {\n messageId: string;\n reactions: ReactionGroup[];\n}\n\n// ─── Send Message ─────────────────────────────────────────────────────────────\n\nexport interface SendMessageAttachment {\n fileId: string;\n type: FileType;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n duration?: number;\n}\n\nexport interface OptimisticAttachment extends SendMessageAttachment {\n id: string;\n slotId?: string;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n isUploading?: boolean;\n uploadProgress?: number;\n}\n\nexport interface SendMessagePayload {\n conversationId: string;\n text?: string;\n attachments?: SendMessageAttachment[];\n replyTo?: string;\n tempId: string;\n /** Mentioned userIds (use `\"all\"` / MENTION_ALL_ID for @all). Derived from the tokens in `text`. */\n mentions?: string[];\n}\n\n// ─── Forward Message (socket transport) ───────────────────────────────────────\n\nexport interface ForwardMessagePayload {\n messageId: string;\n targetConversationIds: string[];\n /** Omit to forward the whole source message, including all its attachments, unchanged. */\n attachmentIds?: string[];\n /** Client-generated idempotency key — see ForwardData.tempId (api/messages.ts) for the full contract. */\n tempId: string;\n}\n\n/** Per-target result for a 'forward_message' socket call. Mirrors the REST ForwardResult shape. */\nexport interface ForwardResultPayload {\n conversationId: string;\n success: boolean;\n message?: Message;\n error?: string;\n}\n\n/** Ack for the 'forward_message' socket event. */\nexport interface ForwardAckPayload {\n tempId?: string;\n results: ForwardResultPayload[];\n /** Present only when the server-side forward() call itself threw (e.g. source message not found) — results is [] in that case. */\n error?: string;\n}\n\n// ── User Preferences ──────────────────────────────────────────────────────────\n\nexport interface QuietHours {\n enabled: boolean;\n /** HH:MM — e.g. \"22:00\" */\n start: string;\n /** HH:MM — e.g. \"08:00\" */\n end: string;\n /** IANA timezone — e.g. \"Asia/Kolkata\" */\n timezone: string;\n}\n\n/**\n * User notification preferences stored in chat_user_prefs.\n * All fields are optional on update — only send what changed.\n * Any future preference field is added here and to the server schema;\n * no new collections or API endpoints needed.\n */\nexport interface UserPreferences {\n /** Master switch — false disables all push notifications */\n notificationsEnabled?: boolean;\n /** Play sound with notifications */\n soundEnabled?: boolean;\n /** Show message text in notification body (false = show \"New message\" only) */\n messagePreview?: boolean;\n /** Notify when @mentioned in a group */\n notifyOnMention?: boolean;\n /** Notify when someone reacts to your message */\n notifyOnReaction?: boolean;\n /** Notify when added to a group */\n notifyOnGroupInvite?: boolean;\n /** Quiet hours window — no push delivered during this period */\n quietHours?: QuietHours;\n}\n","import { MENTION_ALL_ID } from '../types/index.js';\n\n/**\n * Mention token utilities — the shared logic every consumer (web SDK, RN SDK,\n * core-only apps) reuses so name-source is the only thing that differs per app.\n *\n * Storage model:\n * - Each mention is a self-describing token in the message text:\n * `@[DisplayName](userId)` (and `@[all](all)` for @all)\n * - The message also carries a flat `mentions: string[]` of userIds, derived\n * from these tokens. It is a denormalized fan-out list, not a position map.\n *\n * The DisplayName inside the token is a FALLBACK only — consumers should resolve\n * the current name from their user directory and use the token name only when the\n * id cannot be resolved (deleted user, or a departed member with no directory).\n */\n\n/**\n * Matches `@[Display Name](userId)`.\n * group 1 = display name (any char except the closing `]`)\n * group 2 = userId (24-hex ObjectId, or the literal \"all\")\n * The `g` flag is required for iterating all matches; callers must not share a\n * single compiled instance across reentrant calls (each function builds its own).\n */\nconst MENTION_TOKEN_SOURCE = '@\\\\[([^\\\\]]+)\\\\]\\\\((all|[a-fA-F0-9]{24})\\\\)';\n\nexport interface ParsedMention {\n /** userId, or \"all\" */\n id: string;\n /** display name captured from the token (fallback for rendering) */\n displayName: string;\n /** start index of the whole token in the text (UTF-16) */\n start: number;\n /** end index (exclusive) of the whole token in the text (UTF-16) */\n end: number;\n}\n\n/** A segment of a message: either plain text or a mention to render specially. */\nexport type MentionPart =\n | { type: 'text'; text: string }\n | { type: 'mention'; id: string; displayName: string };\n\n/**\n * A piece used to compose outgoing text. Plain strings pass through verbatim;\n * mention objects are serialized to `@[displayName](id)` tokens.\n */\nexport type MentionSegment =\n | string\n | { id: string; displayName: string };\n\n/** Escapes characters in a display name that would break the `@[name](id)` token. */\nfunction sanitizeDisplayName(name: string): string {\n // `]` closes the name group and `)`/`(` frame the id — strip them so a name\n // like \"Bob (HR)\" or \"a]b\" can't produce a malformed or spoofed token.\n return name.replace(/[\\[\\]()]/g, '').trim() || 'user';\n}\n\n/**\n * Finds every `@[name](id)` mention token in the text, in order of appearance.\n * Pure/computed — nothing is stored. Safe to call on every render.\n */\nexport function parseMentions(text: string | undefined | null): ParsedMention[] {\n if (!text) return [];\n const re = new RegExp(MENTION_TOKEN_SOURCE, 'g');\n const out: ParsedMention[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n out.push({\n id: m[2],\n displayName: m[1],\n start: m.index,\n end: m.index + m[0].length,\n });\n }\n return out;\n}\n\n/**\n * Splits text into ordered parts (plain text + mentions) for rendering.\n * The caller supplies `resolveName(id, fallbackName)` to look up the current\n * display name from its own directory; return the fallback when unresolvable.\n * When no resolver is given, the token's embedded name is used.\n */\nexport function renderMentionParts(\n text: string | undefined | null,\n resolveName?: (id: string, fallbackName: string) => string,\n): MentionPart[] {\n if (!text) return [];\n const mentions = parseMentions(text);\n if (mentions.length === 0) return [{ type: 'text', text }];\n\n const parts: MentionPart[] = [];\n let cursor = 0;\n for (const mn of mentions) {\n if (mn.start > cursor) {\n parts.push({ type: 'text', text: text.slice(cursor, mn.start) });\n }\n const displayName = resolveName ? resolveName(mn.id, mn.displayName) : mn.displayName;\n parts.push({ type: 'mention', id: mn.id, displayName });\n cursor = mn.end;\n }\n if (cursor < text.length) {\n parts.push({ type: 'text', text: text.slice(cursor) });\n }\n return parts;\n}\n\n/**\n * Builds outgoing message text + the `mentions` id array from composer segments.\n * Deduplicates ids (a user mentioned twice is fanned out once).\n *\n * Example:\n * buildMentionText(['Hey ', { id: 'u1', displayName: 'Alice' }, ', look'])\n * → { text: 'Hey @[Alice](u1), look', mentions: ['u1'] }\n */\nexport function buildMentionText(segments: MentionSegment[]): { text: string; mentions: string[] } {\n let text = '';\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const seg of segments) {\n if (typeof seg === 'string') {\n text += seg;\n } else {\n text += `@[${sanitizeDisplayName(seg.displayName)}](${seg.id})`;\n if (!seen.has(seg.id)) {\n seen.add(seg.id);\n ids.push(seg.id);\n }\n }\n }\n return { text, mentions: ids };\n}\n\n/**\n * Re-derives the `mentions` id array from message text — the source of truth is\n * always the tokens present in the text. Use this after an edit so a mention whose\n * token was removed drops out of the array (FR-10).\n */\nexport function extractMentionIds(text: string | undefined | null): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const mn of parseMentions(text)) {\n if (!seen.has(mn.id)) {\n seen.add(mn.id);\n ids.push(mn.id);\n }\n }\n return ids;\n}\n\n/** True when the mentions list targets everyone (@all). */\nexport function isMentionAll(mentions: string[] | undefined): boolean {\n return !!mentions?.includes(MENTION_ALL_ID);\n}\n","import type { AntzChatConfig, PlatformUploadFn } from './config/types.js';\nimport { resolveConfig } from './config/types.js';\nimport { initApiClient, setApiClientInstance } from './api/client.js';\nimport { initAuthStore } from './stores/auth.store.js';\nimport { authApi } from './api/auth.js';\nimport { messagesApi } from './api/messages.js';\nimport { conversationsApi } from './api/conversations.js';\nimport { storageApi, uploadBatch } from './api/storage.js';\nimport { usersApi } from './api/users.js';\nimport { connectSocket, disconnectSocket, getSocket } from './socket/socket.js';\nimport { socketEmit } from './socket/emitters.js';\nimport type { UploadableFile } from './types/index.js';\n\ninterface ClientSocketHandle {\n emit: typeof socketEmit;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n/**\n * Headless client for non-React consumers or direct programmatic use.\n * Usage:\n * const client = new AntzChatClient({ apiUrl, authToken, platformUploadFn, persistStorage })\n * await client.connect()\n */\nexport class AntzChatClient {\n private _config;\n private _authStore;\n\n readonly auth = authApi;\n readonly messages = messagesApi;\n readonly conversations = conversationsApi;\n readonly storage = storageApi;\n readonly users = usersApi;\n readonly socket: ClientSocketHandle = {\n emit: socketEmit,\n on: (event, handler) => getSocket().on(event, handler as (...args: any[]) => void),\n off: (event, handler) => getSocket().off(event, handler as (...args: any[]) => void),\n };\n\n constructor(rawConfig: AntzChatConfig) {\n this._config = resolveConfig(rawConfig);\n this._authStore = initAuthStore(rawConfig.persistStorage);\n\n if (rawConfig.authToken) {\n this._authStore.authTokenStore.setTokens({\n accessToken: rawConfig.authToken,\n refreshToken: '',\n tokenType: 'Bearer',\n expiresIn: 0,\n });\n }\n\n const instance = initApiClient(this._config, this._authStore.authTokenStore);\n setApiClientInstance(instance);\n }\n\n async connect(): Promise<void> {\n await connectSocket(this._config, () => this._authStore.authTokenStore.getAccessToken());\n }\n\n disconnect(): void {\n disconnectSocket();\n }\n\n uploadFiles(files: UploadableFile[], conversationId?: string) {\n return uploadBatch(\n files,\n this._config.platformUploadFn,\n conversationId,\n this._config.upload.onProgress,\n this._config.platformCompressFn,\n this._config.compression,\n );\n }\n\n async uploadIcon(conversationId: string, file: UploadableFile) {\n const result = await this.uploadFiles([file], conversationId);\n const fileId = result.successful[0]?.id;\n if (!fileId) throw new Error('Icon upload failed');\n return conversationsApi.uploadIcon(conversationId, fileId);\n }\n}\n"],"mappings":"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","names":["useChatStore"]}
|
|
1
|
+
{"version":3,"sources":["../src/config/types.ts","../src/api/auth.ts","../src/api/sync.ts","../src/api/app-config.ts","../src/stores/auth.store.ts","../src/utils/resolveSystemMessageText.ts","../src/socket/socket.ts","../src/socket/emitters.ts","../src/api/messages.ts","../src/api/conversations.ts","../src/api/devices.ts","../src/api/users.ts","../src/types/index.ts","../src/utils/mentions.ts","../src/client-facade.ts"],"sourcesContent":["import type { FileType, UploadableFile, PresignedUrlResponse, CompressedFile } from '../types/index.js';\n\nexport interface FileSizeLimits {\n image?: number;\n video?: number;\n audio?: number;\n document?: number;\n default?: number;\n}\n\n/**\n * Platform-provided function that performs the actual binary upload to a presigned URL.\n * - Web implementation: XMLHttpRequest with progress events\n * - RN implementation: fetch with FormData or direct body\n *\n * The core calls this — it never does the upload itself.\n */\nexport type PlatformUploadFn = (\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n onProgress?: (pct: number) => void,\n) => Promise<void>;\n\n/**\n * Platform-provided persistent key-value storage for auth token persistence.\n * - Web: localStorage adapter\n * - RN: AsyncStorage adapter\n */\nexport interface PersistStorage {\n getItem(key: string): string | null | Promise<string | null>;\n setItem(key: string, value: string): void | Promise<void>;\n removeItem(key: string): void | Promise<void>;\n}\n\n/**\n * Platform-provided compression function. Optional — if omitted, files are uploaded as-is.\n * - Web: uses canvas (images) + CompressionStream (text/docs) — both browser-native\n * - RN: uses expo-image-manipulator (images) + pako (text/docs)\n * - Node: uses sharp (images) + zlib (text/docs)\n */\nexport type PlatformCompressFn = (\n file: UploadableFile,\n options: ResolvedCompressionConfig,\n) => Promise<CompressedFile>;\n\n/**\n * Platform-provided function for uploading a single multipart part.\n * Takes a presigned PUT URL and a raw blob/buffer slice, returns the ETag\n * from the S3 response (required for CompleteMultipartUpload).\n * Internal — each SDK (web, RN) provides its own; not exposed to consumers.\n */\nexport type PlatformUploadPartFn = (\n uploadUrl: string,\n blob: Blob | ArrayBuffer,\n onProgress?: (pct: number) => void,\n method?: 'PUT' | 'POST',\n) => Promise<string>; // returns ETag\n\nexport interface CompressionConfig {\n /** Master switch. Default: true when platformCompressFn is provided, false otherwise */\n enabled?: boolean;\n /** WebP quality for images, 0–1. Default: 0.85 */\n imageQuality?: number;\n /** Longest side cap in px before encoding. Default: 1920 */\n imageMaxDimension?: number;\n /** Gzip text-based documents (plain, csv, json, xml, yaml, svg). Default: true */\n compressDocuments?: boolean;\n}\n\nexport interface ResolvedCompressionConfig {\n enabled: boolean;\n imageQuality: number;\n imageMaxDimension: number;\n compressDocuments: boolean;\n}\n\nexport interface UploadConfig {\n /**\n * Per-type file size limits in MB. Can also pass a single number for all types.\n * Defaults: image 5MB, video 25MB, audio 10MB, document 10MB.\n */\n maxFileSizeMB?: number | FileSizeLimits;\n\n /** Max files per message. Default: 10 */\n maxFilesPerMessage?: number;\n\n /** Which attachment types users can send. Default: all */\n allowedTypes?: Array<FileType>;\n\n /** Called when a file fails validation or upload */\n onUploadError?: (file: UploadableFile, error: Error) => void;\n\n /** Called with 0–100 aggregate progress during a batch upload */\n onProgress?: (progress: number) => void;\n}\n\n/** Context passed to onSendError. */\nexport interface SendErrorContext {\n conversationId: string;\n /** tempId of the optimistic message the failure belongs to. */\n tempId: string;\n /**\n * True when resending the identical payload can't succeed (file size/type\n * limits, message too long, no longer a participant, …). The SDK has removed\n * the optimistic message from the timeline. False for transient failures\n * (network / socket / generic server error), where a \"! Retry\" bubble stays.\n */\n permanent: boolean;\n}\n\nexport interface AntzChatConfig {\n /** REST API base URL — e.g. \"https://api.yourapp.com/api/v1\" */\n apiUrl: string;\n\n /**\n * WebSocket server URL. Defaults to apiUrl with /api/vN path stripped.\n * SDK connects to {socketUrl}/chat\n */\n socketUrl?: string;\n\n /** Static JWT. Use this OR authProvider, not both. */\n authToken?: string;\n\n /**\n * Dynamic token getter — called before requests and on socket reconnect.\n * Preferred when the host app manages its own auth lifecycle.\n */\n authProvider?: () => Promise<string>;\n\n /**\n * External user ID — required for non-builtin modes (antz / external / wso2).\n * Sent as x-user-id header on every request so the chat server can forward it\n * to the user-service for token validation.\n * Not needed for builtin mode (chat server issues its own JWTs).\n */\n userId?: string;\n\n /**\n * Profile picture for non-builtin modes.\n * Server fetches/decodes, hashes for dedup, uploads to chat storage on first use.\n * Only sent once at init — not re-sent on every request.\n * Client never needs to compute hashes; the server owns all dedup logic.\n */\n avatar?: {\n /** Full URL the server can fetch (preferred — Antz/external/wso2 clients know their own file URLs) */\n url?: string;\n /** Raw base64 string (with or without data:... prefix) as fallback */\n base64?: string;\n };\n\n /** Required for multi-tenant backends. Sent as X-Tenant-ID header. */\n tenantId?: string;\n\n /**\n * Enable payload-level transit encryption (ECDH key exchange + AES-256-GCM).\n * Encrypts every HTTP request/response body and every socket event payload.\n * Server must have TRANSIT_ENCRYPTION_ENABLED=true to match.\n * Default: true — set false only for local development/debugging.\n * Safe to toggle anytime — no data migration needed (wire-only, never affects storage).\n */\n transitEncryption?: boolean;\n\n upload?: UploadConfig;\n\n /**\n * Optional compression config. Compression is disabled if platformCompressFn is not provided.\n */\n compression?: CompressionConfig;\n\n /**\n * Platform-specific compression implementation. Optional — omit to disable compression.\n * Each SDK (web, RN) provides its own default; Node.js users wire in their own.\n */\n platformCompressFn?: PlatformCompressFn;\n\n /**\n * Called when sending a message ultimately fails, after the SDK has updated\n * the timeline (removed the bubble for permanent failures, flagged it retryable\n * otherwise). Use it to surface a toast / error banner to the user.\n */\n onSendError?: (error: Error, context: SendErrorContext) => void;\n\n /**\n * Number of messages fetched per page when loading chat history.\n * Default: 40\n */\n messagePageSize?: number;\n\n /**\n * Number of starred messages fetched per page.\n * Default: 30\n */\n starredMessagePageSize?: number;\n\n /**\n * Number of search results fetched per request.\n * Default: 50\n */\n searchPageSize?: number;\n\n /**\n * Platform-specific binary upload implementation.\n * Required — each SDK (web, RN) provides its own.\n */\n platformUploadFn: PlatformUploadFn;\n\n /**\n * Platform-specific single-part upload for chunked multipart uploads.\n * Internal — web/RN SDKs set this automatically; not set by SDK consumers.\n */\n platformUploadPartFn?: PlatformUploadPartFn;\n\n /**\n * Platform-specific persistent storage for auth tokens.\n * Required — each SDK provides its own (localStorage / AsyncStorage).\n */\n persistStorage: PersistStorage;\n}\n\n// ─── Resolved (all defaults filled) ─────────────────────────────────────────\n\nexport interface ResolvedFileSizeLimits {\n image: number;\n video: number;\n audio: number;\n document: number;\n default: number;\n}\n\nexport interface ResolvedConfig {\n apiUrl: string;\n socketUrl: string;\n socketOrigin: string;\n socketPath: string;\n authToken?: string;\n authProvider?: () => Promise<string>;\n userId?: string;\n tenantId?: string;\n avatar?: { url?: string; base64?: string };\n transitEncryption: boolean;\n upload: {\n maxFileSizeMB: ResolvedFileSizeLimits;\n maxFilesPerMessage: number;\n allowedTypes: FileType[];\n onUploadError?: (file: UploadableFile, error: Error) => void;\n onProgress?: (progress: number) => void;\n };\n platformUploadFn: PlatformUploadFn;\n platformUploadPartFn?: PlatformUploadPartFn;\n platformCompressFn?: PlatformCompressFn;\n compression: ResolvedCompressionConfig;\n persistStorage: PersistStorage;\n onSendError?: (error: Error, context: SendErrorContext) => void;\n messagePageSize: number;\n starredMessagePageSize: number;\n searchPageSize: number;\n}\n\nexport function resolveConfig(config: AntzChatConfig): ResolvedConfig {\n const socketUrl =\n config.socketUrl ??\n config.apiUrl.replace(/\\/api\\/v\\d+\\/?$/, '').replace(/\\/$/, '');\n\n // Auto-derive Socket.IO engine path from the socketUrl pathname.\n // If the server is behind a reverse proxy with a path prefix (e.g. /chat-api),\n // that prefix is already embedded in socketUrl. We append /socket.io to it.\n // Examples:\n // socketUrl = https://host/chat-api → socketPath = /chat-api/socket.io\n // socketUrl = https://host → socketPath = /socket.io\n // No env var or manual config needed — SERVER_URL on the server and apiUrl\n // on the client carry the same prefix, so both sides derive the same path.\n // Derive Socket.IO engine path and origin separately.\n // Socket.IO uses the URL passed to io() as the namespace origin — anything\n // in the pathname of that URL becomes part of the namespace, not the engine path.\n // So we must pass only the bare origin (https://host) to io(), and use the\n // path option for the engine mount point.\n //\n // socketUrl = https://host/chat-api\n // → socketOrigin = https://host (passed to io())\n // → socketPath = /chat-api/socket.io (engine path option)\n // → namespace = /chat (hardcoded in connectSocket)\n //\n // socketUrl = https://host\n // → socketOrigin = https://host\n // → socketPath = /socket.io\n let socketOrigin = socketUrl;\n let socketPath = '/socket.io';\n try {\n const parsed = new URL(socketUrl);\n socketOrigin = parsed.origin; // strips pathname, search, hash\n const pathname = parsed.pathname.replace(/\\/$/, '');\n if (pathname && pathname !== '/') socketPath = `${pathname}/socket.io`;\n } catch {\n // invalid URL — fall back to defaults\n }\n\n const raw = config.upload?.maxFileSizeMB;\n let limits: ResolvedFileSizeLimits;\n\n if (typeof raw === 'number') {\n limits = { image: raw, video: raw, audio: raw, document: raw, default: raw };\n } else {\n const fallback = raw?.default ?? 25;\n limits = {\n image: raw?.image ?? 5,\n video: raw?.video ?? 25,\n audio: raw?.audio ?? 10,\n document: raw?.document ?? 10,\n default: fallback,\n };\n }\n\n return {\n apiUrl: config.apiUrl.replace(/\\/$/, ''),\n socketUrl,\n socketOrigin,\n socketPath,\n authToken: config.authToken,\n authProvider: config.authProvider,\n userId: config.userId,\n tenantId: config.tenantId,\n avatar: config.avatar,\n transitEncryption: config.transitEncryption ?? true,\n upload: {\n maxFileSizeMB: limits,\n maxFilesPerMessage: config.upload?.maxFilesPerMessage ?? 10,\n allowedTypes: config.upload?.allowedTypes ?? ['image', 'video', 'audio', 'document'],\n onUploadError: config.upload?.onUploadError,\n onProgress: config.upload?.onProgress,\n },\n platformUploadFn: config.platformUploadFn,\n platformUploadPartFn: config.platformUploadPartFn,\n platformCompressFn: config.platformCompressFn,\n compression: {\n enabled: config.compression?.enabled ?? (config.platformCompressFn != null),\n imageQuality: config.compression?.imageQuality ?? 0.85,\n imageMaxDimension: config.compression?.imageMaxDimension ?? 1920,\n compressDocuments: config.compression?.compressDocuments ?? true,\n },\n persistStorage: config.persistStorage,\n onSendError: config.onSendError,\n messagePageSize: config.messagePageSize ?? 40,\n starredMessagePageSize: config.starredMessagePageSize ?? 30,\n searchPageSize: config.searchPageSize ?? 50,\n };\n}\n","import type { AuthResponse, AuthTokens, LoginCredentials, RegisterData, User } from '../types/index.js';\nimport { getApiClient, TRANSIT_OPTIONAL_HEADER } from './client.js';\nimport { getTransitSession } from '../crypto/session.js';\n\n// Teardown calls opt out of the transit gate — see TRANSIT_OPTIONAL_HEADER.\nconst teardownRequest = { headers: { [TRANSIT_OPTIONAL_HEADER]: '1' } };\n\nexport const authApi = {\n async login(credentials: LoginCredentials): Promise<AuthResponse> {\n const { data } = await getApiClient().post<AuthResponse>('/auth/login', credentials);\n return data;\n },\n\n async register(payload: RegisterData): Promise<AuthResponse> {\n const { data } = await getApiClient().post<AuthResponse>('/auth/register', payload);\n return data;\n },\n\n async refresh(refreshToken: string): Promise<AuthTokens> {\n const { data } = await getApiClient().post<AuthTokens>('/auth/refresh', { refreshToken });\n return data;\n },\n\n /**\n * Ends the session. Never blocks on the transit handshake: logout has to work\n * on a degraded channel, which is exactly when it matters most.\n *\n * The refreshToken is dropped when no transit session exists, so it is never\n * sent in the clear. Losing it costs nothing meaningful — the server revokes\n * against the JWT's own user id either way; passing it only lets the server\n * target that one refresh token instead of the caller's session set.\n */\n async logout(refreshToken?: string): Promise<void> {\n const encrypted = getTransitSession() != null;\n const body = refreshToken && encrypted ? { refreshToken } : {};\n await getApiClient().post('/auth/logout', body, teardownRequest);\n },\n\n async logoutAll(): Promise<void> {\n // Empty body — nothing to protect, so the gate is pure downside here.\n await getApiClient().post('/auth/logout-all', {}, teardownRequest);\n },\n\n async getMe(): Promise<User> {\n const { data } = await getApiClient().get<User>('/users/me');\n return data;\n },\n\n // Upload a new avatar file (builtin mode — multipart)\n async uploadAvatar(file: File | Blob, mimeType?: string): Promise<{ avatarUrl: string }> {\n const form = new FormData();\n form.append('avatar', file instanceof File ? file : new File([file], 'avatar.jpg', { type: mimeType ?? 'image/jpeg' }));\n const { data } = await getApiClient().post<{ avatarUrl: string }>('/users/me/avatar', form, {\n headers: { 'Content-Type': 'multipart/form-data' },\n });\n return data;\n },\n\n // Sync avatar from URL or base64 (non-builtin modes — or post-init update)\n async syncAvatar(source: { url?: string; base64?: string }): Promise<{ avatarUrl: string }> {\n const headers: Record<string, string> = {};\n if (source.base64) headers['x-avatar-base64'] = source.base64;\n else if (source.url) headers['x-avatar-url'] = source.url;\n const { data } = await getApiClient().post<{ avatarUrl: string }>('/users/me/avatar/sync', {}, { headers });\n return data;\n },\n};\n","import type { CrossConversationSyncResponse, ConversationSyncResponse } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\nexport const syncApi = {\n /**\n * Cross-conversation delta sync — call on every socket reconnect.\n * `since` is the ISO timestamp returned as `syncedAt` from the previous call.\n * Returns `stale: true` (with empty arrays) when the gap exceeds 60 days —\n * the app should mark all conversations `needs_refresh` and lazy-sync on open.\n */\n pull(since: string): Promise<CrossConversationSyncResponse> {\n return getApiClient()\n .get<CrossConversationSyncResponse>('/sync', { params: { since } })\n .then(r => r.data);\n },\n\n /**\n * Per-conversation delta sync — call when a conversation has `needs_refresh = true`\n * or on fresh install (omit all opts to receive full history).\n * Covers messages, deletedForMe, full reaction state, stars, participant changes,\n * and read receipts — everything needed to make local SQLite authoritative.\n *\n * Direction:\n * afterSeq — forward (oldest→newest, catch-up/sync). Default when seq is known.\n * beforeSeq — backward (newest→oldest, scroll-back / load history).\n * since — timestamp fallback for old servers or first sync on a new install.\n *\n * When `hasMore` is true, call again with `nextSeq` as the same cursor type you sent.\n */\n pullConversation(\n conversationId: string,\n opts?: { since?: string; afterSeq?: number; beforeSeq?: number },\n ): Promise<ConversationSyncResponse> {\n const params: Record<string, string | number> = {};\n if (opts?.afterSeq !== undefined) {\n params.afterSeq = opts.afterSeq;\n } else if (opts?.beforeSeq !== undefined) {\n params.beforeSeq = opts.beforeSeq;\n } else if (opts?.since) {\n params.since = opts.since;\n }\n return getApiClient()\n .get<ConversationSyncResponse>(`/conversations/${conversationId}/sync`, { params })\n .then(r => r.data);\n },\n};\n","import { getApiClient } from './client.js';\nimport type { AppConfig } from '../types/index.js';\n\nexport const appConfigApi = {\n async get(): Promise<AppConfig> {\n const { data } = await getApiClient().get<AppConfig>('/app/config');\n return data;\n },\n};\n","import { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport type { AuthTokens, User } from '../types/index.js';\nimport type { PersistStorage } from '../config/types.js';\n\ninterface AuthState {\n user: User | null;\n tokens: AuthTokens | null;\n isAuthenticated: boolean;\n isLoading: boolean;\n isHydrated: boolean;\n\n setAuth: (user: User, tokens: AuthTokens) => void;\n setTokens: (tokens: AuthTokens) => void;\n setUser: (user: User) => void;\n logout: () => void;\n setLoading: (loading: boolean) => void;\n setHydrated: (hydrated: boolean) => void;\n}\n\n/**\n * Creates a platform-specific auth store.\n * Call once during SDK init with the platform's PersistStorage adapter.\n * - Web: pass a localStorage adapter\n * - RN: pass an AsyncStorage adapter\n */\nexport function createAuthStore(storage: PersistStorage) {\n if (!storage) throw new Error('[AntzChat] createAuthStore requires a valid PersistStorage — received undefined. Make sure the SDK config is fully resolved before initializing the store.');\n // Use a ref object so the onRehydrateStorage closure can call store.setState\n // without hitting a temporal dead zone. On web, localStorage is synchronous\n // and onRehydrateStorage fires during create() before `store` is assigned.\n const ref: { store: any } = { store: null };\n\n const store = create<AuthState>()(\n persist(\n (set) => ({\n user: null,\n tokens: null,\n isAuthenticated: false,\n isLoading: false,\n isHydrated: false,\n\n setAuth: (user, tokens) =>\n set({ user, tokens, isAuthenticated: true, isLoading: false }),\n\n setTokens: (tokens) => set({ tokens }),\n\n setUser: (user) => set({ user }),\n\n logout: () =>\n set({ user: null, tokens: null, isAuthenticated: false, isLoading: false }),\n\n setLoading: (isLoading) => set({ isLoading }),\n\n setHydrated: (isHydrated) => set({ isHydrated }),\n }),\n {\n name: 'antz-chat-auth',\n storage: {\n getItem: (name) => {\n const result = storage.getItem(name);\n if (result instanceof Promise) {\n return result.then((str) => (str ? JSON.parse(str) : null));\n }\n return result ? JSON.parse(result) : null;\n },\n setItem: (name, value) => {\n storage.setItem(name, JSON.stringify(value));\n },\n removeItem: (name) => storage.removeItem(name),\n },\n partialize: (state) => ({\n user: state.user,\n tokens: state.tokens,\n isAuthenticated: state.isAuthenticated,\n }) as AuthState,\n onRehydrateStorage: () => (state, error) => {\n if (error) {\n console.warn('[AntzChat] Auth store rehydration failed:', error);\n }\n // Access via ref — safe even when called synchronously during create()\n ref.store?.setState({ isHydrated: true });\n },\n },\n ),\n );\n\n // Assign ref immediately after create() completes\n ref.store = store;\n\n // If already hydrated synchronously (web/localStorage rehydrated during create),\n // set the flag now since the closure above may have called ref.store when it was null.\n if (!store.getState().isHydrated) {\n // Fallback: async storage (RN) — force hydrated after 3s max\n const hydrationTimeout = setTimeout(() => {\n if (!store.getState().isHydrated) {\n store.setState({ isHydrated: true });\n }\n }, 3000);\n\n // Clean up if it resolves naturally before timeout\n const unsub = store.subscribe((s) => {\n if (s.isHydrated) {\n clearTimeout(hydrationTimeout);\n unsub();\n }\n });\n }\n\n const tokenStore = {\n getAccessToken: () => store.getState().tokens?.accessToken,\n getRefreshToken: () => store.getState().tokens?.refreshToken,\n setTokens: (tokens: AuthTokens) => store.getState().setTokens(tokens),\n clearTokens: () => store.getState().logout(),\n };\n\n return { useAuthStore: store, authTokenStore: tokenStore };\n}\n\n// ─── Singleton instances (set during SDK init) ────────────────────────────────\n// Each SDK (web / RN) calls createAuthStore once and exports these.\n// Direct import of these will throw until initAuthStore is called.\n\nlet _authStore: ReturnType<typeof createAuthStore> | null = null;\n\n/**\n * Initialize the auth store singleton.\n * Idempotent — subsequent calls with the same storage return the existing instance.\n * This ensures tokens persisted in localStorage/AsyncStorage survive re-renders\n * and React StrictMode double-invocations.\n */\nexport function initAuthStore(storage: PersistStorage) {\n if (!_authStore) {\n _authStore = createAuthStore(storage);\n }\n return _authStore;\n}\n\nexport function getAuthStore() {\n if (!_authStore) throw new Error('[AntzChat] Auth store not initialized. Call initAuthStore first.');\n return _authStore;\n}\n\n/** Reset the singleton — only for tests / SDK teardown. */\nexport function resetAuthStore() {\n _authStore = null;\n}\n","import type { Message } from '../types/index.js';\n\n/**\n * Resolves viewer-aware display text for system messages.\n *\n * The server stores a neutral third-person text in content.text (used for push\n * notifications and exports) and structured actor/target fields in metadata.\n * This function uses those fields plus the current viewer's userId to produce\n * the correct first-person variant (\"You removed Ajay\", \"Anil removed you\", etc.).\n *\n * Backward compatible: if the server does not send actorUserId (old server), the\n * raw content.text is returned unchanged so old behaviour is preserved.\n */\nexport function resolveSystemMessageText(message: Message, currentUserId: string): string {\n const m = message.metadata;\n\n if (!m?.systemOperationType || !m.actorUserId) {\n return message.content.text ?? '';\n }\n\n const iActor = m.actorUserId === currentUserId;\n const iTarget = m.targetUserId === currentUserId;\n const actor = iActor ? 'You' : (m.actorUserName ?? 'Someone');\n const target = iTarget ? 'you' : (m.targetUserName ?? 'a member');\n\n switch (m.systemOperationType) {\n case 'group_created':\n return iActor ? 'You created the group' : `${actor} created the group`;\n case 'user_added':\n return iActor ? `You added ${target}` : `${actor} added ${target}`;\n case 'user_removed':\n return iActor ? `You removed ${target}` : `${actor} removed ${target}`;\n case 'user_left':\n return iActor ? 'You left the group' : `${actor} left the group`;\n case 'admin_promoted':\n return iActor ? `You made ${target} an admin` : `${actor} made ${target} an admin`;\n case 'admin_demoted':\n return iActor ? `You removed ${target} as admin` : `${actor} removed ${target} as admin`;\n case 'group_updated':\n return iActor ? 'You updated the group' : `${actor} updated the group`;\n default:\n return message.content.text ?? '';\n }\n}\n","import { io, Socket } from 'socket.io-client';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { ReadReceiptEvent, UserStatusEvent } from '../types/index.js';\nimport { AntzChatError, AntzChatNetworkError } from '../errors.js';\nimport { detectTransitAlgo, resetAlgoCache } from '../crypto/detect.js';\nimport { fetchServerKeys, performHandshake, createRestTransitSession } from '../crypto/handshake.js';\nimport { setTransitSession, clearTransitSession, configureTransitIfUnset, isTransitEnabled, isTransitRequired, awaitTransitReadyOr, onTransitReady, getSessionKey, getTransitSession } from '../crypto/session.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getTransitHandshakePromise, ensureRestTransitHandshake } from '../api/client.js';\n\nexport type SocketStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';\nexport type StatusListener = (status: SocketStatus) => void;\n\nlet _socket: Socket | null = null;\nlet _socketProxy: Socket | null = null; // proxy returned by connectSocket — all .on() calls auto-decrypt\nlet _connectingPromise: Promise<Socket> | null = null;\nlet _status: SocketStatus = 'disconnected';\nconst _statusListeners = new Set<StatusListener>();\n\n// Stored so SocketProvider can call refreshAndReconnect() without re-passing config\nlet _getToken: (() => string | null | undefined) | null = null;\nlet _userId: string | undefined;\nlet _tenantId: string | undefined;\n// Retained so reconnectSocket() can re-run the FULL connectSocket() handshake\n// (re-establishing the transit session) instead of a bare auth-swap that would\n// drop the transit fields. See reconnectSocket() for why this matters.\nlet _config: ResolvedConfig | null = null;\nfunction setStatus(s: SocketStatus) {\n _status = s;\n _statusListeners.forEach(l => l(s));\n}\n\nexport function getSocket(): Socket {\n if (!_socketProxy) throw new Error('[AntzChat] Socket not initialized. Call connectSocket first.');\n return _socketProxy;\n}\n\n/** Returns the socket if connected, null otherwise. Use this for fire-and-forget\n * operations (markRead, typing) that should silently no-op when not yet connected. */\nexport function tryGetSocket(): Socket | null {\n return _socket?.connected ? _socketProxy : null;\n}\n\nexport function getSocketStatus(): SocketStatus {\n return _status;\n}\n\nexport function onSocketStatus(listener: StatusListener): () => void {\n _statusListeners.add(listener);\n return () => _statusListeners.delete(listener);\n}\n\n// ─── Transit helpers ──────────────────────────────────────────────────────────\n\n// How long secureEmit will wait for an in-progress handshake to (re-)establish\n// a session key before giving up. Kept under emitters.ts's ACK_TIMEOUT (5s) so a\n// caller's own ack timeout still governs the total wait.\nconst SECURE_EMIT_WAIT_MS = 4000;\n\nfunction emitEncrypted(\n socket: Socket,\n event: string,\n envelope: unknown,\n key: CryptoKey | Uint8Array,\n ack?: (response: unknown) => void,\n): void {\n if (ack) {\n socket.emit(event, envelope, async (encryptedResponse: unknown) => {\n if (isTransitEnvelope(encryptedResponse)) {\n try {\n ack(await decryptPayload(encryptedResponse, key));\n } catch {\n ack(encryptedResponse);\n }\n } else {\n ack(encryptedResponse);\n }\n });\n } else {\n socket.emit(event, envelope);\n }\n}\n\n// Wraps socket.emit. Contract:\n// • transit has a live session key → encrypt, then emit\n// • transit is REQUIRED but no key → wait for the handshake (bounded); if it\n// still fails, throw. NEVER emit plaintext.\n// • transit is genuinely disabled → emit plaintext (server doesn't want it)\nexport async function secureEmit(\n socket: Socket,\n event: string,\n payload: unknown,\n ack?: (response: unknown) => void,\n): Promise<void> {\n let key = getSessionKey();\n if (key && isTransitEnabled()) {\n emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);\n return;\n }\n\n // No usable session key. If transit is required, a plaintext emit here would\n // be a silent security downgrade (the whole point of payload encryption).\n // Give an in-flight / re-established handshake a bounded chance to land.\n if (isTransitRequired()) {\n try { ensureRestTransitHandshake(); } catch { /* REST client not wired — socket-only usage */ }\n const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);\n\n key = getSessionKey();\n if (ok && key && isTransitEnabled()) {\n emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);\n return;\n }\n\n if (isTransitRequired()) {\n // Still required, still no key — refuse rather than leak plaintext.\n throw new AntzChatNetworkError(\n `Transit encryption required but no session established — \"${event}\" not sent`,\n 'TRANSIT_NOT_READY',\n { event },\n );\n }\n // fell through: the wait returned ok because the server told us transit is\n // disabled (configureTransit(false)). Plaintext below is now correct.\n }\n\n // Transit genuinely disabled — send plaintext.\n if (ack) {\n socket.emit(event, payload, ack);\n } else {\n socket.emit(event, payload);\n }\n}\n\n// Registers an incoming event handler that auto-decrypts when transit is enabled.\nexport function secureOn(\n socket: Socket,\n event: string,\n handler: (data: unknown) => void,\n): void {\n socket.on(event, async (raw: unknown) => {\n if (isTransitEnabled() && isTransitEnvelope(raw)) {\n const key = getSessionKey();\n if (key) {\n try {\n const data = await decryptPayload(raw, key);\n handler(data);\n return;\n } catch {\n console.error(`[AntzChat] Transit decryption failed for event: ${event}`);\n return;\n }\n }\n }\n handler(raw);\n });\n}\n\n// ─── Teardown hooks ──────────────────────────────────────────────────────────\n// Lets modules that depend on this one (emitters.ts) clean up on\n// disconnectSocket() without this module importing them back.\nconst _teardownHooks = new Set<() => void>();\n\nexport function registerTeardownHook(hook: () => void): () => void {\n _teardownHooks.add(hook);\n return () => _teardownHooks.delete(hook);\n}\n\nfunction runTeardownHooks(): void {\n _teardownHooks.forEach((hook) => {\n try { hook(); } catch { /* a cleanup hook must never block teardown */ }\n });\n}\n\n// ─── Room-membership tracking ────────────────────────────────────────────────\n// join_room is fire-and-forget and the UI emits it only once per conversation\n// open (re-emitting only on an isConnected transition). A join_room dropped by\n// secureEmit during the transit-handshake gap — or lost on a plain socket\n// reconnect — would leave this socket out of the server-side room with no error\n// and no retry: new_message broadcasts for that room then silently never arrive.\n//\n// So track the rooms the client believes it is in and re-emit join for all of\n// them whenever a transit session (re-)establishes or the socket (re)connects.\n// Server-side join is idempotent; a stale room (user no longer a member) is\n// rejected server-side and the rejection is swallowed.\nconst _joinedRooms = new Set<string>();\n\nexport function trackRoomJoin(conversationId: string): void {\n _joinedRooms.add(conversationId);\n}\nexport function trackRoomLeave(conversationId: string): void {\n _joinedRooms.delete(conversationId);\n}\n/** Drop all tracked rooms. Call on a genuine identity change (user/tenant\n * switch) so the reconnect re-flush can't re-join the previous user's rooms.\n * NOT called from disconnectSocket() — a token refresh is not an intent change. */\nexport function resetTrackedRooms(): void {\n _joinedRooms.clear();\n}\n\nfunction flushJoinedRooms(): void {\n const socket = tryGetSocket();\n if (!socket || _joinedRooms.size === 0) return;\n for (const conversationId of _joinedRooms) {\n void secureEmit(socket, 'join_room', { conversationId }).catch(() => { /* retried on next ready/connect */ });\n }\n}\n\nonTransitReady(flushJoinedRooms);\nonSocketStatus((status) => { if (status === 'connected') flushJoinedRooms(); });\n\n// ─── Connect ──────────────────────────────────────────────────────────────────\n\nexport async function connectSocket(\n config: ResolvedConfig,\n getToken: () => string | null | undefined,\n): Promise<Socket> {\n if (_socket && !_socket.disconnected) return _socket;\n if (_connectingPromise) return _connectingPromise;\n\n _connectingPromise = _doConnect(config, getToken).finally(() => {\n _connectingPromise = null;\n });\n return _connectingPromise;\n}\n\nasync function _doConnect(\n config: ResolvedConfig,\n getToken: () => string | null | undefined,\n): Promise<Socket> {\n _getToken = getToken;\n _userId = config.userId;\n _tenantId = config.tenantId;\n _config = config;\n\n const token = getToken();\n\n const socketHandshakeAuth: Record<string, unknown> = {\n token: token ? `Bearer ${token}` : '',\n ...(config.userId && { userId: config.userId }),\n ...(config.tenantId && { tenantId: config.tenantId }),\n ...(config.avatar?.url && { avatarUrl: config.avatar.url }),\n ...(config.avatar?.base64 && { avatarBase64: config.avatar.base64 }),\n };\n\n // Mirror the SDK's transit intent into the shared transit state up front, so\n // isTransitRequired() is authoritative even in socket-only usage where\n // initApiClient() (the other configureTransit caller) was never invoked.\n // *IfUnset* so we never clobber an authoritative configureTransit(false) that\n // the REST handshake set after GET /crypto/pubkey reported enabled:false.\n configureTransitIfUnset(config.transitEncryption);\n\n // ── Transit handshake setup ──────────────────────────────────────────────\n let boundDeriveSessionKey: ((sessionId: string) => Promise<unknown>) | null = null;\n\n if (config.transitEncryption) {\n // ── New path: HTTPS key exchange (independent of socket) ──────────────\n // initApiClient may have already established the session via HTTPS before\n // connectSocket was called. Reuse it so we don't create a second session.\n // If initApiClient's handshake is still in-flight, wait for it so we never\n // fire two concurrent HTTPS handshakes for the same session.\n const inFlight = getTransitHandshakePromise();\n if (inFlight) await inFlight;\n\n const existingSession = getTransitSession();\n let httpsSession: { sessionId: string; sessionKey: CryptoKey | Uint8Array } | null = null;\n\n if (existingSession?.enabled && existingSession.sessionId) {\n // Session already established by initApiClient — reuse it.\n httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };\n } else {\n // initApiClient hasn't resolved yet or session was cleared — do HTTPS handshake now.\n httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (httpsSession) {\n const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : 'x25519';\n setTransitSession({ sessionKey: httpsSession.sessionKey as CryptoKey, algo, sessionId: httpsSession.sessionId, enabled: true });\n }\n }\n\n if (httpsSession) {\n // Tell the socket middleware to reuse this session instead of doing ECDH.\n socketHandshakeAuth['transitSessionId'] = httpsSession.sessionId;\n boundDeriveSessionKey = null;\n } else {\n // ── Fallback: old socket ECDH path (old server without POST /crypto/session) ──\n try {\n const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (!serverKeys.enabled) {\n throw new AntzChatError(\n 'TRANSIT_MISMATCH',\n '[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server ' +\n 'has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides.',\n false,\n { sdkEnabled: true, serverEnabled: false },\n );\n }\n const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : 'x25519';\n boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);\n } catch (err: any) {\n throw err;\n }\n }\n } else {\n // Explicitly disabled — verify server also has it off to catch config mismatches early\n try {\n const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });\n if (serverKeys.enabled) {\n throw new AntzChatError(\n 'TRANSIT_MISMATCH',\n '[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server ' +\n 'has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides.',\n false,\n { sdkEnabled: false, serverEnabled: true },\n );\n }\n } catch (err: any) {\n if (err?.code === 'TRANSIT_MISMATCH') throw err;\n // /crypto/pubkey unavailable (old server without transit support) — proceed\n }\n }\n\n _socket = io(`${config.socketOrigin}/chat`, {\n auth: socketHandshakeAuth,\n path: config.socketPath,\n transports: ['websocket', 'polling'],\n reconnection: true,\n reconnectionAttempts: 10,\n reconnectionDelay: 1000,\n reconnectionDelayMax: 5000,\n autoConnect: true,\n });\n\n setStatus('connecting');\n\n _socket.on('connect', () => setStatus('connected'));\n _socket.on('disconnect', () => {\n setStatus('disconnected');\n // Do NOT clear the transit session on a transient websocket drop. The REST\n // transit session is independent of the socket transport — wiping it here\n // made every chat REST call 403 (\"Transit encryption required\") after any\n // network blip / tab backgrounding / server redeploy until a full page\n // reload. socket.io auto-reconnect re-runs the handshake auth and the server\n // re-emits `transit_session`, which replaces the key via the persistent\n // listener registered below. The session is only torn down on an explicit\n // disconnectSocket() (teardown) or an auth change.\n });\n _socket.on('connect_error', (err) => {\n console.error('[AntzChat] Socket connect_error:', err?.message, (err as any)?.data);\n setStatus('error');\n });\n _socket.on('reconnecting', () => setStatus('reconnecting'));\n _socket.on('reconnect', () => setStatus('connected'));\n\n // ── Transit session key derivation ──────────────────────────────────────\n // Block connectSocket from resolving until the session key is ready.\n // This ensures all HTTP requests and socket.on() registrations that happen\n // after await connectSocket() already have an active session key.\n if (config.transitEncryption && boundDeriveSessionKey) {\n await new Promise<void>((resolve) => {\n const done = () => {\n clearTimeout(timeout);\n resolve();\n };\n\n // Safety timeout — if transit_session never arrives, stop blocking THIS\n // connect attempt, but do NOT disable transit. A timeout means \"slow / not\n // responding right now\", not \"server doesn't want transit\" — the only\n // authoritative off signal is GET /crypto/pubkey returning enabled:false,\n // which is handled earlier and throws TRANSIT_MISMATCH. Disabling transit\n // here turned a transient stall into every subsequent HTTP request AND\n // socket emit going out as plaintext for the rest of the session.\n //\n // Instead: leave transit required (REST interceptor's ensureRestTransitHandshake\n // keeps retrying with backoff; socket.io auto-reconnect re-runs the handshake\n // auth and the server re-emits transit_session), and let secureEmit /\n // waitForTransitReady keep requests gated until a session lands.\n const timeout = setTimeout(() => {\n console.error(\n '[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED — ' +\n 'chat requests are gated (not downgraded to plaintext) until the handshake ' +\n 'succeeds via REST retry or socket reconnect. Persistent failure = check ' +\n 'POST /crypto/session (rate limit / 5xx) and the socket transit middleware.',\n );\n done();\n }, 5000);\n\n _socket!.on('transit_session', async ({ sessionId }: { sessionId: string }) => {\n try {\n if (boundDeriveSessionKey) {\n const sessionKey = await boundDeriveSessionKey(sessionId) as CryptoKey;\n const algo = (socketHandshakeAuth['transitAlgo'] as 'x25519' | 'p256') ?? 'p256';\n setTransitSession({ sessionKey, algo, sessionId, enabled: true });\n }\n } catch (err: any) {\n console.error('[AntzChat] Failed to derive transit session key:', err.message);\n }\n done();\n });\n\n // Also unblock if the socket fails to connect at all\n _socket!.on('connect_error', () => done());\n });\n }\n\n // ── Application event listeners ──────────────────────────────────────────\n // Use secureOn so incoming events are auto-decrypted when transit is enabled.\n secureOn(_socket, 'read_receipt', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const e = event as ReadReceiptEvent;\n useChatStore.getState().setLastRead(e.conversationId, e.messageId, e.readAt);\n });\n });\n\n secureOn(_socket, 'user_online', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const store = useChatStore.getState();\n const e = event as UserStatusEvent;\n store.setUserOnline(e.userId);\n store.setLastSeen(e.userId, null);\n });\n });\n\n secureOn(_socket, 'user_offline', (event) => {\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n const e = event as UserStatusEvent;\n const store = useChatStore.getState();\n store.setUserOffline(e.userId);\n if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);\n });\n });\n\n // Store and return a proxy so ALL socket.on() calls — whether from\n // SocketProvider, getSocket(), or tryGetSocket() — auto-decrypt via secureOn.\n _socketProxy = createSecureSocketProxy(_socket);\n return _socketProxy;\n}\n\n// Wraps a Socket so every .on() call is transparently routed through secureOn.\n// Events that don't need decryption (transit disabled or no key) pass through unchanged.\nfunction createSecureSocketProxy(socket: Socket): Socket {\n return new Proxy(socket, {\n get(target, prop) {\n if (prop === 'on') {\n return (event: string, handler: (data: unknown) => void) => {\n // Low-level Socket.IO internal events pass through directly\n const internal = ['connect', 'disconnect', 'connect_error', 'reconnect', 'reconnecting', 'error'];\n if (internal.includes(event as string)) {\n return target.on(event as any, handler as any);\n }\n secureOn(target, event, handler);\n return socket; // for chaining\n };\n }\n const val = (target as any)[prop];\n return typeof val === 'function' ? val.bind(target) : val;\n },\n });\n}\n\nexport function disconnectSocket() {\n _connectingPromise = null;\n if (_socket) {\n _socket.disconnect();\n _socket = null;\n _socketProxy = null;\n setStatus('disconnected');\n }\n clearTransitSession();\n resetAlgoCache();\n // Teardown is a genuine intent change (unlike a token refresh), so the\n // outbound typing throttle must not carry its suppression window into the\n // next session — the first keystroke after reconnecting has to emit.\n //\n // Invoked through a registered hook rather than by importing emitters.ts:\n // emitters.ts already imports this module, and a direct import back would\n // close a cycle that ESM resolves by leaving one side's bindings\n // uninitialised at module-eval time.\n runTeardownHooks();\n // Dynamic import to match how every other store access in this module is\n // written — keeps the store out of this module's static import graph.\n import('../stores/chat.store.js').then(({ useChatStore }) => {\n useChatStore.getState().clearTypingUsers();\n });\n _getToken = null;\n _userId = undefined;\n _tenantId = undefined;\n _config = null;\n}\n\nexport function reconnectSocket(token: string, userId?: string, tenantId?: string) {\n if (!_socket) return;\n\n // Transit encryption: reconnectSocket predates transit and was never migrated.\n // It must NOT (a) drop the transit fields from _socket.auth (server rejects the\n // handshake with \"Transit encryption required\"), NOR (b) tear the session down\n // and re-establish a NEW one (server-broadcast events like user_online are\n // encrypted with the session the server still has; a fresh client session key\n // can't decrypt them → continuous \"Transit decryption failed\").\n //\n // reconnectSocket is a TOKEN-only refresh: the transit session is still valid.\n // So preserve the existing session — carry its transitSessionId forward in the\n // handshake auth and re-cycle the SAME socket, keeping the session key intact.\n //\n // IMPORTANT: socket.connect() is a NO-OP on an already-connected socket\n // (socket.io-client: `if (this.connected) return this;`). Since a token\n // refresh typically fires while the user is on a chat screen — socket\n // connected — a bare connect() here would NOT re-send the new auth. The\n // socket keeps running on the now-stale token until the server drops it, and\n // the uncontrolled auto-reconnect that follows would run on the stale token.\n //\n // Fix: force a real transport re-cycle (disconnect → connect) so the fresh\n // auth (token + transitSessionId) is actually sent and the server re-links\n // this socket to the SAME session (same key). The 'disconnect' handler no\n // longer clears the session on a transient drop, so the key we carry forward\n // survives the re-cycle.\n if (_config?.transitEncryption) {\n const existing = getTransitSession();\n if (existing?.enabled && existing.sessionId) {\n _socket.auth = {\n token: `Bearer ${token}`,\n ...(userId && { userId }),\n ...(tenantId && { tenantId }),\n transitSessionId: existing.sessionId,\n };\n if (_socket.connected) _socket.disconnect();\n _socket.connect();\n return;\n }\n // No valid cached session to preserve (e.g. it was cleared by a prior\n // disconnect). Fall back to the full connectSocket() flow, which\n // re-establishes the session and rebuilds the handshake auth. Dedupes via\n // _connectingPromise so repeated calls collapse into one connect.\n if (_getToken) {\n if (!_socket.disconnected) _socket.disconnect();\n void connectSocket(_config, _getToken);\n return;\n }\n }\n\n // Non-transit deployments: keep the original fast bare auth-swap + reconnect.\n _socket.auth = {\n token: `Bearer ${token}`,\n ...(userId && { userId }),\n ...(tenantId && { tenantId }),\n };\n _socket.connect();\n}\n\n/**\n * Updates the socket auth token from the stored getToken function and reconnects.\n * Called by SocketProvider on connect_error to handle expired-token scenarios.\n * Returns true if the token was updated, false if no token source is available.\n */\nexport function refreshSocketAuth(): boolean {\n if (!_socket || !_getToken) return false;\n const fresh = _getToken();\n if (!fresh) return false;\n _socket.auth = {\n token: `Bearer ${fresh}`,\n ...(_userId && { userId: _userId }),\n ...(_tenantId && { tenantId: _tenantId }),\n };\n return true;\n}\n","import type { SendMessagePayload, ForwardMessagePayload, ForwardAckPayload } from '../types/index.js';\nimport { getSocket, tryGetSocket, onSocketStatus, secureEmit, trackRoomJoin, trackRoomLeave, registerTeardownHook } from './socket.js';\nimport { AntzChatNetworkError } from '../errors.js';\n\ntype SendMessageWire = SendMessagePayload & { sentAt: number };\n\nconst ACK_TIMEOUT = 5000;\nconst RECONNECT_WAIT_TIMEOUT = 15000;\n\n// Per-conversation send queue for send_message — preserves ordering within a\n// conversation while allowing messages in different conversations to flow in parallel.\n// Emits are fired without awaiting the ack so a slow server response on one message\n// never blocks subsequent messages.\n//\n// Safety limits:\n// QUEUE_MAX_SIZE — hard cap on pending entries; overflow rejects immediately\n// QUEUE_ENTRY_TTL — max time an entry may wait in the queue before being\n// dropped (before it even reaches the socket), so stale\n// messages never arrive at the server minutes later\nconst QUEUE_MAX_SIZE = 100;\nconst QUEUE_ENTRY_TTL = 30_000; // 30 s\n\ntype QueueEntry = {\n payload: SendMessageWire;\n resolve: (v: unknown) => void;\n reject: (e: unknown) => void;\n enqueuedAt: number;\n};\n\nconst sendQueues = new Map<string, QueueEntry[]>();\n\n// Leading-throttle window for outbound typing:true, per conversation.\n// See socketEmit.typing for the full rationale.\nconst TYPING_THROTTLE_MS = 3_000;\n\n/** conversationId → timestamp of the last typing:true actually emitted. */\nconst _lastTypingSentAt = new Map<string, number>();\n\n/** Clears the typing throttle state. Call on teardown/identity change so a new\n * session cannot inherit the previous one's suppression window. */\nexport function resetTypingThrottle(): void {\n _lastTypingSentAt.clear();\n}\n\n// Cleared on disconnectSocket() teardown. Registered here (rather than called\n// from socket.ts) so socket.ts never has to import this module back.\nregisterTeardownHook(resetTypingThrottle);\nconst sendQueueRunning = new Map<string, boolean>();\n\nasync function drainSendQueue(conversationId: string) {\n if (sendQueueRunning.get(conversationId)) return;\n sendQueueRunning.set(conversationId, true);\n const queue = sendQueues.get(conversationId)!;\n while (queue.length > 0) {\n const entry = queue.shift()!;\n if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {\n entry.reject(new AntzChatNetworkError('Message dropped: queued too long', 'MESSAGE_DROPPED'));\n continue;\n }\n // Await only the EMIT step (transit wait + encryption + socket.emit), not the\n // server ack. This is the ordering guarantee the per-conversation queue\n // exists for: message N's packet is on the wire before message N+1's\n // encryption starts. The ack is awaited out-of-band (below) so a slow server\n // RTT on one message still never blocks the next one.\n let acked: Promise<unknown>;\n try {\n ({ acked } = await emitWithAck('send_message', entry.payload));\n } catch (err) {\n // Phase-1 failure (no socket / reconnect timed out / transit not ready).\n // These conditions apply equally to every still-queued message, so fail\n // the whole remaining batch now rather than make each one re-wait the\n // full reconnect/transit timeout in series. The user retries from the UI.\n entry.reject(err);\n while (queue.length > 0) queue.shift()!.reject(err);\n break;\n }\n acked.then(entry.resolve).catch(entry.reject);\n }\n sendQueues.delete(conversationId);\n sendQueueRunning.delete(conversationId);\n}\n\nfunction queueSendMessage(payload: SendMessageWire): Promise<unknown> {\n const conversationId = payload.conversationId;\n if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);\n const queue = sendQueues.get(conversationId)!;\n if (queue.length >= QUEUE_MAX_SIZE) {\n return Promise.reject(new AntzChatNetworkError('Send queue full: too many messages in flight', 'SEND_QUEUE_FULL', { conversationId }));\n }\n return new Promise((resolve, reject) => {\n queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });\n drainSendQueue(conversationId);\n });\n}\n\nfunction waitForReconnect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n unsubscribe();\n reject(new AntzChatNetworkError('Socket reconnect timeout', 'SOCKET_TIMEOUT'));\n }, RECONNECT_WAIT_TIMEOUT);\n\n const unsubscribe = onSocketStatus((status) => {\n if (status === 'connected') {\n clearTimeout(timer);\n unsubscribe();\n resolve();\n } else if (status === 'error') {\n clearTimeout(timer);\n unsubscribe();\n reject(new AntzChatNetworkError('Socket reconnect failed', 'SOCKET_NOT_CONNECTED'));\n }\n });\n });\n}\n\n// emitWithAck: the two phases of an ack'd emit, separated so callers that need\n// ordering (the send queue) can await phase 1 without holding up on phase 2.\n// Phase 1 (awaited return): reconnect wait → secureEmit (transit wait +\n// encryption + socket.emit). Resolves once the packet is on the wire.\n// Phase 2 (the returned promise): the server ack, bounded by ACK_TIMEOUT which\n// only starts now, so the full budget covers server RTT.\n// Returns a wrapper object (NOT a bare Promise — `await` would recursively\n// unwrap that and collapse the two phases back into one).\nasync function emitWithAck<T>(event: string, payload: unknown): Promise<{ acked: Promise<T> }> {\n let socket = tryGetSocket();\n if (!socket) {\n await waitForReconnect();\n socket = tryGetSocket();\n }\n if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, 'SOCKET_NOT_CONNECTED', { event });\n\n let resolveAck!: (v: T) => void;\n let rejectAck!: (e: unknown) => void;\n const acked = new Promise<T>((res, rej) => { resolveAck = res; rejectAck = rej; });\n\n // A throw here (e.g. TRANSIT_NOT_READY) propagates out of emitWithAck as a\n // phase-1 failure — the packet never went out.\n await secureEmit(socket, event, payload, (response) => resolveAck(response as T));\n\n const timer = setTimeout(\n () => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, 'SOCKET_TIMEOUT', { event })),\n ACK_TIMEOUT,\n );\n void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));\n return { acked };\n}\n\n// withAck: ack'd emit where the caller only wants the final result and does not\n// care about the phase split (reactions, pin, delete, forward, …).\nasync function withAck<T>(event: string, payload: unknown): Promise<T> {\n const { acked } = await emitWithAck<T>(event, payload);\n return acked;\n}\n\n// fireAndForget: best-effort — silently no-ops when not connected, and swallows\n// a transit-not-ready rejection from secureEmit. Safe for genuinely self-healing\n// events (typing is debounced and re-sent; mark_read is re-sent on every new\n// message and on reconnect). join_room additionally relies on socket.ts's\n// room-membership tracking (trackRoomJoin) to be re-sent after a transit gap or\n// reconnect — see there.\nfunction fireAndForget(event: string, payload: unknown): void {\n const socket = tryGetSocket();\n if (!socket) return;\n secureEmit(socket, event, payload).catch(() => { /* best-effort */ });\n}\n\nexport const socketEmit = {\n joinRoom(conversationId: string) {\n trackRoomJoin(conversationId);\n fireAndForget('join_room', { conversationId });\n },\n\n leaveRoom(conversationId: string) {\n trackRoomLeave(conversationId);\n fireAndForget('leave_room', { conversationId });\n },\n\n sendMessage(payload: SendMessagePayload): Promise<unknown> {\n return queueSendMessage({ ...payload, sentAt: Date.now() });\n },\n\n // Not queued like sendMessage — forward targets are independent conversations,\n // not the single conversation ordering that queueSendMessage protects, and each\n // target's own new_message/push already fires server-side via MessagesService.forward()'s\n // internal create() calls, so there's nothing here that needs in-order draining.\n forwardMessage(payload: ForwardMessagePayload): Promise<ForwardAckPayload> {\n return withAck('forward_message', payload);\n },\n\n updateMessage(messageId: string, text: string): Promise<unknown> {\n return withAck('update_message', { messageId, text });\n },\n\n deleteMessage(messageId: string): Promise<unknown> {\n return withAck('delete_message', { messageId });\n },\n\n deleteMessageForMe(messageId: string): Promise<unknown> {\n return withAck('delete_message_for_me', { messageId });\n },\n\n clearChat(conversationId: string): Promise<unknown> {\n return withAck('clear_chat_for_me', { conversationId });\n },\n\n addReaction(messageId: string, emoji: string): Promise<unknown> {\n return withAck('add_reaction', { messageId, emoji });\n },\n\n removeReaction(messageId: string, emoji: string): Promise<unknown> {\n return withAck('remove_reaction', { messageId, emoji });\n },\n\n pinMessage(messageId: string): Promise<unknown> {\n return withAck('pin_message', { messageId });\n },\n\n unpinMessage(messageId: string): Promise<unknown> {\n return withAck('unpin_message', { messageId });\n },\n\n // markRead and typing are best-effort — silently dropped if socket not ready\n //\n // Typing is additionally LEADING-THROTTLED here rather than in the UI layer,\n // so every consumer (both UI SDKs and any host calling socketEmit directly)\n // gets the same emit budget and none can bypass it.\n //\n // The composer calls this on EVERY keystroke. A 40-word message is ~200 calls\n // of which exactly one carries information: \"this person started typing\".\n // Each one previously cost a round trip plus real server work, making typing\n // the single most expensive event in the system per unit of user value.\n //\n // Shape of the throttle:\n // isTyping:true — passed through at most once per TYPING_THROTTLE_MS per\n // conversation. A continuous typist emits ~20/min instead\n // of ~200/min.\n // isTyping:false — ALWAYS passed through, and resets the window. It is the\n // edge that clears the indicator on every peer, it is\n // already debounced by the composer, and dropping it is\n // exactly the failure that leaves \"is typing…\" stuck.\n //\n // The server refreshes its typing key on each true edge (10s TTL) and\n // receivers expire their own indicators after TYPING_EXPIRY_MS, both of which\n // are comfortably longer than TYPING_THROTTLE_MS — so a throttled-away event\n // never lets an indicator lapse mid-typing.\n typing(conversationId: string, isTyping: boolean) {\n if (!isTyping) {\n _lastTypingSentAt.delete(conversationId);\n fireAndForget('typing', { conversationId, isTyping: false });\n return;\n }\n\n const now = Date.now();\n const last = _lastTypingSentAt.get(conversationId) ?? 0;\n if (now - last < TYPING_THROTTLE_MS) return;\n\n _lastTypingSentAt.set(conversationId, now);\n fireAndForget('typing', { conversationId, isTyping: true });\n },\n\n markRead(conversationId: string, messageId?: string) {\n fireAndForget('mark_read', { conversationId, ...(messageId ? { messageId } : {}) });\n },\n\n getOnlineUsers(userIds: string[]): Promise<string[]> {\n const socket = tryGetSocket();\n if (!socket) return Promise.resolve([]);\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n secureEmit(socket, 'get_online_users', { userIds }, (response) => {\n clearTimeout(timer);\n if (response && typeof response === 'object' && 'onlineStatus' in (response as object)) {\n const status = (response as { onlineStatus: Record<string, boolean> }).onlineStatus;\n resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));\n } else if (Array.isArray(response)) {\n resolve(response as string[]);\n } else {\n resolve([]);\n }\n }).then(() => {\n timer = setTimeout(() => reject(new AntzChatNetworkError('Socket ack timeout: get_online_users', 'SOCKET_TIMEOUT', { event: 'get_online_users' })), ACK_TIMEOUT);\n }).catch(reject);\n });\n },\n\n getTypingUsers(conversationId: string): Promise<unknown> {\n return withAck('get_typing_users', { conversationId });\n },\n};\n","import type {\n Message,\n CursorPaginatedResponse,\n PaginatedResponse,\n SendMessagePayload,\n MessageReceiptsResponse,\n MessageReactionsResponse,\n} from '../types/index.js';\nimport { getApiClient } from './client.js';\nimport { getAuthStore } from '../stores/auth.store.js';\nimport { resolveSystemMessageText } from '../utils/resolveSystemMessageText.js';\nimport { generateUUID } from '../crypto/uuid.js';\nimport { socketEmit } from '../socket/emitters.js';\nimport { tryGetSocket } from '../socket/socket.js';\n\nexport interface ListMessagesParams {\n cursor?: string;\n limit?: number;\n direction?: 'before' | 'after';\n}\n\nexport interface SearchParams {\n query: string;\n conversationId?: string;\n page?: number;\n limit?: number;\n}\n\nexport interface SendData {\n text?: string;\n attachments?: SendMessagePayload['attachments'];\n replyTo?: string;\n /**\n * Client-generated idempotency key. Generate one per send attempt and reuse the\n * SAME value if you retry this exact send (e.g. after a client-side timeout) —\n * never mint a new one for a retry. The server recognizes a matching\n * (conversationId, senderId, tempId) and returns the already-created message\n * instead of creating a duplicate. Unlike `messagesApi.forward()`, this is NOT\n * auto-generated if omitted — omitting it (or generating a fresh one on every\n * retry) means no dedup protection.\n */\n tempId?: string;\n}\n\n/** Matches the server-enforced cap in messages/dto/create-message.dto.ts (MAX_FORWARD_TARGETS). */\nexport const MAX_FORWARD_TARGETS = 5;\n\n/**\n * Matches the server's HIGHLY_FORWARDED_DEPTH_THRESHOLD in messages.service.ts — the\n * same forwardDepth value that triggers the server's fan-out-to-1 cap. Also used as\n * the \"Forwarded many times\" label threshold so the label and the cap agree on what\n * \"highly forwarded\" means; do not use a different number for the label alone.\n */\nexport const HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;\n\nexport interface ForwardResult {\n conversationId: string;\n success: boolean;\n message?: Message;\n error?: string;\n}\n\nexport const messagesApi = {\n async list(conversationId: string, params: ListMessagesParams = {}): Promise<CursorPaginatedResponse<Message>> {\n const { cursor, direction, ...rest } = params;\n const serverParams: Record<string, unknown> = { ...rest };\n if (cursor) {\n serverParams[direction === 'after' ? 'after' : 'before'] = cursor;\n }\n const { data } = await getApiClient().get<CursorPaginatedResponse<Message>>(\n `/conversations/${conversationId}/messages`,\n { params: serverParams },\n );\n const currentUserId = getAuthStore().useAuthStore.getState().user?.id;\n if (!currentUserId) return data;\n return {\n ...data,\n data: data.data.map(m =>\n m.content.type === 'system'\n ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } }\n : m,\n ),\n };\n },\n\n async get(messageId: string): Promise<Message> {\n const { data } = await getApiClient().get<Message>(`/messages/${messageId}`);\n return data;\n },\n\n /**\n * Sends a message via REST. For real-time delivery use `socketEmit.sendMessage`\n * instead — this is a lower-level entry point (used e.g. by the socket path's\n * REST-mirror flows). Pass `payload.tempId` and reuse the SAME value on retry to\n * make a retry-after-timeout safe — see `SendData.tempId`.\n */\n async send(conversationId: string, payload: SendData): Promise<Message> {\n const { data } = await getApiClient().post<Message>(\n `/conversations/${conversationId}/messages`,\n payload,\n );\n return data;\n },\n\n async update(messageId: string, text: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/update`, { text });\n return data;\n },\n\n async delete(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/delete`);\n },\n\n async deleteForMe(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/delete-for-me`);\n },\n\n async addReaction(messageId: string, emoji: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/reactions`, { emoji });\n return data;\n },\n\n async removeReaction(messageId: string, emoji: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(\n `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`,\n );\n return data;\n },\n\n async getReactions(messageId: string): Promise<MessageReactionsResponse> {\n const { data } = await getApiClient().post<MessageReactionsResponse>(`/messages/${messageId}/reactions/list`);\n return data;\n },\n\n async star(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/star`);\n },\n\n async unstar(messageId: string): Promise<void> {\n await getApiClient().post(`/messages/${messageId}/unstar`);\n },\n\n async getStarred(params: { page?: number; limit?: number; conversationId?: string } = {}): Promise<PaginatedResponse<Message>> {\n const { data } = await getApiClient().get<PaginatedResponse<Message>>('/messages/starred', { params });\n return data;\n },\n\n async search(params: SearchParams): Promise<PaginatedResponse<Message>> {\n const { data } = await getApiClient().get<PaginatedResponse<Message>>('/messages/search', { params });\n return data;\n },\n\n async getLastRead(conversationId: string): Promise<{ lastReadMessageId: string | null; lastReadAt: string | null }> {\n const { data } = await getApiClient().get<{ lastReadMessageId: string | null; lastReadAt: string | null }>(\n `/conversations/${conversationId}/read-receipt`,\n );\n return data;\n },\n\n async markAsRead(conversationId: string, messageId?: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});\n },\n\n async pin(messageId: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/pin`);\n return data;\n },\n\n async unpin(messageId: string): Promise<Message> {\n const { data } = await getApiClient().post<Message>(`/messages/${messageId}/unpin`);\n return data;\n },\n\n async getPinned(conversationId: string): Promise<Message[]> {\n const { data } = await getApiClient().get<Message[]>(`/conversations/${conversationId}/pinned-messages`);\n return data;\n },\n\n async getReceipts(messageId: string): Promise<MessageReceiptsResponse> {\n const { data } = await getApiClient().get<MessageReceiptsResponse>(`/messages/${messageId}/receipts`);\n return data;\n },\n\n /**\n * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS\n * per call, also enforced server-side). Each target is independent — one failing\n * (e.g. no longer a participant) does not block the others; check `success`/`error`\n * per entry in the returned array.\n *\n * Pass `attachmentIds` to forward only a subset of the source message's attachments\n * (e.g. one image out of a multi-image message) — omit it to forward the whole\n * message, including all its attachments, unchanged. An ID not present on the\n * source message is ignored server-side.\n *\n * Idempotency: pass `tempId` — generate ONE value per forward action (e.g. via\n * `generateUUID` from '@antzsoft/chat-core/internal') and reuse that SAME value if\n * you retry this exact forward (e.g. after a client-side timeout). The server then\n * recognizes the retry per target and returns the already-created message instead\n * of creating a duplicate. Never mint a new tempId for a retry — only when the user\n * initiates a genuinely new forward action. If omitted, a fresh one is generated\n * per call, which means a retry without an explicit tempId gets NO dedup protection.\n *\n * Transport: uses the 'forward_message' socket event when a socket is connected\n * (lower latency, same server-side MessagesService.forward() path, so broadcast/\n * push notification behavior is identical either way) and transparently falls back\n * to the REST endpoint when no socket is available.\n */\n async forward(messageId: string, targetConversationIds: string[], attachmentIds?: string[], tempId?: string): Promise<ForwardResult[]> {\n const resolvedTempId = tempId ?? generateUUID();\n\n if (tryGetSocket()) {\n const ack = await socketEmit.forwardMessage({\n messageId,\n targetConversationIds,\n ...(attachmentIds ? { attachmentIds } : {}),\n tempId: resolvedTempId,\n });\n if (ack.error) throw new Error(ack.error);\n return ack.results;\n }\n\n const { data } = await getApiClient().post<ForwardResult[]>(\n `/messages/${messageId}/forward`,\n { targetConversationIds, ...(attachmentIds ? { attachmentIds } : {}), tempId: resolvedTempId },\n );\n return data;\n },\n};\n","import type { Conversation, ConversationListParams, ConversationUnreadCount, DeliveryStatus, Message, MessageStatus, Participant, PaginatedResponse, UnreadSummary, User } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\nfunction normalizeParticipant(p: any): Participant {\n const hasUserDetails = p.displayName || p.username || p.avatarUrl;\n return {\n userId: p.userId,\n externalId: p.externalId ?? p.user?.externalId,\n role: p.role,\n joinedAt: p.joinedAt,\n isActive: p.isActive,\n user: hasUserDetails\n ? {\n id: p.userId,\n externalId: p.externalId,\n tenantId: '',\n email: '',\n username: p.username ?? '',\n displayName: p.displayName ?? p.username ?? '',\n avatarUrl: p.avatarUrl,\n status: 'offline' as const,\n createdAt: p.joinedAt ?? '',\n updatedAt: p.joinedAt ?? '',\n }\n : (p.user as User | undefined),\n };\n}\n\nfunction normalizeLastMessage(lastMsg: any): Message | undefined {\n if (!lastMsg) return undefined;\n if (lastMsg.content !== undefined) return lastMsg as Message;\n return {\n id: lastMsg.messageId ?? '',\n tenantId: '',\n conversationId: '',\n senderId: lastMsg.senderId ?? '',\n content: {\n type: lastMsg.hasAttachments ? 'attachment' : 'text',\n text: lastMsg.contentPreview,\n },\n reactions: [],\n lastReaction: lastMsg.lastReaction ?? null,\n status: (lastMsg.status ?? 'active') as MessageStatus,\n deliveryStatus: (lastMsg.deliveryStatus ?? 'sent') as DeliveryStatus,\n isEdited: false,\n sentAt: lastMsg.sentAt ?? '',\n createdAt: lastMsg.sentAt ?? '',\n ...(lastMsg.senderName && { senderName: lastMsg.senderName }),\n ...(lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }),\n };\n}\n\nexport function normalizeConversation(conv: any): Conversation {\n return {\n ...conv,\n id: conv.id ?? conv.conversationId,\n participants: (conv.participants ?? []).map(normalizeParticipant),\n lastMessage: normalizeLastMessage(conv.lastMessage),\n };\n}\n\nexport interface CreateGroupData {\n name: string;\n description?: string;\n participantIds: string[];\n}\n\nexport interface CreateDirectData {\n userId: string;\n}\n\nexport interface UpdateConversationData {\n name?: string;\n description?: string;\n}\n\nexport const conversationsApi = {\n async list(params: ConversationListParams = {}): Promise<PaginatedResponse<Conversation>> {\n const { data } = await getApiClient().get<PaginatedResponse<Conversation>>('/conversations', { params });\n return { ...data, data: data.data.map(normalizeConversation) };\n },\n\n async get(conversationId: string): Promise<Conversation> {\n const { data } = await getApiClient().get<Conversation>(`/conversations/${conversationId}`);\n return normalizeConversation(data);\n },\n\n async createGroup(payload: CreateGroupData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>('/conversations', payload);\n return normalizeConversation(data);\n },\n\n async createDirect(payload: CreateDirectData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>('/conversations/direct', payload);\n return normalizeConversation(data);\n },\n\n async update(conversationId: string, payload: UpdateConversationData): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(`/conversations/${conversationId}/update`, payload);\n return normalizeConversation(data);\n },\n\n async delete(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/delete`);\n },\n\n async addParticipants(conversationId: string, userIds: string[], role?: 'admin' | 'member'): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants`,\n { userIds, ...(role && { role }) },\n );\n return normalizeConversation(data);\n },\n\n async removeParticipant(conversationId: string, userId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants/${userId}/remove`,\n );\n return normalizeConversation(data);\n },\n\n async updateParticipantRole(conversationId: string, userId: string, role: 'admin' | 'member'): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/participants/${userId}/role`,\n { role },\n );\n return normalizeConversation(data);\n },\n\n async mute(conversationId: string, mutedUntil?: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});\n },\n\n async unmute(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unmute`);\n },\n\n async pin(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/pin`);\n },\n\n async unpin(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unpin`);\n },\n\n async markUnread(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unread`);\n },\n\n async markRead(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/unread/clear`);\n },\n\n async leave(conversationId: string, andDelete?: boolean): Promise<void> {\n const url = andDelete\n ? `/conversations/${conversationId}/leave?delete=true`\n : `/conversations/${conversationId}/leave`;\n await getApiClient().post(url);\n },\n\n async getMembers(conversationId: string, filter?: 'deleted' | 'all'): Promise<Participant[]> {\n const { data } = await getApiClient().get<Participant[]>(\n `/conversations/${conversationId}/participants`,\n filter ? { params: { filter } } : undefined,\n );\n // Normalize so display-name fields arrive consistently under `user` — the raw\n // endpoint may return flat fields (displayName/username/avatarUrl). This makes\n // getMembers a drop-in name source for the @mention picker without each\n // consumer re-normalizing.\n return (data ?? []).map(normalizeParticipant);\n },\n\n /**\n * Get unread message count for a single conversation.\n * Use this after app foreground or socket reconnect to refresh a specific count.\n */\n async getUnreadCount(conversationId: string): Promise<ConversationUnreadCount> {\n const { data } = await getApiClient().get<ConversationUnreadCount>(\n `/conversations/${conversationId}/unread`,\n );\n return data;\n },\n\n /**\n * Get total unread count across all conversations + per-conversation breakdown.\n * Use on app cold start, foreground resume, or after socket reconnect.\n * The socket keeps counts live while connected — this is the source of truth\n * when the socket was down.\n */\n async getUnreadSummary(): Promise<UnreadSummary> {\n const { data } = await getApiClient().get<UnreadSummary>('/conversations/unread');\n return data;\n },\n\n /**\n * Set the group icon from an already-uploaded file (admin only).\n * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.\n * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.\n */\n async uploadIcon(conversationId: string, fileId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(\n `/conversations/${conversationId}/icon`,\n { fileId },\n );\n return normalizeConversation(data);\n },\n\n async removeIcon(conversationId: string): Promise<Conversation> {\n const { data } = await getApiClient().post<Conversation>(`/conversations/${conversationId}/icon/remove`);\n return normalizeConversation(data);\n },\n\n async clearChat(conversationId: string): Promise<void> {\n await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);\n },\n\n};\n","import { getApiClient, TRANSIT_OPTIONAL_HEADER } from './client.js';\n\n// ─── Types ─────────────────────────────────────────────────────────────────\n\n/** Shared fields for every device registration. */\ninterface DeviceTokenBase {\n /**\n * Client-generated stable UUID for this device.\n * Store this in localStorage / SecureStore / Keychain and reuse it on every\n * app start so the same record is updated rather than duplicated.\n */\n deviceId: string;\n platform: 'ios' | 'android' | 'web';\n provider: 'expo' | 'fcm' | 'apns' | 'web-push';\n userAgent?: string;\n}\n\n/** Mobile push registration (expo | fcm | apns). */\nexport interface MobileDeviceToken extends DeviceTokenBase {\n provider: 'expo' | 'fcm' | 'apns';\n /** FCM registration token / APNs device token / Expo push token. */\n token: string;\n endpoint?: never;\n p256dh?: never;\n auth?: never;\n}\n\n/** Web push (VAPID) registration. */\nexport interface WebPushDeviceToken extends DeviceTokenBase {\n provider: 'web-push';\n /** PushSubscription.endpoint */\n endpoint: string;\n /** base64url-encoded PushSubscription key 'p256dh' */\n p256dh: string;\n /** base64url-encoded PushSubscription key 'auth' */\n auth: string;\n token?: never;\n}\n\nexport type RegisterDeviceTokenPayload = MobileDeviceToken | WebPushDeviceToken;\n\n// ─── API ───────────────────────────────────────────────────────────────────\n\nexport const devicesApi = {\n /**\n * Register or update a device push token with the chat server.\n *\n * Upserts by `deviceId` — calling this multiple times with the same deviceId\n * simply refreshes the token value (tokens can rotate silently on some platforms).\n *\n * The SDK never calls this automatically. The parent app or the `tokenProvider`\n * option in `pushNotifications` config is responsible for calling it after\n * obtaining the token from the OS / browser.\n */\n async register(payload: RegisterDeviceTokenPayload): Promise<void> {\n await getApiClient().post('/users/me/devices', payload);\n },\n\n /**\n * Remove a device token from the chat server.\n * Call this on logout so the user stops receiving push notifications on this device.\n */\n async remove(deviceId: string): Promise<void> {\n // Runs during logout, so it must not block on the transit handshake.\n // Empty body; deviceId is in the URL. See TRANSIT_OPTIONAL_HEADER.\n await getApiClient().post(`/users/me/devices/${deviceId}/remove`, {}, {\n headers: { [TRANSIT_OPTIONAL_HEADER]: '1' },\n });\n },\n};\n","import type { PaginatedResponse, User, UserPreferences } from '../types/index.js';\nimport { getApiClient } from './client.js';\n\n/**\n * Fields for updating the current user's profile.\n *\n * Works in both builtin and non-builtin (external/SSO/WSO2) modes.\n *\n * In non-builtin modes, firstName/lastName/email/displayName/phone are also\n * synced from the upstream user-service (on socket connect if stale, and every\n * 2 hours via cron). Calling updateProfile() is the way to push an immediate\n * update to the chat server without waiting for the next sync cycle — useful\n * when the host app knows a profile change just happened in the identity provider.\n *\n * Uniqueness constraints: email and phone must each be unique within the tenant.\n * A 409 is returned if another user already holds the same email or phone.\n *\n * username and status are intentionally excluded — username is a system\n * identifier and status is lifecycle-managed by user-service.\n */\nexport interface UpdateProfilePayload {\n firstName?: string;\n lastName?: string;\n email?: string;\n displayName?: string;\n phone?: string;\n}\n\nexport const usersApi = {\n async list(params: { query?: string; page?: number; limit?: number } = {}): Promise<PaginatedResponse<User>> {\n const { data } = await getApiClient().get<PaginatedResponse<User>>('/users', { params });\n return data;\n },\n\n async getById(userId: string): Promise<User> {\n const { data } = await getApiClient().get<User>(`/users/${userId}`);\n return data;\n },\n\n async getLastSeen(userId: string): Promise<{ lastSeenAt: string | null }> {\n const { data } = await getApiClient().get<User>(`/users/${userId}`);\n return { lastSeenAt: data.lastSeenAt ?? null };\n },\n\n /**\n * Update basic profile fields for the current user.\n * Works in both builtin and non-builtin modes. Use this to push an immediate\n * profile update to the chat server when the host app knows a change just\n * happened — without waiting for the next 2-hour sync cycle.\n */\n async updateProfile(payload: UpdateProfilePayload): Promise<User> {\n const { data } = await getApiClient().post<User>('/users/me/update', payload);\n return data;\n },\n\n /**\n * Update notification preferences for the current user.\n * Partial update — only send fields you want to change.\n * A prefs record is automatically created with defaults when a device\n * token is first registered, so this never fails with \"not found\".\n */\n async updatePreferences(prefs: UserPreferences): Promise<User> {\n const { data } = await getApiClient().post<User>('/users/me/preferences', prefs);\n return data;\n },\n\n /**\n * Fetch current notification preferences for the current user.\n * Returns null if no prefs record exists yet (all defaults apply).\n */\n async getPreferences(): Promise<UserPreferences | null> {\n try {\n const { data } = await getApiClient().get<UserPreferences>('/users/me/preferences');\n return data;\n } catch {\n return null;\n }\n },\n};\n","// ─── Primitives ───────────────────────────────────────────────────────────────\n\nexport type FileType = 'image' | 'video' | 'audio' | 'document';\nexport type ConversationType = 'direct' | 'group';\nexport type MessageStatus = 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';\nexport type DeliveryStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';\nexport type UserStatus = 'online' | 'offline' | 'away';\nexport type ParticipantRole = 'admin' | 'member';\n\n// ─── Upload ───────────────────────────────────────────────────────────────────\n\n/**\n * Platform-agnostic file descriptor.\n * - Web: built from a browser File object ({ uri: URL.createObjectURL(f), name: f.name, type: f.type, size: f.size })\n * - React Native: built from document/image picker result ({ uri: result.uri, name: result.name, type: result.mimeType, size: result.size })\n */\nexport interface UploadableFile {\n /** Local URI — blob URL on web, file:// URI on RN */\n uri: string;\n name: string;\n /** MIME type, e.g. \"image/jpeg\" */\n type: string;\n size: number;\n}\n\n// ─── User ─────────────────────────────────────────────────────────────────────\n\nexport interface User {\n id: string;\n externalId?: string; // external system user ID (non-builtin modes only)\n tenantId: string;\n email: string;\n username: string;\n firstName?: string;\n lastName?: string;\n displayName: string;\n avatarUrl?: string;\n phone?: string;\n status: UserStatus;\n lastSeenAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\n// ─── Auth ─────────────────────────────────────────────────────────────────────\n\nexport interface AuthTokens {\n accessToken: string;\n refreshToken: string;\n tokenType: string;\n expiresIn: number;\n}\n\nexport interface AuthResponse {\n user: User;\n tokens: AuthTokens;\n}\n\nexport interface LoginCredentials {\n email: string;\n password: string;\n}\n\nexport interface RegisterData {\n email: string;\n password: string;\n username: string;\n firstName: string;\n lastName: string;\n displayName?: string;\n phone?: string;\n tenantId?: string;\n}\n\n// ─── Attachment ───────────────────────────────────────────────────────────────\n\nexport interface Attachment {\n id: string;\n slotId?: string;\n type: FileType;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n dimensions?: { width: number; height: number };\n duration?: number;\n isUploading?: boolean;\n uploadProgress?: number;\n}\n\n\n// ─── Message ──────────────────────────────────────────────────────────────────\n\nexport interface MessageMetadata {\n type: string;\n hasAttachments: boolean;\n containsLink: boolean;\n systemOperationType?: string;\n actorUserId?: string;\n actorUserName?: string;\n targetUserId?: string;\n targetUserName?: string;\n}\n\nexport interface MessageReaction {\n emoji: string;\n userIds: string[];\n count: number;\n}\n\nexport interface MessageContent {\n text?: string;\n type: 'text' | 'attachment' | 'system';\n attachments?: Attachment[];\n}\n\n/**\n * A @mention of a user in a group message.\n *\n * The mention is stored inline in `content.text` as a self-describing token\n * `@[DisplayName](userId)` (and, for @all, `@[all](all)`). This flat array is a\n * denormalized copy of the mentioned userIds, used for notification fan-out and\n * \"who was mentioned\" lookups — it is NOT a positional/offset map.\n *\n * The reserved id `\"all\"` represents an @all mention (server resolves it to every\n * active participant at send time).\n */\nexport const MENTION_ALL_ID = 'all';\n\nexport interface ReplyAttachmentSnapshot {\n type: FileType;\n filename: string;\n mimeType: string;\n size: number;\n duration?: number;\n dimensions?: { width: number; height: number };\n url: string;\n}\n\nexport interface MessageReplyReference {\n messageId?: string;\n contentPreview?: string;\n senderName?: string;\n attachmentSnapshot?: ReplyAttachmentSnapshot;\n // Present on optimistic messages before server confirmation\n id?: string;\n content?: MessageContent;\n sender?: Pick<User, 'displayName' | 'avatarUrl'>;\n}\n\n/**\n * Present on a message that was created via forwarding. Unlike `MessageReplyReference`,\n * this does NOT carry a content preview — the forwarded message's own `content` already\n * holds the text/attachments, so the UI only needs this to render a \"Forwarded\" label.\n *\n * `originalMessageId`/`originalConversationId`/`originalSenderId` always point one hop\n * back (the message actually forwarded), never the root of a longer forward chain — the\n * true original author/conversation is intentionally not reconstructable after more than\n * one hop, matching WhatsApp's forwarding privacy model. `forwardDepth` still accumulates\n * across the whole chain, so \"Forwarded many times\" (depth >= HIGHLY_FORWARDED_DEPTH_THRESHOLD,\n * exported from '../api/messages.js', currently 5 — the same value that triggers the\n * server's fan-out-to-1 cap) keeps working.\n */\nexport interface MessageForwardReference {\n originalMessageId: string;\n originalConversationId: string;\n originalSenderId: string;\n forwardDepth: number;\n}\n\n/** Metadata attached to system messages by the server. */\nexport interface SystemMessageMetadata {\n type?: 'system';\n systemOperationType?: string;\n /** user_removed / user_left */\n removedUserId?: string;\n removedUserName?: string;\n /** user_added */\n addedUserId?: string;\n addedUserName?: string;\n /** admin_promoted / admin_demoted */\n targetUserId?: string;\n targetUserName?: string;\n hasAttachments?: boolean;\n containsLink?: boolean;\n [key: string]: unknown;\n}\n\nexport interface Message {\n id: string;\n tenantId: string;\n conversationId: string;\n senderId: string;\n content: MessageContent;\n replyTo?: MessageReplyReference;\n /** Present when this message was created via forwarding rather than composed directly. */\n forwardedFrom?: MessageForwardReference;\n /**\n * Flat list of mentioned userIds (denormalized from the `@[name](id)` tokens in\n * `content.text`). Contains `\"all\"` for an @all mention. Used for fan-out and\n * \"mentions me\" lookups; not a positional map.\n */\n mentions?: string[];\n reactions: MessageReaction[];\n lastReaction?: LastReaction | null;\n status: MessageStatus;\n deliveryStatus?: DeliveryStatus;\n isEdited: boolean;\n editedAt?: string;\n isStarred?: boolean;\n isPinned?: boolean;\n pinnedBy?: string;\n pinnedAt?: string;\n uploadProgress?: number;\n seq?: number;\n sentAt: string;\n createdAt: string;\n sender?: User;\n senderName?: string;\n /** Present on system messages (content.type === 'system'). */\n metadata?: SystemMessageMetadata;\n readBy?: Array<{ userId: string; readAt: string }>;\n deliveredTo?: Array<{ userId: string; deliveredAt: string }>;\n}\n\n// ─── Conversation ─────────────────────────────────────────────────────────────\n\nexport interface Participant {\n userId: string;\n /** External system user ID of the chat user (non-builtin modes). Mirrors `user.externalId`. */\n externalId?: string;\n role: ParticipantRole;\n joinedAt: string;\n isActive?: boolean;\n user?: User;\n}\n\nexport interface MessageConfig {\n editWindowSeconds?: number;\n deleteWindowSeconds?: number;\n}\n\nexport interface ConversationSettings {\n onlyAdminsCanMessage?: boolean;\n onlyAdminsCanAddMembers?: boolean;\n messageConfig?: MessageConfig;\n}\n\nexport interface Conversation {\n id: string;\n tenantId: string;\n conversationType: ConversationType;\n name?: string;\n description?: string;\n iconUrl?: string;\n participants: Participant[];\n participantCount?: number;\n settings?: ConversationSettings;\n lastMessage?: Message;\n createdBy?: string;\n isActive: boolean;\n createdAt: string;\n updatedAt: string;\n unreadCount?: number;\n isPinned?: boolean;\n isMuted?: boolean;\n mutedUntil?: string;\n /** True if the current user manually marked this conversation unread (independent of unreadCount) */\n isManuallyUnread?: boolean;\n /** Last message sequence number in this conversation — use as afterSeq cursor for sync */\n lastSeq?: number;\n}\n\n// ─── App Config ───────────────────────────────────────────────────────────────\n\nexport interface AppConfig {\n maxPinnedConversations: number;\n}\n\n// ─── Unread Count ─────────────────────────────────────────────────────────────\n\nexport interface ConversationUnreadCount {\n conversationId: string;\n unreadCount: number;\n}\n\nexport interface UnreadSummary {\n /** Total unread messages across all conversations */\n totalUnread: number;\n /** Per-conversation breakdown — only includes conversations with unread > 0 */\n byConversation: ConversationUnreadCount[];\n}\n\n// ─── Conversation Filters ─────────────────────────────────────────────────────\n\nexport interface ConversationListParams {\n page?: number;\n limit?: number;\n /** Filter by conversation type */\n type?: ConversationType;\n /** Only pinned (true) or unpinned (false) conversations */\n isPinned?: boolean;\n /** Only muted (true) or unmuted (false) conversations */\n isMuted?: boolean;\n /** Only conversations with unread messages */\n hasUnread?: boolean;\n /** Search by group name / description (text index) */\n search?: string;\n /** Filter by current user's role in the conversation */\n role?: ParticipantRole;\n /** Filter by whether the last message has attachments */\n hasAttachments?: boolean;\n /** Filter by last message attachment type */\n attachmentType?: FileType;\n /** Filter by notification setting */\n notificationsEnabled?: boolean;\n}\n\n// ─── Pagination ───────────────────────────────────────────────────────────────\n\nexport interface PaginationMeta {\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n}\n\nexport interface PaginatedResponse<T> {\n data: T[];\n meta: PaginationMeta;\n}\n\nexport interface CursorPaginationMeta {\n hasMore: boolean;\n nextCursor?: string;\n prevCursor?: string;\n}\n\nexport interface CursorPaginatedResponse<T> {\n data: T[];\n meta: CursorPaginationMeta;\n}\n\n// ─── Compression ─────────────────────────────────────────────────────────────\n\nexport type CompressionAlgorithm = 'webp' | 'jpeg' | 'gzip' | 'none';\n\nexport interface CompressedFile extends UploadableFile {\n originalSize: number;\n compressed: boolean;\n compressionAlgorithm: CompressionAlgorithm;\n}\n\n// ─── Storage ──────────────────────────────────────────────────────────────────\n\nexport interface PresignedUrlRequest {\n filename: string;\n mimeType: string;\n size: number;\n conversationId?: string;\n folder?: string;\n metadata?: Record<string, unknown>;\n clientIndex?: number;\n}\n\nexport interface MultipartPartUrl {\n partNumber: number;\n uploadUrl: string;\n expiresAt: string;\n method: 'PUT' | 'POST';\n}\n\nexport interface MultipartUploadInfo {\n uploadId: string;\n storageKey: string;\n totalParts: number;\n chunkSize: number;\n partUrls: MultipartPartUrl[];\n}\n\nexport interface CompletedPart {\n partNumber: number;\n etag: string;\n}\n\nexport interface PresignedUrlResponse {\n fileId: string;\n uploadUrl: string;\n method: 'PUT' | 'POST';\n headers: Record<string, string>;\n fields?: Record<string, string>;\n expiresAt: string;\n maxSize: number;\n allowedMimeType: string;\n clientIndex?: number;\n multipart?: MultipartUploadInfo;\n}\n\nexport interface FileResponse {\n id: string;\n filename: string;\n originalFilename: string;\n mimeType: string;\n size: number;\n url: string;\n thumbnailUrl?: string;\n type: FileType;\n uploadedAt?: string;\n confirmedAt?: string;\n}\n\nexport interface UploadProgress {\n fileId: string;\n filename: string;\n progress: number;\n status: 'pending' | 'uploading' | 'confirming' | 'done' | 'error';\n error?: string;\n}\n\nexport interface BatchUploadResult {\n successful: FileResponse[];\n failed: Array<{ filename: string; error: string }>;\n}\n\n// ─── Socket Event Payloads ────────────────────────────────────────────────────\n\nexport interface NewMessageEvent {\n tempId?: string;\n senderName?: string;\n senderAvatarUrl?: string;\n message: Message;\n}\n\nexport interface ConversationUpdatedEvent {\n id: string;\n lastMessage?: Partial<Message> | null;\n unreadCount?: number;\n updatedAt?: string;\n /** Updated when a new message is sent — use to keep sync cursor up to date */\n lastSeq?: number;\n participants?: Participant[];\n}\n\nexport interface MessageUpdatedEvent {\n messageId: string;\n conversationId: string;\n text: string;\n editedAt: string;\n}\n\nexport interface MessageDeletedEvent {\n messageId: string;\n conversationId: string;\n}\n\nexport interface MessageDeletedForMeEvent {\n messageId: string;\n conversationId: string;\n}\n\nexport interface LastReaction {\n emoji: string;\n reactorId: string;\n reactorName: string;\n reactedAt: string;\n}\n\nexport interface ReactionUpdatedEvent {\n messageId: string;\n conversationId: string;\n reactions: MessageReaction[];\n lastReaction: LastReaction | null;\n}\n\nexport interface MessageStarUpdatedEvent {\n messageId: string;\n conversationId: string;\n isStarred: boolean;\n}\n\nexport interface TypingIndicatorEvent {\n conversationId: string;\n userId: string;\n /** External system user ID (non-builtin modes). Absent in builtin mode. */\n externalId?: string;\n username: string;\n displayName: string;\n avatarUrl?: string;\n isTyping: boolean;\n}\n\nexport interface UserStatusEvent {\n userId: string;\n status: UserStatus;\n lastSeenAt?: string | null;\n}\n\nexport interface ReadReceiptEvent {\n conversationId: string;\n messageId: string;\n userId: string;\n readAt: string;\n updatedMessageIds?: string[];\n fullyReadMessageIds?: string[];\n}\n\nexport interface MessageAckEvent {\n tempId: string;\n messageId: string;\n status: MessageStatus;\n /** Present when server supports seq — monotonic counter for this conversation. */\n seq?: number;\n}\n\nexport interface MessageDeliveredEvent {\n messageId: string;\n conversationId: string;\n deliveredTo: { userId: string; deliveredAt: Date };\n}\n\nexport interface MessageReceiptEntry {\n userId: string;\n /** External system user ID (non-builtin modes). */\n externalId?: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface MessageReceiptsResponse {\n messageId: string;\n readBy: Array<MessageReceiptEntry & { readAt: string }>;\n deliveredTo: Array<MessageReceiptEntry & { deliveredAt: string }>;\n}\n\nexport interface MessagesDeliveredEvent {\n conversationId: string;\n messageIds: string[];\n deliveredTo: string;\n deliveredAt: string;\n}\n\n// ─── Sync ─────────────────────────────────────────────────────────────────────\n\nexport interface SyncReadReceipt {\n messageId: string;\n conversationId: string;\n userId: string;\n readAt: string | null;\n}\n\nexport interface SyncDeliveredReceipt {\n messageId: string;\n conversationId: string;\n userId: string;\n deliveredAt: string | null;\n}\n\nexport interface SyncDeletedForMe {\n messageId: string;\n conversationId: string;\n deletedAt: string | null;\n}\n\nexport interface SyncParticipantChange {\n conversationId: string;\n userId: string;\n role: string;\n isActive: boolean;\n isMuted: boolean;\n mutedUntil: string | null;\n updatedAt: string | null;\n}\n\nexport interface SyncStarEntry {\n messageId: string;\n conversationId: string;\n /** true = starred, false = unstarred (soft-deleted) */\n isActive: boolean;\n updatedAt: string | null;\n}\n\nexport interface SyncReactionEntry {\n emoji: string;\n count: number;\n /** True when the current user has reacted with this emoji */\n didIReact: boolean;\n}\n\n/** Full current reaction state keyed by messageId. Only message IDs with activity since `since` are included. */\nexport type SyncReactions = Record<string, SyncReactionEntry[]>;\n\nexport interface CrossConversationSyncResponse {\n syncedAt: string;\n /** When true the gap exceeds 60 days — app should mark all conversations needs_refresh and lazy-sync on open */\n stale: boolean;\n messages: Message[];\n deletedForMe: SyncDeletedForMe[];\n reactions: SyncReactions;\n stars: SyncStarEntry[];\n participantChanges: SyncParticipantChange[];\n readReceipts: SyncReadReceipt[];\n deliveredReceipts: SyncDeliveredReceipt[];\n /**\n * Conversation IDs the client should remove from its local list.\n * Set when the user explicitly deleted their own copy of a conversation\n * (DM or group) via the delete action. Scoped to this user only — other\n * participants are not affected.\n */\n deletedConversationIds: string[];\n}\n\nexport interface ConversationSyncResponse {\n syncedAt: string;\n /** Cursor for the next page in the same direction — pass as afterSeq or beforeSeq matching what you sent. */\n nextSeq?: number;\n /** True when more pages are available — call again with nextSeq as the same cursor type. */\n hasMore?: boolean;\n messages: Message[];\n deletedForMe: SyncDeletedForMe[];\n reactions: SyncReactions;\n stars: SyncStarEntry[];\n participantChanges: SyncParticipantChange[];\n readReceipts: SyncReadReceipt[];\n deliveredReceipts: SyncDeliveredReceipt[];\n}\n\n// ─── Reactions ────────────────────────────────────────────────────────────────\n\nexport interface ReactionUser {\n userId: string;\n /** External system user ID (non-builtin modes). */\n externalId?: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface ReactionGroup {\n emoji: string;\n /** All user IDs who reacted with this emoji */\n userIds: string[];\n count: number;\n /** Resolved user profiles for display in \"who reacted\" sheet */\n users: ReactionUser[];\n}\n\nexport interface MessageReactionsResponse {\n messageId: string;\n reactions: ReactionGroup[];\n}\n\n// ─── Send Message ─────────────────────────────────────────────────────────────\n\nexport interface SendMessageAttachment {\n fileId: string;\n type: FileType;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n duration?: number;\n}\n\nexport interface OptimisticAttachment extends SendMessageAttachment {\n id: string;\n slotId?: string;\n url: string;\n thumbnailUrl?: string;\n filename: string;\n mimeType: string;\n size: number;\n isUploading?: boolean;\n uploadProgress?: number;\n}\n\nexport interface SendMessagePayload {\n conversationId: string;\n text?: string;\n attachments?: SendMessageAttachment[];\n replyTo?: string;\n tempId: string;\n /** Mentioned userIds (use `\"all\"` / MENTION_ALL_ID for @all). Derived from the tokens in `text`. */\n mentions?: string[];\n}\n\n// ─── Forward Message (socket transport) ───────────────────────────────────────\n\nexport interface ForwardMessagePayload {\n messageId: string;\n targetConversationIds: string[];\n /** Omit to forward the whole source message, including all its attachments, unchanged. */\n attachmentIds?: string[];\n /** Client-generated idempotency key — see ForwardData.tempId (api/messages.ts) for the full contract. */\n tempId: string;\n}\n\n/** Per-target result for a 'forward_message' socket call. Mirrors the REST ForwardResult shape. */\nexport interface ForwardResultPayload {\n conversationId: string;\n success: boolean;\n message?: Message;\n error?: string;\n}\n\n/** Ack for the 'forward_message' socket event. */\nexport interface ForwardAckPayload {\n tempId?: string;\n results: ForwardResultPayload[];\n /** Present only when the server-side forward() call itself threw (e.g. source message not found) — results is [] in that case. */\n error?: string;\n}\n\n// ── User Preferences ──────────────────────────────────────────────────────────\n\nexport interface QuietHours {\n enabled: boolean;\n /** HH:MM — e.g. \"22:00\" */\n start: string;\n /** HH:MM — e.g. \"08:00\" */\n end: string;\n /** IANA timezone — e.g. \"Asia/Kolkata\" */\n timezone: string;\n}\n\n/**\n * User notification preferences stored in chat_user_prefs.\n * All fields are optional on update — only send what changed.\n * Any future preference field is added here and to the server schema;\n * no new collections or API endpoints needed.\n */\nexport interface UserPreferences {\n /** Master switch — false disables all push notifications */\n notificationsEnabled?: boolean;\n /** Play sound with notifications */\n soundEnabled?: boolean;\n /** Show message text in notification body (false = show \"New message\" only) */\n messagePreview?: boolean;\n /** Notify when @mentioned in a group */\n notifyOnMention?: boolean;\n /** Notify when someone reacts to your message */\n notifyOnReaction?: boolean;\n /** Notify when added to a group */\n notifyOnGroupInvite?: boolean;\n /** Quiet hours window — no push delivered during this period */\n quietHours?: QuietHours;\n}\n","import { MENTION_ALL_ID } from '../types/index.js';\n\n/**\n * Mention token utilities — the shared logic every consumer (web SDK, RN SDK,\n * core-only apps) reuses so name-source is the only thing that differs per app.\n *\n * Storage model:\n * - Each mention is a self-describing token in the message text:\n * `@[DisplayName](userId)` (and `@[all](all)` for @all)\n * - The message also carries a flat `mentions: string[]` of userIds, derived\n * from these tokens. It is a denormalized fan-out list, not a position map.\n *\n * The DisplayName inside the token is a FALLBACK only — consumers should resolve\n * the current name from their user directory and use the token name only when the\n * id cannot be resolved (deleted user, or a departed member with no directory).\n */\n\n/**\n * Matches `@[Display Name](userId)`.\n * group 1 = display name (any char except the closing `]`)\n * group 2 = userId (24-hex ObjectId, or the literal \"all\")\n * The `g` flag is required for iterating all matches; callers must not share a\n * single compiled instance across reentrant calls (each function builds its own).\n */\nconst MENTION_TOKEN_SOURCE = '@\\\\[([^\\\\]]+)\\\\]\\\\((all|[a-fA-F0-9]{24})\\\\)';\n\nexport interface ParsedMention {\n /** userId, or \"all\" */\n id: string;\n /** display name captured from the token (fallback for rendering) */\n displayName: string;\n /** start index of the whole token in the text (UTF-16) */\n start: number;\n /** end index (exclusive) of the whole token in the text (UTF-16) */\n end: number;\n}\n\n/** A segment of a message: either plain text or a mention to render specially. */\nexport type MentionPart =\n | { type: 'text'; text: string }\n | { type: 'mention'; id: string; displayName: string };\n\n/**\n * A piece used to compose outgoing text. Plain strings pass through verbatim;\n * mention objects are serialized to `@[displayName](id)` tokens.\n */\nexport type MentionSegment =\n | string\n | { id: string; displayName: string };\n\n/** Escapes characters in a display name that would break the `@[name](id)` token. */\nfunction sanitizeDisplayName(name: string): string {\n // `]` closes the name group and `)`/`(` frame the id — strip them so a name\n // like \"Bob (HR)\" or \"a]b\" can't produce a malformed or spoofed token.\n return name.replace(/[\\[\\]()]/g, '').trim() || 'user';\n}\n\n/**\n * Finds every `@[name](id)` mention token in the text, in order of appearance.\n * Pure/computed — nothing is stored. Safe to call on every render.\n */\nexport function parseMentions(text: string | undefined | null): ParsedMention[] {\n if (!text) return [];\n const re = new RegExp(MENTION_TOKEN_SOURCE, 'g');\n const out: ParsedMention[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n out.push({\n id: m[2],\n displayName: m[1],\n start: m.index,\n end: m.index + m[0].length,\n });\n }\n return out;\n}\n\n/**\n * Splits text into ordered parts (plain text + mentions) for rendering.\n * The caller supplies `resolveName(id, fallbackName)` to look up the current\n * display name from its own directory; return the fallback when unresolvable.\n * When no resolver is given, the token's embedded name is used.\n */\nexport function renderMentionParts(\n text: string | undefined | null,\n resolveName?: (id: string, fallbackName: string) => string,\n): MentionPart[] {\n if (!text) return [];\n const mentions = parseMentions(text);\n if (mentions.length === 0) return [{ type: 'text', text }];\n\n const parts: MentionPart[] = [];\n let cursor = 0;\n for (const mn of mentions) {\n if (mn.start > cursor) {\n parts.push({ type: 'text', text: text.slice(cursor, mn.start) });\n }\n const displayName = resolveName ? resolveName(mn.id, mn.displayName) : mn.displayName;\n parts.push({ type: 'mention', id: mn.id, displayName });\n cursor = mn.end;\n }\n if (cursor < text.length) {\n parts.push({ type: 'text', text: text.slice(cursor) });\n }\n return parts;\n}\n\n/**\n * Builds outgoing message text + the `mentions` id array from composer segments.\n * Deduplicates ids (a user mentioned twice is fanned out once).\n *\n * Example:\n * buildMentionText(['Hey ', { id: 'u1', displayName: 'Alice' }, ', look'])\n * → { text: 'Hey @[Alice](u1), look', mentions: ['u1'] }\n */\nexport function buildMentionText(segments: MentionSegment[]): { text: string; mentions: string[] } {\n let text = '';\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const seg of segments) {\n if (typeof seg === 'string') {\n text += seg;\n } else {\n text += `@[${sanitizeDisplayName(seg.displayName)}](${seg.id})`;\n if (!seen.has(seg.id)) {\n seen.add(seg.id);\n ids.push(seg.id);\n }\n }\n }\n return { text, mentions: ids };\n}\n\n/**\n * Re-derives the `mentions` id array from message text — the source of truth is\n * always the tokens present in the text. Use this after an edit so a mention whose\n * token was removed drops out of the array (FR-10).\n */\nexport function extractMentionIds(text: string | undefined | null): string[] {\n const ids: string[] = [];\n const seen = new Set<string>();\n for (const mn of parseMentions(text)) {\n if (!seen.has(mn.id)) {\n seen.add(mn.id);\n ids.push(mn.id);\n }\n }\n return ids;\n}\n\n/** True when the mentions list targets everyone (@all). */\nexport function isMentionAll(mentions: string[] | undefined): boolean {\n return !!mentions?.includes(MENTION_ALL_ID);\n}\n","import type { AntzChatConfig, PlatformUploadFn } from './config/types.js';\nimport { resolveConfig } from './config/types.js';\nimport { initApiClient, setApiClientInstance } from './api/client.js';\nimport { initAuthStore } from './stores/auth.store.js';\nimport { authApi } from './api/auth.js';\nimport { messagesApi } from './api/messages.js';\nimport { conversationsApi } from './api/conversations.js';\nimport { storageApi, uploadBatch } from './api/storage.js';\nimport { usersApi } from './api/users.js';\nimport { connectSocket, disconnectSocket, getSocket } from './socket/socket.js';\nimport { socketEmit } from './socket/emitters.js';\nimport type { UploadableFile } from './types/index.js';\n\ninterface ClientSocketHandle {\n emit: typeof socketEmit;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n}\n\n/**\n * Headless client for non-React consumers or direct programmatic use.\n * Usage:\n * const client = new AntzChatClient({ apiUrl, authToken, platformUploadFn, persistStorage })\n * await client.connect()\n */\nexport class AntzChatClient {\n private _config;\n private _authStore;\n\n readonly auth = authApi;\n readonly messages = messagesApi;\n readonly conversations = conversationsApi;\n readonly storage = storageApi;\n readonly users = usersApi;\n readonly socket: ClientSocketHandle = {\n emit: socketEmit,\n on: (event, handler) => getSocket().on(event, handler as (...args: any[]) => void),\n off: (event, handler) => getSocket().off(event, handler as (...args: any[]) => void),\n };\n\n constructor(rawConfig: AntzChatConfig) {\n this._config = resolveConfig(rawConfig);\n this._authStore = initAuthStore(rawConfig.persistStorage);\n\n if (rawConfig.authToken) {\n this._authStore.authTokenStore.setTokens({\n accessToken: rawConfig.authToken,\n refreshToken: '',\n tokenType: 'Bearer',\n expiresIn: 0,\n });\n }\n\n const instance = initApiClient(this._config, this._authStore.authTokenStore);\n setApiClientInstance(instance);\n }\n\n async connect(): Promise<void> {\n await connectSocket(this._config, () => this._authStore.authTokenStore.getAccessToken());\n }\n\n disconnect(): void {\n disconnectSocket();\n }\n\n uploadFiles(files: UploadableFile[], conversationId?: string) {\n return uploadBatch(\n files,\n this._config.platformUploadFn,\n conversationId,\n this._config.upload.onProgress,\n this._config.platformCompressFn,\n this._config.compression,\n );\n }\n\n async uploadIcon(conversationId: string, file: UploadableFile) {\n const result = await this.uploadFiles([file], conversationId);\n const fileId = result.successful[0]?.id;\n if (!fileId) throw new Error('Icon upload failed');\n return conversationsApi.uploadIcon(conversationId, fileId);\n }\n}\n"],"mappings":"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","names":["useChatStore"]}
|