@anuma/sdk 10.3.1 → 10.4.1

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.
@@ -4629,7 +4629,7 @@ var mergeHeaders = (...headers) => {
4629
4629
  if (!header || typeof header !== "object") {
4630
4630
  continue;
4631
4631
  }
4632
- const iterator = Array.isArray(header) || header instanceof Headers ? headersEntries(new Headers(header)) : Object.entries(header);
4632
+ const iterator = Array.isArray(header) ? header : header instanceof Headers ? headersEntries(header) : Object.entries(header);
4633
4633
  for (const [key3, value] of iterator) {
4634
4634
  if (value === null) {
4635
4635
  mergedHeaders.delete(key3);
@@ -12423,6 +12423,360 @@ async function migrateChatsToRemote(options) {
12423
12423
  return passes(false);
12424
12424
  }
12425
12425
 
12426
+ // src/lib/chatStore/vaultAttachments.ts
12427
+ var frameVersion = 1;
12428
+ var ivBytes = 12;
12429
+ var tagBytes = 16;
12430
+ var frameOverhead = 1 + ivBytes + tagBytes;
12431
+ var maxCiphertextBytes = 50 * 1024 * 1024;
12432
+ var expirySkewMs = 3e4;
12433
+ var maxRetryAfterMs = 3e4;
12434
+ var maxRefWrites = 5;
12435
+ var uploadWindowMs = 16 * 6e4;
12436
+ var attachmentIdPattern2 = /^[0-9a-f]{32}$/;
12437
+ var maxVaultAttachmentBytes = maxCiphertextBytes - frameOverhead;
12438
+ function additionalData(attachmentId) {
12439
+ return new TextEncoder().encode(`anuma-chat-attachment-v1:${attachmentId}`);
12440
+ }
12441
+ function invalidAttachment() {
12442
+ throw new ChatStoreError("Attachment does not decrypt under this key", 0, "invalid_attachment");
12443
+ }
12444
+ function sendableHeaders(headers) {
12445
+ return Object.fromEntries(
12446
+ Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length")
12447
+ );
12448
+ }
12449
+ function transientStatus(status) {
12450
+ return status === 429 || status >= 500;
12451
+ }
12452
+ async function discardBody(response) {
12453
+ await response.body?.cancel().catch(() => void 0);
12454
+ }
12455
+ function createVaultAttachments(options) {
12456
+ const { store, keyRing, cache } = options;
12457
+ const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
12458
+ const retries = options.retries ?? 3;
12459
+ const retryDelayMs = options.retryDelayMs ?? 500;
12460
+ const now = options.now ?? Date.now;
12461
+ const sleep3 = (ms2, signal) => new Promise((resolve, reject) => {
12462
+ signal?.throwIfAborted();
12463
+ const timer = setTimeout(() => {
12464
+ signal?.removeEventListener("abort", onAbort);
12465
+ resolve();
12466
+ }, ms2);
12467
+ const onAbort = () => {
12468
+ clearTimeout(timer);
12469
+ reject(signal?.reason);
12470
+ };
12471
+ signal?.addEventListener("abort", onAbort, { once: true });
12472
+ });
12473
+ const pause = (attempt, signal, atLeast = 0) => sleep3(Math.max(atLeast, retryDelayMs * 2 ** attempt), signal);
12474
+ const withRetry = async (call, signal) => {
12475
+ for (let attempt = 0; ; attempt++) {
12476
+ try {
12477
+ return await call();
12478
+ } catch (error) {
12479
+ if (!(error instanceof ChatStoreError) || !transientStatus(error.status) || attempt >= retries || (error.retryAfterMs ?? 0) > maxRetryAfterMs)
12480
+ throw error;
12481
+ await pause(attempt, signal, error.retryAfterMs);
12482
+ }
12483
+ }
12484
+ };
12485
+ const expired = (url) => now() >= Date.parse(url.expiresAt) - expirySkewMs;
12486
+ const seal = async (bytes4, attachmentId) => {
12487
+ const iv = crypto.getRandomValues(new Uint8Array(ivBytes));
12488
+ const sealed2 = new Uint8Array(
12489
+ await crypto.subtle.encrypt(
12490
+ { name: "AES-GCM", iv, additionalData: additionalData(attachmentId) },
12491
+ keyRing.v3,
12492
+ bytes4
12493
+ )
12494
+ );
12495
+ const frame = new Uint8Array(1 + ivBytes + sealed2.length);
12496
+ frame[0] = frameVersion;
12497
+ frame.set(iv, 1);
12498
+ frame.set(sealed2, 1 + ivBytes);
12499
+ return frame;
12500
+ };
12501
+ const open = async (frame, attachmentId) => {
12502
+ if (frame.length < frameOverhead || frame[0] !== frameVersion) invalidAttachment();
12503
+ try {
12504
+ return new Uint8Array(
12505
+ await crypto.subtle.decrypt(
12506
+ {
12507
+ name: "AES-GCM",
12508
+ iv: frame.slice(1, 1 + ivBytes),
12509
+ additionalData: additionalData(attachmentId)
12510
+ },
12511
+ keyRing.v3,
12512
+ frame.slice(1 + ivBytes)
12513
+ )
12514
+ );
12515
+ } catch {
12516
+ return invalidAttachment();
12517
+ }
12518
+ };
12519
+ const put = async (upload, body, signal) => {
12520
+ for (let attempt = 0; ; attempt++) {
12521
+ if (expired(upload)) return "expired";
12522
+ let response;
12523
+ try {
12524
+ response = await fetcher(upload.url, {
12525
+ method: upload.method,
12526
+ headers: sendableHeaders(upload.headers),
12527
+ body,
12528
+ signal
12529
+ });
12530
+ } catch (error) {
12531
+ if (signal?.aborted || attempt >= retries) throw error;
12532
+ await pause(attempt, signal);
12533
+ continue;
12534
+ }
12535
+ await discardBody(response);
12536
+ if (response.ok) return "stored";
12537
+ if (response.status === 403) return "expired";
12538
+ if (!transientStatus(response.status) || attempt >= retries)
12539
+ throw new ChatStoreError(
12540
+ `Attachment upload failed with ${response.status}`,
12541
+ response.status,
12542
+ "attachment_upload_failed"
12543
+ );
12544
+ await pause(attempt, signal);
12545
+ }
12546
+ };
12547
+ const download = async (url, signal) => {
12548
+ for (let attempt = 0; ; attempt++) {
12549
+ if (expired(url)) return "expired";
12550
+ let response;
12551
+ try {
12552
+ response = await fetcher(url.url, {
12553
+ method: url.method,
12554
+ headers: sendableHeaders(url.headers),
12555
+ signal
12556
+ });
12557
+ if (response.ok) return new Uint8Array(await response.arrayBuffer());
12558
+ } catch (error) {
12559
+ if (signal?.aborted || attempt >= retries) throw error;
12560
+ await pause(attempt, signal);
12561
+ continue;
12562
+ }
12563
+ await discardBody(response);
12564
+ if (response.status === 403) return "expired";
12565
+ if (response.status === 404) return "missing";
12566
+ if (!transientStatus(response.status) || attempt >= retries)
12567
+ throw new ChatStoreError(
12568
+ `Attachment download failed with ${response.status}`,
12569
+ response.status,
12570
+ "attachment_download_failed"
12571
+ );
12572
+ await pause(attempt, signal);
12573
+ }
12574
+ };
12575
+ const remove = async (attachmentId, signal) => {
12576
+ try {
12577
+ await withRetry(() => store.attachments.delete(attachmentId, signal), signal);
12578
+ } catch (error) {
12579
+ if (!(error instanceof ChatStoreError) || error.status !== 404) throw error;
12580
+ }
12581
+ await cache?.delete(attachmentId).catch(() => void 0);
12582
+ };
12583
+ const discardPending = async (attachmentId) => {
12584
+ const found = await store.attachments.get(attachmentId);
12585
+ if (found?.attachment.state === "pending") await remove(attachmentId);
12586
+ };
12587
+ return {
12588
+ async upload(bytes4, uploadOptions) {
12589
+ const signal = uploadOptions?.signal;
12590
+ if (!(bytes4 instanceof Uint8Array) || bytes4.length > maxVaultAttachmentBytes)
12591
+ throw new ChatStoreError(
12592
+ "Attachment is larger than vault accepts",
12593
+ 413,
12594
+ "attachment_too_large"
12595
+ );
12596
+ const size = bytes4.length + frameOverhead;
12597
+ for (let attempt = 0; ; attempt++) {
12598
+ const { attachment: attachment2, upload } = await withRetry(
12599
+ () => store.attachments.create(size, signal),
12600
+ signal
12601
+ );
12602
+ try {
12603
+ await uploadOptions?.onReserved?.(attachment2.id);
12604
+ const frame = await seal(bytes4, attachment2.id);
12605
+ if (await put(upload, frame, signal) === "stored") return attachment2.id;
12606
+ } catch (error) {
12607
+ await discardPending(attachment2.id).catch(() => void 0);
12608
+ throw error;
12609
+ }
12610
+ await remove(attachment2.id).catch(() => void 0);
12611
+ if (attempt >= retries)
12612
+ throw new ChatStoreError(
12613
+ "Attachment storage rejected the upload URL",
12614
+ 403,
12615
+ "attachment_upload_rejected"
12616
+ );
12617
+ }
12618
+ },
12619
+ commit(attachmentId, owner, signal) {
12620
+ return withRetry(() => store.attachments.commit(attachmentId, owner, signal), signal);
12621
+ },
12622
+ async read(attachmentId, signal) {
12623
+ if (!attachmentIdPattern2.test(attachmentId)) invalidAttachment();
12624
+ const cached = await cache?.get(attachmentId).catch(() => null);
12625
+ if (cached) return cached;
12626
+ for (let attempt = 0; ; attempt++) {
12627
+ const found = await withRetry(() => store.attachments.get(attachmentId, signal), signal);
12628
+ if (!found?.download) return null;
12629
+ const frame = await download(found.download, signal);
12630
+ if (frame === "missing") return null;
12631
+ if (frame === "expired") {
12632
+ if (attempt >= retries)
12633
+ throw new ChatStoreError(
12634
+ "Attachment storage rejected the download URL",
12635
+ 403,
12636
+ "attachment_download_rejected"
12637
+ );
12638
+ continue;
12639
+ }
12640
+ if (frame.length !== found.attachment.sizeBytes) invalidAttachment();
12641
+ const bytes4 = await open(frame, attachmentId);
12642
+ await cache?.set(attachmentId, bytes4).catch(() => void 0);
12643
+ return bytes4;
12644
+ }
12645
+ },
12646
+ delete: remove
12647
+ };
12648
+ }
12649
+ function validRef(value) {
12650
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
12651
+ const ref3 = value;
12652
+ return typeof ref3.attachmentId === "string" && attachmentIdPattern2.test(ref3.attachmentId) && typeof ref3.fileId === "string" && ref3.fileId.length > 0 && typeof ref3.name === "string" && typeof ref3.type === "string" && Number.isSafeInteger(ref3.size) && ref3.size >= 0;
12653
+ }
12654
+ function vaultAttachmentRefs(record2) {
12655
+ const refs = record2.data.vaultAttachments;
12656
+ return Array.isArray(refs) ? refs.filter(validRef) : [];
12657
+ }
12658
+ function journalKey(messageId, fileId) {
12659
+ return JSON.stringify([messageId, fileId]);
12660
+ }
12661
+ async function migrateMessageAttachments(options) {
12662
+ const { store, attachments, journal, messageId, signal } = options;
12663
+ const now = options.now ?? Date.now;
12664
+ const owner = { kind: "message", id: messageId };
12665
+ const ownerGone = () => new ChatStoreError("Message is not in vault", 409, "attachment_owner_not_found");
12666
+ let snapshot = await store.get("message", messageId, signal);
12667
+ if (!snapshot || snapshot.record.isDeleted) throw ownerGone();
12668
+ const present = new Map(vaultAttachmentRefs(snapshot.record).map((r) => [r.fileId, r]));
12669
+ const uploadable = (attachment2) => attachment2.state === "pending" && now() < Date.parse(attachment2.createdAt) + uploadWindowMs;
12670
+ const lookup = async (attachmentId) => attachmentId && attachmentIdPattern2.test(attachmentId) ? (await store.attachments.get(attachmentId, signal))?.attachment ?? null : null;
12671
+ const bind = async (found) => {
12672
+ if (!found) return null;
12673
+ if (found.state === "committed")
12674
+ return found.ownerKind === owner.kind && found.ownerId === owner.id ? found : null;
12675
+ try {
12676
+ return await attachments.commit(found.id, owner, signal);
12677
+ } catch (error) {
12678
+ if (!(error instanceof ChatStoreError) || error.code !== "attachment_not_uploaded")
12679
+ throw error;
12680
+ if (uploadable(found)) return "busy";
12681
+ await attachments.delete(found.id, signal);
12682
+ return null;
12683
+ }
12684
+ };
12685
+ const added = [];
12686
+ const settled = [];
12687
+ const missing2 = [];
12688
+ const oversized = [];
12689
+ const deferred = [];
12690
+ const stale = /* @__PURE__ */ new Set();
12691
+ let uploaded = 0;
12692
+ for (const file of options.files) {
12693
+ signal?.throwIfAborted();
12694
+ const key3 = journalKey(messageId, file.fileId);
12695
+ const recorded = present.get(file.fileId);
12696
+ if (recorded) {
12697
+ const bound = await bind(await lookup(recorded.attachmentId));
12698
+ if (bound === "busy") {
12699
+ deferred.push(file.fileId);
12700
+ continue;
12701
+ }
12702
+ if (bound) {
12703
+ const stray = await lookup(await journal.get(key3));
12704
+ if (stray && stray.id !== recorded.attachmentId) {
12705
+ if (uploadable(stray)) continue;
12706
+ await attachments.delete(stray.id, signal);
12707
+ }
12708
+ settled.push(key3);
12709
+ continue;
12710
+ }
12711
+ stale.add(recorded.attachmentId);
12712
+ }
12713
+ const meta = { fileId: file.fileId, name: file.name, type: file.type };
12714
+ const resumed = await bind(await lookup(await journal.get(key3)));
12715
+ if (resumed === "busy") {
12716
+ deferred.push(file.fileId);
12717
+ continue;
12718
+ }
12719
+ if (resumed) {
12720
+ added.push({ attachmentId: resumed.id, ...meta, size: resumed.sizeBytes - frameOverhead });
12721
+ settled.push(key3);
12722
+ continue;
12723
+ }
12724
+ const bytes4 = await file.read();
12725
+ if (!bytes4) {
12726
+ missing2.push(file.fileId);
12727
+ continue;
12728
+ }
12729
+ if (bytes4.length > maxVaultAttachmentBytes) {
12730
+ oversized.push(file.fileId);
12731
+ continue;
12732
+ }
12733
+ const attachmentId = await attachments.upload(bytes4, {
12734
+ signal,
12735
+ onReserved: (id2) => journal.set(key3, id2)
12736
+ });
12737
+ uploaded++;
12738
+ await attachments.commit(attachmentId, owner, signal);
12739
+ added.push({ attachmentId, ...meta, size: bytes4.length });
12740
+ settled.push(key3);
12741
+ }
12742
+ const superseded = [];
12743
+ for (let attempt = 0; added.length > 0; attempt++) {
12744
+ const refs = new Map(vaultAttachmentRefs(snapshot.record).map((r) => [r.fileId, r]));
12745
+ superseded.length = 0;
12746
+ for (const ref3 of added) {
12747
+ const prior = refs.get(ref3.fileId);
12748
+ if (prior && prior.attachmentId !== ref3.attachmentId && !stale.has(prior.attachmentId))
12749
+ superseded.push(ref3.attachmentId);
12750
+ else refs.set(ref3.fileId, ref3);
12751
+ }
12752
+ if (superseded.length === added.length) break;
12753
+ try {
12754
+ [snapshot] = await store.putMany(
12755
+ [
12756
+ {
12757
+ record: {
12758
+ ...snapshot.record,
12759
+ data: { ...snapshot.record.data, ...plain({ vaultAttachments: [...refs.values()] }) }
12760
+ },
12761
+ expectedVersion: snapshot.version
12762
+ }
12763
+ ],
12764
+ signal
12765
+ );
12766
+ break;
12767
+ } catch (error) {
12768
+ if (!(error instanceof ChatStoreError) || error.code !== "version_conflict" || attempt + 1 >= maxRefWrites)
12769
+ throw error;
12770
+ const latest = await store.get("message", messageId, signal);
12771
+ if (!latest || latest.record.isDeleted) throw ownerGone();
12772
+ snapshot = latest;
12773
+ }
12774
+ }
12775
+ for (const id2 of superseded) await attachments.delete(id2, signal);
12776
+ for (const key3 of settled) await journal.set(key3, null);
12777
+ return { refs: vaultAttachmentRefs(snapshot.record), uploaded, missing: missing2, oversized, deferred };
12778
+ }
12779
+
12426
12780
  // src/lib/chatStore/vaultBackend.ts
12427
12781
  import { v7 as uuidv73 } from "uuid";
12428
12782
 
@@ -12562,6 +12916,7 @@ var resumableFields = [
12562
12916
  "imageModel",
12563
12917
  "files",
12564
12918
  "fileIds",
12919
+ "vaultAttachments",
12565
12920
  "usage",
12566
12921
  "sources",
12567
12922
  "responseDuration",
@@ -40245,6 +40600,7 @@ export {
40245
40600
  createSelectPaymentWalletTool,
40246
40601
  createServerToolsFilter,
40247
40602
  createStockPricePreProcessor,
40603
+ createVaultAttachments,
40248
40604
  createVaultChatBackend,
40249
40605
  createVaultEmbeddingCache,
40250
40606
  createVaultFolderOp,
@@ -40394,11 +40750,13 @@ export {
40394
40750
  linkMemoryEntitiesOp,
40395
40751
  localChatBackend,
40396
40752
  maskScopedEmbeddingCache,
40753
+ maxVaultAttachmentBytes,
40397
40754
  mediaToStored,
40398
40755
  memoryCipher,
40399
40756
  migrateChatsToRemote,
40400
40757
  migrateDisplayResult,
40401
40758
  migrateMemoriesToRemote,
40759
+ migrateMessageAttachments,
40402
40760
  migrateNotionClientRegistration,
40403
40761
  migrateNotionToken,
40404
40762
  moveMemoriesToFolderOp,
@@ -40507,6 +40865,7 @@ export {
40507
40865
  useVoice,
40508
40866
  useWalletBinding,
40509
40867
  userPreferencesStorageSchema,
40868
+ vaultAttachmentRefs,
40510
40869
  vaultChatBackend,
40511
40870
  verifyMemoriesForPublish,
40512
40871
  walk,
@@ -20208,7 +20208,7 @@ var mergeHeaders = (...headers) => {
20208
20208
  if (!header || typeof header !== "object") {
20209
20209
  continue;
20210
20210
  }
20211
- const iterator = Array.isArray(header) || header instanceof Headers ? headersEntries(new Headers(header)) : Object.entries(header);
20211
+ const iterator = Array.isArray(header) ? header : header instanceof Headers ? headersEntries(header) : Object.entries(header);
20212
20212
  for (const [key, value] of iterator) {
20213
20213
  if (value === null) {
20214
20214
  mergedHeaders.delete(key);
@@ -7389,6 +7389,14 @@ interface FileMetadata {
7389
7389
  */
7390
7390
  sourceUrl?: string;
7391
7391
  }
7392
+ /** Points a message's local file (`FileMetadata.id` or a `fileIds` media id) at its encrypted copy in vault, so any device can fetch it. */
7393
+ interface VaultAttachmentRef {
7394
+ attachmentId: string;
7395
+ fileId: string;
7396
+ name: string;
7397
+ type: string;
7398
+ size: number;
7399
+ }
7392
7400
  interface ChatCompletionUsage {
7393
7401
  promptTokens?: number;
7394
7402
  completionTokens?: number;
@@ -7492,6 +7500,7 @@ interface StoredMessage {
7492
7500
  files?: FileMetadata[];
7493
7501
  /** Array of media_id references for direct lookup in media table */
7494
7502
  fileIds?: string[];
7503
+ vaultAttachments?: VaultAttachmentRef[];
7495
7504
  createdAt: Date;
7496
7505
  updatedAt: Date;
7497
7506
  vector?: number[];
@@ -7668,6 +7677,7 @@ interface CreateMessageOptions {
7668
7677
  files?: FileMetadata[];
7669
7678
  /** Array of media_id references for direct lookup in media table */
7670
7679
  fileIds?: string[];
7680
+ vaultAttachments?: VaultAttachmentRef[];
7671
7681
  usage?: ChatCompletionUsage;
7672
7682
  sources?: SearchSource[];
7673
7683
  responseDuration?: number;
@@ -7389,6 +7389,14 @@ interface FileMetadata {
7389
7389
  */
7390
7390
  sourceUrl?: string;
7391
7391
  }
7392
+ /** Points a message's local file (`FileMetadata.id` or a `fileIds` media id) at its encrypted copy in vault, so any device can fetch it. */
7393
+ interface VaultAttachmentRef {
7394
+ attachmentId: string;
7395
+ fileId: string;
7396
+ name: string;
7397
+ type: string;
7398
+ size: number;
7399
+ }
7392
7400
  interface ChatCompletionUsage {
7393
7401
  promptTokens?: number;
7394
7402
  completionTokens?: number;
@@ -7492,6 +7500,7 @@ interface StoredMessage {
7492
7500
  files?: FileMetadata[];
7493
7501
  /** Array of media_id references for direct lookup in media table */
7494
7502
  fileIds?: string[];
7503
+ vaultAttachments?: VaultAttachmentRef[];
7495
7504
  createdAt: Date;
7496
7505
  updatedAt: Date;
7497
7506
  vector?: number[];
@@ -7668,6 +7677,7 @@ interface CreateMessageOptions {
7668
7677
  files?: FileMetadata[];
7669
7678
  /** Array of media_id references for direct lookup in media table */
7670
7679
  fileIds?: string[];
7680
+ vaultAttachments?: VaultAttachmentRef[];
7671
7681
  usage?: ChatCompletionUsage;
7672
7682
  sources?: SearchSource[];
7673
7683
  responseDuration?: number;
@@ -18496,7 +18496,7 @@ var mergeHeaders = (...headers) => {
18496
18496
  if (!header || typeof header !== "object") {
18497
18497
  continue;
18498
18498
  }
18499
- const iterator = Array.isArray(header) || header instanceof Headers ? headersEntries(new Headers(header)) : Object.entries(header);
18499
+ const iterator = Array.isArray(header) ? header : header instanceof Headers ? headersEntries(header) : Object.entries(header);
18500
18500
  for (const [key, value] of iterator) {
18501
18501
  if (value === null) {
18502
18502
  mergedHeaders.delete(key);
@@ -643,7 +643,7 @@ var mergeHeaders = (...headers) => {
643
643
  if (!header || typeof header !== "object") {
644
644
  continue;
645
645
  }
646
- const iterator = Array.isArray(header) || header instanceof Headers ? headersEntries(new Headers(header)) : Object.entries(header);
646
+ const iterator = Array.isArray(header) ? header : header instanceof Headers ? headersEntries(header) : Object.entries(header);
647
647
  for (const [key, value] of iterator) {
648
648
  if (value === null) {
649
649
  mergedHeaders.delete(key);
@@ -525,7 +525,7 @@ var mergeHeaders = (...headers) => {
525
525
  if (!header || typeof header !== "object") {
526
526
  continue;
527
527
  }
528
- const iterator = Array.isArray(header) || header instanceof Headers ? headersEntries(new Headers(header)) : Object.entries(header);
528
+ const iterator = Array.isArray(header) ? header : header instanceof Headers ? headersEntries(header) : Object.entries(header);
529
529
  for (const [key, value] of iterator) {
530
530
  if (value === null) {
531
531
  mergedHeaders.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "10.3.1",
3
+ "version": "10.4.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",