@antzsoft/chat-core 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/{chunk-3QYXS3IW.js → chunk-XSGY3VBK.js} +31 -15
- package/dist/chunk-XSGY3VBK.js.map +1 -0
- package/dist/index.cjs +53 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +25 -27
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +26 -11
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +5 -1
- package/dist/internal.d.ts +5 -1
- package/dist/internal.js +3 -1
- package/dist/{internal-CACK6gGJ.d.cts → storage-Dc0__sXE.d.cts} +12 -1
- package/dist/{internal-CACK6gGJ.d.ts → storage-Dc0__sXE.d.ts} +12 -1
- package/docs/integration-guide.html +174 -5
- package/package.json +1 -1
- package/dist/chunk-3QYXS3IW.js.map +0 -1
package/README.md
CHANGED
|
@@ -2451,6 +2451,23 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2451
2451
|
|
|
2452
2452
|
## Changelog
|
|
2453
2453
|
|
|
2454
|
+
### v1.1.6
|
|
2455
|
+
- **Fix: Messages arrive out of order when typing and sending rapidly** — When a user sent 10–15+ messages in quick succession, the SDK fired all `send_message` socket emits concurrently. Each emit raced independently against the 5-second ACK timeout, and the server received them in an unpredictable order, causing messages to appear jumbled in the recipient's view. The SDK now serialises sends through a per-conversation FIFO queue — the next message is only emitted after the previous ACK is received, ensuring the server always processes them in the order the user sent them. Queue limits: max 100 pending entries (overflow rejects immediately) and a 30-second per-entry TTL (stale entries are dropped before reaching the socket). **No integration changes required.**
|
|
2456
|
+
- **New: POST mirrors for all PUT and DELETE endpoints** — Every `PUT` and `DELETE` route on the server now has an equivalent `POST` endpoint running alongside it. The SDK calls the POST mirrors exclusively. This allows the SDK to work through black-box proxies and infrastructure that blocks non-POST/GET methods (a common requirement in enterprise and carrier-grade deployments). Original PUT/DELETE routes remain fully operational for direct REST clients and legacy integrations — nothing is removed. The complete mirror map is documented in `POST-MIRRORS.md` in the server repo. **No integration changes required.**
|
|
2457
|
+
- **New: `lastReaction` on `lastMessage` in conversation responses** — `Conversation.lastMessage` now includes a `lastReaction` field (`{ emoji, userId, displayName } | null`) reflecting the most recent reaction added to the last message. Conversation list UIs can surface reaction activity (e.g. "👍 Alice") without fetching the full message. The field is `null` when the last message has no reactions or when the conversation has no messages. The `Message` type already carries this field on individual message responses; it is now also propagated to the `lastMessage` snapshot on the `Conversation` object.
|
|
2458
|
+
- **Improvement: Query optimisations across conversation and message APIs** — Several high-traffic database queries have been rewritten with targeted compound indexes. Conversation list load times are significantly reduced for tenants with large numbers of conversations or participants. Message pagination and unread-count aggregations are also faster. No API surface changes.
|
|
2459
|
+
|
|
2460
|
+
### v1.1.5
|
|
2461
|
+
- **Fix: Hermes crash on image upload — `crypto.randomUUID()` not available on React Native** — Hermes (React Native's JS engine) does not expose `globalThis.crypto.randomUUID`, causing a hard crash when `uploadBatch` was called on iOS or Android. A new `generateUUID()` helper is included in `@antzsoft/chat-core` that falls back to a `Math.random`-based RFC 4122 v4 UUID when the Web Crypto API is unavailable. Used in `uploadBatch`, the RN `useChat` hook, and the web `useChat` hook. **No action required** — handled automatically.
|
|
2462
|
+
- **Fix: Batch upload slot misalignment when one or more files fail presigned-URL generation** — The previous slot-mapping logic assumed failed presigned-URL entries appear at the end of the server's `errors` array, which is incorrect. If file at position 1 failed, the SDK mapped position 2's presigned URL to the wrong file slot, corrupting upload-to-slot tracking. The fix: the SDK now sends a `clientIndex` (0-based position) with each file in the batch request. The server echoes it back in both `urls` and `errors` entries. The SDK uses the echoed `clientIndex` for all slot resolution — no positional assumptions, works correctly for same-named files and any failure pattern. A filename-based fallback is retained for older server versions.
|
|
2463
|
+
- **Improvement: `conversation_updated` fan-out batch-optimised — eliminates N×3 DB round trips** — When a conversation update was broadcast to all participants (e.g. on message, reaction, role change, member add/remove), the server performed 3 sequential DB queries per participant to build each recipient's response DTO. For a 100-member group this was 300 round trips per event. The fan-out now runs a single batch: 1 participants query, 1 batch user lookup, 1 batch read-receipt query, then parallel unread counts — building one shared DTO emitted to all recipients. **No API or SDK changes.**
|
|
2464
|
+
- **Improvement: Conversation list reaction counts update live without a refetch** — `SocketProvider` (web and RN) now handles `reaction_updated` socket events by patching the affected conversation's `lastMessage.reactions` in the local cache. Previously, the emoji counts on the conversation list were stale until the next full list refresh. **No integration changes required.**
|
|
2465
|
+
- **Improvement: Create conversation no longer blocks on socket broadcast for large groups** — `POST /conversations` previously awaited the full `conversation_created` socket fan-out before returning the API response. For 300 participants this triggered ~900 DB queries before the caller received an HTTP response. The emit is now fire-and-forget with a single batched DTO built once and shared across all recipients. **No API changes.**
|
|
2466
|
+
|
|
2467
|
+
### v1.1.4
|
|
2468
|
+
- **Fix: Socket ack timeout eliminated for rapid or large messages** — Sending many messages quickly in group chats could trigger a `Socket ack timeout: send_message` error even though messages were delivered. The server was performing delivery tracking, room broadcasts, and push notifications synchronously before returning the ack, exceeding the 5-second window. The server now returns the ack immediately after writing to the database; all subsequent work runs in the background. No client impact.
|
|
2469
|
+
- **Fix: Per-conversation send queue — parallel sends across conversations** — The SDK's internal send queue was previously global. Messages in Conversation A blocked Conversation B until each ack completed. The queue is now keyed per `conversationId` so independent conversations send in parallel. Messages within the same conversation remain serialised. The 100-message `QUEUE_MAX_SIZE` cap also now applies per conversation. **No integration changes required.**
|
|
2470
|
+
|
|
2454
2471
|
### v1.1.3
|
|
2455
2472
|
- **New: `conversationsApi.leave(conversationId, andDelete?)` — `andDelete` param** — Pass `andDelete: true` to exit a group and hide it from the caller's list in a single atomic server call ("Exit and Delete"). Previously required two separate calls.
|
|
2456
2473
|
- **New: Auto-promote on last-admin exit** — When the only admin leaves a group, the server now automatically promotes the longest-standing active member to admin before completing the exit. Previously the server returned `400 Bad Request` requiring the admin to manually promote someone first.
|
|
@@ -194,8 +194,10 @@ function initApiClient(config, tokenStore) {
|
|
|
194
194
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
195
195
|
_avatarSent = true;
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
const TRANSIT_EXEMPT = ["/auth/login", "/auth/register", "/auth/refresh", "/crypto/pubkey"];
|
|
198
|
+
const isExempt = TRANSIT_EXEMPT.some((p) => req.url?.endsWith(p));
|
|
199
|
+
if (!isExempt) await waitForTransitReady();
|
|
200
|
+
if (!isExempt && isTransitEnabled()) {
|
|
199
201
|
const sessionId = getSessionId();
|
|
200
202
|
const key = getSessionKey();
|
|
201
203
|
if (sessionId && key) {
|
|
@@ -291,6 +293,17 @@ function getApiClient() {
|
|
|
291
293
|
return _instance;
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
// src/crypto/uuid.ts
|
|
297
|
+
function generateUUID() {
|
|
298
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
299
|
+
return globalThis.crypto.randomUUID();
|
|
300
|
+
}
|
|
301
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
302
|
+
const r = Math.random() * 16 | 0;
|
|
303
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
294
307
|
// src/api/storage.ts
|
|
295
308
|
var storageApi = {
|
|
296
309
|
async requestPresignedUrl(payload) {
|
|
@@ -316,7 +329,7 @@ var storageApi = {
|
|
|
316
329
|
return data;
|
|
317
330
|
},
|
|
318
331
|
async deleteFile(fileId) {
|
|
319
|
-
await getApiClient().
|
|
332
|
+
await getApiClient().post(`/storage/files/${fileId}/delete`);
|
|
320
333
|
},
|
|
321
334
|
async getConversationFiles(conversationId, params = {}) {
|
|
322
335
|
const { data } = await getApiClient().get(
|
|
@@ -334,12 +347,13 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
334
347
|
const compressedFiles = await Promise.all(
|
|
335
348
|
files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
|
|
336
349
|
);
|
|
337
|
-
const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i] }));
|
|
338
|
-
const requests = slotted.map(({ file: f }) => ({
|
|
350
|
+
const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
|
|
351
|
+
const requests = slotted.map(({ file: f, clientIndex }) => ({
|
|
339
352
|
filename: f.name,
|
|
340
353
|
mimeType: f.type,
|
|
341
354
|
size: f.size,
|
|
342
355
|
conversationId,
|
|
356
|
+
clientIndex,
|
|
343
357
|
...f.compressed && {
|
|
344
358
|
metadata: {
|
|
345
359
|
compressed: f.compressed,
|
|
@@ -350,12 +364,12 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
350
364
|
}));
|
|
351
365
|
const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
|
|
352
366
|
const failedSlotIds = /* @__PURE__ */ new Set();
|
|
353
|
-
const failed = requestErrors.map((e
|
|
354
|
-
const
|
|
355
|
-
|
|
367
|
+
const failed = requestErrors.map((e) => {
|
|
368
|
+
const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
|
|
369
|
+
const slotId = slotted[idx]?.slotId;
|
|
370
|
+
if (slotId) failedSlotIds.add(slotId);
|
|
356
371
|
return { filename: e.filename, error: e.error };
|
|
357
372
|
});
|
|
358
|
-
const uploadableSlots = slotted.filter(({ slotId }) => !failedSlotIds.has(slotId));
|
|
359
373
|
const progressMap = {};
|
|
360
374
|
const reportProgress = () => {
|
|
361
375
|
if (!onProgress) return;
|
|
@@ -367,15 +381,16 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
367
381
|
const slotToFile = /* @__PURE__ */ new Map();
|
|
368
382
|
await Promise.all(
|
|
369
383
|
urls.map(async (presigned, idx) => {
|
|
370
|
-
const
|
|
371
|
-
|
|
384
|
+
const originalIdx = presigned.clientIndex ?? idx;
|
|
385
|
+
const { file, slotId } = slotted[originalIdx];
|
|
386
|
+
progressMap[originalIdx] = 0;
|
|
372
387
|
try {
|
|
373
388
|
await platformUploadFn(presigned, file, (pct) => {
|
|
374
|
-
progressMap[
|
|
389
|
+
progressMap[originalIdx] = Math.round(pct * 0.9);
|
|
375
390
|
reportProgress();
|
|
376
391
|
});
|
|
377
392
|
const fileResponse = await storageApi.confirmUpload(presigned.fileId);
|
|
378
|
-
progressMap[
|
|
393
|
+
progressMap[originalIdx] = 100;
|
|
379
394
|
reportProgress();
|
|
380
395
|
successful.push(fileResponse);
|
|
381
396
|
slotToFile.set(slotId, fileResponse);
|
|
@@ -387,7 +402,7 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
387
402
|
return { result: { successful, failed }, slotToFile };
|
|
388
403
|
}
|
|
389
404
|
async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig) {
|
|
390
|
-
const slotIds = files.map(() =>
|
|
405
|
+
const slotIds = files.map(() => generateUUID());
|
|
391
406
|
const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);
|
|
392
407
|
return result;
|
|
393
408
|
}
|
|
@@ -408,8 +423,9 @@ export {
|
|
|
408
423
|
initApiClient,
|
|
409
424
|
setApiClientInstance,
|
|
410
425
|
getApiClient,
|
|
426
|
+
generateUUID,
|
|
411
427
|
storageApi,
|
|
412
428
|
uploadBatch,
|
|
413
429
|
uploadBatchWithSlots
|
|
414
430
|
};
|
|
415
|
-
//# sourceMappingURL=chunk-
|
|
431
|
+
//# sourceMappingURL=chunk-XSGY3VBK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compression/compress.ts","../src/crypto/session.ts","../src/api/client.ts","../src/crypto/transit.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","import type { TransitAlgo } from './detect.js';\n\ninterface TransitSession {\n sessionKey: CryptoKey;\n algo: TransitAlgo;\n sessionId: string;\n enabled: boolean;\n}\n\n// All state lives on globalThis so Turbopack <locals> module splits and any\n// other bundler that creates multiple instances of this module still share a\n// single source of truth. The symbol key prevents accidental collisions.\nconst _KEY = Symbol.for('__antz_chat_transit__');\n\ninterface TransitState {\n session: TransitSession | null;\n sessionEverEstablished: boolean;\n readyResolve: (() => void) | null;\n readyPromise: Promise<void> | null;\n transitConfigured: boolean | null;\n}\n\nfunction getState(): TransitState {\n const g = globalThis as any;\n if (!g[_KEY]) {\n g[_KEY] = {\n session: null,\n sessionEverEstablished: false,\n readyResolve: null,\n readyPromise: null,\n transitConfigured: null,\n } satisfies TransitState;\n }\n return g[_KEY] as TransitState;\n}\n\nexport function configureTransit(enabled: boolean): void {\n const s = getState();\n s.transitConfigured = enabled;\n if (!enabled) {\n // Transit disabled — resolve immediately so HTTP requests don't block\n s.readyResolve?.();\n s.readyResolve = null;\n }\n}\n\nexport function waitForTransitReady(): Promise<void> {\n const s = getState();\n // Not configured yet or disabled — resolve immediately\n if (!s.transitConfigured) return Promise.resolve();\n // Already have an active session — resolve immediately\n if (s.session) return Promise.resolve();\n // Session was previously established but is now cleared (socket dropped/reconnecting).\n // The encryption block in the interceptor is guarded by isTransitEnabled() and will\n // skip itself with no session, so there is no point blocking here.\n if (s.sessionEverEstablished) return Promise.resolve();\n // First startup — block until the initial ECDH handshake completes.\n if (!s.readyPromise) {\n s.readyPromise = new Promise<void>((resolve) => {\n s.readyResolve = resolve;\n });\n }\n return s.readyPromise;\n}\n\nexport function setTransitSession(session: TransitSession): void {\n const s = getState();\n s.session = session;\n s.sessionEverEstablished = true;\n // Resolve any pending HTTP requests waiting for the session key\n s.readyResolve?.();\n s.readyResolve = null;\n}\n\nexport function getTransitSession(): TransitSession | null {\n return getState().session;\n}\n\nexport function clearTransitSession(): void {\n const s = getState();\n s.session = null;\n // Reset the ready promise so it can be recreated on next waitForTransitReady call.\n // sessionEverEstablished intentionally left true — HTTP requests fired while\n // reconnecting should not block (isTransitEnabled() guards encryption anyway).\n s.readyPromise = null;\n s.readyResolve = null;\n}\n\nexport function isTransitEnabled(): boolean {\n return getState().session?.enabled === true;\n}\n\nexport function getSessionKey(): CryptoKey | null {\n return getState().session?.sessionKey ?? null;\n}\n\n// Returns sessionId for the x-transit-session header sent with HTTP requests.\nexport function getSessionId(): string | null {\n return getState().session?.sessionId ?? null;\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, waitForTransitReady, configureTransit } from '../crypto/session.js';\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\n// Avatar headers are sent only on the first authenticated request after init.\n// The server hashes on receive and deduplicates — subsequent requests don't need them.\nlet _avatarSent = false;\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Auth endpoints fire before the socket/ECDH handshake exists — never block them.\n const TRANSIT_EXEMPT = ['/auth/login', '/auth/register', '/auth/refresh', '/crypto/pubkey'];\n const isExempt = TRANSIT_EXEMPT.some(p => req.url?.endsWith(p));\n\n // Wait for the transit session key to be established before sending.\n // HTTP requests that fire before connectSocket completes (e.g. getMe, getConfig)\n // are held here until the ECDH handshake finishes. Resolves immediately when\n // transit is disabled or already established.\n if (!isExempt) await waitForTransitReady();\n\n // Transit encryption — attach session ID on every request so the server\n // can look up the session key. Encrypt body when present.\n if (!isExempt && isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","export interface TransitEnvelope {\n v: 1;\n iv: string; // base64, 12 bytes\n tag: string; // base64, 16 bytes\n ct: string; // base64, ciphertext\n}\n\n// ─── Encrypt ─────────────────────────────────────────────────────────────────\n\nexport async function encryptPayload(\n data: unknown,\n sessionKey: CryptoKey,\n): Promise<TransitEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const plaintext = new TextEncoder().encode(JSON.stringify(data));\n\n const encrypted = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n sessionKey,\n plaintext,\n );\n\n // AES-GCM appends the 16-byte auth tag to the ciphertext buffer\n const ct = encrypted.slice(0, encrypted.byteLength - 16);\n const tag = encrypted.slice(encrypted.byteLength - 16);\n\n return {\n v: 1,\n iv: bufToB64(iv),\n tag: bufToB64(tag),\n ct: bufToB64(ct),\n };\n}\n\n// ─── Decrypt ─────────────────────────────────────────────────────────────────\n\nexport async function decryptPayload(\n envelope: TransitEnvelope,\n sessionKey: CryptoKey,\n): Promise<unknown> {\n const iv = b64ToBuf(envelope.iv);\n const tag = b64ToBuf(envelope.tag);\n const ct = b64ToBuf(envelope.ct);\n\n // Reassemble ct + tag as SubtleCrypto expects them concatenated\n const combined = new Uint8Array(ct.byteLength + tag.byteLength);\n combined.set(new Uint8Array(ct), 0);\n combined.set(new Uint8Array(tag), ct.byteLength);\n\n const decrypted = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: new Uint8Array(iv) },\n sessionKey,\n combined,\n );\n\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nexport function isTransitEnvelope(v: unknown): v is TransitEnvelope {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as any).v === 1 &&\n typeof (v as any).iv === 'string' &&\n typeof (v as any).tag === 'string' &&\n typeof (v as any).ct === 'string'\n );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\n/**\n * High-level batch upload.\n * The actual binary transfer is delegated to platformUploadFn so this\n * function is platform-agnostic (works on web and React Native).\n * If platformCompressFn + compressionConfig are provided, each file is\n * compressed before the presigned URL is requested (so the server receives\n * the correct compressed size and MIME type).\n */\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n\n const fileResponse = await storageApi.confirmUpload(presigned.fileId);\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -381,8 +381,10 @@ function initApiClient(config, tokenStore) {
|
|
|
381
381
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
382
382
|
_avatarSent = true;
|
|
383
383
|
}
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
const TRANSIT_EXEMPT = ["/auth/login", "/auth/register", "/auth/refresh", "/crypto/pubkey"];
|
|
385
|
+
const isExempt = TRANSIT_EXEMPT.some((p) => req.url?.endsWith(p));
|
|
386
|
+
if (!isExempt) await waitForTransitReady();
|
|
387
|
+
if (!isExempt && isTransitEnabled()) {
|
|
386
388
|
const sessionId = getSessionId();
|
|
387
389
|
const key = getSessionKey();
|
|
388
390
|
if (sessionId && key) {
|
|
@@ -506,7 +508,7 @@ var authApi = {
|
|
|
506
508
|
async uploadAvatar(file, mimeType) {
|
|
507
509
|
const form = new FormData();
|
|
508
510
|
form.append("avatar", file instanceof File ? file : new File([file], "avatar.jpg", { type: mimeType ?? "image/jpeg" }));
|
|
509
|
-
const { data } = await getApiClient().
|
|
511
|
+
const { data } = await getApiClient().post("/users/me/avatar", form, {
|
|
510
512
|
headers: { "Content-Type": "multipart/form-data" }
|
|
511
513
|
});
|
|
512
514
|
return data;
|
|
@@ -555,22 +557,22 @@ var messagesApi = {
|
|
|
555
557
|
return data;
|
|
556
558
|
},
|
|
557
559
|
async update(messageId, text) {
|
|
558
|
-
const { data } = await getApiClient().
|
|
560
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
|
|
559
561
|
return data;
|
|
560
562
|
},
|
|
561
563
|
async delete(messageId) {
|
|
562
|
-
await getApiClient().
|
|
564
|
+
await getApiClient().post(`/messages/${messageId}/delete`);
|
|
563
565
|
},
|
|
564
566
|
async deleteForMe(messageId) {
|
|
565
|
-
await getApiClient().
|
|
567
|
+
await getApiClient().post(`/messages/${messageId}/delete-for-me`);
|
|
566
568
|
},
|
|
567
569
|
async addReaction(messageId, emoji) {
|
|
568
570
|
const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
|
|
569
571
|
return data;
|
|
570
572
|
},
|
|
571
573
|
async removeReaction(messageId, emoji) {
|
|
572
|
-
const { data } = await getApiClient().
|
|
573
|
-
`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`
|
|
574
|
+
const { data } = await getApiClient().post(
|
|
575
|
+
`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
|
|
574
576
|
);
|
|
575
577
|
return data;
|
|
576
578
|
},
|
|
@@ -578,7 +580,7 @@ var messagesApi = {
|
|
|
578
580
|
await getApiClient().post(`/messages/${messageId}/star`);
|
|
579
581
|
},
|
|
580
582
|
async unstar(messageId) {
|
|
581
|
-
await getApiClient().
|
|
583
|
+
await getApiClient().post(`/messages/${messageId}/unstar`);
|
|
582
584
|
},
|
|
583
585
|
async getStarred(params = {}) {
|
|
584
586
|
const { data } = await getApiClient().get("/messages/starred", { params });
|
|
@@ -602,7 +604,7 @@ var messagesApi = {
|
|
|
602
604
|
return data;
|
|
603
605
|
},
|
|
604
606
|
async unpin(messageId) {
|
|
605
|
-
const { data } = await getApiClient().
|
|
607
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
|
|
606
608
|
return data;
|
|
607
609
|
},
|
|
608
610
|
async getPinned(conversationId) {
|
|
@@ -645,6 +647,7 @@ function normalizeLastMessage(lastMsg) {
|
|
|
645
647
|
text: lastMsg.contentPreview
|
|
646
648
|
},
|
|
647
649
|
reactions: [],
|
|
650
|
+
lastReaction: lastMsg.lastReaction ?? null,
|
|
648
651
|
status: lastMsg.status ?? "sent",
|
|
649
652
|
isEdited: false,
|
|
650
653
|
sentAt: lastMsg.sentAt ?? "",
|
|
@@ -678,11 +681,11 @@ var conversationsApi = {
|
|
|
678
681
|
return normalizeConversation(data);
|
|
679
682
|
},
|
|
680
683
|
async update(conversationId, payload) {
|
|
681
|
-
const { data } = await getApiClient().
|
|
684
|
+
const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
|
|
682
685
|
return normalizeConversation(data);
|
|
683
686
|
},
|
|
684
687
|
async delete(conversationId) {
|
|
685
|
-
await getApiClient().
|
|
688
|
+
await getApiClient().post(`/conversations/${conversationId}/delete`);
|
|
686
689
|
},
|
|
687
690
|
async addParticipants(conversationId, userIds, role) {
|
|
688
691
|
const { data } = await getApiClient().post(
|
|
@@ -692,13 +695,13 @@ var conversationsApi = {
|
|
|
692
695
|
return normalizeConversation(data);
|
|
693
696
|
},
|
|
694
697
|
async removeParticipant(conversationId, userId) {
|
|
695
|
-
const { data } = await getApiClient().
|
|
696
|
-
`/conversations/${conversationId}/participants/${userId}`
|
|
698
|
+
const { data } = await getApiClient().post(
|
|
699
|
+
`/conversations/${conversationId}/participants/${userId}/remove`
|
|
697
700
|
);
|
|
698
701
|
return normalizeConversation(data);
|
|
699
702
|
},
|
|
700
703
|
async updateParticipantRole(conversationId, userId, role) {
|
|
701
|
-
const { data } = await getApiClient().
|
|
704
|
+
const { data } = await getApiClient().post(
|
|
702
705
|
`/conversations/${conversationId}/participants/${userId}/role`,
|
|
703
706
|
{ role }
|
|
704
707
|
);
|
|
@@ -708,17 +711,17 @@ var conversationsApi = {
|
|
|
708
711
|
await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
|
|
709
712
|
},
|
|
710
713
|
async unmute(conversationId) {
|
|
711
|
-
await getApiClient().
|
|
714
|
+
await getApiClient().post(`/conversations/${conversationId}/unmute`);
|
|
712
715
|
},
|
|
713
716
|
async pin(conversationId) {
|
|
714
717
|
await getApiClient().post(`/conversations/${conversationId}/pin`);
|
|
715
718
|
},
|
|
716
719
|
async unpin(conversationId) {
|
|
717
|
-
await getApiClient().
|
|
720
|
+
await getApiClient().post(`/conversations/${conversationId}/unpin`);
|
|
718
721
|
},
|
|
719
722
|
async leave(conversationId, andDelete) {
|
|
720
723
|
const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
|
|
721
|
-
await getApiClient().
|
|
724
|
+
await getApiClient().post(url);
|
|
722
725
|
},
|
|
723
726
|
async getMembers(conversationId, filter) {
|
|
724
727
|
const { data } = await getApiClient().get(
|
|
@@ -753,20 +756,29 @@ var conversationsApi = {
|
|
|
753
756
|
* Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
|
|
754
757
|
*/
|
|
755
758
|
async uploadIcon(conversationId, fileId) {
|
|
756
|
-
const { data } = await getApiClient().
|
|
759
|
+
const { data } = await getApiClient().post(
|
|
757
760
|
`/conversations/${conversationId}/icon`,
|
|
758
761
|
{ fileId }
|
|
759
762
|
);
|
|
760
763
|
return normalizeConversation(data);
|
|
761
764
|
},
|
|
762
765
|
async removeIcon(conversationId) {
|
|
763
|
-
const { data } = await getApiClient().
|
|
764
|
-
`/conversations/${conversationId}/icon`
|
|
765
|
-
);
|
|
766
|
+
const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
|
|
766
767
|
return normalizeConversation(data);
|
|
767
768
|
}
|
|
768
769
|
};
|
|
769
770
|
|
|
771
|
+
// src/crypto/uuid.ts
|
|
772
|
+
function generateUUID() {
|
|
773
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
774
|
+
return globalThis.crypto.randomUUID();
|
|
775
|
+
}
|
|
776
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
777
|
+
const r = Math.random() * 16 | 0;
|
|
778
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
770
782
|
// src/api/storage.ts
|
|
771
783
|
var storageApi = {
|
|
772
784
|
async requestPresignedUrl(payload) {
|
|
@@ -792,7 +804,7 @@ var storageApi = {
|
|
|
792
804
|
return data;
|
|
793
805
|
},
|
|
794
806
|
async deleteFile(fileId) {
|
|
795
|
-
await getApiClient().
|
|
807
|
+
await getApiClient().post(`/storage/files/${fileId}/delete`);
|
|
796
808
|
},
|
|
797
809
|
async getConversationFiles(conversationId, params = {}) {
|
|
798
810
|
const { data } = await getApiClient().get(
|
|
@@ -810,12 +822,13 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
810
822
|
const compressedFiles = await Promise.all(
|
|
811
823
|
files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
|
|
812
824
|
);
|
|
813
|
-
const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i] }));
|
|
814
|
-
const requests = slotted.map(({ file: f }) => ({
|
|
825
|
+
const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
|
|
826
|
+
const requests = slotted.map(({ file: f, clientIndex }) => ({
|
|
815
827
|
filename: f.name,
|
|
816
828
|
mimeType: f.type,
|
|
817
829
|
size: f.size,
|
|
818
830
|
conversationId,
|
|
831
|
+
clientIndex,
|
|
819
832
|
...f.compressed && {
|
|
820
833
|
metadata: {
|
|
821
834
|
compressed: f.compressed,
|
|
@@ -826,12 +839,12 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
826
839
|
}));
|
|
827
840
|
const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
|
|
828
841
|
const failedSlotIds = /* @__PURE__ */ new Set();
|
|
829
|
-
const failed = requestErrors.map((e
|
|
830
|
-
const
|
|
831
|
-
|
|
842
|
+
const failed = requestErrors.map((e) => {
|
|
843
|
+
const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
|
|
844
|
+
const slotId = slotted[idx]?.slotId;
|
|
845
|
+
if (slotId) failedSlotIds.add(slotId);
|
|
832
846
|
return { filename: e.filename, error: e.error };
|
|
833
847
|
});
|
|
834
|
-
const uploadableSlots = slotted.filter(({ slotId }) => !failedSlotIds.has(slotId));
|
|
835
848
|
const progressMap = {};
|
|
836
849
|
const reportProgress = () => {
|
|
837
850
|
if (!onProgress) return;
|
|
@@ -843,15 +856,16 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
843
856
|
const slotToFile = /* @__PURE__ */ new Map();
|
|
844
857
|
await Promise.all(
|
|
845
858
|
urls.map(async (presigned, idx) => {
|
|
846
|
-
const
|
|
847
|
-
|
|
859
|
+
const originalIdx = presigned.clientIndex ?? idx;
|
|
860
|
+
const { file, slotId } = slotted[originalIdx];
|
|
861
|
+
progressMap[originalIdx] = 0;
|
|
848
862
|
try {
|
|
849
863
|
await platformUploadFn(presigned, file, (pct) => {
|
|
850
|
-
progressMap[
|
|
864
|
+
progressMap[originalIdx] = Math.round(pct * 0.9);
|
|
851
865
|
reportProgress();
|
|
852
866
|
});
|
|
853
867
|
const fileResponse = await storageApi.confirmUpload(presigned.fileId);
|
|
854
|
-
progressMap[
|
|
868
|
+
progressMap[originalIdx] = 100;
|
|
855
869
|
reportProgress();
|
|
856
870
|
successful.push(fileResponse);
|
|
857
871
|
slotToFile.set(slotId, fileResponse);
|
|
@@ -863,7 +877,7 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
863
877
|
return { result: { successful, failed }, slotToFile };
|
|
864
878
|
}
|
|
865
879
|
async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig) {
|
|
866
|
-
const slotIds = files.map(() =>
|
|
880
|
+
const slotIds = files.map(() => generateUUID());
|
|
867
881
|
const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);
|
|
868
882
|
return result;
|
|
869
883
|
}
|
|
@@ -888,7 +902,7 @@ var devicesApi = {
|
|
|
888
902
|
* Call this on logout so the user stops receiving push notifications on this device.
|
|
889
903
|
*/
|
|
890
904
|
async remove(deviceId) {
|
|
891
|
-
await getApiClient().
|
|
905
|
+
await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
|
|
892
906
|
}
|
|
893
907
|
};
|
|
894
908
|
|
|
@@ -913,7 +927,7 @@ var usersApi = {
|
|
|
913
927
|
* happened — without waiting for the next 2-hour sync cycle.
|
|
914
928
|
*/
|
|
915
929
|
async updateProfile(payload) {
|
|
916
|
-
const { data } = await getApiClient().
|
|
930
|
+
const { data } = await getApiClient().post("/users/me/update", payload);
|
|
917
931
|
return data;
|
|
918
932
|
},
|
|
919
933
|
/**
|
|
@@ -923,7 +937,7 @@ var usersApi = {
|
|
|
923
937
|
* token is first registered, so this never fails with "not found".
|
|
924
938
|
*/
|
|
925
939
|
async updatePreferences(prefs) {
|
|
926
|
-
const { data } = await getApiClient().
|
|
940
|
+
const { data } = await getApiClient().post("/users/me/preferences", prefs);
|
|
927
941
|
return data;
|
|
928
942
|
},
|
|
929
943
|
/**
|
|
@@ -1136,8 +1150,7 @@ async function _doConnect(config, getToken) {
|
|
|
1136
1150
|
const algo = await detectTransitAlgo();
|
|
1137
1151
|
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1138
1152
|
} catch (err) {
|
|
1139
|
-
|
|
1140
|
-
console.warn("[AntzChat] Transit handshake setup failed, connecting without transit encryption:", err.message);
|
|
1153
|
+
throw err;
|
|
1141
1154
|
}
|
|
1142
1155
|
} else {
|
|
1143
1156
|
try {
|
|
@@ -1358,7 +1371,7 @@ var socketEmit = {
|
|
|
1358
1371
|
fireAndForget("leave_room", { conversationId });
|
|
1359
1372
|
},
|
|
1360
1373
|
sendMessage(payload) {
|
|
1361
|
-
return queueSendMessage(payload);
|
|
1374
|
+
return queueSendMessage({ ...payload, sentAt: Date.now() });
|
|
1362
1375
|
},
|
|
1363
1376
|
updateMessage(messageId, text) {
|
|
1364
1377
|
return withAck("update_message", { messageId, text });
|