@antzsoft/chat-core 1.2.3 → 1.2.5

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.
@@ -155,7 +155,7 @@ section.sec>h2:hover{color:#fff}
155
155
 
156
156
  <div class="section-label">What's New</div>
157
157
  <ul>
158
- <li><a href="#whats-new">v1.2.3 Release Notes</a></li>
158
+ <li><a href="#whats-new">v1.2.4 Release Notes</a></li>
159
159
  </ul>
160
160
 
161
161
  <div class="section-label">Getting Started</div>
@@ -243,16 +243,113 @@ section.sec>h2:hover{color:#fff}
243
243
  <h2>What's New</h2>
244
244
  <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
245
245
 
246
- <!-- ── v1.2.3 (current) ── -->
247
- <div class="wn-version open" id="wn-123">
248
- <div class="wn-header" onclick="toggleVersion('wn-123')">
246
+ <!-- ── v1.2.4 (current) ── -->
247
+ <div class="wn-version open" id="wn-124">
248
+ <div class="wn-header" onclick="toggleVersion('wn-124')">
249
249
  <div class="wn-title">
250
- <span class="wn-ver">v1.2.3</span>
250
+ <span class="wn-ver">v1.2.4</span>
251
251
  <span class="wn-badge current">Current</span>
252
252
  <span class="wn-date">May 2026</span>
253
253
  </div>
254
254
  <span class="wn-chevron">▲</span>
255
255
  </div>
256
+ <div class="wn-body">
257
+
258
+ <div class="wn-item" id="wn-124-mp1">
259
+ <div class="wn-item-header" onclick="toggleItem('wn-124-mp1')">
260
+ <span class="wn-tag new">New</span>
261
+ <span class="wn-item-title">Chunked multipart upload for files ≥ 10 MB (S3 and local)</span>
262
+ <span class="wn-chevron-sm">▾</span>
263
+ </div>
264
+ <div class="wn-item-body">
265
+ <p>Files at or above <strong>10 MB</strong> on S3 and local storage now use chunked multipart upload instead of a single presigned POST. The SDK splits the file into 10 MB chunks, uploads up to 3 parts in parallel, then completes the upload server-side.</p>
266
+ <p><strong>What changes per provider:</strong></p>
267
+ <table>
268
+ <thead><tr><th>Provider</th><th>File &lt; 10 MB</th><th>File ≥ 10 MB</th></tr></thead>
269
+ <tbody>
270
+ <tr><td>AWS S3</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart — N presigned PUT URLs (one per part) → <code>CompleteMultipartUpload</code></td></tr>
271
+ <tr><td>Local (same-server / mount)</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart — N signed PUT URLs (one per part) → <code>POST /storage/multipart/complete/:fileId</code> assembles parts on disk</td></tr>
272
+ <tr><td>Azure Blob Storage</td><td>Presigned PUT (unchanged)</td><td>Presigned PUT (unchanged)</td></tr>
273
+ </tbody>
274
+ </table>
275
+ <p><strong>Benefits at 50 MB+:</strong> resumes from last completed part on network failure, parallel throughput, no single-request timeout, supports files up to 5 TB.</p>
276
+ <p><strong>No integration changes required</strong> for Web SDK (<code>@antzsoft/chat-web-sdk</code>) and RN SDK (<code>@antzsoft/chat-rn-sdk</code>) users — <code>webUploadPartFn</code> and <code>rnUploadPartFn</code> are wired in automatically. Node.js / custom integrators must pass <code>platformUploadPartFn</code> to <code>uploadBatch</code> to enable chunked uploads.</p>
277
+ <p class="wn-ref">→ <a href="#step-upload">Step 16 — Upload Files</a></p>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="wn-item" id="wn-124-mp2">
282
+ <div class="wn-item-header" onclick="toggleItem('wn-124-mp2')">
283
+ <span class="wn-tag new">New</span>
284
+ <span class="wn-item-title"><code>storageApi.completeMultipartUpload()</code> · <code>PlatformUploadPartFn</code> type · <code>CompletedPart</code> / <code>MultipartUploadInfo</code> types</span>
285
+ <span class="wn-chevron-sm">▾</span>
286
+ </div>
287
+ <div class="wn-item-body">
288
+ <p><code>storageApi.completeMultipartUpload(fileId, uploadId, parts)</code> is called automatically by <code>uploadBatch</code> — only needed in custom manual upload flows. It assembles S3 parts and transitions the file record to active in one round-trip (replaces the separate <code>confirmUpload</code> call for the multipart path).</p>
289
+ <p><code>PlatformUploadPartFn</code> is the new platform adapter type: <code>(uploadUrl: string, blob: Blob | ArrayBuffer, onProgress?) =&gt; Promise&lt;string&gt;</code> — returns the ETag S3 sends back for the part. Exported from <code>@antzsoft/chat-core</code>.</p>
290
+ <p>New exported types: <code>MultipartUploadInfo</code>, <code>MultipartPartUrl</code>, <code>CompletedPart</code>.</p>
291
+ </div>
292
+ </div>
293
+
294
+
295
+ <div class="wn-item" id="wn-124-1">
296
+ <div class="wn-item-header" onclick="toggleItem('wn-124-1')">
297
+ <span class="wn-tag improvement">Improvement</span>
298
+ <span class="wn-item-title">S3 and local storage uploads now use presigned POST multipart — file bytes never touch the chat server</span>
299
+ <span class="wn-chevron-sm">▾</span>
300
+ </div>
301
+ <div class="wn-item-body">
302
+ <p>File uploads to S3 and the local storage provider now use <strong>presigned POST</strong> (AWS POST policy / HMAC-signed form fields) instead of presigned PUT. The client sends a <code>multipart/form-data</code> request directly to the storage endpoint — S3 or the chat server's own <code>/storage/files/upload</code> endpoint — with the signed fields embedded in the form body. No file bytes pass through the chat server.</p>
303
+ <p><strong>What changed per provider:</strong></p>
304
+ <table>
305
+ <thead><tr><th>Provider</th><th>Upload method</th><th>Change</th></tr></thead>
306
+ <tbody>
307
+ <tr><td>AWS S3</td><td>Presigned POST (multipart/form-data)</td><td>Changed from presigned PUT → POST. AWS signs a policy; client POSTs a FormData with all fields + file.</td></tr>
308
+ <tr><td>Local (same-server / mount)</td><td>Presigned POST (multipart/form-data)</td><td>HMAC-signed fields now go in the form body (not query string) — WAF-friendly.</td></tr>
309
+ <tr><td>Azure Blob Storage</td><td>Presigned PUT (SAS token)</td><td>Unchanged — Azure has no form-based upload API.</td></tr>
310
+ </tbody>
311
+ </table>
312
+ <p><strong>How the SDK handles both paths:</strong> The <code>PresignedUrlResponse</code> carries <code>method: 'POST'</code> and a <code>fields</code> object for S3/local. Your <code>platformUploadFn</code> should branch on this:</p>
313
+ <pre><code><span class="cm">// S3 / local → multipart FormData (method: 'POST', fields present)</span>
314
+ <span class="cm">// Azure → raw PUT body (method: 'PUT', no fields)</span>
315
+ <span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> &amp;&amp; presigned.fields) {
316
+ <span class="kw">const</span> fd = <span class="kw">new</span> <span class="fn">FormData</span>();
317
+ Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k, v]) => fd.<span class="fn">append</span>(k, v));
318
+ fd.<span class="fn">append</span>(<span class="str">'file'</span>, blob, file.name); <span class="cm">// file must be last</span>
319
+ xhr.<span class="fn">send</span>(fd);
320
+ } <span class="kw">else</span> {
321
+ xhr.<span class="fn">send</span>(blob); <span class="cm">// Azure PUT</span>
322
+ }</code></pre>
323
+ <p><strong>No integration changes required.</strong> Both the web SDK and RN SDK already implement this branching. If you are using the recommended <code>platformUploadFn</code> from Step 1 of this guide, it already handles both paths correctly. Only custom <code>platformUploadFn</code> implementations that unconditionally used <code>presigned.method</code> as PUT need updating — see the updated examples in <a href="#step1">Step 1</a>.</p>
324
+ <p class="wn-ref">→ <a href="#step1">Step 1 — platformUploadFn examples</a> · <a href="#step-upload">Step 16 — Upload Files</a></p>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="wn-item" id="wn-124-2">
329
+ <div class="wn-item-header" onclick="toggleItem('wn-124-2')">
330
+ <span class="wn-tag fix">Fix</span>
331
+ <span class="wn-item-title"><code>POST /storage/files/upload</code> returns <code>403</code> when transit encryption is enabled</span>
332
+ <span class="wn-chevron-sm">▾</span>
333
+ </div>
334
+ <div class="wn-item-body">
335
+ <p>The local-storage direct upload endpoint (<code>POST /storage/files/upload</code>) was missing the <code>@PreTransit()</code> decorator. With <code>transitEncryption: true</code>, the <code>TransitEnforceGuard</code> rejected every upload request with <code>403 Transit encryption required</code>.</p>
336
+ <p><strong>Why this endpoint is pre-transit:</strong> The client uploads directly via XHR/fetch — not through the Axios client — so no <code>x-transit-session</code> header is ever present on this request. Auth is the HMAC signature embedded in the <code>multipart/form-data</code> fields (<code>token</code>, <code>ts</code>) which the server validates independently.</p>
337
+ <p><strong>No integration changes required.</strong> This fix unblocks uploads on deployments with <code>transitEncryption: true</code>. No client-side changes needed.</p>
338
+ </div>
339
+ </div>
340
+
341
+ </div>
342
+ </div>
343
+
344
+ <!-- ── v1.2.3 ── -->
345
+ <div class="wn-version" id="wn-123">
346
+ <div class="wn-header" onclick="toggleVersion('wn-123')">
347
+ <div class="wn-title">
348
+ <span class="wn-ver">v1.2.3</span>
349
+ <span class="wn-date">May 2026</span>
350
+ </div>
351
+ <span class="wn-chevron">▼</span>
352
+ </div>
256
353
  <div class="wn-body">
