@meistrari/vault-sdk 1.4.3 → 1.6.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.mjs CHANGED
@@ -96,7 +96,7 @@ class Permalink {
96
96
  /**
97
97
  * Get a new permalink instance from its ID.
98
98
  *
99
- * @param config - The vault config.
99
+ * @param vaultConfig - The vault config.
100
100
  * @param id - The permalink ID.
101
101
  * @returns The permalink.
102
102
  */
@@ -114,7 +114,7 @@ class Permalink {
114
114
  /**
115
115
  * Create a new permalink.
116
116
  *
117
- * @param config - The vault config.
117
+ * @param vaultConfig - The vault config.
118
118
  * @param params - The parameters for the permalink.
119
119
  * @param params.expiresIn - Time, in seconds, the permalink will be valid for.
120
120
  * @param params.fileId - The ID of the file to create a permalink for.
@@ -195,9 +195,92 @@ async function detectFileMimeType(blob) {
195
195
  if (result?.mime) {
196
196
  return result.mime;
197
197
  }
198
+ const text = await blob.text();
199
+ const trimmedText = text.trim();
200
+ if (trimmedText.startsWith("{") && trimmedText.endsWith("}") || trimmedText.startsWith("[") && trimmedText.endsWith("]")) {
201
+ try {
202
+ JSON.parse(trimmedText);
203
+ return "application/json";
204
+ } catch {
205
+ }
206
+ }
207
+ const lines = text.split("\n").slice(0, 5).filter((line) => line.trim() !== "");
208
+ if (lines.length > 1) {
209
+ const commaCounts = lines.map((line) => (line.match(/,/g) || []).length);
210
+ const allSame = commaCounts.every((count) => count === commaCounts[0]);
211
+ if (allSame && commaCounts[0] > 0) {
212
+ return "text/csv";
213
+ }
214
+ }
215
+ if (!text.includes("\0") && /^[\s\S]{1,1000}$/.test(text.slice(0, 1e3))) {
216
+ return "text/plain";
217
+ }
198
218
  return void 0;
199
219
  }
200
220
 
221
+ const name = "@meistrari/vault-sdk";
222
+ const version = "1.6.0";
223
+ const license = "UNLICENSED";
224
+ const repository = {
225
+ type: "git",
226
+ url: "https://github.com/meistrari/vault.git"
227
+ };
228
+ const exports = {
229
+ ".": {
230
+ types: "./dist/index.d.ts",
231
+ "import": "./dist/index.mjs",
232
+ require: "./dist/index.cjs"
233
+ }
234
+ };
235
+ const main = "dist/index.mjs";
236
+ const types = "dist/index.d.ts";
237
+ const files = [
238
+ "dist"
239
+ ];
240
+ const scripts = {
241
+ test: "vitest --no-watch",
242
+ "test:watch": "vitest",
243
+ build: "unbuild",
244
+ lint: "eslint .",
245
+ "lint:fix": "eslint . --fix",
246
+ check: "bun run lint && bun tsc --noEmit"
247
+ };
248
+ const dependencies = {
249
+ "@meistrari/vault-shared": "workspace:*",
250
+ "file-type": "21.0.0",
251
+ "mime-types": "3.0.1",
252
+ ofetch: "1.4.1",
253
+ zod: "3.23.8"
254
+ };
255
+ const devDependencies = {
256
+ "@types/bun": "latest",
257
+ "@types/mime-types": "3.0.1",
258
+ msw: "2.6.8",
259
+ unbuild: "2.0.0",
260
+ vitest: "2.1.9"
261
+ };
262
+ const peerDependencies = {
263
+ typescript: "^5.0.0"
264
+ };
265
+ const publishConfig = {
266
+ access: "public"
267
+ };
268
+ const packageJson = {
269
+ name: name,
270
+ version: version,
271
+ license: license,
272
+ repository: repository,
273
+ exports: exports,
274
+ main: main,
275
+ types: types,
276
+ files: files,
277
+ scripts: scripts,
278
+ dependencies: dependencies,
279
+ devDependencies: devDependencies,
280
+ peerDependencies: peerDependencies,
281
+ publishConfig: publishConfig
282
+ };
283
+
201
284
  var __defProp = Object.defineProperty;
202
285
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
203
286
  var __publicField = (obj, key, value) => {
@@ -208,8 +291,23 @@ const compatibilityDate = "2025-05-19";
208
291
  function removeVaultPrefix(url) {
209
292
  return url.replace("vault://", "");
210
293
  }
211
- async function wrappedFetch(...params) {
212
- const request = new Request(...params);
294
+ function detectMimeTypeFromFilename(filename) {
295
+ const extension = filename.split(".").pop()?.toLowerCase();
296
+ if (extension) {
297
+ const mimeType = lookup(`.${extension}`);
298
+ if (mimeType) {
299
+ return mimeType;
300
+ }
301
+ }
302
+ return void 0;
303
+ }
304
+ async function wrappedFetch(url, requestInit) {
305
+ const options = {
306
+ ...requestInit,
307
+ duplex: requestInit.body instanceof ReadableStream ? "half" : void 0
308
+ };
309
+ const request = new Request(url, options);
310
+ request.headers.set("User-Agent", `vault-js-sdk:${packageJson.version}`);
213
311
  const response = await fetch(request);
214
312
  if (!response.ok) {
215
313
  throw await FetchError.from(request.url, request.method, response);
@@ -218,9 +316,11 @@ async function wrappedFetch(...params) {
218
316
  }
219
317
  class VaultFile {
220
318
  /**
221
- * Constructs a new VaultFile instance. Direct usage of the constructor is not recommended,
222
- * instead use the static methods {@link VaultFile.fromVaultReference} when dealing with an existing file in the vault,
223
- * or {@link VaultFile.fromContent} when preparing a new file for upload.
319
+ * Constructs a new VaultFile instance. Direct usage of the constructor is not recommended.
320
+ *
321
+ * Use the static methods {@link VaultFile.fromVaultReference} when dealing with an existing file in the vault,
322
+ * {@link VaultFile.fromContent} when preparing a new file for upload,
323
+ * or {@link VaultFile.fromStream} when preparing a new file for streaming upload.
224
324
  *
225
325
  * @param params - The parameters for the VaultFile constructor
226
326
  * @param params.config - The configuration for the VaultFile
@@ -251,7 +351,9 @@ class VaultFile {
251
351
  * @returns The headers for the request
252
352
  */
253
353
  get headers() {
254
- return this.config.authStrategy.getHeaders();
354
+ const headers = this.config.authStrategy.getHeaders();
355
+ headers.set("User-Agent", `vault-js-sdk:${packageJson.version}`);
356
+ return headers;
255
357
  }
256
358
  /**
257
359
  * Performs a request to the vault service and handles the response or errors.
@@ -274,12 +376,16 @@ class VaultFile {
274
376
  url.searchParams.set(key, value);
275
377
  });
276
378
  }
277
- const response = await wrappedFetch(url, {
379
+ const requestInit = {
278
380
  method,
279
381
  body,
280
382
  headers,
281
383
  signal
282
- });
384
+ };
385
+ if (body && body instanceof ReadableStream) {
386
+ requestInit.duplex = "half";
387
+ }
388
+ const response = await wrappedFetch(url, requestInit);
283
389
  if (response.status === 204 || response.headers.get("content-length") === "0") {
284
390
  return null;
285
391
  }
@@ -452,6 +558,48 @@ class VaultFile {
452
558
  }
453
559
  return file;
454
560
  }
561
+ /**
562
+ * Creates a new VaultFile instance for streaming upload workflows.
563
+ * This method creates a VaultFile with placeholder content that's optimized for streaming uploads.
564
+ *
565
+ * @param params - The parameters for creating a VaultFile for streaming
566
+ * @param params.name - The name of the file
567
+ * @param params.contentLength - The size of the content in bytes
568
+ * @param params.config - The configuration for the VaultFile
569
+ * @param params.contentType - The MIME type of the content (optional)
570
+ * @param options - The options for the request
571
+ * @param options.signal - The signal to abort the request
572
+ *
573
+ * @returns A new VaultFile instance ready for streaming upload
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * // Create VaultFile for streaming
578
+ * const vaultFile = await VaultFile.fromStream({
579
+ * name: 'large-video.mp4',
580
+ * contentLength: 100 * 1024 * 1024, // 100MB
581
+ * contentType: 'video/mp4',
582
+ * config: { vaultUrl, authStrategy }
583
+ * })
584
+ *
585
+ * // Upload using a stream
586
+ * const fileStream = file.stream()
587
+ * await vaultFile.uploadStream(fileStream, {
588
+ * contentLength: file.size,
589
+ * contentType: file.type
590
+ * })
591
+ * ```
592
+ */
593
+ static async fromStream(params, options) {
594
+ const { name, contentLength, config: vaultConfig, contentType } = params;
595
+ const config = resolveConfig(vaultConfig);
596
+ const file = new VaultFile({ config, name });
597
+ await file._createFile({
598
+ size: contentLength,
599
+ mimeType: contentType || "application/octet-stream"
600
+ }, { signal: options?.signal });
601
+ return file;
602
+ }
455
603
  /**
456
604
  * Populates the metadata of the file instance.
457
605
  * @param options - The options for the request
@@ -619,6 +767,90 @@ class VaultFile {
619
767
  return blob;
620
768
  return await blobToBase64(blob);
621
769
  }
770
+ /**
771
+ * Downloads a file from the vault as a stream for memory-efficient processing.
772
+ *
773
+ * @param options - The options for the request
774
+ * @param options.signal - The signal to abort the request
775
+ *
776
+ * @returns A ReadableStream that yields chunks of the file data
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * const vaultFile = await VaultFile.fromVaultReference('vault://1234567890', { vaultUrl, authStrategy })
781
+ * const stream = await vaultFile.downloadStream()
782
+ *
783
+ * // Process the stream chunk by chunk
784
+ * const reader = stream.getReader()
785
+ * try {
786
+ * while (true) {
787
+ * const { done, value } = await reader.read()
788
+ * if (done) break
789
+ * // Process the chunk (Uint8Array)
790
+ * console.log('Received chunk of size:', value.length)
791
+ * }
792
+ * } finally {
793
+ * reader.releaseLock()
794
+ * }
795
+ * ```
796
+ */
797
+ async downloadStream(options) {
798
+ const downloadUrl = await this.getDownloadUrl({ signal: options?.signal });
799
+ const response = await wrappedFetch(downloadUrl, {
800
+ method: "GET",
801
+ signal: options?.signal
802
+ });
803
+ if (!response.body) {
804
+ throw new Error("Response body is not readable");
805
+ }
806
+ return response.body;
807
+ }
808
+ /**
809
+ * Uploads a file to the vault using a stream for memory-efficient processing.
810
+ *
811
+ * @param stream - The readable stream of file data to upload
812
+ * @param options - The options for the request
813
+ * @param options.signal - The signal to abort the request
814
+ * @param options.contentLength - The total size of the content (required for S3 uploads)
815
+ * @param options.contentType - The MIME type of the content (will be detected if not provided)
816
+ *
817
+ * @throws {Error} If contentLength is not provided
818
+ * @throws {FetchError} If the upload fails
819
+ * @returns Promise that resolves when upload is complete
820
+ *
821
+ * @example
822
+ * ```ts
823
+ * const file = new File(['content'], 'document.txt')
824
+ * const vaultFile = await VaultFile.fromStream('document.txt', file.size, {
825
+ * contentType: file.type,
826
+ * config: { vaultUrl, authStrategy }
827
+ * })
828
+ *
829
+ * // Upload using the stream directly
830
+ * const stream = file.stream()
831
+ * await vaultFile.uploadStream(stream, {
832
+ * contentLength: file.size,
833
+ * contentType: file.type
834
+ * })
835
+ * ```
836
+ */
837
+ async uploadStream(stream, options) {
838
+ const { contentLength, contentType, signal } = options;
839
+ if (contentLength === void 0 || contentLength < 0) {
840
+ throw new Error("contentLength must be provided and non-negative for streaming uploads");
841
+ }
842
+ const uploadUrl = await this.getUploadUrl({ signal });
843
+ const mimeType = contentType ?? this.metadata?.mimeType ?? (this.name ? detectMimeTypeFromFilename(this.name) : void 0) ?? "application/octet-stream";
844
+ const headers = new Headers();
845
+ headers.set("Content-Type", mimeType);
846
+ headers.set("Content-Length", contentLength.toString());
847
+ await wrappedFetch(uploadUrl, {
848
+ method: "PUT",
849
+ body: stream,
850
+ headers,
851
+ signal
852
+ });
853
+ }
622
854
  /**
623
855
  * Deletes the file from the vault.
624
856
  * @param options - The options for the request
@@ -677,6 +909,79 @@ class VaultFile {
677
909
  }
678
910
  }
679
911
 
912
+ function isS3UrlExpired(url) {
913
+ try {
914
+ const urlObj = new URL(url);
915
+ const amzDate = urlObj.searchParams.get("X-Amz-Date");
916
+ const amzExpires = urlObj.searchParams.get("X-Amz-Expires");
917
+ if (!amzDate || !amzExpires)
918
+ return false;
919
+ const year = Number.parseInt(amzDate.substring(0, 4));
920
+ const month = Number.parseInt(amzDate.substring(4, 6)) - 1;
921
+ const day = Number.parseInt(amzDate.substring(6, 8));
922
+ const hours = Number.parseInt(amzDate.substring(9, 11));
923
+ const minutes = Number.parseInt(amzDate.substring(11, 13));
924
+ const seconds = Number.parseInt(amzDate.substring(13, 15));
925
+ const signedDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
926
+ const expiresInSeconds = Number.parseInt(amzExpires);
927
+ const expirationTime = signedDate.getTime() + expiresInSeconds * 1e3;
928
+ const currentTime = Date.now();
929
+ return currentTime > expirationTime;
930
+ } catch {
931
+ return false;
932
+ }
933
+ }
934
+ function isVaultReference(url) {
935
+ return url.startsWith("vault://") && url.length === 10;
936
+ }
937
+ function isVaultFileS3Url(url) {
938
+ const urlObj = new URL(url);
939
+ const amzDate = urlObj.searchParams.get("X-Amz-Date");
940
+ const amzExpires = urlObj.searchParams.get("X-Amz-Expires");
941
+ return urlObj.hostname.includes("amazonaws.com") && Boolean(amzDate) && Boolean(amzExpires);
942
+ }
943
+ const URL_STRATEGIES = [
944
+ {
945
+ separator: "/",
946
+ extractSegments: ([workspaceId, vaultFileId]) => ({
947
+ workspaceId,
948
+ vaultFileId
949
+ })
950
+ },
951
+ {
952
+ separator: "_",
953
+ extractSegments: ([vaultFileId, workspaceId]) => ({
954
+ vaultFileId,
955
+ workspaceId
956
+ })
957
+ }
958
+ ];
959
+ function extractVaultFileIdFromS3Url(url) {
960
+ if (isVaultReference(url))
961
+ return url.replace("vault://", "");
962
+ try {
963
+ if (!isVaultFileS3Url(url))
964
+ return null;
965
+ const urlObj = new URL(url);
966
+ const strategy = URL_STRATEGIES.find((strategy2) => urlObj.pathname.includes(strategy2.separator));
967
+ if (!strategy)
968
+ return null;
969
+ const segments = urlObj.pathname.split(strategy.separator).filter((segment) => segment.length > 0);
970
+ if (segments.length < 2)
971
+ return null;
972
+ const extractedIds = strategy.extractSegments(segments);
973
+ if (!extractedIds || !extractedIds.vaultFileId || !extractedIds.workspaceId || extractedIds.vaultFileId.length < 32)
974
+ return null;
975
+ return extractedIds.vaultFileId;
976
+ } catch {
977
+ return null;
978
+ }
979
+ }
980
+ function convertS3UrlToVaultReference(url) {
981
+ const vaultFileId = extractVaultFileIdFromS3Url(url);
982
+ return vaultFileId ? `vault://${vaultFileId}` : null;
983
+ }
984
+
680
985
  function vaultClient(vaultConfig) {
681
986
  const config = resolveConfig(vaultConfig);
682
987
  function createFromContent(name, content, options) {
@@ -692,7 +997,15 @@ function vaultClient(vaultConfig) {
692
997
  config
693
998
  }, { signal: options?.signal });
694
999
  }
695
- return { createFromContent, createFromReference };
1000
+ async function createFromStream(name, contentLength, options) {
1001
+ return VaultFile.fromStream({
1002
+ name,
1003
+ contentLength,
1004
+ config,
1005
+ contentType: options?.contentType
1006
+ }, { signal: options?.signal });
1007
+ }
1008
+ return { createFromContent, createFromReference, createFromStream };
696
1009
  }
697
1010
 
698
- export { APIKeyAuthStrategy, DataTokenAuthStrategy, FetchError, VaultFile, vaultClient };
1011
+ export { APIKeyAuthStrategy, DataTokenAuthStrategy, FetchError, VaultFile, convertS3UrlToVaultReference, extractVaultFileIdFromS3Url, isS3UrlExpired, isVaultFileS3Url, vaultClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/vault-sdk",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",