@antzsoft/chat-core 1.4.5 → 1.4.6

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 CHANGED
@@ -380,9 +380,48 @@ interface AntzChatConfig {
380
380
  * Optional — sensible defaults are applied for all sub-fields.
381
381
  */
382
382
  upload?: UploadConfig;
383
+
384
+ /**
385
+ * Called when sending a message ultimately fails, after the SDK has already
386
+ * updated the timeline (removed the bubble for permanent failures, flagged it
387
+ * retryable otherwise). Use it to surface a toast or error banner. v1.4.5+.
388
+ * Optional.
389
+ */
390
+ onSendError?: (error: Error, context: SendErrorContext) => void;
383
391
  }
384
392
  ```
385
393
 
394
+ ### `SendErrorContext` (v1.4.5+)
395
+
396
+ Passed as the second argument to `onSendError`. The callback fires *after* the SDK has reconciled the timeline, so it is for user-facing notification only — you do not need to remove or flag the bubble yourself.
397
+
398
+ ```typescript
399
+ interface SendErrorContext {
400
+ conversationId: string;
401
+ /** tempId of the optimistic message the failure belongs to. */
402
+ tempId: string;
403
+ /**
404
+ * True when resending the identical payload cannot succeed (file size/type
405
+ * limits, message too long, no longer a participant, ...). The SDK has removed
406
+ * the optimistic message from the timeline. False for transient failures
407
+ * (network / socket / generic server error), where a "! Retry" bubble stays.
408
+ */
409
+ permanent: boolean;
410
+ }
411
+ ```
412
+
413
+ ```typescript
414
+ const client = new AntzChatClient({
415
+ apiUrl, persistStorage, platformUploadFn,
416
+ onSendError: (error, { permanent }) => {
417
+ if (permanent) toast.error(error.message);
418
+ else toast.warn('Message not sent — tap Retry on the message.');
419
+ },
420
+ });
421
+ ```
422
+
423
+ Pair `permanent: false` with `useChat().retrySendMessage(messageId)` (v1.4.4+) if you want a retry affordance outside the built-in `MessageItem`.
424
+
386
425
  ### `PersistStorage`
387
426
 
388
427
  Supports both synchronous (localStorage) and asynchronous (AsyncStorage) storage backends.
@@ -3244,6 +3283,28 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
3244
3283
 
3245
3284
  ## Changelog
3246
3285
 
3286
+ > **Full history lives in [CHANGELOG.md](CHANGELOG.md).** That file is the source of truth and covers every release. This section carries only the headline notes for recent versions; v1.4.2, v1.4.3 and v1.4.4 (message forwarding, per-attachment forwarding, and send/forward retry-safety) are documented there rather than duplicated here.
3287
+
3288
+ ### v1.4.5
3289
+
3290
+ Published as `1.4.5`; built and reviewed internally as `1.4.7` (same code — there is no `1.4.6`, and no separate `1.4.7` will be published).
3291
+
3292
+ - **Fix: transit encryption no longer degrades to plaintext or wedges after a socket drop.** Six related fixes, all of them closing a path where the SDK either sent an unencrypted request the server was certain to reject with `403 "Transit encryption required"`, or blocked forever on a handshake nothing was driving:
3293
+ - The socket `disconnect` handler cleared the REST transit session on *every* websocket blip (network hiccup, backgrounded tab, server redeploy), after which every chat REST call 403'd until a full page reload. The REST session is independent of the socket transport and is now torn down only on an explicit `disconnectSocket()` or auth change.
3294
+ - `clearTransitSession()` left `sessionEverEstablished = true`, making `waitForTransitReady()` ("don't block") and `isTransitEnabled()` ("don't encrypt") permanently disagree — requests went out unencrypted and 403'd with no recovery path. The flag now resets with the session.
3295
+ - The 5s `transit_session` safety timeout called `configureTransit(false)`, turning a transient stall into plaintext for the rest of the session. A timeout now leaves transit required; only `GET /crypto/pubkey` returning `enabled: false` can authoritatively disable it.
3296
+ - The interceptor gated its transit wait on the presence of an auth token, so pre-auth calls (`GET /app/config` before an async `authProvider` resolves) raced ahead and 403'd. It now gates on whether transit is configured.
3297
+ - A `join_room` dropped during the handshake gap left the client outside the server-side room with no error and no retry, silently killing `new_message` delivery for that conversation. Joined rooms are now tracked and re-emitted whenever a transit session (re-)establishes or the socket reconnects.
3298
+ - The per-conversation send queue awaited the full server ack before starting the next message. It now awaits only the emit phase, preserving on-the-wire ordering without paying round-trip latency.
3299
+
3300
+ - **Changed: requests fail loudly instead of hanging or downgrading.** With transit required and no session key, a REST request waits up to 30s for an in-flight handshake, then throws a **retryable** `AntzChatNetworkError` with code `TRANSIT_NOT_READY`; socket emits do the same on a 4s budget. Neither ever falls through to plaintext. The REST handshake retries itself up to 5 times with exponential backoff.
3301
+
3302
+ - **New: `onSendError`.** A config callback — `(error, { conversationId, tempId, permanent })` — fired when a send ultimately fails, after the SDK has updated the timeline, so you can raise a toast. `permanent: true` means a retry of the identical payload cannot succeed and the optimistic bubble was removed; `false` leaves a retryable "! Retry" bubble. New type: `SendErrorContext`.
3303
+
3304
+ - **New exports:** `setAuthReadyPromise()` (gate the interceptor until your auth token resolves), `resetTrackedRooms()` (call on a user/tenant switch so the reconnect re-flush cannot re-join the previous user's rooms — a token refresh is *not* an identity change), and `isApiClientConfigured()`.
3305
+
3306
+ **Backward compatible at the API level** — additive exports only, no signature changes, no call-site updates required. **One behavioral change needs your attention:** a call that previously went out as plaintext and returned `403` now throws a retryable error instead. Audit any REST call you fire and forget during startup — `getMe().then(...).catch(() => {})` will swallow `TRANSIT_NOT_READY`, leaving auth unresolved, the socket unconnected, and the app stuck on a loading screen with no visible cause. See [CHANGELOG.md](CHANGELOG.md) → *Upgrading from 1.4.4* for the retry patterns.
3307
+
3247
3308
  ### v1.4.1
3248
3309
 
3249
3310
  - **New: manual "mark as unread."** A conversation can now be flagged unread independently of `unreadCount` — the same UX as WhatsApp/Telegram's "mark as unread": re-flag a conversation you've already read so it stands out again, without fabricating unread messages.
@@ -3258,7 +3319,9 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
3258
3319
 
3259
3320
  - **Fix: stale transit encryption key after token refresh, causing continuous `"Transit decryption failed for event: user_online"` spam.** `reconnectSocket()` called `socket.connect()` to apply a refreshed token while preserving the transit session — but socket.io-client's `connect()` is a no-op on an already-connected socket. A token refresh fires while the user is actively on a chat screen (socket still connected), so the new auth was never actually sent; the socket kept running on the stale token until the server eventually dropped it, and the disconnect handler then cleared the transit session, desyncing the client's key from the server's.
3260
3321
 
3261
- **Fix:** in the transit branch of `reconnectSocket()`, force a real transport re-cycle (disconnect → connect) so the fresh auth (token + `transitSessionId`) is actually sent and the server re-links the *same* session (same key), instead of silently continuing on stale auth. A new internal `_preservingSession` guard stops the disconnect handler from wiping the key being deliberately carried forward. Non-transit and already-disconnected paths are unchanged.
3322
+ **Fix:** in the transit branch of `reconnectSocket()`, force a real transport re-cycle (disconnect → connect) so the fresh auth (token + `transitSessionId`) is actually sent and the server re-links the *same* session (same key), instead of silently continuing on stale auth. Non-transit and already-disconnected paths are unchanged.
3323
+
3324
+ > **Superseded in v1.4.5.** This fix originally added an internal `_preservingSession` guard to stop the disconnect handler from wiping the key being carried through the re-cycle. v1.4.5 removed that guard: the disconnect handler no longer clears the transit session on a transient drop at all, so there is nothing to guard against. The transport re-cycle described above is unchanged.
3262
3325
 
3263
3326
  **Backward compatible.** Only affects transit-encryption deployments that refresh auth tokens while the socket is connected. **No integration changes required** — the fix is entirely internal to `reconnectSocket()`.
3264
3327
 
@@ -263,11 +263,36 @@ function resetAlgoCache() {
263
263
  }
264
264
 
265
265
  // src/crypto/handshake.ts
266
+ function readRetryAfterMs(headers) {
267
+ const parse = (raw) => {
268
+ if (!raw) return void 0;
269
+ const secs = Number(raw);
270
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
271
+ const when = Date.parse(raw);
272
+ return Number.isNaN(when) ? void 0 : Math.max(0, when - Date.now());
273
+ };
274
+ const found = ["Retry-After-identity", "Retry-After-ip", "Retry-After"].map((name) => parse(headers.get(name))).filter((ms) => ms != null);
275
+ return found.length > 0 ? Math.max(...found) : void 0;
276
+ }
277
+ var TransitRateLimitedError = class extends Error {
278
+ constructor(retryAfterMs) {
279
+ super("[AntzChat] transit handshake rate-limited (429)");
280
+ this.name = "TransitRateLimitedError";
281
+ this.retryAfterMs = retryAfterMs;
282
+ }
283
+ };
284
+ function identityHeaders(identity) {
285
+ const headers = {};
286
+ if (identity?.userId) headers["x-user-id"] = identity.userId;
287
+ if (identity?.tenantId) headers["X-Tenant-ID"] = identity.tenantId;
288
+ return headers;
289
+ }
266
290
  function hasWebCrypto2() {
267
291
  return typeof globalThis.crypto?.subtle !== "undefined";
268
292
  }
269
- async function fetchServerKeys(apiUrl) {
270
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
293
+ async function fetchServerKeys(apiUrl, identity) {
294
+ const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });
295
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
271
296
  if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
272
297
  const body = await res.json();
273
298
  return body?.data ?? body;
@@ -278,24 +303,26 @@ async function generateEphemeralKey(algo, serverKeys) {
278
303
  }
279
304
  return generateNobleEphemeralKey(serverKeys);
280
305
  }
281
- async function createRestTransitSession(apiUrl) {
306
+ async function createRestTransitSession(apiUrl, identity) {
282
307
  try {
283
- const serverKeys = await fetchServerKeys(apiUrl);
308
+ const serverKeys = await fetchServerKeys(apiUrl, identity);
284
309
  if (!serverKeys.enabled) return null;
285
310
  const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
286
311
  const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
287
312
  const res = await fetch(`${apiUrl}/crypto/session`, {
288
313
  method: "POST",
289
- headers: { "Content-Type": "application/json" },
314
+ headers: { "Content-Type": "application/json", ...identityHeaders(identity) },
290
315
  body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
291
316
  });
317
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
292
318
  if (!res.ok) return null;
293
319
  const body = await res.json();
294
320
  const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
295
321
  if (!sessionId) return null;
296
322
  const sessionKey = await deriveSessionKey(sessionId);
297
323
  return { sessionId, sessionKey };
298
- } catch {
324
+ } catch (err) {
325
+ if (err instanceof TransitRateLimitedError) throw err;
299
326
  return null;
300
327
  }
301
328
  }
@@ -494,26 +521,46 @@ function ensureRestTransitHandshake() {
494
521
  const apiUrl = _config.apiUrl;
495
522
  _transitHandshakePromise = (async () => {
496
523
  try {
497
- for (let attempt = 0; attempt < 5; attempt++) {
524
+ const MAX_FAILURES = 5;
525
+ const BACKSTOP_MS = 2 * 6e4;
526
+ const deadline = Date.now() + BACKSTOP_MS;
527
+ let failures = 0;
528
+ let rateLimitHits = 0;
529
+ while (failures < MAX_FAILURES && Date.now() < deadline) {
498
530
  if (getTransitSession()) return;
531
+ let waitMs;
499
532
  try {
500
- const keys = await fetchServerKeys(apiUrl);
533
+ const identity = { userId: _config?.userId, tenantId: _config?.tenantId };
534
+ const keys = await fetchServerKeys(apiUrl, identity);
501
535
  if (!keys?.enabled) {
502
536
  configureTransit(false);
503
537
  return;
504
538
  }
505
- const session = await createRestTransitSession(apiUrl);
539
+ const session = await createRestTransitSession(apiUrl, identity);
506
540
  if (session && !getTransitSession()) {
507
541
  const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
508
542
  setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
509
543
  return;
510
544
  }
511
- } catch {
545
+ failures++;
546
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
547
+ } catch (err) {
548
+ if (err instanceof TransitRateLimitedError) {
549
+ const blind = Math.min(15e3 * 2 ** rateLimitHits, 6e4);
550
+ waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), 6e4) : blind;
551
+ rateLimitHits++;
552
+ console.warn(
553
+ `[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
554
+ );
555
+ } else {
556
+ failures++;
557
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
558
+ }
512
559
  }
