@anuma/sdk 10.3.1 → 10.4.0

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.
@@ -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,
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "10.3.1",
3
+ "version": "10.4.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",