257
354
 
258
355
  <div class="wn-item" id="wn-123-1">
@@ -335,6 +432,27 @@ client.socket.on('new_message', (event) => {
335
432
  </div>
336
433
  </div><!-- /.wn-version -->
337
434
 
435
+ <!-- ── Version history button ── -->
436
+ <button class="wn-history-btn" onclick="openVersionHistory()">
437
+ <span class="wn-history-icon">⏱</span>
438
+ View version history
439
+ <span class="wn-history-count">15 older versions</span>
440
+ </button>
441
+
442
+ </section>
443
+
444
+ <!-- ── Version history modal ── -->
445
+ <div id="version-history-modal" class="vh-overlay" onclick="if(event.target===this)closeVersionHistory()">
446
+ <div class="vh-drawer">
447
+ <div class="vh-header">
448
+ <div class="vh-header-left">
449
+ <span class="vh-title">Version History</span>
450
+ <span class="vh-subtitle">All previous releases</span>
451
+ </div>
452
+ <button class="vh-close" onclick="closeVersionHistory()">✕</button>
453
+ </div>
454
+ <div class="vh-body">
455
+
338
456
  <!-- ── v1.2.0 ── -->
339
457
  <div class="wn-version" id="wn-120">
340
458
  <div class="wn-header" onclick="toggleVersion('wn-120')">
@@ -372,7 +490,7 @@ client.socket.on('new_message', (event) => {
372
490
  </div>
373
491
  <span class="wn-chevron">▾</span>
374
492
  </div>
375
- <div class="wn-body" style="display:none">
493
+ <div class="wn-body">
376
494
 
377
495
  <div class="wn-item" id="wn-119-1">
378
496
  <div class="wn-item-header" onclick="toggleItem('wn-119-1')">
@@ -412,7 +530,7 @@ client.socket.on('new_message', (event) => {
412
530
  </div>
413
531
  <span class="wn-chevron">▾</span>
414
532
  </div>
415
- <div class="wn-body" style="display:none">
533
+ <div class="wn-body">
416
534
 
417
535
  <div class="wn-item" id="wn-118-1">
418
536
  <div class="wn-item-header" onclick="toggleItem('wn-118-1')">
@@ -465,7 +583,7 @@ client.socket.on('new_message', (event) => {
465
583
  </div>
466
584
  <span class="wn-chevron">▾</span>
467
585
  </div>
468
- <div class="wn-body" style="display:none">
586
+ <div class="wn-body">
469
587
 
470
588
  <div class="wn-item" id="wn-117-1">
471
589
  <div class="wn-item-header" onclick="toggleItem('wn-117-1')">
@@ -505,7 +623,7 @@ client.socket.on('new_message', (event) => {
505
623
  </div>
506
624
  <span class="wn-chevron">▾</span>
507
625
  </div>
508
- <div class="wn-body" style="display:none">
626
+ <div class="wn-body">
509
627
 
510
628
  <div class="wn-item" id="wn-116-1">
511
629
  <div class="wn-item-header" onclick="toggleItem('wn-116-1')">
@@ -1632,7 +1750,9 @@ messaging().<span class="fn">onTokenRefresh</span>(<span class="kw">async</span>
1632
1750
  </div><!-- /.wn-body -->
1633
1751
  </div><!-- /.wn-version -->
1634
1752
 
1635
- </section>
1753
+ </div><!-- /.vh-body -->
1754
+ </div><!-- /.vh-drawer -->
1755
+ </div><!-- /#version-history-modal -->
1636
1756
 
1637
1757
  <style>
1638
1758
  /* ── What's New ── */
@@ -1667,7 +1787,29 @@ messaging().<span class="fn">onTokenRefresh</span>(<span class="kw">async</span>
1667
1787
  .wn-tag{font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;letter-spacing:.4px;white-space:nowrap}
1668
1788
  .wn-tag.new{background:rgba(108,140,255,.15);color:var(--accent);border:1px solid rgba(108,140,255,.3)}
1669
1789
  .wn-tag.fix{background:rgba(251,191,36,.12);color:var(--yellow);border:1px solid rgba(251,191,36,.25)}
1790
+ .wn-tag.improvement{background:rgba(52,211,153,.1);color:var(--green);border:1px solid rgba(52,211,153,.25)}
1670
1791
  .wn-tag.break{background:rgba(248,113,113,.12);color:var(--red);border:1px solid rgba(248,113,113,.25)}
1792
+
1793
+ /* ── Version history button ── */
1794
+ .wn-history-btn{display:flex;align-items:center;gap:10px;width:100%;padding:13px 18px;background:transparent;border:1px dashed var(--border);border-radius:var(--radius);cursor:pointer;color:var(--muted);font-size:13px;font-family:var(--font-sans);transition:all .15s;margin-top:4px}
1795
+ .wn-history-btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(108,140,255,.04)}
1796
+ .wn-history-icon{font-size:14px}
1797
+ .wn-history-count{margin-left:auto;font-size:11px;background:var(--surface2);padding:2px 8px;border-radius:20px;border:1px solid var(--border)}
1798
+ .wn-history-btn:hover .wn-history-count{border-color:rgba(108,140,255,.4);color:var(--accent)}
1799
+
1800
+ /* ── Version history modal ── */
1801
+ .vh-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);z-index:500;align-items:flex-start;justify-content:flex-end}
1802
+ .vh-overlay.open{display:flex}
1803
+ .vh-drawer{width:min(680px,92vw);height:100vh;background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;animation:slideIn .22s ease}
1804
+ @keyframes slideIn{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:1}}
1805
+ .vh-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border);flex-shrink:0}
1806
+ .vh-header-left{display:flex;flex-direction:column;gap:2px}
1807
+ .vh-title{font-size:16px;font-weight:800;color:#fff}
1808
+ .vh-subtitle{font-size:12px;color:var(--muted)}
1809
+ .vh-close{background:var(--surface2);border:1px solid var(--border);color:var(--muted);width:30px;height:30px;border-radius:6px;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:all .15s;font-family:var(--font-sans)}
1810
+ .vh-close:hover{color:#fff;border-color:var(--accent);background:rgba(108,140,255,.1)}
1811
+ .vh-body{flex:1;overflow-y:auto;padding:20px 24px 40px}
1812
+ .vh-body .wn-version{margin-bottom:10px}
1671
1813
  </style>
1672
1814
 
1673
1815
  <!-- ─── OVERVIEW ──────────────────────────────────────────────────────── -->
@@ -1816,14 +1958,16 @@ npx pod-install</code></pre>
1816
1958
 
1817
1959
  <span class="kw">export const</span> <span class="at">rnUploadFn</span>: <span class="tp">PlatformUploadFn</span> = <span class="kw">async</span> (presigned, file, onProgress) => {
1818
1960
  <span class="kw">let</span> body: <span class="tp">Blob</span> | <span class="tp">FormData</span>;
1819
- <span class="kw">if</span> (presigned.method === <span class="str">'PUT'</span>) {
1820
- <span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(file.uri);
1821
- body = <span class="kw">await</span> res.<span class="fn">blob</span>();
1822
- } <span class="kw">else</span> {
1961
+ <span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> &amp;&amp; presigned.fields) {
1962
+ <span class="cm">// S3 / local multipart FormData with signed policy fields</span>
1823
1963
  <span class="kw">const</span> form = <span class="kw">new</span> <span class="fn">FormData</span>();
1824
- <span class="kw">if</span> (presigned.fields) Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k,v <span class="kw">as</span> <span class="tp">string</span>));
1964
+ Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k, v <span class="kw">as</span> <span class="tp">string</span>));
1825
1965
  form.<span class="fn">append</span>(<span class="str">'file'</span>, { <span class="at">uri</span>: file.uri, <span class="at">type</span>: file.type, <span class="at">name</span>: file.name } <span class="kw">as any</span>);
1826
1966
  body = form;
1967
+ } <span class="kw">else</span> {
1968
+ <span class="cm">// Azure — raw blob PUT</span>
1969
+ <span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(file.uri);
1970
+ body = <span class="kw">await</span> res.<span class="fn">blob</span>();
1827
1971
  }
1828
1972
  <span class="kw">await</span> <span class="fn">fetch</span>(presigned.uploadUrl, { <span class="at">method</span>: presigned.method, <span class="at">headers</span>: presigned.headers, body });
1829
1973
  onProgress?.(<span class="num">100</span>);
@@ -1840,26 +1984,37 @@ npx pod-install</code></pre>
1840
1984
  Object.<span class="fn">entries</span>(presigned.headers ?? {}).<span class="fn">forEach</span>(([k,v]) => xhr.<span class="fn">setRequestHeader</span>(k, v));
1841
1985
  xhr.onload = () => xhr.status &lt; <span class="num">300</span> ? <span class="fn">resolve</span>() : <span class="fn">reject</span>(<span class="kw">new</span> <span class="fn">Error</span>(`Upload failed: ${xhr.status}`));
1842
1986
  xhr.onerror = () => <span class="fn">reject</span>(<span class="kw">new</span> <span class="fn">Error</span>(<span class="str">'Upload network error'</span>));
1843
- <span class="kw">if</span> (presigned.method === <span class="str">'PUT'</span>) {
1844
- xhr.<span class="fn">send</span>(file <span class="kw">as any</span>);
1845
- } <span class="kw">else</span> {
1987
+ <span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> &amp;&amp; presigned.fields) {
1988
+ <span class="cm">// S3 / local — multipart FormData with signed policy fields</span>
1846
1989
  <span class="kw">const</span> form = <span class="kw">new</span> <span class="fn">FormData</span>();
1847
- <span class="kw">if</span> (presigned.fields) Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k,v));
1848
- form.<span class="fn">append</span>(<span class="str">'file'</span>, file <span class="kw">as any</span>);
1849
- xhr.<span class="fn">send</span>(form);
1990
+ Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k, v));
1991
+ <span class="fn">fetch</span>(file.uri).<span class="fn">then</span>(r => r.<span class="fn">blob</span>()).<span class="fn">then</span>(blob => { form.<span class="fn">append</span>(<span class="str">'file'</span>, blob, file.name); xhr.<span class="fn">send</span>(form); }).<span class="fn">catch</span>(reject);
1992
+ } <span class="kw">else</span> {
1993
+ <span class="cm">// Azure — raw blob PUT</span>
1994
+ <span class="fn">fetch</span>(file.uri).<span class="fn">then</span>(r => r.<span class="fn">blob</span>()).<span class="fn">then</span>(blob => xhr.<span class="fn">send</span>(blob)).<span class="fn">catch</span>(reject);
1850
1995
  }
1851
1996
  });</code></pre>