513
- await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** attempt, 8e3)));
560
+ await new Promise((r) => setTimeout(r, waitMs));
514
561
  }
515
562
  console.error(
516
- "[AntzChat] transit handshake could not establish a session after 5 attempts \u2014 chat requests stay gated until one succeeds (server requires transit)."
563
+ "[AntzChat] transit handshake could not establish a session \u2014 chat requests stay gated until one succeeds (server requires transit)."
517
564
  );
518
565
  } finally {
519
566
  _transitHandshakePromise = null;
@@ -842,6 +889,8 @@ export {
842
889
  getSessionId,
843
890
  detectTransitAlgo,
844
891
  resetAlgoCache,
892
+ readRetryAfterMs,
893
+ TransitRateLimitedError,
845
894
  fetchServerKeys,
846
895
  generateEphemeralKey,
847
896
  createRestTransitSession,
@@ -865,4 +914,4 @@ export {
865
914
  uploadBatch,
866
915
  uploadBatchWithSlots
867
916
  };
868
- //# sourceMappingURL=chunk-WUNH3UTE.js.map
917
+ //# sourceMappingURL=chunk-U637W5MD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/compression/compress.ts","../src/crypto/transit.ts","../src/crypto/session.ts","../src/crypto/detect.ts","../src/crypto/handshake.ts","../src/errors.ts","../src/api/client.ts","../src/crypto/uuid.ts","../src/api/storage.ts"],"sourcesContent":["import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","export interface TransitEnvelope {\n v: 1;\n iv: string; // base64, 12 bytes\n tag: string; // base64, 16 bytes\n ct: string; // base64, ciphertext\n}\n\n// sessionKey is CryptoKey on Web Crypto path, Uint8Array on noble/RN path.\ntype AnySessionKey = CryptoKey | Uint8Array;\n\nfunction hasWebCrypto(): boolean {\n return typeof globalThis.crypto?.subtle !== 'undefined';\n}\n\n// ─── Encrypt ─────────────────────────────────────────────────────────────────\n\nexport async function encryptPayload(\n data: unknown,\n sessionKey: AnySessionKey,\n): Promise<TransitEnvelope> {\n const plaintext = new TextEncoder().encode(JSON.stringify(data));\n\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return encryptNoble(plaintext, sessionKey as Uint8Array);\n }\n return encryptWebCrypto(plaintext, sessionKey as CryptoKey);\n}\n\nasync function encryptWebCrypto(plaintext: Uint8Array, sessionKey: CryptoKey): Promise<TransitEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv as Uint8Array<ArrayBuffer> }, sessionKey, plaintext as Uint8Array<ArrayBuffer>);\n const ct = encrypted.slice(0, encrypted.byteLength - 16);\n const tag = encrypted.slice(encrypted.byteLength - 16);\n return { v: 1, iv: bufToB64(iv), tag: bufToB64(tag), ct: bufToB64(ct) };\n}\n\nasync function encryptNoble(plaintext: Uint8Array, sessionKey: Uint8Array): Promise<TransitEnvelope> {\n const { gcm } = await import('@noble/ciphers/aes');\n const { randomBytes } = await import('@noble/hashes/utils');\n const iv = randomBytes(12);\n const cipher = gcm(sessionKey, iv);\n const encrypted = cipher.encrypt(plaintext); // ct + 16-byte tag appended\n const ct = encrypted.slice(0, encrypted.length - 16);\n const tag = encrypted.slice(encrypted.length - 16);\n return { v: 1, iv: uint8ToB64(iv), tag: uint8ToB64(tag), ct: uint8ToB64(ct) };\n}\n\n// ─── Decrypt ─────────────────────────────────────────────────────────────────\n\nexport async function decryptPayload(\n envelope: TransitEnvelope,\n sessionKey: AnySessionKey,\n): Promise<unknown> {\n if (!hasWebCrypto() || sessionKey instanceof Uint8Array) {\n return decryptNoble(envelope, sessionKey as Uint8Array);\n }\n return decryptWebCrypto(envelope, sessionKey as CryptoKey);\n}\n\nasync function decryptWebCrypto(envelope: TransitEnvelope, sessionKey: CryptoKey): Promise<unknown> {\n const iv = b64ToBuf(envelope.iv);\n const tag = b64ToBuf(envelope.tag);\n const ct = b64ToBuf(envelope.ct);\n const combined = new Uint8Array(ct.byteLength + tag.byteLength);\n combined.set(new Uint8Array(ct), 0);\n combined.set(new Uint8Array(tag), ct.byteLength);\n const decrypted = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: new Uint8Array(iv) },\n sessionKey,\n combined,\n );\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nasync function decryptNoble(envelope: TransitEnvelope, sessionKey: Uint8Array): Promise<unknown> {\n const { gcm } = await import('@noble/ciphers/aes');\n const iv = base64ToUint8(envelope.iv);\n const tag = base64ToUint8(envelope.tag);\n const ct = base64ToUint8(envelope.ct);\n const combined = new Uint8Array(ct.length + tag.length);\n combined.set(ct, 0);\n combined.set(tag, ct.length);\n const cipher = gcm(sessionKey, iv);\n const decrypted = cipher.decrypt(combined);\n return JSON.parse(new TextDecoder().decode(decrypted));\n}\n\nexport function isTransitEnvelope(v: unknown): v is TransitEnvelope {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as any).v === 1 &&\n typeof (v as any).iv === 'string' &&\n typeof (v as any).tag === 'string' &&\n typeof (v as any).ct === 'string'\n );\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction uint8ToB64(bytes: Uint8Array): string {\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf;\n}\n","import type { TransitAlgo } from './detect.js';\n\ninterface TransitSession {\n sessionKey: CryptoKey | Uint8Array; // CryptoKey on Web Crypto, Uint8Array on noble/RN\n algo: TransitAlgo;\n sessionId: string;\n enabled: boolean;\n}\n\n// All state lives on globalThis so Turbopack <locals> module splits and any\n// other bundler that creates multiple instances of this module still share a\n// single source of truth. The symbol key prevents accidental collisions.\nconst _KEY = Symbol.for('__antz_chat_transit__');\n\ninterface TransitState {\n session: TransitSession | null;\n sessionEverEstablished: boolean;\n readyResolve: (() => void) | null;\n readyPromise: Promise<void> | null;\n transitConfigured: boolean | null;\n /** Edge-triggered listeners fired each time a session (re-)establishes. Used\n * by the socket emitters to re-send fire-and-forget state (join_room) that\n * was dropped while the handshake was settling. On globalThis like the rest\n * of transit state so bundler module-duplication can't split the set. */\n readyListeners: Set<() => void>;\n}\n\nfunction getState(): TransitState {\n const g = globalThis as any;\n if (!g[_KEY]) {\n g[_KEY] = {\n session: null,\n sessionEverEstablished: false,\n readyResolve: null,\n readyPromise: null,\n transitConfigured: null,\n readyListeners: new Set<() => void>(),\n } satisfies TransitState;\n }\n return g[_KEY] as TransitState;\n}\n\nexport function configureTransit(enabled: boolean): void {\n const s = getState();\n s.transitConfigured = enabled;\n if (!enabled) {\n // Transit disabled — resolve immediately so HTTP requests don't block\n s.readyResolve?.();\n s.readyResolve = null;\n }\n}\n\n/**\n * Set the transit-required flag ONLY if nothing has configured it yet. Used by\n * connectSocket() for socket-only consumers that never call initApiClient().\n * Must not override an explicit configureTransit(false) — that flag can carry\n * the authoritative \"server reported transit disabled\" signal, and re-gating\n * after it would wedge every request.\n */\nexport function configureTransitIfUnset(enabled: boolean): void {\n if (getState().transitConfigured === null) configureTransit(enabled);\n}\n\nexport function waitForTransitReady(): Promise<void> {\n const s = getState();\n // Not configured yet or disabled — resolve immediately\n if (!s.transitConfigured) return Promise.resolve();\n // Already have an active session — resolve immediately\n if (s.session) return Promise.resolve();\n // Transit is required but there is no session (first startup, or a socket\n // disconnect cleared it). Block until the handshake (re-)establishes one.\n // Resolving early here would let the request go out as plaintext and the\n // server rejects it with 403 \"Transit encryption required\". sessionEverEstablished\n // is deliberately NOT consulted — a stale `true` from a prior session must not\n // unblock a now-sessionless request. The request interceptor kicks a fresh\n // handshake before awaiting this, so the promise is guaranteed a resolver.\n if (!s.readyPromise) {\n s.readyPromise = new Promise<void>((resolve) => {\n s.readyResolve = resolve;\n });\n }\n return s.readyPromise;\n}\n\nexport function setTransitSession(session: TransitSession): void {\n const s = getState();\n s.session = session;\n s.sessionEverEstablished = true;\n // Resolve any pending HTTP requests waiting for the session key\n s.readyResolve?.();\n s.readyResolve = null;\n // Notify edge-triggered listeners (join_room re-flush, etc.)\n s.readyListeners.forEach((fn) => { try { fn(); } catch { /* listener must not break transit */ } });\n}\n\n/**\n * Register a listener fired every time a transit session (re-)establishes via\n * setTransitSession(). For re-sending fire-and-forget socket state that\n * secureEmit dropped during the handshake gap. Returns an unsubscribe fn.\n */\nexport function onTransitReady(listener: () => void): () => void {\n const s = getState();\n s.readyListeners.add(listener);\n return () => { s.readyListeners.delete(listener); };\n}\n\n/**\n * Wait until a transit session is available OR `timeoutMs` elapses, whichever\n * comes first. Never rejects. Returns true if it is now safe to proceed\n * (session present, or transit not required), false if it timed out with\n * transit still required and no session — the caller decides what a false\n * means (REST: fail the request loudly; socket emit: throw), so that a\n * handshake that never completes surfaces as a retryable error instead of an\n * infinite pending request that react-query can never recover.\n */\nexport async function awaitTransitReadyOr(timeoutMs: number): Promise<boolean> {\n const s = getState();\n if (!s.transitConfigured || s.session) return true;\n await Promise.race([\n waitForTransitReady(),\n new Promise<void>((r) => setTimeout(r, timeoutMs)),\n ]);\n const now = getState();\n return Boolean(now.session) || now.transitConfigured !== true;\n}\n\nexport function getTransitSession(): TransitSession | null {\n return getState().session;\n}\n\nexport function clearTransitSession(): void {\n const s = getState();\n s.session = null;\n // Reset sessionEverEstablished too. Leaving it `true` made waitForTransitReady()\n // (which trusted the flag) and isTransitEnabled() (which checks the live session)\n // permanently disagree after any clear-following-success: requests then went out\n // unencrypted and 403'd (\"Transit encryption required\") forever, with no path to\n // recovery. The ready promise is recreated lazily on the next waitForTransitReady().\n s.sessionEverEstablished = false;\n s.readyPromise = null;\n s.readyResolve = null;\n}\n\nexport function isTransitEnabled(): boolean {\n return getState().session?.enabled === true;\n}\n\n/**\n * True when transit encryption is *required* — i.e. the SDK was configured with\n * transitEncryption and the server has NOT authoritatively told us it is off\n * (which is the only thing that sets transitConfigured back to false).\n *\n * This is the correct signal for \"must this payload be encrypted?\". It is\n * deliberately distinct from isTransitEnabled() (which is \"is a live session\n * key available right now?\"): the gap between the two — required but no key —\n * is a handshake-in-progress / reconnect window where callers must WAIT or\n * FAIL, never fall through to plaintext.\n */\nexport function isTransitRequired(): boolean {\n return getState().transitConfigured === true;\n}\n\nexport function getSessionKey(): CryptoKey | Uint8Array | null {\n return getState().session?.sessionKey ?? null;\n}\n\n// Returns sessionId for the x-transit-session header sent with HTTP requests.\nexport function getSessionId(): string | null {\n return getState().session?.sessionId ?? null;\n}\n","export type TransitAlgo = 'x25519' | 'p256';\n\nlet _cached: TransitAlgo | null = null;\n\n// Probes Web Crypto API for X25519 support once; caches result for the session.\n// RN and Node callers never call this — they always get X25519 via @noble.\nexport async function detectTransitAlgo(): Promise<TransitAlgo> {\n if (_cached) return _cached;\n\n try {\n await globalThis.crypto.subtle.generateKey(\n { name: 'X25519' } as any,\n false,\n ['deriveKey'],\n );\n _cached = 'x25519';\n } catch {\n _cached = 'p256';\n }\n\n return _cached;\n}\n\nexport function getCachedAlgo(): TransitAlgo | null {\n return _cached;\n}\n\nexport function resetAlgoCache(): void {\n _cached = null;\n}\n","import type { TransitAlgo } from './detect.js';\nimport { detectTransitAlgo } from './detect.js';\n\n/**\n * Caller identity attached to the pre-auth transit handshake requests.\n *\n * GET /crypto/pubkey and POST /crypto/session run BEFORE a transit session (and\n * therefore before the authenticated axios client) exists, so they bypass the\n * request interceptor that normally adds these headers. Without them the server\n * can only rate-limit these two routes by client IP — which means every user\n * behind one NAT, office proxy or ALB shares a single bucket, and one client's\n * reload loop 429s everyone else.\n *\n * These values are NOT used for authentication: the endpoints are unauthenticated\n * by design and the server treats the headers as a fairness hint only, with a\n * per-IP ceiling underneath as the real abuse limit. Sending them is therefore\n * safe, optional, and backward compatible — an older SDK that omits them simply\n * falls back to the shared per-IP bucket.\n */\nexport interface TransitIdentity {\n /** External user id — same value sent as x-user-id on authenticated requests. */\n userId?: string;\n /** Tenant id — same value sent as X-Tenant-ID on authenticated requests. */\n tenantId?: string;\n}\n\n/**\n * How long the server asked us to wait, in ms, from a 429's Retry-After.\n *\n * The chat server runs TWO named rate-limit layers, and @nestjs/throttler\n * suffixes its headers with the throttler name unless that name is literally\n * \"default\". So a 429 carries `Retry-After-identity` or `Retry-After-ip`, not a\n * bare `Retry-After`. Older servers and intermediary proxies may still send the\n * bare name, so all three are read; when more than one is present the LONGER\n * wait wins, since retrying before the slower bucket drains just earns another\n * 429.\n *\n * Returns undefined when no variant is readable — in a browser that also\n * happens when the server omits these from Access-Control-Expose-Headers, in\n * which case callers must fall back to their own backoff.\n */\nexport function readRetryAfterMs(headers: Headers): number | undefined {\n const parse = (raw: string | null): number | undefined => {\n if (!raw) return undefined;\n const secs = Number(raw);\n if (Number.isFinite(secs)) return Math.max(0, secs * 1000);\n const when = Date.parse(raw);\n return Number.isNaN(when) ? undefined : Math.max(0, when - Date.now());\n };\n const found = ['Retry-After-identity', 'Retry-After-ip', 'Retry-After']\n .map((name) => parse(headers.get(name)))\n .filter((ms): ms is number => ms != null);\n return found.length > 0 ? Math.max(...found) : undefined;\n}\n\n/**\n * Thrown when the transit handshake is rate-limited (HTTP 429).\n *\n * Distinct from a generic failure because the correct response differs: a 429\n * means \"wait\", not \"this is broken\", so callers must not burn their retry\n * budget on it and should honour `retryAfterMs` when the server supplied it.\n */\nexport class TransitRateLimitedError extends Error {\n readonly retryAfterMs?: number;\n constructor(retryAfterMs?: number) {\n super('[AntzChat] transit handshake rate-limited (429)');\n this.name = 'TransitRateLimitedError';\n this.retryAfterMs = retryAfterMs;\n }\n}\n\n/** Build the identity headers, omitting whichever values the host app did not configure. */\nfunction identityHeaders(identity?: TransitIdentity): Record<string, string> {\n const headers: Record<string, string> = {};\n if (identity?.userId) headers['x-user-id'] = identity.userId;\n if (identity?.tenantId) headers['X-Tenant-ID'] = identity.tenantId;\n return headers;\n}\n\nexport interface ServerPublicKeys {\n x25519: string; // base64\n p256: string; // base64\n enabled: boolean;\n}\n\n// Returns true when the Web Crypto API is available (browser, Node 18+).\n// Hermes (React Native) does not expose crypto.subtle — use noble fallback.\nfunction hasWebCrypto(): boolean {\n return typeof globalThis.crypto?.subtle !== 'undefined';\n}\n\n// ─── Fetch ───────────────────────────────────────────────────────────────────\n\n// Fetches server public keys + enabled flag. Called once per init.\n// Unwraps the server's standard { success, data } envelope if present.\n// `identity` is optional and only affects server-side rate-limit bucketing —\n// see TransitIdentity. Omitting it preserves the previous (per-IP) behaviour.\nexport async function fetchServerKeys(\n apiUrl: string,\n identity?: TransitIdentity,\n): Promise<ServerPublicKeys> {\n const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });\n if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));\n if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);\n const body = await res.json() as any;\n return (body?.data ?? body) as ServerPublicKeys;\n}\n\n// ─── Core key generation ──────────────────────────────────────────────────────\n\n// Generates an ephemeral key pair and returns the public key (base64) plus a\n// bound deriveSessionKey closure that captures the private key.\n// Used by both the HTTPS handshake path and the socket handshake path.\nexport async function generateEphemeralKey(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<unknown> }> {\n if (hasWebCrypto()) {\n return generateWebCryptoEphemeralKey(algo, serverKeys);\n }\n return generateNobleEphemeralKey(serverKeys);\n}\n\n// ─── New HTTPS handshake (browser, Node 18+, React Native via noble) ─────────\n\n// Self-contained REST key exchange — no socket required.\n// 1. Fetch server public keys\n// 2. Generate ephemeral key pair\n// 3. POST /crypto/session { ephemeralPub, algo } → { sessionId }\n// 4. Derive session key locally via HKDF\n// Returns null when the server doesn't support the endpoint (old server) so\n// callers can fall back to the socket handshake path gracefully.\nexport async function createRestTransitSession(\n apiUrl: string,\n identity?: TransitIdentity,\n): Promise<{ sessionId: string; sessionKey: CryptoKey | Uint8Array } | null> {\n try {\n const serverKeys = await fetchServerKeys(apiUrl, identity);\n if (!serverKeys.enabled) return null;\n\n const algo = hasWebCrypto() ? await detectTransitAlgo() : 'x25519';\n const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);\n\n const res = await fetch(`${apiUrl}/crypto/session`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...identityHeaders(identity) },\n body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo }),\n });\n // A 429 must NOT collapse into `null`: null means \"old server, fall back to\n // the socket handshake\", whereas a rate limit means \"this endpoint is fine,\n // wait and retry\". Conflating them makes the caller abandon a working path.\n if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));\n if (!res.ok) return null; // old server without the endpoint — caller falls back\n\n const body = await res.json() as any;\n const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;\n if (!sessionId) return null;\n\n const sessionKey = await deriveSessionKey(sessionId) as CryptoKey | Uint8Array;\n return { sessionId, sessionKey };\n } catch (err) {\n // Rate limiting is a distinct, actionable condition — rethrow so the caller\n // can wait the requested interval. Everything else stays a soft null.\n if (err instanceof TransitRateLimitedError) throw err;\n return null;\n }\n}\n\n// ─── Socket handshake entry point (backward compat) ──────────────────────────\n\n// Performs the client side of the ECDH handshake via socket auth.\n// Injects ephemeralPub + algo into socketHandshakeAuth (mutates in place).\n// Returns a bound deriveSessionKey closure called after transit_session arrives.\n// Kept for old-server compatibility — new path uses createRestTransitSession.\nexport async function performHandshake(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n socketHandshakeAuth: Record<string, unknown>,\n): Promise<(sessionId: string) => Promise<unknown>> {\n const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);\n socketHandshakeAuth['transitEphemeralPub'] = ephemeralPubB64;\n socketHandshakeAuth['transitAlgo'] = algo;\n return deriveSessionKey;\n}\n\n// ─── Web Crypto path (browser, Node 18+) ─────────────────────────────────────\n\nasync function generateWebCryptoEphemeralKey(\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<CryptoKey> }> {\n const ephemeral = await globalThis.crypto.subtle.generateKey(\n algo === 'x25519'\n ? { name: 'X25519' }\n : { name: 'ECDH', namedCurve: 'P-256' } as any,\n false,\n ['deriveBits'],\n );\n\n const pubRaw = await globalThis.crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey);\n const ephemeralPriv = (ephemeral as CryptoKeyPair).privateKey;\n\n return {\n ephemeralPubB64: bufToB64(pubRaw),\n deriveSessionKey: (sessionId: string) =>\n deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId),\n };\n}\n\nasync function deriveWebCryptoSessionKey(\n ephemeralPriv: CryptoKey,\n algo: TransitAlgo,\n serverKeys: ServerPublicKeys,\n sessionId: string,\n): Promise<CryptoKey> {\n const serverPubRaw = b64ToBuf(algo === 'x25519' ? serverKeys.x25519 : serverKeys.p256);\n const keyAlgoParams = algo === 'x25519' ? { name: 'X25519' } : { name: 'ECDH', namedCurve: 'P-256' };\n\n const serverPubKey = await globalThis.crypto.subtle.importKey('raw', serverPubRaw, keyAlgoParams as any, false, []);\n const sharedBits = await globalThis.crypto.subtle.deriveBits(\n { name: algo === 'x25519' ? 'X25519' : 'ECDH', public: serverPubKey } as any,\n ephemeralPriv,\n 256,\n );\n const hkdfKey = await globalThis.crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);\n const salt = new TextEncoder().encode(sessionId);\n const info = new TextEncoder().encode('antz-transit-v1');\n\n return globalThis.crypto.subtle.deriveKey(\n { name: 'HKDF', hash: 'SHA-256', salt, info },\n hkdfKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\n// ─── Noble path (React Native / Hermes) ──────────────────────────────────────\n\nasync function generateNobleEphemeralKey(\n serverKeys: ServerPublicKeys,\n): Promise<{ ephemeralPubB64: string; deriveSessionKey: (sessionId: string) => Promise<Uint8Array> }> {\n const { x25519 } = await import('@noble/curves/ed25519');\n const { hkdf } = await import('@noble/hashes/hkdf');\n const { sha256 } = await import('@noble/hashes/sha256');\n const { randomBytes } = await import('@noble/hashes/utils');\n\n const ephemeralPriv = randomBytes(32);\n const ephemeralPub = x25519.getPublicKey(ephemeralPriv);\n const serverPubBytes = base64ToUint8(serverKeys.x25519);\n\n return {\n ephemeralPubB64: uint8ToBase64(ephemeralPub),\n deriveSessionKey: (sessionId: string): Promise<Uint8Array> => {\n const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);\n const salt = new TextEncoder().encode(sessionId);\n const info = new TextEncoder().encode('antz-transit-v1');\n return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32) as Uint8Array);\n },\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction bufToB64(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction b64ToBuf(b64: string): ArrayBuffer {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf.buffer;\n}\n\nfunction uint8ToBase64(bytes: Uint8Array): string {\n let str = '';\n bytes.forEach(b => { str += String.fromCharCode(b); });\n return btoa(str);\n}\n\nfunction base64ToUint8(b64: string): Uint8Array {\n const bin = atob(b64);\n const buf = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n return buf;\n}\n","import { isAxiosError } from 'axios';\nimport { isTransitEnvelope } from './crypto/transit.js';\n\n// ─── Base error class ─────────────────────────────────────────────────────────\n\nexport class AntzChatError extends Error {\n readonly code: string;\n readonly retryable: boolean;\n readonly context?: Record<string, unknown>;\n\n constructor(\n code: string,\n message: string,\n retryable = false,\n context?: Record<string, unknown>,\n ) {\n super(message);\n this.name = 'AntzChatError';\n this.code = code;\n this.retryable = retryable;\n this.context = context;\n // Maintain proper prototype chain in transpiled environments\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Semantic subclasses ──────────────────────────────────────────────────────\n\n/** Thrown on 401 (after refresh also fails) or when no refresh token exists. */\nexport class AntzChatAuthError extends AntzChatError {\n constructor(message: string, code = 'AUTH_FAILED', context?: Record<string, unknown>) {\n super(code, message, false, context);\n this.name = 'AntzChatAuthError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 400 / 422 — bad input, validation failure. */\nexport class AntzChatValidationError extends AntzChatError {\n /** Server-returned field error array (when the server sends message as string[]). */\n readonly fields?: string[];\n\n constructor(message: string | string[], context?: Record<string, unknown>) {\n const msg = Array.isArray(message) ? message.join('; ') : message;\n super('VALIDATION_ERROR', msg, false, context);\n this.name = 'AntzChatValidationError';\n this.fields = Array.isArray(message) ? message : undefined;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on network failures, timeouts, socket disconnections, and queue overflow. retryable = true. */\nexport class AntzChatNetworkError extends AntzChatError {\n constructor(message: string, code = 'NETWORK_ERROR', context?: Record<string, unknown>) {\n super(code, message, true, context);\n this.name = 'AntzChatNetworkError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 403 — insufficient permissions. */\nexport class AntzChatPermissionError extends AntzChatError {\n constructor(message: string, context?: Record<string, unknown>) {\n super('PERMISSION_DENIED', message, false, context);\n this.name = 'AntzChatPermissionError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Thrown on 5xx or other unexpected server errors. retryable = true. */\nexport class AntzChatServerError extends AntzChatError {\n readonly httpStatus?: number;\n\n constructor(message: string, httpStatus?: number, context?: Record<string, unknown>) {\n super('SERVER_ERROR', message, true, context);\n this.name = 'AntzChatServerError';\n this.httpStatus = httpStatus;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─── Error code reference ─────────────────────────────────────────────────────\n//\n// Code Class Source\n// ───────────────────── ──────────────────────── ─────────────────────────\n// AUTH_FAILED AntzChatAuthError 401 after refresh fails\n// SESSION_EXPIRED AntzChatAuthError 401, no refresh token\n// PERMISSION_DENIED AntzChatPermissionError 403\n// VALIDATION_ERROR AntzChatValidationError 400 / 422\n// NOT_FOUND AntzChatServerError 404\n// RATE_LIMITED AntzChatNetworkError 429\n// NETWORK_ERROR AntzChatNetworkError No response / conn failure\n// SOCKET_TIMEOUT AntzChatNetworkError ACK timeout / reconnect timeout\n// SOCKET_NOT_CONNECTED AntzChatNetworkError withAck when socket is down\n// SEND_QUEUE_FULL AntzChatNetworkError Queue overflow (>100 msgs)\n// MESSAGE_DROPPED AntzChatNetworkError Queue TTL expired (30s)\n// TRANSIT_MISMATCH AntzChatError SDK/server encryption config mismatch\n// SERVER_ERROR AntzChatServerError 5xx or unknown HTTP error\n\n// ─── REST error normaliser ────────────────────────────────────────────────────\n\n/**\n * Converts a raw axios error (or any unknown throw) into a typed AntzChatError.\n *\n * Call site: client.ts response interceptor — runs AFTER transit decryption,\n * so error.response.data is always plaintext by the time this function sees it.\n * If decryption itself failed, error.response.data remains the raw encrypted\n * envelope — detected via isTransitEnvelope() and noted in context.\n */\nexport function normalizeAxiosError(error: unknown): AntzChatError {\n if (error instanceof AntzChatError) return error;\n\n if (isAxiosError(error)) {\n const status = error.response?.status;\n const body = error.response?.data;\n\n // Detect if decryption failed — body is still an encrypted envelope\n const decryptionFailed = body != null && isTransitEnvelope(body);\n\n const rawMessage: string | string[] | undefined = decryptionFailed\n ? undefined\n : (body?.message ?? undefined);\n\n const message: string =\n (Array.isArray(rawMessage) ? rawMessage.join('; ') : rawMessage) ||\n error.message ||\n 'Request failed';\n\n const ctx: Record<string, unknown> = {\n ...(status != null && { httpStatus: status }),\n ...(body?.path != null && { path: body.path }),\n ...(body?.error != null && { serverError: body.error }),\n ...(error.code != null && { axiosCode: error.code }),\n ...(decryptionFailed && { decryptionFailed: true, note: 'Transit decryption failed — server error body is an encrypted envelope' }),\n };\n\n // No response at all — network/timeout failure\n if (!error.response) {\n return new AntzChatNetworkError(message || 'Network error', 'NETWORK_ERROR', ctx);\n }\n\n if (status === 401) {\n // AUTH_FAILED is used when a refresh was attempted but failed (set by interceptor).\n // SESSION_EXPIRED is the default: 401 with no prior retry = token simply expired.\n const code = (error.config as any)?._retry ? 'AUTH_FAILED' : 'SESSION_EXPIRED';\n return new AntzChatAuthError(message, code, ctx);\n }\n if (status === 403) return new AntzChatPermissionError(message, ctx);\n if (status === 400 || status === 422) {\n return new AntzChatValidationError(\n Array.isArray(rawMessage) ? rawMessage : message,\n ctx,\n );\n }\n if (status === 404) return new AntzChatServerError(message, 404, ctx);\n if (status === 429) return new AntzChatNetworkError(message, 'RATE_LIMITED', ctx);\n if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);\n\n return new AntzChatServerError(message, status, ctx);\n }\n\n const msg = error instanceof Error ? error.message : String(error);\n return new AntzChatError('UNKNOWN_ERROR', msg, false);\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, awaitTransitReadyOr, configureTransit, setTransitSession, getTransitSession } from '../crypto/session.js';\nimport { createRestTransitSession, fetchServerKeys, TransitRateLimitedError } from '../crypto/handshake.js';\nimport { detectTransitAlgo } from '../crypto/detect.js';\nimport { normalizeAxiosError, AntzChatNetworkError } from '../errors.js';\n\n// Hard ceiling on how long a single request will block waiting for the transit\n// handshake. A legitimate cold-start handshake resolves in well under this even\n// on a slow link (TransitGate already spent ~6s, establishTransit keeps\n// retrying). Past this we FAIL the request with a retryable error rather than\n// leave it pending forever — react-query cannot retry / refetch-on-focus a\n// request that never settles, so an unbounded wait here is an unrecoverable\n// silent hang. Each failed+retried request also re-arms ensureRestTransitHandshake.\nconst TRANSIT_GATE_MAX_WAIT_MS = 30_000;\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\nlet _avatarSent = false;\n// In-flight transit handshake promise — shared between initApiClient and connectSocket\n// so they never fire two concurrent HTTPS handshakes for the same session.\nlet _transitHandshakePromise: Promise<void> | null = null;\n// Gate the request interceptor until the SDK has resolved a token (async\n// authProvider) and, where wired, the transit handshake. Set by the SDK\n// provider; the interceptor awaits it before attaching the Authorization\n// header so early requests (e.g. useConversations' initial fetch) don't race\n// ahead unauthenticated.\nlet _authReadyPromise: Promise<unknown> | null = null;\n\nexport function getTransitHandshakePromise(): Promise<void> | null {\n return _transitHandshakePromise;\n}\n\nexport function setAuthReadyPromise(promise: Promise<unknown> | null): void {\n _authReadyPromise = promise;\n}\n\n// True once initApiClient() has run and its config has not been torn down by a\n// subsequent disconnectSocket(). Lets the SDK provider detect the case where a\n// React remount skipped re-init (its key was unchanged) but disconnectSocket()\n// had nulled _config in between — leaving the request interceptor unable to see\n// transitEncryption and firing every request as unencrypted plaintext.\nexport function isApiClientConfigured(): boolean {\n return _config !== null;\n}\n\n// (Re-)kick the HTTPS transit handshake. Idempotent: no-ops when transit is\n// disabled, a session already exists, or an attempt is already in flight.\n// Called both at init and from the request interceptor when a request is about\n// to block on waitForTransitReady() with no session — e.g. after a socket\n// disconnect cleared the session and nothing else re-established it.\n//\n// It only calls configureTransit(false) — which un-gates the interceptor and\n// lets requests go out as PLAINTEXT — when the server itself reports transit\n// disabled (GET /crypto/pubkey → enabled:false, i.e. an old server). A transient\n// failure of POST /crypto/session (network blip, rate limit, 5xx) must NOT\n// disable transit: the server still requires it, so plaintext would just 403.\n// Instead we retry with backoff; waitForTransitReady() keeps requests pending\n// and they dispatch the moment a retry sets the session.\nexport function ensureRestTransitHandshake(): void {\n if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;\n const apiUrl = _config.apiUrl;\n _transitHandshakePromise = (async () => {\n try {\n // A 429 is \"wait\", not \"broken\", so it must NOT consume the attempt\n // budget — otherwise a rate-limited client exhausts 5 attempts in a few\n // seconds and gives up on an endpoint that was working fine. Failures are\n // counted separately from rate-limit hits, and the loop is additionally\n // bounded by wall-clock time so a persistently limited server cannot keep\n // it running forever.\n const MAX_FAILURES = 5;\n const BACKSTOP_MS = 2 * 60_000;\n const deadline = Date.now() + BACKSTOP_MS;\n let failures = 0;\n let rateLimitHits = 0;\n\n while (failures < MAX_FAILURES && Date.now() < deadline) {\n if (getTransitSession()) return;\n let waitMs: number;\n try {\n // Identity is sent so the server can rate-limit these pre-auth routes\n // per user rather than per IP (see TransitIdentity in handshake.ts).\n const identity = { userId: _config?.userId, tenantId: _config?.tenantId };\n const keys = await fetchServerKeys(apiUrl, identity);\n if (!keys?.enabled) {\n configureTransit(false); // server genuinely doesn't want transit\n return;\n }\n const session = await createRestTransitSession(apiUrl, identity);\n if (session && !getTransitSession()) {\n const algo = typeof globalThis.crypto?.subtle !== 'undefined'\n ? await detectTransitAlgo()\n : 'x25519';\n setTransitSession({ sessionKey: session.sessionKey as CryptoKey, algo, sessionId: session.sessionId, enabled: true });\n return;\n }\n // Reachable when the server returned no session but did not throw\n // (e.g. an old server without the endpoint) — treat as a failure.\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n } catch (err) {\n if (err instanceof TransitRateLimitedError) {\n // Honour the server's own figure when it sent one (clamped to a\n // sane 1-60s), else escalate blind since the window is unknown.\n const blind = Math.min(15_000 * 2 ** rateLimitHits, 60_000);\n waitMs = err.retryAfterMs != null\n ? Math.min(Math.max(err.retryAfterMs, 1_000), 60_000)\n : blind;\n rateLimitHits++;\n console.warn(\n `[AntzChat] transit handshake rate-limited (429) — retrying in ${Math.round(waitMs / 1000)}s` +\n `${err.retryAfterMs != null ? ' (per Retry-After)' : ''}.`,\n );\n } else {\n failures++;\n waitMs = Math.min(500 * 2 ** failures, 8_000);\n }\n }\n await new Promise((r) => setTimeout(r, waitMs));\n }\n console.error(\n '[AntzChat] transit handshake could not establish a session — ' +\n \"chat requests stay gated until one succeeds (server requires transit).\",\n );\n } finally {\n _transitHandshakePromise = null;\n }\n })();\n}\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // Kick off the HTTPS transit handshake immediately so REST calls that fire\n // before connectSocket (e.g. getMe() right after initApiClient) are not\n // blocked indefinitely. Store the promise so connectSocket can await it\n // instead of firing a duplicate handshake.\n ensureRestTransitHandshake();\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n // Wait for the SDK's auth (and, where wired, transit) gate before reading\n // the token — otherwise a request fired during boot goes out with no\n // Authorization header.\n if (_authReadyPromise) await _authReadyPromise;\n\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key before sending any request. The server\n // enforces transit encryption independent of auth (e.g. GET /app/config\n // fires before the async authProvider token resolves) — gating this on\n // `token` let those pre-auth requests race ahead of the handshake and get\n // rejected with 403 \"Transit encryption required\".\n if (_config?.transitEncryption) {\n // If the session is gone (socket disconnect cleared it, first boot still\n // pending), make sure a handshake is running before we block — otherwise\n // the wait could hang with nothing to resolve it.\n if (!getTransitSession()) ensureRestTransitHandshake();\n const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);\n if (!ready) {\n // Handshake still hasn't produced a session. Do NOT send plaintext\n // (server requires transit); fail loudly instead so the error surfaces\n // in the UI and react-query's retry re-drives the handshake.\n throw new AntzChatNetworkError(\n 'Secure channel to chat server not established — request not sent. It will retry automatically.',\n 'TRANSIT_NOT_READY',\n { url: req.url },\n );\n }\n }\n\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(normalizeAxiosError(error));\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(normalizeAxiosError(error));\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","// crypto.randomUUID() doesn't exist in React Native's Hermes engine.\n// Fall back to a RFC 4122 v4 UUID built from Math.random() when unavailable.\nexport function generateUUID(): string {\n if (typeof globalThis.crypto?.randomUUID === 'function') {\n return globalThis.crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n CompletedPart,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, PlatformUploadPartFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\nimport { generateUUID } from '../crypto/uuid.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string; clientIndex?: number }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().post(`/storage/files/${fileId}/delete`);\n },\n\n async completeMultipartUpload(\n fileId: string,\n uploadId: string,\n parts: CompletedPart[],\n ): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(\n `/storage/multipart/complete/${fileId}`,\n { uploadId, parts },\n );\n return data;\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\nasync function runMultipartUpload(\n presigned: PresignedUrlResponse,\n file: UploadableFile,\n platformUploadPartFn: PlatformUploadPartFn,\n onProgress?: (pct: number) => void,\n): Promise<FileResponse> {\n const { multipart } = presigned;\n if (!multipart) throw new Error('No multipart info on presigned response');\n\n const CONCURRENCY = 3;\n const completedParts: CompletedPart[] = [];\n const partProgress: Record<number, number> = {};\n\n multipart.partUrls.forEach(({ partNumber }) => { partProgress[partNumber] = 0; });\n\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(partProgress);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg * 0.95));\n };\n\n const uploadPart = async (partNumber: number, uploadUrl: string, method: 'PUT' | 'POST'): Promise<void> => {\n const offset = (partNumber - 1) * multipart.chunkSize;\n const end = Math.min(offset + multipart.chunkSize, file.size);\n const blob = await fetch(file.uri).then((r) => r.blob());\n const slice = blob.slice(offset, end);\n\n const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {\n partProgress[partNumber] = pct;\n reportProgress();\n }, method);\n\n completedParts.push({ partNumber, etag });\n partProgress[partNumber] = 100;\n reportProgress();\n };\n\n for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {\n const batch = multipart.partUrls.slice(i, i + CONCURRENCY);\n const results = await Promise.allSettled(\n batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? 'PUT')),\n );\n const failed = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined;\n if (failed) throw failed.reason;\n }\n\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n const fileResponse = await storageApi.completeMultipartUpload(\n presigned.fileId,\n multipart.uploadId,\n completedParts,\n );\n onProgress?.(100);\n return fileResponse;\n}\n\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID and a clientIndex.\n // clientIndex is sent to the server and echoed back in both urls and errors,\n // giving us a reliable position mapping regardless of which files fail.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f, clientIndex }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n clientIndex,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // Use the echoed clientIndex to identify which original slots failed.\n // This is reliable even for same-named files and any failure pattern.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e) => {\n const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);\n const slotId = slotted[idx]?.slotId;\n if (slotId) failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n // Map each presigned URL back to its original slot via clientIndex.\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n // Resolve the original slot via echoed clientIndex; fall back to position\n // in urls[] only if the server didn't echo it (older server version).\n const originalIdx = presigned.clientIndex ?? idx;\n const { file, slotId } = slotted[originalIdx];\n progressMap[originalIdx] = 0;\n try {\n let fileResponse: FileResponse;\n if (presigned.multipart && platformUploadPartFn) {\n fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {\n progressMap[originalIdx] = pct;\n reportProgress();\n });\n } else {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[originalIdx] = Math.round(pct * 0.9);\n reportProgress();\n });\n fileResponse = await storageApi.confirmUpload(presigned.fileId);\n }\n progressMap[originalIdx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => generateUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n platformUploadPartFn?: PlatformUploadPartFn,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);\n}\n"],"mappings":"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","names":["hasWebCrypto","bufToB64","b64ToBuf","base64ToUint8"]}
package/dist/index.cjs CHANGED
@@ -111,6 +111,7 @@ __export(src_exports, {
111
111
  HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
112
112
  MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
113
113
  MENTION_ALL_ID: () => MENTION_ALL_ID,
114
+ TransitRateLimitedError: () => TransitRateLimitedError,
114
115
  appConfigApi: () => appConfigApi,
115
116
  authApi: () => authApi,
116
117
  buildMentionText: () => buildMentionText,
@@ -143,6 +144,7 @@ __export(src_exports, {
143
144
  onSocketStatus: () => onSocketStatus,
144
145
  parseMentions: () => parseMentions,
145
146
  performHandshake: () => performHandshake,
147
+ readRetryAfterMs: () => readRetryAfterMs,
146
148
  reconnectSocket: () => reconnectSocket,
147
149
  refreshSocketAuth: () => refreshSocketAuth,
148
150
  renderMentionParts: () => renderMentionParts,
@@ -492,11 +494,36 @@ function resetAlgoCache() {
492
494
  }
493
495
 
494
496
  // src/crypto/handshake.ts
497
+ function readRetryAfterMs(headers) {
498
+ const parse = (raw) => {
499
+ if (!raw) return void 0;
500
+ const secs = Number(raw);
501
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
502
+ const when = Date.parse(raw);
503
+ return Number.isNaN(when) ? void 0 : Math.max(0, when - Date.now());
504
+ };
505
+ const found = ["Retry-After-identity", "Retry-After-ip", "Retry-After"].map((name) => parse(headers.get(name))).filter((ms) => ms != null);
506
+ return found.length > 0 ? Math.max(...found) : void 0;
507
+ }
508
+ var TransitRateLimitedError = class extends Error {
509
+ constructor(retryAfterMs) {
510
+ super("[AntzChat] transit handshake rate-limited (429)");
511
+ this.name = "TransitRateLimitedError";
512
+ this.retryAfterMs = retryAfterMs;
513
+ }
514
+ };
515
+ function identityHeaders(identity) {
516
+ const headers = {};
517
+ if (identity?.userId) headers["x-user-id"] = identity.userId;
518
+ if (identity?.tenantId) headers["X-Tenant-ID"] = identity.tenantId;
519
+ return headers;
520
+ }
495
521
  function hasWebCrypto2() {
496
522
  return typeof globalThis.crypto?.subtle !== "undefined";
497
523
  }
498
- async function fetchServerKeys(apiUrl) {
499
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
524
+ async function fetchServerKeys(apiUrl, identity) {
525
+ const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });
526
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
500
527
  if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
501
528
  const body = await res.json();
502
529
  return body?.data ?? body;
@@ -507,24 +534,26 @@ async function generateEphemeralKey(algo, serverKeys) {
507
534
  }
508
535
  return generateNobleEphemeralKey(serverKeys);
509
536
  }
510
- async function createRestTransitSession(apiUrl) {
537
+ async function createRestTransitSession(apiUrl, identity) {
511
538
  try {
512
- const serverKeys = await fetchServerKeys(apiUrl);
539
+ const serverKeys = await fetchServerKeys(apiUrl, identity);
513
540
  if (!serverKeys.enabled) return null;
514
541
  const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
515
542
  const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
516
543
  const res = await fetch(`${apiUrl}/crypto/session`, {
517
544
  method: "POST",
518
- headers: { "Content-Type": "application/json" },
545
+ headers: { "Content-Type": "application/json", ...identityHeaders(identity) },
519
546
  body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
520
547
  });
548
+ if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
521
549
  if (!res.ok) return null;
522
550
  const body = await res.json();
523
551
  const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
524
552
  if (!sessionId) return null;
525
553
  const sessionKey = await deriveSessionKey(sessionId);
526
554
  return { sessionId, sessionKey };
527
- } catch {
555
+ } catch (err) {
556
+ if (err instanceof TransitRateLimitedError) throw err;
528
557
  return null;
529
558
  }
530
559
  }
@@ -722,26 +751,46 @@ function ensureRestTransitHandshake() {
722
751
  const apiUrl = _config.apiUrl;
723
752
  _transitHandshakePromise = (async () => {
724
753
  try {
725
- for (let attempt = 0; attempt < 5; attempt++) {
754
+ const MAX_FAILURES = 5;
755
+ const BACKSTOP_MS = 2 * 6e4;
756
+ const deadline = Date.now() + BACKSTOP_MS;
757
+ let failures = 0;
758
+ let rateLimitHits = 0;
759
+ while (failures < MAX_FAILURES && Date.now() < deadline) {
726
760
  if (getTransitSession()) return;
761
+ let waitMs;
727
762
  try {
728
- const keys = await fetchServerKeys(apiUrl);
763
+ const identity = { userId: _config?.userId, tenantId: _config?.tenantId };
764
+ const keys = await fetchServerKeys(apiUrl, identity);
729
765
  if (!keys?.enabled) {
730
766
  configureTransit(false);
731
767
  return;
732
768
  }
733
- const session = await createRestTransitSession(apiUrl);
769
+ const session = await createRestTransitSession(apiUrl, identity);
734
770
  if (session && !getTransitSession()) {
735
771
  const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
736
772
  setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
737
773
  return;
738
774
  }
739
- } catch {
775
+ failures++;
776
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
777
+ } catch (err) {
778
+ if (err instanceof TransitRateLimitedError) {
779
+ const blind = Math.min(15e3 * 2 ** rateLimitHits, 6e4);
780
+ waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), 6e4) : blind;
781
+ rateLimitHits++;
782
+ console.warn(
783
+ `[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
784
+ );
785
+ } else {
786
+ failures++;
787
+ waitMs = Math.min(500 * 2 ** failures, 8e3);
788
+ }
740
789
  }
741
- await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** attempt, 8e3)));
790
+ await new Promise((r) => setTimeout(r, waitMs));
742
791
  }
743
792
  console.error(
744
- "[AntzChat] transit handshake could not establish a session after 5 attempts \u2014 chat requests stay gated until one succeeds (server requires transit)."
793
+ "[AntzChat] transit handshake could not establish a session \u2014 chat requests stay gated until one succeeds (server requires transit)."
745
794
  );
746
795
  } finally {
747
796
  _transitHandshakePromise = null;
@@ -1241,7 +1290,7 @@ async function _doConnect(config, getToken) {
1241
1290
  if (existingSession?.enabled && existingSession.sessionId) {
1242
1291
  httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
1243
1292
  } else {
1244
- httpsSession = await createRestTransitSession(config.apiUrl);
1293
+ httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1245
1294
  if (httpsSession) {
1246
1295
  const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1247
1296
  setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
@@ -1252,7 +1301,7 @@ async function _doConnect(config, getToken) {
1252
1301
  boundDeriveSessionKey = null;
1253
1302
  } else {
1254
1303
  try {
1255
- const serverKeys = await fetchServerKeys(config.apiUrl);
1304
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1256
1305
  if (!serverKeys.enabled) {
1257
1306
  throw new AntzChatError(
1258
1307
  "TRANSIT_MISMATCH",
@@ -1269,7 +1318,7 @@ async function _doConnect(config, getToken) {
1269
1318
  }
1270
1319
  } else {
1271
1320
  try {
1272
- const serverKeys = await fetchServerKeys(config.apiUrl);
1321
+ const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
1273
1322
  if (serverKeys.enabled) {
1274
1323
  throw new AntzChatError(
1275
1324
  "TRANSIT_MISMATCH",
@@ -2283,6 +2332,7 @@ var AntzChatClient = class {
2283
2332
  HIGHLY_FORWARDED_DEPTH_THRESHOLD,
2284
2333
  MAX_FORWARD_TARGETS,
2285
2334
  MENTION_ALL_ID,
2335
+ TransitRateLimitedError,
2286
2336
  appConfigApi,
2287
2337
  authApi,
2288
2338
  buildMentionText,
@@ -2315,6 +2365,7 @@ var AntzChatClient = class {
2315
2365
  onSocketStatus,
2316
2366
  parseMentions,
2317
2367
  performHandshake,
2368
+ readRetryAfterMs,
2318
2369
  reconnectSocket,
2319
2370
  refreshSocketAuth,
2320
2371
  renderMentionParts,