@meistrari/vault-sdk 1.8.3 → 2.0.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.
package/dist/index.cjs CHANGED
@@ -237,7 +237,7 @@ function getFileName(content) {
237
237
  }
238
238
 
239
239
  const name = "@meistrari/vault-sdk";
240
- const version = "1.8.3";
240
+ const version = "2.0.0";
241
241
  const license = "UNLICENSED";
242
242
  const repository = {
243
243
  type: "git",
@@ -264,8 +264,8 @@ const scripts = {
264
264
  check: "bun run lint && bun tsc --noEmit"
265
265
  };
266
266
  const dependencies = {
267
- "@meistrari/vault-shared": "workspace:*",
268
267
  "@meistrari/file-type": "22.0.0",
268
+ "@meistrari/vault-shared": "workspace:*",
269
269
  "mime-types": "3.0.1",
270
270
  ofetch: "1.4.1",
271
271
  zod: "3.23.8"
@@ -326,6 +326,7 @@ async function wrappedFetch(url, requestInit) {
326
326
  };
327
327
  const request = new Request(url, options);
328
328
  request.headers.set("User-Agent", userAgent);
329
+ request.headers.set("x-compatibility-date", compatibilityDate);
329
330
  const response = await fetch(request);
330
331
  if (!response.ok) {
331
332
  throw await FetchError.from(request.url, request.method, response);
@@ -369,9 +370,7 @@ class VaultFile {
369
370
  * @returns The headers for the request
370
371
  */
371
372
  get headers() {
372
- const headers = this.config.authStrategy.getHeaders();
373
- headers.set("User-Agent", userAgent);
374
- return headers;
373
+ return this.config.authStrategy.getHeaders();
375
374
  }
376
375
  /**
377
376
  * Performs a request to the vault service and handles the response or errors.
@@ -388,7 +387,6 @@ class VaultFile {
388
387
  const { method, path, body, signal, query } = params;
389
388
  const url = new URL(path, this.baseUrl);
390
389
  const headers = new Headers(this.headers);
391
- headers.set("x-compatibility-date", compatibilityDate);
392
390
  if (query) {
393
391
  Object.entries(query).forEach(([key, value]) => {
394
392
  url.searchParams.set(key, value);
@@ -417,6 +415,7 @@ class VaultFile {
417
415
  * @param metadata.mimeType - The mime type of the file
418
416
  * @param metadata.parentId - The ID of the parent file for hierarchical file relationships
419
417
  * @param metadata.onMissingParent - What to do if the parent file does not exist or has been deleted
418
+ * @param metadata.onParentConflict - What to do if the file already exists as an active child of another file
420
419
  * @param options - The options for the request
421
420
  * @param options.signal - The signal to abort the request
422
421
  *
@@ -433,6 +432,7 @@ class VaultFile {
433
432
  mimeType: metadata.mimeType,
434
433
  parentId: metadata.parentId,
435
434
  onMissingParent: metadata.onMissingParent,
435
+ onParentConflict: metadata.onParentConflict,
436
436
  fileName: this.name,
437
437
  sha256sum: this.id ?? this.metadata?.id ?? (this.content ? await getFileHash(this.content) : void 0)
438
438
  }),
@@ -524,6 +524,7 @@ class VaultFile {
524
524
  * @param params.upload - Whether to upload the file (default: false)
525
525
  * @param params.parentId - The ID of the parent file for hierarchical file relationships
526
526
  * @param params.onMissingParent - What to do if the parent file does not exist or has been deleted
527
+ * @param params.onParentConflict - What to do if the file already exists as an active child of another file
527
528
  * @param options - The options for the request
528
529
  * @param options.signal - The signal to abort the request
529
530
  *
@@ -576,7 +577,7 @@ class VaultFile {
576
577
  * ```
577
578
  */
578
579
  static async fromContent(params, options) {
579
- const { content, config: vaultConfig, upload = false, parentId, onMissingParent } = params;
580
+ const { content, config: vaultConfig, upload = false, parentId, onMissingParent, onParentConflict } = params;
580
581
  const name = basename(params.name) ?? getFileName(content);
581
582
  const config = resolveConfig(vaultConfig);
582
583
  const { vaultUrl, authStrategy } = config;
@@ -596,7 +597,8 @@ class VaultFile {
596
597
  size,
597
598
  mimeType,
598
599
  parentId,
599
- onMissingParent
600
+ onMissingParent,
601
+ onParentConflict
600
602
  }, { signal: options?.signal });
601
603
  if (upload) {
602
604
  await file.upload(file.content, createdFile.uploadUrl, { signal: options?.signal });
@@ -614,6 +616,7 @@ class VaultFile {
614
616
  * @param params.contentType - The MIME type of the content (optional)
615
617
  * @param params.parentId - The ID of the parent file for hierarchical file relationships
616
618
  * @param params.onMissingParent - What to do if the parent file does not exist or has been deleted
619
+ * @param params.onParentConflict - What to do if the file already exists as an active child of another file
617
620
  * @param options - The options for the request
618
621
  * @param options.signal - The signal to abort the request
619
622
  *
@@ -650,7 +653,14 @@ class VaultFile {
650
653
  * ```
651
654
  */
652
655
  static async fromStream(params, options) {
653
- const { contentLength, config: vaultConfig, contentType, parentId, onMissingParent } = params;
656
+ const {
657
+ contentLength,
658
+ config: vaultConfig,
659
+ contentType,
660
+ parentId,
661
+ onMissingParent,
662
+ onParentConflict
663
+ } = params;
654
664
  const name = basename(params.name);
655
665
  const config = resolveConfig(vaultConfig);
656
666
  const file = new VaultFile({ config, name });
@@ -658,10 +668,89 @@ class VaultFile {
658
668
  size: contentLength,
659
669
  mimeType: contentType || "application/octet-stream",
660
670
  parentId,
661
- onMissingParent
671
+ onMissingParent,
672
+ onParentConflict
662
673
  }, { signal: options?.signal });
663
674
  return file;
664
675
  }
676
+ /**
677
+ * Creates multiple VaultFile instances from given content in a single request.
678
+ *
679
+ * @param params - Parameters for creating VaultFiles from content
680
+ * @param params.files - Array of file inputs with content and optional metadata
681
+ * @param params.config - The configuration for the VaultFiles
682
+ * @param params.upload - Whether to upload all files (default: false)
683
+ * @param options - The options for the request
684
+ * @param options.signal - The signal to abort the request
685
+ *
686
+ * @returns Array of new VaultFile instances
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * const files = await VaultFile.fromContentBulk({
691
+ * files: [
692
+ * { content: blob1, name: 'file1.txt' },
693
+ * { content: blob2, name: 'file2.txt', parentId: 'parent-id' },
694
+ * ],
695
+ * config: { vaultUrl, authStrategy },
696
+ * upload: true
697
+ * })
698
+ * ```
699
+ */
700
+ static async fromContentBulk(params, options) {
701
+ const { files: fileInputs, config: vaultConfig, upload = false } = params;
702
+ const config = resolveConfig(vaultConfig);
703
+ const preparedFiles = await Promise.all(
704
+ fileInputs.map(async (input) => {
705
+ const name = basename(input.name) ?? getFileName(input.content);
706
+ const sha256sum = await getFileHash(input.content);
707
+ const mimeType = input.mimeType ?? await detectFileMimeType(input.content);
708
+ const size = input.content.size;
709
+ return { ...input, name, sha256sum, mimeType, size };
710
+ })
711
+ );
712
+ const url = new URL("files/bulk", config.vaultUrl);
713
+ const headers = config.authStrategy.getHeaders();
714
+ headers.set("Content-Type", "application/json");
715
+ const body = preparedFiles.map((file) => ({
716
+ fileName: file.name,
717
+ sha256sum: file.sha256sum,
718
+ mimeType: file.mimeType,
719
+ size: file.size,
720
+ parentId: file.parentId,
721
+ onMissingParent: file.onMissingParent,
722
+ onParentConflict: file.onParentConflict
723
+ }));
724
+ const response = await wrappedFetch(url, {
725
+ method: "POST",
726
+ headers,
727
+ body: JSON.stringify(body),
728
+ signal: options?.signal
729
+ });
730
+ const data = await response.json();
731
+ const vaultFiles = data.map((item, index) => {
732
+ const file = new VaultFile({
733
+ config,
734
+ id: item.id,
735
+ name: item.metadata?.originalFileName ?? preparedFiles[index].name,
736
+ content: preparedFiles[index].content,
737
+ metadata: item.metadata
738
+ });
739
+ file.lastUploadUrl = {
740
+ url: new URL(item.uploadUrl),
741
+ expiresAt: new Date(item.expiresAt)
742
+ };
743
+ return file;
744
+ });
745
+ if (upload) {
746
+ await Promise.all(
747
+ vaultFiles.map(
748
+ (file) => file.upload(void 0, void 0, { signal: options?.signal })
749
+ )
750
+ );
751
+ }
752
+ return vaultFiles;
753
+ }
665
754
  /**
666
755
  * Fetches and populates the metadata fields for this VaultFile instance.
667
756
  *
@@ -1583,8 +1672,80 @@ class VaultFile {
1583
1672
  options
1584
1673
  );
1585
1674
  }
1675
+ /**
1676
+ * Updates the metadata of this file in the vault.
1677
+ *
1678
+ * This method allows you to modify specific metadata properties of the file, such as renaming it.
1679
+ * The instance's metadata property is automatically updated with the response from the server.
1680
+ * Only the provided parameters will be updated; all other metadata remains unchanged.
1681
+ *
1682
+ * @param params - The metadata properties to update
1683
+ * @param params.name - The new name to assign to the file
1684
+ * @param options - Additional options for the request
1685
+ * @param options.signal - AbortSignal to cancel the request
1686
+ *
1687
+ * @returns A Promise that resolves when the update completes successfully
1688
+ * @throws {Error} If the file ID is not set
1689
+ * @throws {FetchError} If the update request fails
1690
+ *
1691
+ * @example
1692
+ * ```ts
1693
+ * // Rename a file
1694
+ * const vaultFile = await VaultFile.fromVaultReference({
1695
+ * reference: 'vault://file-id',
1696
+ * config: { vaultUrl, authStrategy }
1697
+ * })
1698
+ * await vaultFile.updateMetadata({ name: 'new-name.txt' })
1699
+ * console.log('File renamed to:', vaultFile.metadata?.originalFileName)
1700
+ * ```
1701
+ *
1702
+ * @example
1703
+ * ```ts
1704
+ * // Rename a file with abort signal
1705
+ * const controller = new AbortController()
1706
+ * await vaultFile.updateMetadata(
1707
+ * { name: 'document-v2.pdf' },
1708
+ * { signal: controller.signal }
1709
+ * )
1710
+ * // Later: controller.abort()
1711
+ * ```
1712
+ *
1713
+ * @example
1714
+ * ```ts
1715
+ * // Rename after upload
1716
+ * const file = new File(['content'], 'temp.txt')
1717
+ * const vaultFile = await VaultFile.fromContent({
1718
+ * name: 'temp.txt',
1719
+ * content: file,
1720
+ * config: { vaultUrl, authStrategy },
1721
+ * upload: true
1722
+ * })
1723
+ * // Later, rename the file
1724
+ * await vaultFile.updateMetadata({ name: 'final-document.txt' })
1725
+ * ```
1726
+ */
1727
+ async updateMetadata(params, options) {
1728
+ if (!this.id) {
1729
+ throw new Error("File ID is not set");
1730
+ }
1731
+ const updatedMetadata = await this._fetch({
1732
+ method: "PATCH",
1733
+ path: `files/${this.id}`,
1734
+ body: JSON.stringify(params),
1735
+ signal: options?.signal
1736
+ });
1737
+ this.metadata = updatedMetadata;
1738
+ }
1586
1739
  }
1587
1740
 
1741
+ const VAULT_REFERENCE_UUID_REGEX = /^vault:\/\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1742
+ const VAULT_REFERENCE_SHA256_REGEX = /^vault:\/\/[a-f0-9]{64}$/i;
1743
+ function isValidUuidV4(uuid) {
1744
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid);
1745
+ }
1746
+ function isValidSha256(sha256) {
1747
+ return /^[a-f0-9]{64}$/i.test(sha256);
1748
+ }
1588
1749
  function isS3UrlExpired(url) {
1589
1750
  try {
1590
1751
  const urlObj = new URL(url);
@@ -1608,59 +1769,94 @@ function isS3UrlExpired(url) {
1608
1769
  }
1609
1770
  }
1610
1771
  function isVaultReference(url) {
1611
- return url.startsWith("vault://") && url.length === 10;
1772
+ return VAULT_REFERENCE_UUID_REGEX.test(url) || VAULT_REFERENCE_SHA256_REGEX.test(url);
1612
1773
  }
1613
- function isVaultFileS3Url(url) {
1614
- const urlObj = new URL(url);
1615
- const amzDate = urlObj.searchParams.get("X-Amz-Date");
1616
- const amzExpires = urlObj.searchParams.get("X-Amz-Expires");
1617
- return urlObj.hostname.includes("amazonaws.com") && Boolean(amzDate) && Boolean(amzExpires);
1774
+ function isPresignedS3Url(url) {
1775
+ try {
1776
+ const urlObj = new URL(url);
1777
+ const amzDate = urlObj.searchParams.get("X-Amz-Date");
1778
+ const amzExpires = urlObj.searchParams.get("X-Amz-Expires");
1779
+ return urlObj.hostname.includes("amazonaws.com") && Boolean(amzDate) && Boolean(amzExpires);
1780
+ } catch {
1781
+ return false;
1782
+ }
1618
1783
  }
1619
1784
  const URL_STRATEGIES = [
1620
- {
1621
- separator: "/",
1622
- extractSegments: ([workspaceId, vaultFileId]) => ({
1623
- workspaceId,
1624
- vaultFileId
1625
- })
1626
- },
1627
1785
  {
1628
1786
  separator: "_",
1629
1787
  extractSegments: ([vaultFileId, workspaceId]) => ({
1630
1788
  vaultFileId,
1631
1789
  workspaceId
1632
1790
  })
1791
+ },
1792
+ {
1793
+ separator: "/",
1794
+ extractSegments: ([workspaceId, vaultFileId]) => ({
1795
+ workspaceId,
1796
+ vaultFileId
1797
+ })
1633
1798
  }
1634
1799
  ];
1635
- function extractVaultFileIdFromS3Url(url) {
1636
- if (isVaultReference(url))
1637
- return vaultUtils__default.file.getFileIdFromVaultReference(url);
1800
+ function extractVaultFileIdFromLegacyS3Url(url) {
1638
1801
  try {
1639
- if (!isVaultFileS3Url(url))
1640
- return null;
1641
1802
  const urlObj = new URL(url);
1642
1803
  const strategy = URL_STRATEGIES.find((strategy2) => urlObj.pathname.includes(strategy2.separator));
1643
1804
  if (!strategy)
1644
1805
  return null;
1645
- const segments = urlObj.pathname.split(strategy.separator).filter((segment) => segment.length > 0);
1806
+ const segments = urlObj.pathname.split(strategy.separator).map((segment) => segment.replace(/^\/+|\/+$/g, "")).filter((segment) => segment.length > 0);
1646
1807
  if (segments.length < 2)
1647
1808
  return null;
1648
1809
  const extractedIds = strategy.extractSegments(segments);
1649
- if (!extractedIds || !extractedIds.vaultFileId || !extractedIds.workspaceId || extractedIds.vaultFileId.length < 32)
1810
+ if (!extractedIds.vaultFileId || !extractedIds.workspaceId || extractedIds.vaultFileId.length < 32)
1650
1811
  return null;
1651
1812
  return extractedIds.vaultFileId;
1652
1813
  } catch {
1653
1814
  return null;
1654
1815
  }
1655
1816
  }
1817
+ function extractVaultFileIdFromS3Url(url) {
1818
+ if (isVaultReference(url))
1819
+ return vaultUtils__default.file.getFileIdFromVaultReference(url);
1820
+ if (!isPresignedS3Url(url))
1821
+ return null;
1822
+ if (isTaggedVaultPresignedUrl(url))
1823
+ return getVaultParamsFromS3Url(url)?.fileId ?? null;
1824
+ return extractVaultFileIdFromLegacyS3Url(url);
1825
+ }
1656
1826
  function convertS3UrlToVaultReference(url) {
1657
1827
  const vaultFileId = extractVaultFileIdFromS3Url(url);
1658
1828
  return vaultFileId ? `vault://${vaultFileId}` : null;
1659
1829
  }
1830
+ function isTaggedVaultPresignedUrl(url) {
1831
+ try {
1832
+ const urlObj = new URL(url);
1833
+ const workspaceId = urlObj.searchParams.get("vault-workspace-id");
1834
+ const fileId = urlObj.searchParams.get("vault-file-id");
1835
+ if (!workspaceId || !fileId) {
1836
+ return false;
1837
+ }
1838
+ const hasWorkspaceId = isValidUuidV4(workspaceId);
1839
+ const hasFileId = isValidSha256(fileId) || isValidUuidV4(fileId);
1840
+ const hasAmazonDomain = urlObj.hostname.endsWith("amazonaws.com");
1841
+ return hasWorkspaceId && hasFileId && hasAmazonDomain;
1842
+ } catch {
1843
+ return false;
1844
+ }
1845
+ }
1846
+ function getVaultParamsFromS3Url(url) {
1847
+ if (!isTaggedVaultPresignedUrl(url))
1848
+ return null;
1849
+ const urlObj = new URL(url);
1850
+ return {
1851
+ fileId: urlObj.searchParams.get("vault-file-id"),
1852
+ workspaceId: urlObj.searchParams.get("vault-workspace-id"),
1853
+ parentId: urlObj.searchParams.get("vault-parent-id")
1854
+ };
1855
+ }
1660
1856
 
1661
1857
  function vaultClient(vaultConfig) {
1662
1858
  const config = resolveConfig(vaultConfig);
1663
- function createFromContent(nameOrContent, contentOrNameOrOptions, options) {
1859
+ async function createFromContent(nameOrContent, contentOrNameOrOptions, options) {
1664
1860
  const isOptionsObject = (value) => {
1665
1861
  return typeof value === "object" && value !== null && !(value instanceof Blob) && !(value instanceof File);
1666
1862
  };
@@ -1671,7 +1867,7 @@ function vaultClient(vaultConfig) {
1671
1867
  const parentId2 = options?.parentId;
1672
1868
  const mimeType2 = options?.mimeType;
1673
1869
  const upload2 = options?.upload;
1674
- return VaultFile.fromContent({ content: content2, name: name2, config, parentId: parentId2, mimeType: mimeType2, upload: upload2 }, { signal: signal2 });
1870
+ return await VaultFile.fromContent({ content: content2, name: name2, config, parentId: parentId2, mimeType: mimeType2, upload: upload2 }, { signal: signal2 });
1675
1871
  }
1676
1872
  const content = nameOrContent;
1677
1873
  const name = typeof contentOrNameOrOptions === "string" ? contentOrNameOrOptions : void 0;
@@ -1679,16 +1875,16 @@ function vaultClient(vaultConfig) {
1679
1875
  const parentId = isOptionsObject(contentOrNameOrOptions) ? contentOrNameOrOptions.parentId : options?.parentId;
1680
1876
  const mimeType = isOptionsObject(contentOrNameOrOptions) ? contentOrNameOrOptions.mimeType : options?.mimeType;
1681
1877
  const upload = isOptionsObject(contentOrNameOrOptions) ? contentOrNameOrOptions.upload : options?.upload;
1682
- return VaultFile.fromContent({ content, name, config, parentId, mimeType, upload }, { signal });
1878
+ return await VaultFile.fromContent({ content, name, config, parentId, mimeType, upload }, { signal });
1683
1879
  }
1684
- function createFromReference(reference, options) {
1685
- return VaultFile.fromVaultReference({
1880
+ async function createFromReference(reference, options) {
1881
+ return await VaultFile.fromVaultReference({
1686
1882
  reference,
1687
1883
  config
1688
1884
  }, { signal: options?.signal });
1689
1885
  }
1690
1886
  async function createFromStream(name, contentLength, options) {
1691
- return VaultFile.fromStream({
1887
+ return await VaultFile.fromStream({
1692
1888
  name,
1693
1889
  contentLength,
1694
1890
  config,
@@ -1696,7 +1892,14 @@ function vaultClient(vaultConfig) {
1696
1892
  parentId: options?.parentId
1697
1893
  }, { signal: options?.signal });
1698
1894
  }
1699
- return { createFromContent, createFromReference, createFromStream };
1895
+ async function createFromContentBulk(files, options) {
1896
+ return await VaultFile.fromContentBulk({
1897
+ files,
1898
+ config,
1899
+ upload: options?.upload
1900
+ }, { signal: options?.signal });
1901
+ }
1902
+ return { createFromContent, createFromReference, createFromStream, createFromContentBulk };
1700
1903
  }
1701
1904
 
1702
1905
  exports.APIKeyAuthStrategy = APIKeyAuthStrategy;
@@ -1705,6 +1908,9 @@ exports.FetchError = FetchError;
1705
1908
  exports.VaultFile = VaultFile;
1706
1909
  exports.convertS3UrlToVaultReference = convertS3UrlToVaultReference;
1707
1910
  exports.extractVaultFileIdFromS3Url = extractVaultFileIdFromS3Url;
1911
+ exports.getVaultParamsFromS3Url = getVaultParamsFromS3Url;
1708
1912
  exports.isS3UrlExpired = isS3UrlExpired;
1709
- exports.isVaultFileS3Url = isVaultFileS3Url;
1913
+ exports.isTaggedVaultPresignedUrl = isTaggedVaultPresignedUrl;
1914
+ exports.isVaultFileS3Url = isPresignedS3Url;
1915
+ exports.isVaultReference = isVaultReference;
1710
1916
  exports.vaultClient = vaultClient;