1852
1997
  </div>
1853
1998
  <div data-p="node">
1854
- <pre><code><span class="kw">import</span> { createReadStream } <span class="kw">from</span> <span class="str">'fs'</span>;
1999
+ <pre><code><span class="kw">import</span> { readFileSync } <span class="kw">from</span> <span class="str">'fs'</span>;
1855
2000
  <span class="kw">import</span> { <span class="tp">PlatformUploadFn</span> } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
1856
2001
 
1857
2002
  <span class="kw">export const</span> <span class="at">nodeUploadFn</span>: <span class="tp">PlatformUploadFn</span> = <span class="kw">async</span> (presigned, file, onProgress) => {
1858
- <span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(presigned.uploadUrl, {
1859
- <span class="at">method</span>: presigned.method,
1860
- <span class="at">headers</span>: { <span class="str">'Content-Type'</span>: file.type, ...presigned.headers },
1861
- <span class="at">body</span>: <span class="fn">createReadStream</span>(file.uri) <span class="kw">as any</span>,
1862
- });
2003
+ <span class="kw">let</span> body: <span class="tp">any</span>;
2004
+ <span class="kw">let</span> headers: <span class="tp">Record</span>&lt;<span class="tp">string</span>, <span class="tp">string</span>&gt; = { ...presigned.headers };
2005
+ <span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> &amp;&amp; presigned.fields) {
2006
+ <span class="cm">// S3 / local multipart FormData with signed policy fields</span>
2007
+ <span class="kw">const</span> { <span class="fn">FormData</span>, <span class="fn">Blob</span> } = <span class="kw">await</span> <span class="kw">import</span>(<span class="str">'node:buffer'</span>) <span class="kw">as any</span>;
2008
+ <span class="kw">const</span> fd = <span class="kw">new</span> <span class="fn">FormData</span>();
2009
+ Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k, v]) => fd.<span class="fn">append</span>(k, v <span class="kw">as</span> <span class="tp">string</span>));
2010
+ fd.<span class="fn">append</span>(<span class="str">'file'</span>, <span class="kw">new</span> <span class="fn">Blob</span>([<span class="fn">readFileSync</span>(file.uri.<span class="fn">replace</span>(<span class="str">'file://'</span>, <span class="str">''</span>))], { <span class="at">type</span>: file.type }), file.name);
2011
+ body = fd;
2012
+ } <span class="kw">else</span> {
2013
+ <span class="cm">// Azure — raw buffer PUT</span>
2014
+ body = <span class="fn">readFileSync</span>(file.uri.<span class="fn">replace</span>(<span class="str">'file://'</span>, <span class="str">''</span>));
2015
+ headers[<span class="str">'Content-Type'</span>] = file.type;
2016
+ }
2017
+ <span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(presigned.uploadUrl, { <span class="at">method</span>: presigned.method, headers, body });
1863
2018
  <span class="kw">if</span> (!res.ok) <span class="kw">throw new</span> <span class="fn">Error</span>(`Upload failed: ${res.status}`);
