@meistrari/vault-sdk 0.0.12 → 1.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.mjs CHANGED
@@ -1,9 +1,20 @@
1
+ import { GetUploadUrlResponseV2, GetDownloadUrlResponse } from '@meistrari/vault-shared/schemas';
2
+
1
3
  class FetchError extends Error {
2
- constructor(message, response) {
4
+ constructor(message, url, method, response) {
3
5
  super(message);
6
+ this.message = message;
7
+ this.url = url;
8
+ this.method = method;
4
9
  this.response = response;
5
10
  this.name = "FetchError";
6
11
  }
12
+ static async from(url, method, response) {
13
+ const text = await response.clone().json().then((json) => JSON.stringify(json, null, 2)).catch(() => response.clone().text());
14
+ const error = new FetchError(`Failed to ${method} ${url}: ${response.status} ${response.statusText}:
15
+ ${text}`, url, method, response);
16
+ return error;
17
+ }
7
18
  }
8
19
 
9
20
  async function blobToBase64(blob) {
@@ -28,114 +39,348 @@ var __publicField$1 = (obj, key, value) => {
28
39
  __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
29
40
  return value;
30
41
  };
42
+ const compatibilityDate = "2025-05-19";
43
+ function removeVaultPrefix(url) {
44
+ return url.replace("vault://", "");
45
+ }
46
+ async function wrappedFetch(...params) {
47
+ const request = new Request(...params);
48
+ const response = await fetch(request);
49
+ if (!response.ok) {
50
+ throw await FetchError.from(request.url, request.method, response);
51
+ }
52
+ return response;
53
+ }
31
54
  class VaultFile {
32
- constructor(params) {
33
- __publicField$1(this, "vaultUrl");
55
+ /**
56
+ * Constructs a new VaultFile instance. Direct usage of the constructor is not recommended,
57
+ * instead use the static methods {@link VaultFile.fromVaultReference} when dealing with an existing file in the vault,
58
+ * or {@link VaultFile.fromContent} when preparing a new file for upload.
59
+ *
60
+ * @param params - The parameters for the VaultFile constructor
61
+ * @param params.config - The configuration for the VaultFile
62
+ * @param params.content - The content of the file
63
+ * @param params.id - The ID of the file
64
+ * @param params.name - The name of the file
65
+ * @param params.metadata - The metadata of the file
66
+ */
67
+ constructor({ config, content, id, name, metadata }) {
68
+ __publicField$1(this, "id");
34
69
  __publicField$1(this, "name");
35
- __publicField$1(this, "headers");
36
- this.name = params.name;
37
- this.vaultUrl = params.vaultUrl;
38
- this.headers = params.authStrategy.getHeaders();
39
- }
40
- getVaultUrl() {
41
- if (this.name.includes("vault://")) {
42
- return this.name;
43
- }
44
- return `vault://${this.name}`;
70
+ __publicField$1(this, "metadata");
71
+ __publicField$1(this, "config");
72
+ __publicField$1(this, "content");
73
+ __publicField$1(this, "lastDownloadUrl");
74
+ __publicField$1(this, "lastUploadUrl");
75
+ this.config = config;
76
+ this.content = content;
77
+ this.id = id;
78
+ this.name = name;
79
+ this.metadata = metadata;
45
80
  }
46
- removeVaultPrefix(url) {
47
- return url.replace("vault://", "");
48
- }
49
- async getUploadUrl() {
50
- const response = await this._fetch({
51
- method: "POST",
52
- path: `/v2/files/${this.removeVaultPrefix(this.name)}`
53
- });
54
- return new URL(response.url);
55
- }
56
- async getDownloadUrl() {
57
- const response = await this._fetch({
58
- method: "GET",
59
- path: `/v2/files/${this.removeVaultPrefix(this.name)}`
60
- });
61
- return new URL(response.url);
62
- }
63
- refreshAuth(authStrategy) {
64
- this.headers = authStrategy.getHeaders();
81
+ /**
82
+ * Gets the headers for the request based on the auth strategy.
83
+ *
84
+ * @returns The headers for the request
85
+ */
86
+ get headers() {
87
+ return this.config.authStrategy.getHeaders();
65
88
  }
89
+ /**
90
+ * Performs a request to the vault service and handles the response or errors.
91
+ *
92
+ * @param params - The parameters for the fetch
93
+ * @param params.method - The method to use for the fetch
94
+ * @param params.path - The path to fetch
95
+ * @param params.body - The body of the request
96
+ * @returns The response from the vault
97
+ * @throws {FetchError} If the fetch fails
98
+ */
66
99
  async _fetch(params) {
67
- const { method, path, body, ignoreHeaders } = params;
68
- const url = new URL(this.vaultUrl + path).toString();
69
- const response = await fetch(url, {
100
+ const { method, path, body } = params;
101
+ const url = new URL(this.config.vaultUrl + path).toString();
102
+ const headers = new Headers(this.headers);
103
+ headers.set("x-compatibility-date", compatibilityDate);
104
+ const response = await wrappedFetch(url, {
70
105
  method,
71
106
  body,
72
- headers: ignoreHeaders ? void 0 : this.headers
107
+ headers
73
108
  });
74
- if (!response.ok) {
75
- throw new FetchError(`Failed to ${method} ${url}: ${response.status} ${response.statusText}`, response);
76
- }
77
109
  const content = await response.json();
78
110
  return content;
79
111
  }
80
112
  /**
81
- * Adds a SHA-256 hash of the file content as a prefix to the filename. This ensures uniqueness and prevents
82
- * files with identical names but different content from overwriting each other in the vault.
113
+ * Creates a new file in the vault.
114
+ *
115
+ * @returns The metadata of the file
116
+ * @throws {Error} If the file ID is not set
117
+ * @throws {FetchError} If the metadata fetch fails
118
+ */
119
+ async _createFile() {
120
+ const response = await this._fetch({
121
+ method: "POST",
122
+ path: `/v2/files`,
123
+ body: JSON.stringify({
124
+ fileName: this.name,
125
+ sha256sum: this.id ?? this.metadata?.id ?? (this.content ? await getFileHash(this.content) : void 0)
126
+ })
127
+ }).then((data) => GetUploadUrlResponseV2.safeParse(data));
128
+ if (!response.success) {
129
+ throw new Error(`Invalid response from vault service. ${JSON.stringify(response.error)}`);
130
+ }
131
+ this.id = response.data.id;
132
+ this.metadata = response.data.metadata;
133
+ this.name = response.data.metadata?.originalFileName;
134
+ return response.data;
135
+ }
136
+ /**
137
+ * Creates a new VaultFile instance from a vault reference.
83
138
  *
84
- * The resulting filename format is: "{hash}-{originalName}"
139
+ * @param params - The parameters for creating a VaultFile from a vault reference
140
+ * @param params.reference - The reference to the file in the vault
141
+ * @param params.config - The configuration for the VaultFile
142
+ * @param params.download - Whether to download the file content (default: false)
143
+ * @returns A new VaultFile instance
85
144
  *
86
- * IMPORTANT: The modified filename must be stored and used for all future operations with this file in the vault,
87
- * as it becomes the file's unique identifier. The original filename alone will not be sufficient to retrieve
88
- * the file later.
145
+ * @example
146
+ * ```ts
147
+ * // Lazily download the file content
148
+ * const vaultFile = await VaultFile.fromVaultReference({
149
+ * reference: 'vault://1234567890',
150
+ * config: {
151
+ * vaultUrl,
152
+ * authStrategy,
153
+ * }
154
+ * })
155
+ * const content = await vaultFile.download()
156
+ * ```
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * // Download the file content while creating the instance
161
+ * const vaultFile = await VaultFile.fromVaultReference({
162
+ * reference: 'vault://1234567890',
163
+ * config: {
164
+ * vaultUrl,
165
+ * authStrategy,
166
+ * },
167
+ * download: true
168
+ * })
169
+ * const content = vaultFile.content
170
+ * ```
171
+ */
172
+ static async fromVaultReference(params) {
173
+ const { reference, config, download = false } = params;
174
+ const { vaultUrl, authStrategy } = config;
175
+ const id = removeVaultPrefix(reference);
176
+ const response = await wrappedFetch(`${vaultUrl}/v2/files/${id}`, {
177
+ method: "GET",
178
+ headers: authStrategy.getHeaders()
179
+ }).then((response2) => response2.json()).then((data) => GetDownloadUrlResponse.safeParse(data));
180
+ if (!response.success) {
181
+ throw new Error("Invalid response from vault service");
182
+ }
183
+ const fileParams = {
184
+ id,
185
+ metadata: response.data.metadata,
186
+ config: {
187
+ vaultUrl,
188
+ authStrategy
189
+ },
190
+ name: response.data.metadata?.originalFileName
191
+ };
192
+ if (download) {
193
+ await wrappedFetch(response.data.url, { method: "GET" }).then((response2) => response2.blob()).then((blob) => fileParams.content = blob);
194
+ }
195
+ return new VaultFile(fileParams);
196
+ }
197
+ /**
198
+ * Creates a new VaultFile instance from given content.
199
+ *
200
+ * @param params - The parameters for creating a VaultFile from content
201
+ * @param params.name - The name of the file
202
+ * @param params.content - The content of the file
203
+ * @param params.config - The configuration for the VaultFile
204
+ * @param params.upload - Whether to upload the file (default: false)
205
+ * @returns A new VaultFile instance
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * // Lazily upload the file content
210
+ * const file = new File(['content'], 'document.txt')
211
+ * const vaultFile = await VaultFile.fromContent({
212
+ * name: 'document.txt',
213
+ * content: file,
214
+ * config: {
215
+ * vaultUrl,
216
+ * authStrategy,
217
+ * }
218
+ * })
219
+ * await vaultFile.upload()
220
+ * ```
89
221
  *
90
222
  * @example
223
+ * ```ts
224
+ * // Upload the file content while creating the instance
91
225
  * const file = new File(['content'], 'document.txt')
92
- * await vaultFile.addHashToName(file)
93
- * // vaultFile.name becomes: "a1b2c3...xyz-document.txt"
226
+ * const vaultFile = await VaultFile.fromContent({
227
+ * name: 'document.txt',
228
+ * content: file,
229
+ * config: {
230
+ * vaultUrl,
231
+ * authStrategy,
232
+ * },
233
+ * upload: true
234
+ * })
235
+ * ```
236
+ */
237
+ static async fromContent(params) {
238
+ const { name, content, config, upload = false } = params;
239
+ const { vaultUrl, authStrategy } = config;
240
+ const sha256sum = await getFileHash(content);
241
+ const file = new VaultFile({
242
+ content,
243
+ config: {
244
+ vaultUrl,
245
+ authStrategy
246
+ },
247
+ id: sha256sum,
248
+ name
249
+ });
250
+ const createdFile = await file._createFile();
251
+ if (upload) {
252
+ await file.upload(file.content, createdFile.uploadUrl);
253
+ }
254
+ return file;
255
+ }
256
+ /**
257
+ * Populates the metadata of the file instance.
94
258
  *
95
- * @param file - The file to generate a hash for
96
- * @returns The new filename with the hash prefix
259
+ * @returns The file instance
260
+ * @throws {Error} If the file ID is not set
261
+ * @throws {FetchError} If the metadata fetch fails
97
262
  */
98
- async addHashToName(file) {
99
- const fileHash = await getFileHash(file);
100
- if (!this.name.includes(fileHash))
101
- this.name = `${fileHash}-${this.name}`;
102
- return this.name;
263
+ async populateMetadata() {
264
+ try {
265
+ this.metadata = await this.getFileMetadata();
266
+ this.name = this.metadata.originalFileName;
267
+ this.id = this.metadata.id;
268
+ return this;
269
+ } catch (error) {
270
+ console.error("Error fetching file metadata", error);
271
+ }
103
272
  }
104
273
  /**
105
- * Uploads a file to the vault.
274
+ * Gets the vault reference for this file.
275
+ *
276
+ * @returns The vault reference in the format `vault://{fileId}`
277
+ * @throws {Error} If the file ID is not set
278
+ */
279
+ getVaultReference() {
280
+ if (!this.id) {
281
+ throw new Error("File ID is not set");
282
+ }
283
+ return `vault://${this.id}`;
284
+ }
285
+ /**
286
+ * Fetches the metadata of the file.
287
+ *
288
+ * @returns The metadata of the file
289
+ * @throws {Error} If the file ID is not set
290
+ * @throws {FetchError} If the metadata fetch fails
291
+ */
292
+ async getFileMetadata() {
293
+ if (!this.id) {
294
+ throw new Error("File ID is not set");
295
+ }
296
+ const response = await this._fetch({
297
+ method: "GET",
298
+ path: `/v2/files/${this.id}/metadata`
299
+ });
300
+ return response;
301
+ }
302
+ /**
303
+ * Fetches a upload URL for the file.
304
+ *
305
+ * @returns The upload URL for the file
306
+ * @throws {Error} If the vault service returns an invalid response
307
+ * @throws {FetchError} If the upload URL fetch fails
308
+ */
309
+ async getUploadUrl() {
310
+ if (this.lastUploadUrl && this.lastUploadUrl.expiresAt > /* @__PURE__ */ new Date()) {
311
+ return this.lastUploadUrl.url;
312
+ }
313
+ if (!this.id) {
314
+ const createdFile = await this._createFile();
315
+ this.id = createdFile.id;
316
+ this.metadata = createdFile.metadata;
317
+ this.name = createdFile.metadata?.originalFileName;
318
+ this.lastUploadUrl = { url: new URL(createdFile.uploadUrl), expiresAt: new Date(createdFile.expiresAt) };
319
+ return this.lastUploadUrl.url;
320
+ }
321
+ const response = await this._fetch({
322
+ method: "PUT",
323
+ path: `/v2/files/${this.id}`
324
+ }).then(GetUploadUrlResponseV2.safeParse);
325
+ if (!response.success) {
326
+ throw new Error(`Invalid response from vault service. ${JSON.stringify(response.error)}`);
327
+ }
328
+ this.lastUploadUrl = { url: new URL(response.data.uploadUrl), expiresAt: new Date(response.data.expiresAt) };
329
+ return this.lastUploadUrl.url;
330
+ }
331
+ /**
332
+ * Fetches a download URL for the file.
106
333
  *
107
- * Files are saved with the given file names, so files with the same name within the same workspace
108
- * will overwrite each other. To prevent accidental overwrites and support multiple files with the
109
- * same original name, you should call addHashToName() before uploading to add a unique content-based
110
- * hash to the filename.
334
+ * @returns The download URL for the file
335
+ * @throws {Error} If the vault service returns an invalid response
336
+ * @throws {Error} If not file ID, name or content is set
337
+ * @throws {FetchError} If the download URL fetch fails
338
+ */
339
+ async getDownloadUrl() {
340
+ if (this.lastDownloadUrl && this.lastDownloadUrl.expiresAt > /* @__PURE__ */ new Date()) {
341
+ return this.lastDownloadUrl.url;
342
+ }
343
+ if (!this.id && !this.name && !this.content) {
344
+ throw new Error("File was not created yet");
345
+ }
346
+ const id = this.id ?? this.metadata?.id ?? (this.content ? await getFileHash(this.content) : this.name);
347
+ const response = await this._fetch({
348
+ method: "GET",
349
+ path: `/v2/files/${id}`
350
+ });
351
+ this.lastDownloadUrl = { url: new URL(response.url), expiresAt: new Date(response.expiresAt) };
352
+ return this.lastDownloadUrl.url;
353
+ }
354
+ /**
355
+ * Uploads a file to the vault.
111
356
  *
112
357
  * @example
358
+ * ```ts
113
359
  * const file = new File(['content'], 'document.txt')
114
- * await vaultFile.addHashToName(file) // Adds hash prefix to filename
360
+ * const vaultFile = await VaultFile.fromBlob('document.txt', file, { vaultUrl, authStrategy })
115
361
  * await vaultFile.upload(file)
362
+ * ```
116
363
  *
117
- * @param file - The file to upload to the vault
364
+ * @param file - The file to upload to the vault. If not provided, the file content will be taken from the `content` property.
118
365
  * @throws {FetchError} If the upload fails
366
+ * @throws {Error} If the file content is not set and no file is provided
119
367
  * @returns Promise that resolves when upload is complete
120
368
  */
121
- async upload(file) {
122
- const uploadUrl = await this.getUploadUrl();
123
- const response = await fetch(uploadUrl, {
369
+ async upload(file, url) {
370
+ if (!file && !this.content) {
371
+ throw new Error("Missing file content. Use fromBlob() to create a file with content, or provide a file to upload.");
372
+ }
373
+ const uploadUrl = url ?? await this.getUploadUrl();
374
+ await wrappedFetch(uploadUrl, {
124
375
  method: "PUT",
125
- body: file
376
+ body: file ?? this.content
126
377
  });
127
- if (!response.ok) {
128
- throw new FetchError(`Error uploading file ${this.name}: ${response.status} ${response.statusText}`, response);
129
- }
130
378
  }
131
379
  async download(responseType = "blob") {
132
380
  const downloadUrl = await this.getDownloadUrl();
133
- const response = await fetch(downloadUrl, {
381
+ const response = await wrappedFetch(downloadUrl, {
134
382
  method: "GET"
135
383
  });
136
- if (!response.ok) {
137
- throw new FetchError(`Error downloading file ${this.name}: ${response.status} ${response.statusText}`, response);
138
- }
139
384
  const blob = await response.blob();
140
385
  if (responseType === "blob")
141
386
  return blob;
@@ -172,15 +417,21 @@ class APIKeyAuthStrategy {
172
417
  }
173
418
  }
174
419
 
175
- function useVault(vaultUrl, authStrategy) {
176
- function createVaultFile(name) {
177
- return new VaultFile({
420
+ function vaultClient(config) {
421
+ function createFromContent(name, content) {
422
+ return VaultFile.fromContent({
178
423
  name,
179
- authStrategy,
180
- vaultUrl
424
+ content,
425
+ config
426
+ });
427
+ }
428
+ function createFromReference(reference) {
429
+ return VaultFile.fromVaultReference({
430
+ reference,
431
+ config
181
432
  });
182
433
  }
183
- return { createVaultFile };
434
+ return { createFromContent, createFromReference };
184
435
  }
185
436
 
186
- export { APIKeyAuthStrategy, DataTokenAuthStrategy, FetchError, VaultFile, useVault };
437
+ export { APIKeyAuthStrategy, DataTokenAuthStrategy, FetchError, VaultFile, vaultClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/vault-sdk",
3
- "version": "0.0.12",
3
+ "version": "1.0.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,8 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs"
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
13
14
  }
14
15
  },
15
16
  "main": "dist/index.mjs",
@@ -17,26 +18,28 @@
17
18
  "files": [
18
19
  "dist"
19
20
  ],
21
+ "scripts": {
22
+ "test": "vitest",
23
+ "build": "unbuild",
24
+ "lint": "eslint .",
25
+ "lint:fix": "eslint . --fix",
26
+ "check": "bun run lint && bun tsc --noEmit"
27
+ },
20
28
  "dependencies": {
21
- "ofetch": "1.4.1"
29
+ "@meistrari/vault-shared": "workspace:*",
30
+ "ofetch": "1.4.1",
31
+ "zod": "3.23.8"
22
32
  },
23
33
  "devDependencies": {
24
- "vitest": "2.1.9",
25
34
  "@types/bun": "latest",
26
35
  "msw": "2.6.8",
27
- "unbuild": "2.0.0"
36
+ "unbuild": "2.0.0",
37
+ "vitest": "2.1.9"
28
38
  },
29
39
  "peerDependencies": {
30
40
  "typescript": "^5.0.0"
31
41
  },
32
42
  "publishConfig": {
33
43
  "access": "public"
34
- },
35
- "scripts": {
36
- "test": "vitest",
37
- "build": "unbuild",
38
- "lint": "eslint .",
39
- "lint:fix": "eslint . --fix",
40
- "check": "bun run lint && bun tsc --noEmit"
41
44
  }
42
- }
45
+ }