1864
2019
  onProgress?.(<span class="num">100</span>);
1865
2020
  };</code></pre>
@@ -3100,6 +3255,8 @@ socket?.<span class="fn">on</span>(<span class="str">'message_delivered'</span>,
3100
3255
  <h2><span class="step">STEP 16</span> Upload Files</h2>
3101
3256
  <p>The SDK handles the full flow: presigned URL request → binary upload via your <code>platformUploadFn</code> → server confirmation. You get back <code>fileId</code>s to attach to messages.</p>
3102
3257
 
3258
+ <div class="callout tip"><strong>Direct-to-storage upload · Chunked multipart for large files (v1.2.4+)</strong> File bytes go directly from the client to the storage provider — no file data passes through the chat server. S3 and local use presigned POST (<code>multipart/form-data</code>). Azure uses presigned PUT. <strong>For files ≥ 10 MB on S3 or local</strong>, the SDK automatically uses chunked multipart upload (10 MB parts, up to 3 parallel) — Web and RN SDK users get this for free. Your <code>platformUploadFn</code> still handles small files via <code>presigned.method === 'POST' &amp;&amp; presigned.fields</code>; the new <code>platformUploadPartFn</code> handles individual parts — see <a href="#step1">Step 1</a>.</div>
3259
+
3103
3260
  <div data-p="rn">
3104
3261
  <pre><code><span class="kw">import</span> * <span class="kw">as</span> ImagePicker <span class="kw">from</span> <span class="str">'expo-image-picker'</span>;
3105
3262
 
@@ -4270,12 +4427,36 @@ applyGating();
4270
4427
 
4271
4428
  // ── What's New toggles ───────────────────────────────────────────────────
4272
4429
  function toggleVersion(id) {
4273
- document.getElementById(id).classList.toggle('open');
4430
+ const el = document.getElementById(id);
4431
+ const isInsideModal = !!el.closest('#version-history-modal');
4432
+ if (isInsideModal) {
4433
+ // Accordion: close all siblings first, then open the clicked one
4434
+ const alreadyOpen = el.classList.contains('open');
4435
+ document.querySelectorAll('#version-history-modal .wn-version.open').forEach(v => v.classList.remove('open'));
4436
+ if (!alreadyOpen) el.classList.add('open');
4437
+ } else {
4438
+ el.classList.toggle('open');
4439
+ }
4274
4440
  }
4275
4441
  function toggleItem(id) {
4276
4442
  document.getElementById(id).classList.toggle('open');
4277
4443
  }
4278
4444
 
4445
+ // ── Version history modal ─────────────────────────────────────────────────
4446
+ function openVersionHistory() {
4447
+ const modal = document.getElementById('version-history-modal');
4448
+ modal.classList.add('open');
4449
+ document.body.style.overflow = 'hidden';
4450
+ }
4451
+ function closeVersionHistory() {
4452
+ const modal = document.getElementById('version-history-modal');
4453
+ modal.classList.remove('open');
4454
+ document.body.style.overflow = '';
4455
+ }
4456
+ document.addEventListener('keydown', (e) => {
4457
+ if (e.key === 'Escape') closeVersionHistory();
4458
+ });
4459
+
4279
4460
  // ── Progress bar + go-to-top FAB ─────────────────────────────────────────
4280
4461
  const _fab = document.getElementById('gototop');
4281
4462
  window.addEventListener('scroll', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Platform-agnostic core for Antz Chat — API, socket, stores, types. Works in browser, React Native (Expo or bare), and Node.js.",
5
5
  "author": "Antz",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- import {
2
- useChatStore
3
- } from "./chunk-GUO5QQGK.js";
4
- export {
5
- useChatStore
6
- };
7
- //# sourceMappingURL=chat.store-JC6QYDDL.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/stores/chat.store.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { Message } from '../types/index.js';\n\ninterface TypingUser {\n userId: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface LastReadEntry {\n messageId: string;\n readAt: string;\n}\n\ninterface ChatState {\n activeConversationId: string | null;\n pendingTarget: { conversationId: string; messageId: string } | null;\n typingUsers: Record<string, TypingUser[]>;\n onlineUsers: string[];\n /** keyed by conversationId — current user's last read pointer per conversation */\n lastRead: Record<string, LastReadEntry>;\n /** keyed by userId — last seen timestamp for each user */\n lastSeen: Record<string, string>;\n replyingTo: Message | null;\n editingMessage: Message | null;\n isSidebarOpen: boolean;\n isGroupInfoOpen: boolean;\n isStarredPanelOpen: boolean;\n\n setActiveConversation: (id: string | null) => void;\n setPendingTarget: (target: { conversationId: string; messageId: string } | null) => void;\n addTypingUser: (conversationId: string, user: TypingUser) => void;\n removeTypingUser: (conversationId: string, userId: string) => void;\n setUserOnline: (userId: string) => void;\n setUserOffline: (userId: string) => void;\n setOnlineUsers: (userIds: string[]) => void;\n setLastRead: (conversationId: string, messageId: string, readAt: string) => void;\n setLastSeen: (userId: string, lastSeenAt: string | null) => void;\n setReplyingTo: (message: Message | null) => void;\n setEditingMessage: (message: Message | null) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (open: boolean) => void;\n toggleGroupInfo: () => void;\n setGroupInfoOpen: (open: boolean) => void;\n toggleStarredPanel: () => void;\n setStarredPanelOpen: (open: boolean) => void;\n}\n\nexport const useChatStore = create<ChatState>((set) => ({\n activeConversationId: null,\n pendingTarget: null,\n typingUsers: {},\n onlineUsers: [],\n lastRead: {},\n lastSeen: {},\n replyingTo: null,\n editingMessage: null,\n isSidebarOpen: true,\n isGroupInfoOpen: false,\n isStarredPanelOpen: false,\n\n setActiveConversation: (id) =>\n set({ activeConversationId: id, replyingTo: null, editingMessage: null }),\n\n setPendingTarget: (target) => set({ pendingTarget: target }),\n\n addTypingUser: (conversationId, user) =>\n set((state) => {\n const existing = state.typingUsers[conversationId] ?? [];\n const deduped = existing.filter((u) => u.userId !== user.userId);\n return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };\n }),\n\n removeTypingUser: (conversationId, userId) =>\n set((state) => ({\n typingUsers: {\n ...state.typingUsers,\n [conversationId]: (state.typingUsers[conversationId] ?? []).filter(\n (u) => u.userId !== userId,\n ),\n },\n })),\n\n setUserOnline: (userId) =>\n set((state) => ({\n onlineUsers: state.onlineUsers.includes(userId)\n ? state.onlineUsers\n : [...state.onlineUsers, userId],\n })),\n\n setUserOffline: (userId) =>\n set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),\n\n setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),\n\n setLastRead: (conversationId, messageId, readAt) =>\n set((state) => ({\n lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } },\n })),\n\n setLastSeen: (userId, lastSeenAt) =>\n set((state) => {\n if (lastSeenAt === null) {\n const { [userId]: _, ...rest } = state.lastSeen;\n return { lastSeen: rest };\n }\n return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };\n }),\n\n setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),\n\n setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),\n\n toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),\n setSidebarOpen: (open) => set({ isSidebarOpen: open }),\n\n toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),\n setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),\n\n toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),\n setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),\n}));\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
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 | 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}\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 | 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","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 // Wait for the transit session key before sending authenticated requests.\n // Unauthenticated requests (no token) fire before login/socket connect —\n // they can never have a transit session so never block.\n if (token) await waitForTransitReady();\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(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// 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","// 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":[]}