vault-sdk-prod 1.0.0 → 2.2.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.
- package/README.md +343 -87
- package/package.json +9 -6
- package/src/Vault.js +2357 -535
- package/src/utils/file.js +270 -0
- package/src/utils/sanitizeFileName.js +67 -0
- package/src/utils/validationError.js +85 -7
package/src/Vault.js
CHANGED
|
@@ -2,7 +2,73 @@ import crypto from "crypto";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import WebSocket from "ws";
|
|
4
4
|
import EventEmitter from "events";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
validator,
|
|
7
|
+
ValidationError,
|
|
8
|
+
VaultError,
|
|
9
|
+
HTTP_ERROR_MAP,
|
|
10
|
+
safeErrorDetails,
|
|
11
|
+
} from "./utils/validationError.js";
|
|
12
|
+
import { sanitizeFileName } from "./utils/sanitizeFileName.js";
|
|
13
|
+
import {
|
|
14
|
+
MAX_FILE_SIZE,
|
|
15
|
+
baseName,
|
|
16
|
+
contentTypeFor,
|
|
17
|
+
formatFileSize,
|
|
18
|
+
resolveFile,
|
|
19
|
+
} from "./utils/file.js";
|
|
20
|
+
|
|
21
|
+
const WS_CHAT_PROTOCOL = "vault-chat";
|
|
22
|
+
|
|
23
|
+
/** Storage hosts the presign step is allowed to point uploads at. */
|
|
24
|
+
const DEFAULT_UPLOAD_HOSTS = ["s3.filebase.com", ".s3.filebase.com"];
|
|
25
|
+
|
|
26
|
+
const MAX_PAGE_SIZE = 100;
|
|
27
|
+
|
|
28
|
+
const DEFAULT_REQUEST_TIMEOUT = 30000;
|
|
29
|
+
const DEFAULT_UPLOAD_CONCURRENCY = 3;
|
|
30
|
+
const MAX_UPLOAD_CONCURRENCY = 10;
|
|
31
|
+
|
|
32
|
+
/** Slowest upload we still wait for, used to derive a per-file timeout. */
|
|
33
|
+
const ASSUMED_UPLOAD_BYTES_PER_SECOND = 100 * 1024;
|
|
34
|
+
const MIN_UPLOAD_TIMEOUT = 60000;
|
|
35
|
+
const MAX_UPLOAD_TIMEOUT = 2 * 60 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
const isLocalHostname = (hostname) =>
|
|
38
|
+
["localhost", "127.0.0.1", "::1", "[::1]"].includes(hostname);
|
|
39
|
+
|
|
40
|
+
const isEncryptedProtocol = (protocol) =>
|
|
41
|
+
protocol === "https:" || protocol === "wss:";
|
|
42
|
+
|
|
43
|
+
const REDACTED = "[redacted]";
|
|
44
|
+
|
|
45
|
+
/** Run `worker` over `items`, at most `limit` at a time, keeping input order. */
|
|
46
|
+
async function runWithConcurrency(items, limit, worker) {
|
|
47
|
+
const results = new Array(items.length);
|
|
48
|
+
let next = 0;
|
|
49
|
+
|
|
50
|
+
const runners = Array.from(
|
|
51
|
+
{ length: Math.max(1, Math.min(limit, items.length)) },
|
|
52
|
+
async () => {
|
|
53
|
+
while (next < items.length) {
|
|
54
|
+
const index = next;
|
|
55
|
+
next += 1;
|
|
56
|
+
results[index] = await worker(items[index], index);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await Promise.all(runners);
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const toBase64Url = (text) => {
|
|
66
|
+
const base64 =
|
|
67
|
+
typeof Buffer !== "undefined"
|
|
68
|
+
? Buffer.from(text, "utf8").toString("base64")
|
|
69
|
+
: btoa(unescape(encodeURIComponent(text)));
|
|
70
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
71
|
+
};
|
|
6
72
|
|
|
7
73
|
class Vault extends EventEmitter {
|
|
8
74
|
/**
|
|
@@ -14,6 +80,12 @@ class Vault extends EventEmitter {
|
|
|
14
80
|
* @param {string} config.VAULT_CLIENT_API_KEY - Your client-specific API key
|
|
15
81
|
* @param {string} config.VAULT_BASE_URL - Base URL of the Vault API (e.g. "https://api.example.com")
|
|
16
82
|
* @param {string} [config.VAULT_WS_URL] - WebSocket URL for real-time events (e.g. "wss://api.example.com/ws")
|
|
83
|
+
* @param {boolean} [config.VAULT_ALLOW_INSECURE=false] - Allow http:// / ws:// to a non-local host (test servers only)
|
|
84
|
+
* @param {string} [config.VAULT_UPLOAD_ROOT] - Directory that file paths must stay inside (defaults to the working directory)
|
|
85
|
+
* @param {string|string[]} [config.VAULT_UPLOAD_HOSTS] - Extra storage hosts uploads may be sent to
|
|
86
|
+
* @param {number} [config.VAULT_TIMEOUT=30000] - Timeout in ms for API requests
|
|
87
|
+
* @param {number} [config.VAULT_UPLOAD_TIMEOUT] - Timeout in ms for one file upload (default: scaled to the file size)
|
|
88
|
+
* @param {number} [config.VAULT_UPLOAD_CONCURRENCY=3] - How many files upload at once in uploadFiles/uploadFilesToBot
|
|
17
89
|
*
|
|
18
90
|
* @throws {VaultError} If any required configuration parameter is missing
|
|
19
91
|
*
|
|
@@ -32,6 +104,12 @@ class Vault extends EventEmitter {
|
|
|
32
104
|
VAULT_CLIENT_API_KEY,
|
|
33
105
|
VAULT_BASE_URL,
|
|
34
106
|
VAULT_WS_URL,
|
|
107
|
+
VAULT_ALLOW_INSECURE,
|
|
108
|
+
VAULT_UPLOAD_ROOT,
|
|
109
|
+
VAULT_UPLOAD_HOSTS,
|
|
110
|
+
VAULT_TIMEOUT,
|
|
111
|
+
VAULT_UPLOAD_TIMEOUT,
|
|
112
|
+
VAULT_UPLOAD_CONCURRENCY,
|
|
35
113
|
} = {}) {
|
|
36
114
|
super();
|
|
37
115
|
|
|
@@ -49,19 +127,85 @@ class Vault extends EventEmitter {
|
|
|
49
127
|
);
|
|
50
128
|
}
|
|
51
129
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
// Hidden from enumeration so console.log(vault), JSON.stringify and APM
|
|
131
|
+
// snapshots cannot print the signing secret.
|
|
132
|
+
for (const [property, secret] of [
|
|
133
|
+
["apiKey", VAULT_ACCESS_KEY],
|
|
134
|
+
["apiSecret", VAULT_SECRET_KEY],
|
|
135
|
+
["clientApiKey", VAULT_CLIENT_API_KEY],
|
|
136
|
+
]) {
|
|
137
|
+
Object.defineProperty(this, property, {
|
|
138
|
+
value: secret,
|
|
139
|
+
enumerable: false,
|
|
140
|
+
writable: false,
|
|
141
|
+
configurable: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
55
145
|
this.baseUrl = VAULT_BASE_URL;
|
|
56
146
|
this.wsUrl = VAULT_WS_URL;
|
|
57
147
|
this.ws = null;
|
|
148
|
+
this.botChatWs = null;
|
|
58
149
|
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
150
|
+
this.allowInsecure = VAULT_ALLOW_INSECURE === true;
|
|
151
|
+
|
|
152
|
+
this._assertEncryptedTransport(
|
|
153
|
+
this.normalizeAbsoluteUrl(this.baseUrl),
|
|
154
|
+
"VAULT_BASE_URL",
|
|
155
|
+
"constructor"
|
|
156
|
+
);
|
|
157
|
+
if (this.wsUrl) {
|
|
158
|
+
this._assertEncryptedTransport(
|
|
159
|
+
this.normalizeAbsoluteUrl(this.wsUrl, "wss:"),
|
|
160
|
+
"VAULT_WS_URL",
|
|
161
|
+
"constructor"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.uploadRoot = VAULT_UPLOAD_ROOT || undefined;
|
|
166
|
+
|
|
167
|
+
const positive = (value, fallback) => {
|
|
168
|
+
const number = Number(value);
|
|
169
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.requestTimeout = positive(VAULT_TIMEOUT, DEFAULT_REQUEST_TIMEOUT);
|
|
173
|
+
this.uploadTimeout = positive(VAULT_UPLOAD_TIMEOUT, undefined);
|
|
174
|
+
this.uploadConcurrency = Math.min(
|
|
175
|
+
positive(VAULT_UPLOAD_CONCURRENCY, DEFAULT_UPLOAD_CONCURRENCY),
|
|
176
|
+
MAX_UPLOAD_CONCURRENCY
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Uploads may only go to storage hosts we expect, so a tampered presign
|
|
180
|
+
// response cannot redirect a file to someone else's server.
|
|
181
|
+
const extraHosts = Array.isArray(VAULT_UPLOAD_HOSTS)
|
|
182
|
+
? VAULT_UPLOAD_HOSTS
|
|
183
|
+
: String(VAULT_UPLOAD_HOSTS || "")
|
|
184
|
+
.split(",")
|
|
185
|
+
.filter(Boolean);
|
|
186
|
+
this.allowedUploadHosts = [
|
|
187
|
+
...DEFAULT_UPLOAD_HOSTS,
|
|
188
|
+
...extraHosts.map((host) => String(host).trim().toLowerCase()),
|
|
189
|
+
];
|
|
190
|
+
try {
|
|
191
|
+
this.allowedUploadHosts.push(new URL(this.baseUrl).hostname.toLowerCase());
|
|
192
|
+
} catch {
|
|
193
|
+
// A base URL that will not parse fails on the first request instead.
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Not enumerable either: its default headers carry the access key.
|
|
197
|
+
Object.defineProperty(this, "httpClient", {
|
|
198
|
+
value: axios.create({
|
|
199
|
+
baseURL: this.baseUrl,
|
|
200
|
+
timeout: this.requestTimeout,
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
"API-Key": this.apiKey,
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
enumerable: false,
|
|
207
|
+
writable: true,
|
|
208
|
+
configurable: true,
|
|
65
209
|
});
|
|
66
210
|
}
|
|
67
211
|
|
|
@@ -78,7 +222,7 @@ class Vault extends EventEmitter {
|
|
|
78
222
|
*/
|
|
79
223
|
async request(method, endpoint, payload, options = {}) {
|
|
80
224
|
const timestamp = Date.now().toString();
|
|
81
|
-
const signature = this.
|
|
225
|
+
const signature = this.signRequest(method, endpoint, timestamp, payload);
|
|
82
226
|
|
|
83
227
|
const headers = {
|
|
84
228
|
timestamp,
|
|
@@ -112,13 +256,26 @@ class Vault extends EventEmitter {
|
|
|
112
256
|
? `[Vault SDK] ${operation}: ${serverMessage}`
|
|
113
257
|
: `[Vault SDK] ${operation}: ${errorInfo.description}`;
|
|
114
258
|
|
|
259
|
+
const details = safeErrorDetails(data);
|
|
260
|
+
const requestId =
|
|
261
|
+
details?.requestId || error.response.headers?.["x-request-id"] || null;
|
|
262
|
+
|
|
115
263
|
throw new VaultError(message, {
|
|
116
264
|
status,
|
|
117
265
|
code: errorInfo.code,
|
|
118
266
|
operation,
|
|
119
|
-
data,
|
|
267
|
+
data: details,
|
|
268
|
+
requestId,
|
|
120
269
|
});
|
|
121
270
|
} else if (error.request) {
|
|
271
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
272
|
+
throw new VaultError(
|
|
273
|
+
`[Vault SDK] ${operation}: The request timed out after ${this.requestTimeout}ms. ` +
|
|
274
|
+
`Raise VAULT_TIMEOUT if your network needs longer.`,
|
|
275
|
+
{ code: "REQUEST_TIMEOUT", operation }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
122
279
|
throw new VaultError(
|
|
123
280
|
`[Vault SDK] ${operation}: No response received from the server. ` +
|
|
124
281
|
`Please check your network connection and ensure VAULT_BASE_URL ("${this.baseUrl}") is correct.`,
|
|
@@ -134,19 +291,87 @@ class Vault extends EventEmitter {
|
|
|
134
291
|
}
|
|
135
292
|
|
|
136
293
|
/**
|
|
137
|
-
* Internal: Generate HMAC-SHA256 signature
|
|
294
|
+
* Internal: Generate an HMAC-SHA256 signature bound to the full HTTP request.
|
|
138
295
|
*
|
|
296
|
+
* @param {string} method - HTTP method
|
|
297
|
+
* @param {string} endpoint - API endpoint path, including query string
|
|
139
298
|
* @param {string} timestamp - Current timestamp string
|
|
299
|
+
* @param {Object} [payload] - Request body
|
|
140
300
|
* @returns {string} Hex-encoded HMAC signature
|
|
141
301
|
*/
|
|
142
|
-
|
|
143
|
-
const
|
|
302
|
+
signRequest(method, endpoint, timestamp, payload) {
|
|
303
|
+
const bodylessMethod = ["GET", "HEAD"].includes(
|
|
304
|
+
String(method).toUpperCase()
|
|
305
|
+
);
|
|
306
|
+
const emptyObject =
|
|
307
|
+
payload !== null &&
|
|
308
|
+
typeof payload === "object" &&
|
|
309
|
+
!Array.isArray(payload) &&
|
|
310
|
+
Object.keys(payload).length === 0;
|
|
311
|
+
const serializedBody =
|
|
312
|
+
bodylessMethod || payload == null || emptyObject
|
|
313
|
+
? ""
|
|
314
|
+
: JSON.stringify(payload);
|
|
315
|
+
const bodyHash = crypto
|
|
316
|
+
.createHash("sha256")
|
|
317
|
+
.update(serializedBody)
|
|
318
|
+
.digest("hex");
|
|
319
|
+
const message = [
|
|
320
|
+
String(method).toUpperCase(),
|
|
321
|
+
endpoint,
|
|
322
|
+
timestamp,
|
|
323
|
+
this.apiKey,
|
|
324
|
+
bodyHash,
|
|
325
|
+
].join("\n");
|
|
326
|
+
|
|
144
327
|
return crypto
|
|
145
328
|
.createHmac("sha256", this.apiSecret)
|
|
146
329
|
.update(message)
|
|
147
330
|
.digest("hex");
|
|
148
331
|
}
|
|
149
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Legacy signature used by the Twin WebSocket endpoint.
|
|
335
|
+
* HTTP Vault SDK requests use signRequest() above.
|
|
336
|
+
*/
|
|
337
|
+
signLegacy(timestamp) {
|
|
338
|
+
return crypto
|
|
339
|
+
.createHmac("sha256", this.apiSecret)
|
|
340
|
+
.update(this.apiKey + timestamp)
|
|
341
|
+
.digest("hex");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Internal: For bulk-style SDK operations, surface a real SDK error when the
|
|
346
|
+
* server processed the request but every requested item failed.
|
|
347
|
+
*
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
assertNotAllItemsFailed(responseData, operation, itemLabel) {
|
|
351
|
+
const bulkData = responseData?.data;
|
|
352
|
+
const results = Array.isArray(bulkData?.results) ? bulkData.results : null;
|
|
353
|
+
const successCount = Number(bulkData?.successCount ?? 0);
|
|
354
|
+
const failureCount = Number(bulkData?.failureCount ?? 0);
|
|
355
|
+
|
|
356
|
+
if (!results || results.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (successCount > 0 || failureCount !== results.length) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const message =
|
|
365
|
+
responseData?.message ||
|
|
366
|
+
`All requested ${itemLabel} failed to be added to the bot`;
|
|
367
|
+
|
|
368
|
+
throw new VaultError(`[Vault SDK] ${operation}: ${message}`, {
|
|
369
|
+
code: "BAD_REQUEST",
|
|
370
|
+
operation,
|
|
371
|
+
data: responseData,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
150
375
|
// ─── WebSocket ────────────────────────────────────────────────
|
|
151
376
|
|
|
152
377
|
/**
|
|
@@ -169,28 +394,71 @@ class Vault extends EventEmitter {
|
|
|
169
394
|
);
|
|
170
395
|
}
|
|
171
396
|
|
|
397
|
+
if (this.ws && this.ws.readyState < WebSocket.CLOSING) {
|
|
398
|
+
this.ws.close();
|
|
399
|
+
}
|
|
400
|
+
|
|
172
401
|
const timestamp = Date.now().toString();
|
|
173
|
-
const
|
|
402
|
+
const credentials = toBase64Url(
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
apikey: this.apiKey,
|
|
405
|
+
signature: this.signLegacy(timestamp),
|
|
406
|
+
timestamp,
|
|
407
|
+
clientApiKey: this.clientApiKey,
|
|
408
|
+
})
|
|
409
|
+
);
|
|
174
410
|
|
|
175
411
|
return new Promise((resolve, reject) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
412
|
+
let settled = false;
|
|
413
|
+
// Credentials travel in the handshake, not the URL, so proxies and access
|
|
414
|
+
// logs never record them.
|
|
415
|
+
const socket = new WebSocket(new URL(this.wsUrl).toString(), [
|
|
416
|
+
WS_CHAT_PROTOCOL,
|
|
417
|
+
`vault-auth.${credentials}`,
|
|
418
|
+
]);
|
|
419
|
+
this.ws = socket;
|
|
420
|
+
|
|
421
|
+
socket.onopen = () => {
|
|
422
|
+
settled = true;
|
|
181
423
|
resolve();
|
|
182
424
|
};
|
|
183
425
|
|
|
184
|
-
|
|
185
|
-
this.ws.onclose = () => {};
|
|
426
|
+
socket.onmessage = this.wsOnMessage.bind(this);
|
|
186
427
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{ code: "WEBSOCKET_ERROR", operation: "connectToWebsocket" }
|
|
192
|
-
)
|
|
428
|
+
socket.onerror = (error) => {
|
|
429
|
+
const wrapped = new VaultError(
|
|
430
|
+
`[Vault SDK] 'connectToWebsocket': WebSocket connection failed — ${error.message || "Unknown error"}`,
|
|
431
|
+
{ code: "WEBSOCKET_ERROR", operation: "connectToWebsocket" }
|
|
193
432
|
);
|
|
433
|
+
|
|
434
|
+
if (!settled) {
|
|
435
|
+
settled = true;
|
|
436
|
+
reject(wrapped);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.wsOnError(wrapped);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
socket.onclose = (event) => {
|
|
444
|
+
if (this.ws === socket) {
|
|
445
|
+
this.ws = null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.emit("websocket_close", {
|
|
449
|
+
code: event?.code,
|
|
450
|
+
reason: event?.reason,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (!settled) {
|
|
454
|
+
settled = true;
|
|
455
|
+
reject(
|
|
456
|
+
new VaultError(
|
|
457
|
+
"[Vault SDK] 'connectToWebsocket': WebSocket closed before the connection was established.",
|
|
458
|
+
{ code: "WEBSOCKET_CLOSED", operation: "connectToWebsocket" }
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
194
462
|
};
|
|
195
463
|
});
|
|
196
464
|
}
|
|
@@ -221,741 +489,2373 @@ class Vault extends EventEmitter {
|
|
|
221
489
|
return error;
|
|
222
490
|
}
|
|
223
491
|
|
|
224
|
-
// ─── File Upload ──────────────────────────────────────────────
|
|
225
|
-
|
|
226
492
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* The upload is a 3-step process:
|
|
230
|
-
* 1. Compute SHA-256 hash of the file content
|
|
231
|
-
* 2. Get a presigned S3 URL from the server
|
|
232
|
-
* 3. Upload directly to S3, then register the upload
|
|
233
|
-
*
|
|
234
|
-
* @param {Object} file - File object to upload
|
|
235
|
-
* @param {Buffer|Uint8Array} file.buffer - File content as a buffer
|
|
236
|
-
* @param {string} file.name - File name (e.g. "document.pdf")
|
|
237
|
-
* @param {string} [file.type] - MIME type (e.g. "application/pdf"). Defaults to "application/octet-stream"
|
|
238
|
-
* @param {string} vaultId - The vault ID to upload to
|
|
239
|
-
* @param {string} [parentId] - Parent folder ID (omit or null for root)
|
|
240
|
-
* @returns {Promise<Object>} Upload result with file details
|
|
241
|
-
*
|
|
242
|
-
* @throws {VaultError} If upload fails at any step
|
|
243
|
-
* @throws {ValidationError} If required parameters are missing/invalid
|
|
244
|
-
*
|
|
245
|
-
* @example
|
|
246
|
-
* import fs from "fs";
|
|
247
|
-
* const fileBuffer = fs.readFileSync("./photo.jpg");
|
|
248
|
-
* const result = await vault.uploadFile(
|
|
249
|
-
* { buffer: fileBuffer, name: "photo.jpg", type: "image/jpeg" },
|
|
250
|
-
* "your-vault-id"
|
|
251
|
-
* );
|
|
493
|
+
* Internal: Resolve the payload body from the standard API response wrapper.
|
|
494
|
+
* @private
|
|
252
495
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
message:
|
|
260
|
-
"[Vault SDK] 'uploadFile': The 'file' parameter must be an object with { buffer, name } properties.",
|
|
261
|
-
},
|
|
262
|
-
vaultId: { value: vaultId, type: "string" },
|
|
263
|
-
},
|
|
264
|
-
"uploadFile"
|
|
265
|
-
);
|
|
496
|
+
getResponseData(responseData) {
|
|
497
|
+
if (responseData && typeof responseData === "object" && "data" in responseData) {
|
|
498
|
+
return responseData.data;
|
|
499
|
+
}
|
|
500
|
+
return responseData;
|
|
501
|
+
}
|
|
266
502
|
|
|
267
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Internal: Normalize a user-provided base URL into an absolute URL object.
|
|
505
|
+
* Accepts http(s), ws(s), protocol-relative, root-relative, and bare host forms.
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
normalizeAbsoluteUrl(rawUrl, fallbackProtocol = "https:") {
|
|
509
|
+
const value = typeof rawUrl === "string" ? rawUrl.trim() : "";
|
|
510
|
+
if (!value) {
|
|
268
511
|
throw new VaultError(
|
|
269
|
-
"[Vault SDK]
|
|
270
|
-
{ code: "INVALID_PARAMETER", operation: "
|
|
512
|
+
"[Vault SDK] Invalid URL configuration. Expected an absolute base URL.",
|
|
513
|
+
{ code: "INVALID_PARAMETER", operation: "normalizeAbsoluteUrl" }
|
|
271
514
|
);
|
|
272
515
|
}
|
|
273
516
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
517
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value)) {
|
|
518
|
+
return new URL(value);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (value.startsWith("//")) {
|
|
522
|
+
return new URL(`${fallbackProtocol}${value}`);
|
|
279
523
|
}
|
|
280
524
|
|
|
281
|
-
|
|
282
|
-
|
|
525
|
+
if (value.startsWith("/")) {
|
|
526
|
+
if (typeof window !== "undefined" && window.location?.origin) {
|
|
527
|
+
return new URL(value, window.location.origin);
|
|
528
|
+
}
|
|
283
529
|
|
|
284
|
-
if (size === 0) {
|
|
285
530
|
throw new VaultError(
|
|
286
|
-
|
|
287
|
-
{ code: "INVALID_PARAMETER", operation: "
|
|
531
|
+
`[Vault SDK] Cannot resolve relative URL "${value}" without a browser origin.`,
|
|
532
|
+
{ code: "INVALID_PARAMETER", operation: "normalizeAbsoluteUrl" }
|
|
288
533
|
);
|
|
289
534
|
}
|
|
290
535
|
|
|
291
|
-
|
|
292
|
-
|
|
536
|
+
return new URL(`${fallbackProtocol}//${value}`);
|
|
537
|
+
}
|
|
293
538
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
fileSize: size,
|
|
302
|
-
contentHash: hash,
|
|
303
|
-
folderId: parentId,
|
|
304
|
-
});
|
|
305
|
-
} catch (error) {
|
|
306
|
-
if (error instanceof VaultError) throw error;
|
|
539
|
+
/**
|
|
540
|
+
* Internal: Build the bot chat WebSocket URL.
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
getBotChatWebSocketUrl(overrideUrl) {
|
|
544
|
+
const base = overrideUrl || this.wsUrl || this.baseUrl;
|
|
545
|
+
if (!base) {
|
|
307
546
|
throw new VaultError(
|
|
308
|
-
|
|
309
|
-
{ code: "
|
|
547
|
+
"[Vault SDK] 'connectToBotChat': VAULT_BASE_URL or VAULT_WS_URL is required to build the bot chat WebSocket URL.",
|
|
548
|
+
{ code: "MISSING_CONFIG", operation: "connectToBotChat" }
|
|
310
549
|
);
|
|
311
550
|
}
|
|
312
551
|
|
|
313
|
-
const
|
|
552
|
+
const normalizedBase = this.normalizeAbsoluteUrl(
|
|
553
|
+
base,
|
|
554
|
+
typeof base === "string" && base.trim().startsWith("ws") ? "wss:" : "https:"
|
|
555
|
+
);
|
|
314
556
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
await axios.put(url, buffer, {
|
|
318
|
-
headers: {
|
|
319
|
-
"Content-Type": contentType,
|
|
320
|
-
"x-amz-meta-original-filename": sanitizedName || name,
|
|
321
|
-
"x-amz-meta-content-hash": hash,
|
|
322
|
-
"x-amz-meta-vault-id": vaultId,
|
|
323
|
-
"x-amz-meta-folder-id": parentId || "root",
|
|
324
|
-
"x-amz-meta-file-size": size.toString(),
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
} catch (error) {
|
|
328
|
-
const status = error.response?.status;
|
|
329
|
-
let detail = error.message;
|
|
330
|
-
if (status === 403)
|
|
331
|
-
detail =
|
|
332
|
-
"The presigned URL has expired or required signing headers are missing. Please try uploading again.";
|
|
333
|
-
if (status === 413)
|
|
334
|
-
detail = `File "${name}" exceeds the maximum allowed upload size.`;
|
|
557
|
+
const url = new URL(normalizedBase.toString());
|
|
558
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
335
559
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{
|
|
339
|
-
status,
|
|
340
|
-
code: "STORAGE_UPLOAD_FAILED",
|
|
341
|
-
operation: "uploadFile",
|
|
342
|
-
}
|
|
343
|
-
);
|
|
560
|
+
if (path !== "/ws/bot-chat") {
|
|
561
|
+
url.pathname = "/ws/bot-chat";
|
|
344
562
|
}
|
|
345
563
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
fileSize: size,
|
|
353
|
-
contentHash: hash,
|
|
354
|
-
folderId: parentId,
|
|
355
|
-
});
|
|
356
|
-
} catch (error) {
|
|
357
|
-
if (error instanceof VaultError) throw error;
|
|
358
|
-
throw new VaultError(
|
|
359
|
-
`[Vault SDK] 'uploadFile': File "${name}" was uploaded to storage but failed to register. ` +
|
|
360
|
-
`Please contact support if this persists — ${error.message}`,
|
|
361
|
-
{ code: "REGISTER_FAILED", operation: "uploadFile" }
|
|
362
|
-
);
|
|
564
|
+
if (url.protocol === "https:") {
|
|
565
|
+
url.protocol = "wss:";
|
|
566
|
+
} else if (url.protocol === "http:") {
|
|
567
|
+
url.protocol = "ws:";
|
|
568
|
+
} else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
569
|
+
url.protocol = "wss:";
|
|
363
570
|
}
|
|
571
|
+
|
|
572
|
+
this._assertEncryptedTransport(url, "The bot chat WebSocket URL", "connectToBotChat");
|
|
573
|
+
|
|
574
|
+
return url.toString();
|
|
364
575
|
}
|
|
365
576
|
|
|
366
577
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* Each file is uploaded independently. Failed uploads do not block others.
|
|
370
|
-
*
|
|
371
|
-
* @param {Array<Object>} files - Array of file objects, each with { buffer, name, type? }
|
|
372
|
-
* @param {string} vaultId - The vault ID to upload to
|
|
373
|
-
* @param {string} [parentId] - Parent folder ID (omit or null for root)
|
|
374
|
-
* @returns {Promise<Array<Object>>} Array of results, each with status "success" or "failed"
|
|
578
|
+
* Create a short-lived launch token that can be redeemed into a vault JWT.
|
|
375
579
|
*
|
|
376
|
-
* @
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* { buffer: buf1, name: "file1.pdf", type: "application/pdf" },
|
|
380
|
-
* { buffer: buf2, name: "file2.jpg", type: "image/jpeg" },
|
|
381
|
-
* ],
|
|
382
|
-
* "your-vault-id"
|
|
383
|
-
* );
|
|
580
|
+
* @param {string} vaultId - The vault ID to create the token for
|
|
581
|
+
* @param {Object} [options] - Optional launch context
|
|
582
|
+
* @returns {Promise<Object>} Standard API response containing launchToken and launchUrl
|
|
384
583
|
*/
|
|
385
|
-
async
|
|
584
|
+
async createVaultLaunchToken(vaultId, options = {}) {
|
|
386
585
|
validator.validate(
|
|
387
586
|
{
|
|
388
|
-
files: {
|
|
389
|
-
value: files,
|
|
390
|
-
type: "array",
|
|
391
|
-
message:
|
|
392
|
-
"[Vault SDK] 'uploadFiles': 'files' must be a non-empty array of file objects ({ buffer, name }).",
|
|
393
|
-
},
|
|
394
587
|
vaultId: { value: vaultId, type: "string" },
|
|
588
|
+
options: { value: options, type: "object", required: false },
|
|
395
589
|
},
|
|
396
|
-
"
|
|
590
|
+
"createVaultLaunchToken"
|
|
397
591
|
);
|
|
398
592
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
593
|
+
if (options.returnTo !== undefined && options.returnTo !== null) {
|
|
594
|
+
if (typeof options.returnTo !== "string" || !options.returnTo.trim()) {
|
|
595
|
+
throw new ValidationError(
|
|
596
|
+
"createVaultLaunchToken",
|
|
597
|
+
"options.returnTo",
|
|
598
|
+
"string",
|
|
599
|
+
"[Vault SDK] 'createVaultLaunchToken': options.returnTo must be a non-empty string when provided."
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Browsers drop tabs and newlines while parsing a URL, so a path that
|
|
604
|
+
// hides them can turn into "//evil.com". Paths must resolve back to the
|
|
605
|
+
// same origin; anything else must be a plain http(s) URL.
|
|
606
|
+
const returnTo = options.returnTo.trim();
|
|
607
|
+
const hasUnsafeChar = Array.from(returnTo).some((char) => {
|
|
608
|
+
const code = char.charCodeAt(0);
|
|
609
|
+
return code < 32 || code === 127 || code === 92;
|
|
610
|
+
});
|
|
611
|
+
const lowered = returnTo.toLowerCase();
|
|
612
|
+
let isSafe = false;
|
|
613
|
+
if (!hasUnsafeChar) {
|
|
614
|
+
try {
|
|
615
|
+
const base = "https://vault.invalid";
|
|
616
|
+
isSafe = returnTo.startsWith("/")
|
|
617
|
+
? new URL(returnTo, base).origin === base
|
|
618
|
+
: (lowered.startsWith("http://") || lowered.startsWith("https://")) &&
|
|
619
|
+
Boolean(new URL(returnTo));
|
|
620
|
+
} catch {
|
|
621
|
+
isSafe = false;
|
|
406
622
|
}
|
|
407
|
-
const result = await this.uploadFile(file, vaultId, parentId);
|
|
408
|
-
return { ...result, status: "success", fileName: file.name };
|
|
409
|
-
} catch (error) {
|
|
410
|
-
return {
|
|
411
|
-
status: "failed",
|
|
412
|
-
fileName: file?.name || `file[${index}]`,
|
|
413
|
-
error: error.message,
|
|
414
|
-
code: error.code || "UPLOAD_FAILED",
|
|
415
|
-
};
|
|
416
623
|
}
|
|
417
|
-
|
|
624
|
+
if (!isSafe) {
|
|
625
|
+
throw new ValidationError(
|
|
626
|
+
"createVaultLaunchToken",
|
|
627
|
+
"options.returnTo",
|
|
628
|
+
"safe URL",
|
|
629
|
+
"[Vault SDK] 'createVaultLaunchToken': options.returnTo must be an internal path or an http(s) URL allowed by the Vault server."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
418
633
|
|
|
419
|
-
|
|
420
|
-
|
|
634
|
+
const payload = { vaultId };
|
|
635
|
+
for (const key of ["returnTo", "clientId", "adminId", "sourceUserId"]) {
|
|
636
|
+
if (typeof options[key] === "string" && options[key].trim()) {
|
|
637
|
+
payload[key] = options[key].trim();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
421
640
|
|
|
422
|
-
|
|
641
|
+
const response = await this.request(
|
|
642
|
+
"POST",
|
|
643
|
+
"/v1/vault-sdk/launch-token",
|
|
644
|
+
payload,
|
|
645
|
+
{ operation: "createVaultLaunchToken" }
|
|
646
|
+
);
|
|
647
|
+
return response.data;
|
|
648
|
+
}
|
|
423
649
|
|
|
424
650
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* @param {string} vaultId - The vault ID to search in
|
|
428
|
-
* @param {string} [query=""] - Search query to filter files by name
|
|
429
|
-
* @returns {Promise<Object>} Matching files
|
|
651
|
+
* Redeem a launch token into a normal vault access token.
|
|
430
652
|
*
|
|
431
|
-
* @
|
|
432
|
-
*
|
|
653
|
+
* @param {string} launchToken - One-time launch token from createVaultLaunchToken()
|
|
654
|
+
* @returns {Promise<Object>} Standard API response containing user.accessToken
|
|
433
655
|
*/
|
|
434
|
-
async
|
|
656
|
+
async redeemVaultLaunchToken(launchToken) {
|
|
435
657
|
validator.validate(
|
|
436
658
|
{
|
|
437
|
-
|
|
438
|
-
query: { value: query, type: "string", required: false },
|
|
659
|
+
launchToken: { value: launchToken, type: "string" },
|
|
439
660
|
},
|
|
440
|
-
"
|
|
661
|
+
"redeemVaultLaunchToken"
|
|
441
662
|
);
|
|
442
663
|
|
|
443
|
-
const queryString = `?vaultId=${encodeURIComponent(vaultId)}&query=${encodeURIComponent(query)}`;
|
|
444
664
|
const response = await this.request(
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
{ operation: "
|
|
665
|
+
"POST",
|
|
666
|
+
"/v1/vault-sdk/launch/redeem",
|
|
667
|
+
{ token: launchToken.trim() },
|
|
668
|
+
{ operation: "redeemVaultLaunchToken" }
|
|
449
669
|
);
|
|
450
670
|
return response.data;
|
|
451
671
|
}
|
|
452
672
|
|
|
453
673
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* @param {string} vaultId - The vault ID
|
|
457
|
-
* @returns {Promise<Object>} All files in the vault
|
|
674
|
+
* Create and redeem a launch token into the JWT required by the bot chat socket.
|
|
458
675
|
*
|
|
459
|
-
* @
|
|
460
|
-
*
|
|
676
|
+
* @param {string} vaultId - The vault ID to authenticate for chat
|
|
677
|
+
* @param {Object} [options] - Optional launch context
|
|
678
|
+
* @returns {Promise<string>} Vault access token for bot chat
|
|
461
679
|
*/
|
|
462
|
-
async
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
},
|
|
467
|
-
"getAllFiles"
|
|
468
|
-
);
|
|
680
|
+
async createBotChatAccessToken(vaultId, options = {}) {
|
|
681
|
+
const launchResponse = await this.createVaultLaunchToken(vaultId, options);
|
|
682
|
+
const launchData = this.getResponseData(launchResponse);
|
|
683
|
+
const launchToken = launchData?.launchToken;
|
|
469
684
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
477
|
-
return response.data;
|
|
478
|
-
}
|
|
685
|
+
if (!launchToken) {
|
|
686
|
+
throw new VaultError(
|
|
687
|
+
"[Vault SDK] 'createBotChatAccessToken': Launch token was not returned by the server.",
|
|
688
|
+
{ code: "BAD_RESPONSE", operation: "createBotChatAccessToken", data: launchResponse }
|
|
689
|
+
);
|
|
690
|
+
}
|
|
479
691
|
|
|
480
|
-
|
|
692
|
+
const redeemResponse = await this.redeemVaultLaunchToken(launchToken);
|
|
693
|
+
const redeemData = this.getResponseData(redeemResponse);
|
|
694
|
+
const accessToken = redeemData?.user?.accessToken;
|
|
695
|
+
|
|
696
|
+
if (!accessToken) {
|
|
697
|
+
throw new VaultError(
|
|
698
|
+
"[Vault SDK] 'createBotChatAccessToken': Access token was not returned by the server.",
|
|
699
|
+
{ code: "BAD_RESPONSE", operation: "createBotChatAccessToken", data: redeemResponse }
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return accessToken;
|
|
704
|
+
}
|
|
481
705
|
|
|
482
706
|
/**
|
|
483
|
-
*
|
|
707
|
+
* Open a WebSocket connection to the live bot chat service.
|
|
484
708
|
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
709
|
+
* If `token` is omitted, the SDK will mint one from the vault SDK auth flow.
|
|
710
|
+
* Pass `botId` to automatically join a bot chat once the socket opens.
|
|
487
711
|
*
|
|
488
|
-
* @
|
|
489
|
-
*
|
|
712
|
+
* @param {string} vaultId - The vault ID to authenticate for chat
|
|
713
|
+
* @param {Object} [options]
|
|
714
|
+
* @param {string} [options.token] - Existing vault access token
|
|
715
|
+
* @param {string} [options.launchToken] - Existing launch token to redeem
|
|
716
|
+
* @param {string} [options.botId] - Bot ID to auto-join after connect
|
|
717
|
+
* @param {string} [options.sessionId] - Existing chat session ID to resume
|
|
718
|
+
* @param {string} [options.wsUrl] - Optional explicit WebSocket base URL
|
|
719
|
+
* @returns {Promise<Object>} Connection metadata
|
|
490
720
|
*/
|
|
491
|
-
async
|
|
721
|
+
async connectToBotChat(vaultId, options = {}) {
|
|
492
722
|
validator.validate(
|
|
493
723
|
{
|
|
494
724
|
vaultId: { value: vaultId, type: "string" },
|
|
725
|
+
options: { value: options, type: "object", required: false },
|
|
495
726
|
},
|
|
496
|
-
"
|
|
727
|
+
"connectToBotChat"
|
|
497
728
|
);
|
|
498
729
|
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
730
|
+
const {
|
|
731
|
+
token,
|
|
732
|
+
launchToken,
|
|
733
|
+
botId,
|
|
734
|
+
sessionId,
|
|
735
|
+
wsUrl,
|
|
736
|
+
...launchOptions
|
|
737
|
+
} = options;
|
|
738
|
+
|
|
739
|
+
let accessToken = typeof token === "string" && token.trim() ? token.trim() : null;
|
|
740
|
+
|
|
741
|
+
if (!accessToken && typeof launchToken === "string" && launchToken.trim()) {
|
|
742
|
+
const redeemResponse = await this.redeemVaultLaunchToken(launchToken.trim());
|
|
743
|
+
const redeemData = this.getResponseData(redeemResponse);
|
|
744
|
+
accessToken = redeemData?.user?.accessToken || null;
|
|
745
|
+
}
|
|
508
746
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
747
|
+
if (!accessToken) {
|
|
748
|
+
accessToken = await this.createBotChatAccessToken(vaultId, launchOptions);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (this.botChatWs && this.botChatWs.readyState < WebSocket.CLOSING) {
|
|
752
|
+
this.botChatWs.close();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const socketUrl = this.getBotChatWebSocketUrl(wsUrl);
|
|
756
|
+
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
let settled = false;
|
|
759
|
+
const socket = new WebSocket(socketUrl, [
|
|
760
|
+
WS_CHAT_PROTOCOL,
|
|
761
|
+
`vault-token.${accessToken}`,
|
|
762
|
+
]);
|
|
763
|
+
this.botChatWs = socket;
|
|
764
|
+
|
|
765
|
+
socket.onopen = () => {
|
|
766
|
+
this.emit("bot_chat_open");
|
|
767
|
+
|
|
768
|
+
if (botId) {
|
|
769
|
+
this.joinBotChat(botId, sessionId);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
settled = true;
|
|
773
|
+
resolve({
|
|
774
|
+
url: socketUrl,
|
|
775
|
+
botId: botId || null,
|
|
776
|
+
sessionId: sessionId || null,
|
|
777
|
+
});
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
socket.onmessage = this.botChatOnMessage.bind(this);
|
|
781
|
+
|
|
782
|
+
socket.onclose = (event) => {
|
|
783
|
+
if (this.botChatWs === socket) {
|
|
784
|
+
this.botChatWs = null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
this.emit("bot_chat_close", event);
|
|
788
|
+
|
|
789
|
+
if (!settled) {
|
|
790
|
+
reject(
|
|
791
|
+
new VaultError(
|
|
792
|
+
"[Vault SDK] 'connectToBotChat': Bot chat socket closed before the connection was established.",
|
|
793
|
+
{ code: "WEBSOCKET_CLOSED", operation: "connectToBotChat" }
|
|
794
|
+
)
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
socket.onerror = (error) => {
|
|
800
|
+
const wrapped = new VaultError(
|
|
801
|
+
`[Vault SDK] 'connectToBotChat': WebSocket connection failed — ${error.message || "Unknown error"}`,
|
|
802
|
+
{ code: "WEBSOCKET_ERROR", operation: "connectToBotChat" }
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
this.emit("bot_chat_stream_error", wrapped);
|
|
806
|
+
|
|
807
|
+
if (!settled) {
|
|
808
|
+
settled = true;
|
|
809
|
+
reject(wrapped);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Internal: Parse and emit bot chat messages.
|
|
817
|
+
* @private
|
|
818
|
+
*/
|
|
819
|
+
botChatOnMessage(event) {
|
|
820
|
+
try {
|
|
821
|
+
const response = JSON.parse(event.data);
|
|
822
|
+
this.emit("bot_chat_message", response);
|
|
823
|
+
|
|
824
|
+
if (response?.type) {
|
|
825
|
+
this.emit(`bot_chat_${response.type}`, response.payload);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return response;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
this.emit(
|
|
831
|
+
"bot_chat_stream_error",
|
|
832
|
+
new VaultError(
|
|
833
|
+
`[Vault SDK] Failed to parse bot chat WebSocket message: ${error.message}`,
|
|
834
|
+
{ code: "BAD_RESPONSE", operation: "botChatOnMessage" }
|
|
835
|
+
)
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Internal: Send an event through the bot chat socket.
|
|
842
|
+
* @private
|
|
843
|
+
*/
|
|
844
|
+
sendBotChatEvent(type, payload = {}) {
|
|
845
|
+
if (!this.botChatWs || this.botChatWs.readyState !== WebSocket.OPEN) {
|
|
846
|
+
throw new VaultError(
|
|
847
|
+
`[Vault SDK] '${type}': Bot chat socket is not connected.`,
|
|
848
|
+
{ code: "WEBSOCKET_NOT_CONNECTED", operation: type }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
this.botChatWs.send(JSON.stringify({ type, payload }));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Join a bot's live chat stream.
|
|
857
|
+
*
|
|
858
|
+
* @param {string} botId - Target bot ID
|
|
859
|
+
* @param {string} [sessionId] - Optional existing session to resume
|
|
860
|
+
*/
|
|
861
|
+
joinBotChat(botId, sessionId = null) {
|
|
862
|
+
validator.validate(
|
|
863
|
+
{
|
|
864
|
+
botId: { value: botId, type: "string" },
|
|
865
|
+
sessionId: { value: sessionId, type: "string", required: false },
|
|
866
|
+
},
|
|
867
|
+
"joinBotChat"
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
const payload = { botId: botId.trim() };
|
|
871
|
+
if (typeof sessionId === "string" && sessionId.trim()) {
|
|
872
|
+
payload.sessionId = sessionId.trim();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
this.sendBotChatEvent("join_chat", payload);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Send a chat message to the currently joined bot.
|
|
880
|
+
*
|
|
881
|
+
* @param {string} message - User message content
|
|
882
|
+
* @param {Array<{role: string, content: string}>} [history] - Ignored. The server
|
|
883
|
+
* rebuilds the conversation from the stored session; kept so existing calls still work.
|
|
884
|
+
*/
|
|
885
|
+
sendBotChatMessage(message, history = []) {
|
|
886
|
+
validator.validate(
|
|
887
|
+
{
|
|
888
|
+
message: { value: message, type: "string" },
|
|
889
|
+
},
|
|
890
|
+
"sendBotChatMessage"
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
if (!Array.isArray(history)) {
|
|
894
|
+
throw new VaultError(
|
|
895
|
+
"[Vault SDK] 'sendBotChatMessage': history must be an array when provided.",
|
|
896
|
+
{ code: "INVALID_PARAMETER", operation: "sendBotChatMessage" }
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const trimmedMessage = message.trim();
|
|
901
|
+
if (!trimmedMessage) {
|
|
902
|
+
throw new VaultError(
|
|
903
|
+
"[Vault SDK] 'sendBotChatMessage': Message cannot be empty.",
|
|
904
|
+
{ code: "INVALID_PARAMETER", operation: "sendBotChatMessage" }
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (history.length && !this.warnedHistoryIgnored) {
|
|
909
|
+
this.warnedHistoryIgnored = true;
|
|
910
|
+
console.warn(
|
|
911
|
+
"[Vault SDK] 'sendBotChatMessage': the history argument is ignored. " +
|
|
912
|
+
"The server rebuilds the conversation from the stored session."
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
this.sendBotChatEvent("send_message", { message: trimmedMessage });
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Broadcast a typing indicator to the user's other active chat tabs.
|
|
921
|
+
*/
|
|
922
|
+
sendBotChatTyping() {
|
|
923
|
+
this.sendBotChatEvent("typing", { isTyping: true });
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Close the bot chat socket if it is open.
|
|
928
|
+
*
|
|
929
|
+
* @param {number} [code=1000] - WebSocket close code
|
|
930
|
+
* @param {string} [reason="Bot chat closed by client"] - Close reason
|
|
931
|
+
*/
|
|
932
|
+
disconnectBotChat(code = 1000, reason = "Bot chat closed by client") {
|
|
933
|
+
if (!this.botChatWs) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
this.botChatWs.close(code, reason);
|
|
938
|
+
this.botChatWs = null;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ─── File Upload ──────────────────────────────────────────────
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Upload a file to the vault.
|
|
945
|
+
*
|
|
946
|
+
* @param {string|Object|Blob} file - A path on disk, a File/Blob, or an
|
|
947
|
+
* object with the content — { buffer, name } / { path } / { data, name }.
|
|
948
|
+
* `type` (or `mimeType`/`contentType`) is optional; it is derived from the
|
|
949
|
+
* file extension when omitted.
|
|
950
|
+
* @param {string} vaultId - The vault ID to upload to
|
|
951
|
+
* @param {string} [parentId] - Parent folder ID (omit or null for root)
|
|
952
|
+
* @returns {Promise<Object>} Registration response with the stored file details
|
|
953
|
+
*
|
|
954
|
+
* @throws {VaultError} If the file is unreadable, invalid, or the upload fails at any step
|
|
955
|
+
* @throws {ValidationError} If required parameters are missing/invalid
|
|
956
|
+
*/
|
|
957
|
+
/**
|
|
958
|
+
* Internal: turn a caller-supplied page number or page size into a usable
|
|
959
|
+
* integer, rejecting values that are not numbers and clamping the rest.
|
|
960
|
+
*
|
|
961
|
+
* @param {*} value - Raw value from the caller
|
|
962
|
+
* @param {string} field - Field name, used in the message
|
|
963
|
+
* @param {string} operation - Calling method name
|
|
964
|
+
* @param {number} max - Largest value allowed
|
|
965
|
+
* @returns {number} An integer between 1 and max
|
|
966
|
+
* @throws {ValidationError} If the value is not a number
|
|
967
|
+
*/
|
|
968
|
+
_pagingValue(value, field, operation, max) {
|
|
969
|
+
const numeric =
|
|
970
|
+
typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
971
|
+
|
|
972
|
+
if (typeof numeric !== "number" || !Number.isFinite(numeric)) {
|
|
973
|
+
throw new ValidationError(
|
|
974
|
+
operation,
|
|
975
|
+
field,
|
|
976
|
+
"number",
|
|
977
|
+
`[Vault SDK] '${operation}': '${field}' must be a number. Received: ${typeof value}.`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return Math.min(Math.max(Math.trunc(numeric), 1), max);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Internal: refuse a URL that would carry credentials in the clear.
|
|
986
|
+
*
|
|
987
|
+
* @param {URL} parsed - The URL to check
|
|
988
|
+
* @param {string} label - What the URL is, used in the message
|
|
989
|
+
* @param {string} operation - Calling method name
|
|
990
|
+
* @returns {URL} The same URL, when it is acceptable
|
|
991
|
+
* @throws {VaultError} If the URL is unencrypted, non-local and not explicitly allowed
|
|
992
|
+
*/
|
|
993
|
+
_assertEncryptedTransport(parsed, label, operation) {
|
|
994
|
+
if (
|
|
995
|
+
isEncryptedProtocol(parsed.protocol) ||
|
|
996
|
+
this.allowInsecure ||
|
|
997
|
+
isLocalHostname(parsed.hostname.toLowerCase())
|
|
998
|
+
) {
|
|
999
|
+
return parsed;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
throw new VaultError(
|
|
1003
|
+
`[Vault SDK] ${operation}: ${label} uses "${parsed.protocol}//", so API keys, signatures and file contents would travel unencrypted. ` +
|
|
1004
|
+
`Use https:// (or wss://), or set VAULT_ALLOW_INSECURE: true for a local test server.`,
|
|
1005
|
+
{ code: "INSECURE_TRANSPORT", operation }
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* A redacted view of this client, used by JSON.stringify and console.log.
|
|
1011
|
+
*
|
|
1012
|
+
* @returns {Object} Configuration with the secrets replaced
|
|
1013
|
+
*/
|
|
1014
|
+
toJSON() {
|
|
1015
|
+
return {
|
|
1016
|
+
baseUrl: this.baseUrl,
|
|
1017
|
+
wsUrl: this.wsUrl,
|
|
1018
|
+
apiKey: REDACTED,
|
|
1019
|
+
apiSecret: REDACTED,
|
|
1020
|
+
clientApiKey: REDACTED,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
1025
|
+
return `Vault ${JSON.stringify(this.toJSON())}`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Internal: check a presigned upload URL before sending any bytes to it.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {string} rawUrl - URL returned by the presign step
|
|
1032
|
+
* @param {string} operation - Calling method name
|
|
1033
|
+
* @param {string} fileName - File name, used in error messages
|
|
1034
|
+
* @returns {string} The URL to upload to
|
|
1035
|
+
* @throws {VaultError} If the URL is malformed, not HTTPS, or on an unexpected host
|
|
1036
|
+
*/
|
|
1037
|
+
_checkUploadUrl(rawUrl, operation, fileName) {
|
|
1038
|
+
let parsed;
|
|
1039
|
+
try {
|
|
1040
|
+
parsed = new URL(String(rawUrl));
|
|
1041
|
+
} catch {
|
|
1042
|
+
throw new VaultError(
|
|
1043
|
+
`[Vault SDK] '${operation}': The upload URL returned for "${fileName}" is not a valid URL.`,
|
|
1044
|
+
{ code: "UPLOAD_URL_REJECTED", operation }
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1049
|
+
|
|
1050
|
+
if (parsed.protocol !== "https:" && !isLocalHostname(hostname)) {
|
|
1051
|
+
throw new VaultError(
|
|
1052
|
+
`[Vault SDK] '${operation}': The upload URL returned for "${fileName}" is not HTTPS. ` +
|
|
1053
|
+
`Refusing to send the file over an unencrypted connection.`,
|
|
1054
|
+
{ code: "UPLOAD_URL_REJECTED", operation }
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const allowed = this.allowedUploadHosts.some((entry) =>
|
|
1059
|
+
entry.startsWith(".") ? hostname.endsWith(entry) : hostname === entry
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
if (!allowed) {
|
|
1063
|
+
throw new VaultError(
|
|
1064
|
+
`[Vault SDK] '${operation}': The server returned an upload URL on an unexpected host ("${hostname}") for "${fileName}". ` +
|
|
1065
|
+
`Add it to VAULT_UPLOAD_HOSTS if your deployment stores files there.`,
|
|
1066
|
+
{ code: "UPLOAD_URL_REJECTED", operation }
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return parsed.toString();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Internal: how long to wait for one upload, scaled to the file size.
|
|
1075
|
+
*
|
|
1076
|
+
* @param {number} bytes - File size in bytes
|
|
1077
|
+
* @returns {number} Timeout in milliseconds
|
|
1078
|
+
*/
|
|
1079
|
+
_uploadTimeoutFor(bytes) {
|
|
1080
|
+
if (this.uploadTimeout !== undefined) return this.uploadTimeout;
|
|
1081
|
+
|
|
1082
|
+
const scaled =
|
|
1083
|
+
Math.ceil((Number(bytes) || 0) / ASSUMED_UPLOAD_BYTES_PER_SECOND) * 1000;
|
|
1084
|
+
return Math.min(Math.max(MIN_UPLOAD_TIMEOUT, scaled), MAX_UPLOAD_TIMEOUT);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async uploadFile(file, vaultId, parentId = null) {
|
|
1088
|
+
validator.validate(
|
|
1089
|
+
{
|
|
1090
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1091
|
+
},
|
|
1092
|
+
"uploadFile"
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
// Read the file and derive everything the upload needs from it.
|
|
1096
|
+
const { buffer, name, type } = await resolveFile(file, "uploadFile", {
|
|
1097
|
+
uploadRoot: this.uploadRoot,
|
|
1098
|
+
});
|
|
1099
|
+
const fileSize = buffer.length;
|
|
1100
|
+
|
|
1101
|
+
if (fileSize === 0) {
|
|
1102
|
+
throw new VaultError(
|
|
1103
|
+
`[Vault SDK] 'uploadFile': "${name}" is empty. Cannot upload a zero-byte file.`,
|
|
1104
|
+
{ code: "INVALID_PARAMETER", operation: "uploadFile" }
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
1109
|
+
throw new VaultError(
|
|
1110
|
+
`[Vault SDK] 'uploadFile': "${name}" is ${formatFileSize(fileSize)}, ` +
|
|
1111
|
+
`which exceeds the maximum upload size of ${formatFileSize(MAX_FILE_SIZE)}.`,
|
|
1112
|
+
{ code: "FILE_TOO_LARGE", operation: "uploadFile" }
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const fileName = sanitizeFileName(name);
|
|
1117
|
+
const fileType = type || contentTypeFor(fileName);
|
|
1118
|
+
const contentHash = crypto
|
|
1119
|
+
.createHash("sha256")
|
|
1120
|
+
.update(buffer)
|
|
1121
|
+
.digest("hex");
|
|
1122
|
+
|
|
1123
|
+
// Step 1: Get the presigned storage URL
|
|
1124
|
+
let presign;
|
|
1125
|
+
try {
|
|
1126
|
+
const response = await this.request(
|
|
1127
|
+
"POST",
|
|
1128
|
+
"/v1/vault-sdk/get-presigned-url",
|
|
1129
|
+
{
|
|
1130
|
+
vaultId,
|
|
1131
|
+
fileName,
|
|
1132
|
+
fileType,
|
|
1133
|
+
fileSize,
|
|
1134
|
+
contentHash,
|
|
1135
|
+
folderId: parentId,
|
|
1136
|
+
},
|
|
1137
|
+
{ operation: "uploadFile" }
|
|
1138
|
+
);
|
|
1139
|
+
presign = response.data?.data ?? response.data;
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (error instanceof VaultError) throw error;
|
|
1142
|
+
throw new VaultError(
|
|
1143
|
+
`[Vault SDK] 'uploadFile': Failed to get an upload URL for "${fileName}" — ${error.message}`,
|
|
1144
|
+
{ code: "PRESIGN_FAILED", operation: "uploadFile" }
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const { url, key, contentType, sanitizedName, userId, metadata } =
|
|
1149
|
+
presign || {};
|
|
1150
|
+
|
|
1151
|
+
if (!url || !key) {
|
|
1152
|
+
throw new VaultError(
|
|
1153
|
+
`[Vault SDK] 'uploadFile': The server did not return an upload URL for "${fileName}".`,
|
|
1154
|
+
{
|
|
1155
|
+
code: "PRESIGN_FAILED",
|
|
1156
|
+
operation: "uploadFile",
|
|
1157
|
+
data: safeErrorDetails(presign),
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Step 2: Upload the bytes to storage.
|
|
1163
|
+
const metaHeaders = metadata
|
|
1164
|
+
? Object.fromEntries(
|
|
1165
|
+
Object.entries(metadata).map(([metaKey, value]) => [
|
|
1166
|
+
`x-amz-meta-${metaKey}`,
|
|
1167
|
+
String(value),
|
|
1168
|
+
])
|
|
1169
|
+
)
|
|
1170
|
+
: {
|
|
1171
|
+
"x-amz-meta-original-filename": sanitizedName || fileName,
|
|
1172
|
+
"x-amz-meta-content-hash": contentHash,
|
|
1173
|
+
"x-amz-meta-user-id": String(userId ?? ""),
|
|
1174
|
+
"x-amz-meta-folder-id": parentId || "root",
|
|
1175
|
+
"x-amz-meta-file-size": fileSize.toString(),
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const uploadUrl = this._checkUploadUrl(url, "uploadFile", fileName);
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
await axios.put(uploadUrl, buffer, {
|
|
1182
|
+
headers: {
|
|
1183
|
+
"Content-Type": contentType || fileType,
|
|
1184
|
+
...metaHeaders,
|
|
1185
|
+
},
|
|
1186
|
+
maxBodyLength: Infinity,
|
|
1187
|
+
maxContentLength: Infinity,
|
|
1188
|
+
maxRedirects: 0,
|
|
1189
|
+
timeout: this._uploadTimeoutFor(fileSize),
|
|
1190
|
+
});
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const status = error.response?.status;
|
|
1193
|
+
let detail = error.message;
|
|
1194
|
+
if (status === 403)
|
|
1195
|
+
detail =
|
|
1196
|
+
"The presigned URL has expired or required signing headers are missing. Please try uploading again.";
|
|
1197
|
+
if (status === 413)
|
|
1198
|
+
detail = `File "${fileName}" exceeds the maximum allowed upload size.`;
|
|
1199
|
+
|
|
1200
|
+
throw new VaultError(
|
|
1201
|
+
`[Vault SDK] 'uploadFile': Failed to upload "${fileName}" to storage — ${detail}`,
|
|
1202
|
+
{
|
|
1203
|
+
status,
|
|
1204
|
+
code: "STORAGE_UPLOAD_FAILED",
|
|
1205
|
+
operation: "uploadFile",
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Step 3: Register the upload
|
|
1211
|
+
try {
|
|
1212
|
+
const response = await this.request(
|
|
1213
|
+
"POST",
|
|
1214
|
+
"/v1/vault-sdk/register-upload",
|
|
1215
|
+
{
|
|
1216
|
+
vaultId,
|
|
1217
|
+
fileName: sanitizedName || fileName,
|
|
1218
|
+
filebaseKey: key,
|
|
1219
|
+
fileSize,
|
|
1220
|
+
contentHash,
|
|
1221
|
+
folderId: parentId,
|
|
1222
|
+
},
|
|
1223
|
+
{ operation: "uploadFile" }
|
|
1224
|
+
);
|
|
1225
|
+
return response.data;
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (error instanceof VaultError) throw error;
|
|
1228
|
+
throw new VaultError(
|
|
1229
|
+
`[Vault SDK] 'uploadFile': File "${fileName}" was uploaded to storage but failed to register. ` +
|
|
1230
|
+
`Please contact support if this persists — ${error.message}`,
|
|
1231
|
+
{ code: "REGISTER_FAILED", operation: "uploadFile" }
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Upload multiple files to the vault in parallel.
|
|
1238
|
+
*
|
|
1239
|
+
* Files upload a few at a time (VAULT_UPLOAD_CONCURRENCY, 3 by default) and
|
|
1240
|
+
* each one is independent, so a single failure does not block the others.
|
|
1241
|
+
* If every file fails, the call throws instead of returning an all-failed list.
|
|
1242
|
+
*
|
|
1243
|
+
* @param {Array<string|Object|Blob>} files - Array of files, in any form uploadFile() accepts
|
|
1244
|
+
* @param {string} vaultId - The vault ID to upload to
|
|
1245
|
+
* @param {string} [parentId] - Parent folder ID (omit or null for root)
|
|
1246
|
+
* @returns {Promise<Array<Object>>} Array of results, each with status "success" or "failed"
|
|
1247
|
+
*
|
|
1248
|
+
* @example
|
|
1249
|
+
* const results = await vault.uploadFiles(
|
|
1250
|
+
* ["./file1.pdf", { buffer: buf2, name: "file2.jpg" }],
|
|
1251
|
+
* "your-vault-id"
|
|
1252
|
+
* );
|
|
1253
|
+
*/
|
|
1254
|
+
async uploadFiles(files, vaultId, parentId = null) {
|
|
1255
|
+
validator.validate(
|
|
1256
|
+
{
|
|
1257
|
+
files: {
|
|
1258
|
+
value: files,
|
|
1259
|
+
type: "array",
|
|
1260
|
+
message:
|
|
1261
|
+
"[Vault SDK] 'uploadFiles': 'files' must be a non-empty array of file objects ({ buffer, name }).",
|
|
1262
|
+
},
|
|
1263
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1264
|
+
},
|
|
1265
|
+
"uploadFiles"
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
const results = await runWithConcurrency(
|
|
1269
|
+
files,
|
|
1270
|
+
this.uploadConcurrency,
|
|
1271
|
+
async (file, index) => {
|
|
1272
|
+
const label =
|
|
1273
|
+
baseName(
|
|
1274
|
+
typeof file === "string" ? file : file?.name || file?.path || ""
|
|
1275
|
+
) || `file[${index}]`;
|
|
1276
|
+
|
|
1277
|
+
try {
|
|
1278
|
+
const result = await this.uploadFile(file, vaultId, parentId);
|
|
1279
|
+
return { ...result, status: "success", fileName: label };
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return {
|
|
1282
|
+
status: "failed",
|
|
1283
|
+
fileName: label,
|
|
1284
|
+
error: error.message,
|
|
1285
|
+
code: error.code || "UPLOAD_FAILED",
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
if (results.length && results.every((result) => result.status === "failed")) {
|
|
1292
|
+
throw new VaultError(
|
|
1293
|
+
`[Vault SDK] 'uploadFiles': All ${results.length} file${results.length === 1 ? "" : "s"} failed to upload.`,
|
|
1294
|
+
{ code: "UPLOAD_FAILED", operation: "uploadFiles", data: { results } }
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return results;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ─── File Retrieval ───────────────────────────────────────────
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Search for files in the vault by name or query.
|
|
1305
|
+
*
|
|
1306
|
+
* @param {string} vaultId - The vault ID to search in
|
|
1307
|
+
* @param {string} [query=""] - Search query to filter files by name
|
|
1308
|
+
* @returns {Promise<Object>} Matching files
|
|
1309
|
+
*
|
|
1310
|
+
* @example
|
|
1311
|
+
* const files = await vault.getFiles("your-vault-id", "report");
|
|
1312
|
+
*/
|
|
1313
|
+
async getFiles(vaultId, query = "") {
|
|
1314
|
+
validator.validate(
|
|
1315
|
+
{
|
|
1316
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1317
|
+
query: { value: query, type: "string", required: false },
|
|
1318
|
+
},
|
|
1319
|
+
"getFiles"
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}&query=${encodeURIComponent(query)}`;
|
|
1323
|
+
const response = await this.request(
|
|
1324
|
+
"GET",
|
|
1325
|
+
`/v1/vault-sdk/get-files${queryString}`,
|
|
1326
|
+
undefined,
|
|
1327
|
+
{ operation: "getFiles" }
|
|
1328
|
+
);
|
|
1329
|
+
return response.data;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Get all files in the vault.
|
|
1334
|
+
*
|
|
1335
|
+
* @param {string} vaultId - The vault ID
|
|
1336
|
+
* @returns {Promise<Object>} All files in the vault
|
|
1337
|
+
*
|
|
1338
|
+
* @example
|
|
1339
|
+
* const allFiles = await vault.getAllFiles("your-vault-id");
|
|
1340
|
+
*/
|
|
1341
|
+
async getAllFiles(vaultId) {
|
|
1342
|
+
validator.validate(
|
|
1343
|
+
{
|
|
1344
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1345
|
+
},
|
|
1346
|
+
"getAllFiles"
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1350
|
+
const response = await this.request(
|
|
1351
|
+
"GET",
|
|
1352
|
+
`/v1/vault-sdk/all-files${queryString}`,
|
|
1353
|
+
undefined,
|
|
1354
|
+
{ operation: "getAllFiles" }
|
|
1355
|
+
);
|
|
1356
|
+
return response.data;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ─── Storage & Plans ──────────────────────────────────────────
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Get storage usage details for the vault (used space, total space, etc.).
|
|
1363
|
+
*
|
|
1364
|
+
* @param {string} vaultId - The vault ID
|
|
1365
|
+
* @returns {Promise<Object>} Storage usage information
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* const storage = await vault.getStorageDetails("your-vault-id");
|
|
1369
|
+
*/
|
|
1370
|
+
async getStorageDetails(vaultId) {
|
|
1371
|
+
validator.validate(
|
|
1372
|
+
{
|
|
1373
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1374
|
+
},
|
|
1375
|
+
"getStorageDetails"
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1379
|
+
const response = await this.request(
|
|
1380
|
+
"GET",
|
|
1381
|
+
`/v1/vault-sdk/storage-details${queryString}`,
|
|
1382
|
+
undefined,
|
|
1383
|
+
{ operation: "getStorageDetails" }
|
|
1384
|
+
);
|
|
1385
|
+
return response.data;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Get all available storage plans.
|
|
1390
|
+
*
|
|
1391
|
+
* @param {string} vaultId - The vault ID
|
|
1392
|
+
* @returns {Promise<Object>} Available storage plans
|
|
1393
|
+
*
|
|
1394
|
+
* @example
|
|
1395
|
+
* const plans = await vault.getAllPlans("your-vault-id");
|
|
1396
|
+
*/
|
|
1397
|
+
async getAllPlans(vaultId) {
|
|
1398
|
+
validator.validate(
|
|
1399
|
+
{
|
|
1400
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1401
|
+
},
|
|
1402
|
+
"getAllPlans"
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1406
|
+
const response = await this.request(
|
|
1407
|
+
"GET",
|
|
1408
|
+
`/v1/vault-sdk/all-plans${queryString}`,
|
|
1409
|
+
undefined,
|
|
1410
|
+
{ operation: "getAllPlans" }
|
|
1411
|
+
);
|
|
1412
|
+
return response.data;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Purchase a storage plan.
|
|
1417
|
+
*
|
|
1418
|
+
* @param {string} vaultId - The vault ID
|
|
1419
|
+
* @param {string} priceId - The price ID of the plan to purchase
|
|
1420
|
+
* @returns {Promise<Object>} Purchase confirmation
|
|
1421
|
+
*
|
|
1422
|
+
* @example
|
|
1423
|
+
* const purchase = await vault.buyPlan("your-vault-id", "price-id");
|
|
1424
|
+
*/
|
|
1425
|
+
async buyPlan(vaultId, priceId) {
|
|
1426
|
+
validator.validate(
|
|
1427
|
+
{
|
|
1428
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1429
|
+
priceId: { value: priceId, type: "string" },
|
|
1430
|
+
},
|
|
1431
|
+
"buyPlan"
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
const response = await this.request(
|
|
1435
|
+
"POST",
|
|
1436
|
+
"/v1/vault-sdk/buy-plan",
|
|
1437
|
+
{ vaultId, priceId },
|
|
1438
|
+
{ operation: "buyPlan" }
|
|
1439
|
+
);
|
|
1440
|
+
return response.data;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Cancel the active subscription at period end.
|
|
1445
|
+
*
|
|
1446
|
+
* @param {string} vaultId - The vault ID
|
|
1447
|
+
* @returns {Promise<Object>} Cancellation scheduling details
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* const result = await vault.cancelSubscription("your-vault-id");
|
|
1451
|
+
*/
|
|
1452
|
+
async cancelSubscription(vaultId) {
|
|
1453
|
+
validator.validate(
|
|
1454
|
+
{
|
|
1455
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1456
|
+
},
|
|
1457
|
+
"cancelSubscription"
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
const response = await this.request(
|
|
1461
|
+
"POST",
|
|
1462
|
+
"/v1/vault-sdk/cancel-subscription",
|
|
1463
|
+
{ vaultId },
|
|
1464
|
+
{ operation: "cancelSubscription" }
|
|
1465
|
+
);
|
|
1466
|
+
return response.data;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Schedule an upcoming plan to start after current plan expiry.
|
|
1471
|
+
*
|
|
1472
|
+
* @param {string} vaultId - The vault ID
|
|
1473
|
+
* @param {string} priceId - Stripe price ID for the upcoming plan
|
|
1474
|
+
* @returns {Promise<Object>} Upcoming plan scheduling result
|
|
1475
|
+
*
|
|
1476
|
+
* @example
|
|
1477
|
+
* const result = await vault.createUpcomingPlan("your-vault-id", "price-id");
|
|
1478
|
+
*/
|
|
1479
|
+
async createUpcomingPlan(vaultId, priceId) {
|
|
1480
|
+
validator.validate(
|
|
1481
|
+
{
|
|
1482
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1483
|
+
priceId: { value: priceId, type: "string" },
|
|
1484
|
+
},
|
|
1485
|
+
"createUpcomingPlan"
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
const response = await this.request(
|
|
1489
|
+
"POST",
|
|
1490
|
+
"/v1/vault-sdk/upcoming",
|
|
1491
|
+
{ vaultId, priceId },
|
|
1492
|
+
{ operation: "createUpcomingPlan" }
|
|
1493
|
+
);
|
|
1494
|
+
return response.data;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Cancel auto-renewal for a pending upcoming plan.
|
|
1499
|
+
*
|
|
1500
|
+
* @param {string} vaultId - The vault ID
|
|
1501
|
+
* @returns {Promise<Object>} Upcoming plan cancellation result
|
|
1502
|
+
*
|
|
1503
|
+
* @example
|
|
1504
|
+
* const result = await vault.cancelUpcomingPlan("your-vault-id");
|
|
1505
|
+
*/
|
|
1506
|
+
async cancelUpcomingPlan(vaultId) {
|
|
1507
|
+
validator.validate(
|
|
1508
|
+
{
|
|
1509
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1510
|
+
},
|
|
1511
|
+
"cancelUpcomingPlan"
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
const response = await this.request(
|
|
1515
|
+
"POST",
|
|
1516
|
+
"/v1/vault-sdk/upcoming/cancel",
|
|
1517
|
+
{ vaultId },
|
|
1518
|
+
{ operation: "cancelUpcomingPlan" }
|
|
1519
|
+
);
|
|
1520
|
+
return response.data;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Get active subscriptions for the vault.
|
|
1525
|
+
*
|
|
1526
|
+
* @param {string} vaultId - The vault ID
|
|
1527
|
+
* @returns {Promise<Object>} Active subscriptions
|
|
1528
|
+
*
|
|
1529
|
+
* @example
|
|
1530
|
+
* const subs = await vault.getSubscriptions("your-vault-id");
|
|
1531
|
+
*/
|
|
1532
|
+
async getSubscriptions(vaultId) {
|
|
1533
|
+
validator.validate(
|
|
1534
|
+
{
|
|
1535
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1536
|
+
},
|
|
1537
|
+
"getSubscriptions"
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1541
|
+
const response = await this.request(
|
|
1542
|
+
"GET",
|
|
1543
|
+
`/v1/vault-sdk/subscriptions${queryString}`,
|
|
1544
|
+
undefined,
|
|
1545
|
+
{ operation: "getSubscriptions" }
|
|
1546
|
+
);
|
|
1547
|
+
return response.data;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Get the wallet summary for the authenticated vault user.
|
|
1552
|
+
*
|
|
1553
|
+
* @param {string} vaultId - The vault ID
|
|
1554
|
+
* @returns {Promise<Object>} Wallet summary including points and status
|
|
1555
|
+
*
|
|
1556
|
+
* @example
|
|
1557
|
+
* const wallet = await vault.getWalletInfo("your-vault-id");
|
|
1558
|
+
*/
|
|
1559
|
+
async getWalletInfo(vaultId) {
|
|
1560
|
+
validator.validate(
|
|
1561
|
+
{
|
|
1562
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1563
|
+
},
|
|
1564
|
+
"getWalletInfo"
|
|
1565
|
+
);
|
|
1566
|
+
|
|
1567
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1568
|
+
const response = await this.request(
|
|
1569
|
+
"GET",
|
|
1570
|
+
`/v1/vault-sdk/wallet/info${queryString}`,
|
|
1571
|
+
undefined,
|
|
1572
|
+
{ operation: "getWalletInfo" }
|
|
1573
|
+
);
|
|
1574
|
+
return response.data;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Get paginated wallet transaction history for the authenticated vault user.
|
|
1579
|
+
*
|
|
1580
|
+
* @param {string} vaultId - The vault ID
|
|
1581
|
+
* @param {Object} [query]
|
|
1582
|
+
* @param {number} [query.page] - Page number (defaults to 1)
|
|
1583
|
+
* @param {number} [query.limit] - Page size (defaults to 20)
|
|
1584
|
+
* @param {string} [query.category] - Optional transaction category filter
|
|
1585
|
+
* @returns {Promise<Object>} Transaction history and pagination metadata
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* const history = await vault.getTransactionHistory("your-vault-id");
|
|
1589
|
+
* const filtered = await vault.getTransactionHistory("your-vault-id", {
|
|
1590
|
+
* page: 2,
|
|
1591
|
+
* limit: 10,
|
|
1592
|
+
* category: "credit",
|
|
1593
|
+
* });
|
|
1594
|
+
*/
|
|
1595
|
+
async getTransactionHistory(vaultId, query = {}) {
|
|
1596
|
+
validator.validate(
|
|
1597
|
+
{
|
|
1598
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1599
|
+
query: { value: query, type: "object", required: false },
|
|
1600
|
+
},
|
|
1601
|
+
"getTransactionHistory"
|
|
1602
|
+
);
|
|
1603
|
+
|
|
1604
|
+
const params = new URLSearchParams({ vaultId });
|
|
1605
|
+
|
|
1606
|
+
if (query.page !== undefined && query.page !== null) {
|
|
1607
|
+
params.set(
|
|
1608
|
+
"page",
|
|
1609
|
+
String(
|
|
1610
|
+
this._pagingValue(query.page, "page", "getTransactionHistory", Number.MAX_SAFE_INTEGER)
|
|
1611
|
+
)
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
if (query.limit !== undefined && query.limit !== null) {
|
|
1615
|
+
params.set(
|
|
1616
|
+
"limit",
|
|
1617
|
+
String(this._pagingValue(query.limit, "limit", "getTransactionHistory", MAX_PAGE_SIZE))
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
if (typeof query.category === "string" && query.category.trim()) {
|
|
1621
|
+
params.set("category", query.category.trim());
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const response = await this.request(
|
|
1625
|
+
"GET",
|
|
1626
|
+
`/v1/vault-sdk/wallet/transactions?${params.toString()}`,
|
|
1627
|
+
undefined,
|
|
1628
|
+
{ operation: "getTransactionHistory" }
|
|
1629
|
+
);
|
|
1630
|
+
return response.data;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// ─── Folder Operations ────────────────────────────────────────
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Create a new folder in the vault.
|
|
1637
|
+
*
|
|
1638
|
+
* @param {string} vaultId - The vault ID
|
|
1639
|
+
* @param {string} folderName - Name for the new folder
|
|
1640
|
+
* @param {string} [parentId] - Parent folder ID for nested folders (omit for root)
|
|
1641
|
+
* @returns {Promise<Object>} Created folder details
|
|
1642
|
+
*
|
|
1643
|
+
* @example
|
|
1644
|
+
* await vault.createFolder("your-vault-id", "Documents");
|
|
1645
|
+
* await vault.createFolder("your-vault-id", "Invoices", "parent-folder-id");
|
|
1646
|
+
*/
|
|
1647
|
+
async createFolder(vaultId, folderName, parentId = null) {
|
|
1648
|
+
validator.validate(
|
|
1649
|
+
{
|
|
1650
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1651
|
+
folderName: { value: folderName, type: "string" },
|
|
1652
|
+
},
|
|
1653
|
+
"createFolder"
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
const response = await this.request(
|
|
1657
|
+
"POST",
|
|
1658
|
+
"/v1/vault-sdk/create-folder",
|
|
1659
|
+
{ vaultId, folderName, parentId },
|
|
1660
|
+
{ operation: "createFolder" }
|
|
1661
|
+
);
|
|
1662
|
+
return response.data;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* Rename a file or folder in the vault.
|
|
1667
|
+
*
|
|
1668
|
+
* @param {string} vaultId - The vault ID
|
|
1669
|
+
* @param {string} itemId - The ID of the file or folder to rename
|
|
1670
|
+
* @param {string} newName - The new name
|
|
1671
|
+
* @returns {Promise<Object>} Updated item details
|
|
1672
|
+
*
|
|
1673
|
+
* @example
|
|
1674
|
+
* await vault.renameItem("your-vault-id", "item-id", "New Name.pdf");
|
|
1675
|
+
*/
|
|
1676
|
+
async renameItem(vaultId, itemId, newName) {
|
|
1677
|
+
validator.validate(
|
|
1678
|
+
{
|
|
1679
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1680
|
+
itemId: { value: itemId, type: "string" },
|
|
1681
|
+
newName: { value: newName, type: "string" },
|
|
1682
|
+
},
|
|
1683
|
+
"renameItem"
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
const response = await this.request(
|
|
1687
|
+
"POST",
|
|
1688
|
+
"/v1/vault-sdk/rename",
|
|
1689
|
+
{ vaultId, itemId, newName },
|
|
1690
|
+
{ operation: "renameItem" }
|
|
1691
|
+
);
|
|
1692
|
+
return response.data;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/**
|
|
1696
|
+
* Delete a folder from the vault.
|
|
1697
|
+
*
|
|
1698
|
+
* @param {string} vaultId - The vault ID
|
|
1699
|
+
* @param {string} folderId - The ID of the folder to delete
|
|
1700
|
+
* @returns {Promise<Object>} Deletion confirmation
|
|
1701
|
+
*
|
|
1702
|
+
* @example
|
|
1703
|
+
* await vault.deleteFolder("your-vault-id", "folder-id");
|
|
1704
|
+
*/
|
|
1705
|
+
async deleteFolder(vaultId, folderId) {
|
|
1706
|
+
validator.validate(
|
|
1707
|
+
{
|
|
1708
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1709
|
+
folderId: { value: folderId, type: "string" },
|
|
1710
|
+
},
|
|
1711
|
+
"deleteFolder"
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
const response = await this.request(
|
|
1715
|
+
"DELETE",
|
|
1716
|
+
"/v1/vault-sdk/delete-folder",
|
|
1717
|
+
{ vaultId, folderId },
|
|
1718
|
+
{ operation: "deleteFolder" }
|
|
1719
|
+
);
|
|
1720
|
+
return response.data;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Delete a file from the vault.
|
|
1725
|
+
*
|
|
1726
|
+
* @param {string} vaultId - The vault ID
|
|
1727
|
+
* @param {string} fileId - The ID of the file to delete
|
|
1728
|
+
* @returns {Promise<Object>} Deletion confirmation
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* await vault.deleteFile("your-vault-id", "file-id");
|
|
1732
|
+
*/
|
|
1733
|
+
async deleteFile(vaultId, fileId) {
|
|
1734
|
+
validator.validate(
|
|
1735
|
+
{
|
|
1736
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1737
|
+
fileId: { value: fileId, type: "string" },
|
|
1738
|
+
},
|
|
1739
|
+
"deleteFile"
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
const response = await this.request(
|
|
1743
|
+
"DELETE",
|
|
1744
|
+
"/v1/vault-sdk/delete-file",
|
|
1745
|
+
{ vaultId, fileId },
|
|
1746
|
+
{ operation: "deleteFile" }
|
|
1747
|
+
);
|
|
1748
|
+
return response.data;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// ─── Starred Files ────────────────────────────────────────────
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Mark or unmark a file as starred.
|
|
1755
|
+
*
|
|
1756
|
+
* @param {string} vaultId - The vault ID
|
|
1757
|
+
* @param {string} fileId - The file ID to star/unstar
|
|
1758
|
+
* @param {boolean} isStarred - true to star, false to unstar
|
|
1759
|
+
* @returns {Promise<Object>} Updated file details
|
|
1760
|
+
*
|
|
1761
|
+
* @example
|
|
1762
|
+
* await vault.addToStarred("your-vault-id", "file-id", true);
|
|
1763
|
+
*/
|
|
1764
|
+
async addToStarred(vaultId, fileId, isStarred) {
|
|
1765
|
+
validator.validate(
|
|
1766
|
+
{
|
|
1767
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1768
|
+
fileId: { value: fileId, type: "string" },
|
|
1769
|
+
isStarred: { value: isStarred, type: "boolean" },
|
|
1770
|
+
},
|
|
1771
|
+
"addToStarred"
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
const response = await this.request(
|
|
1775
|
+
"POST",
|
|
1776
|
+
"/v1/vault-sdk/favorites",
|
|
1777
|
+
{ vaultId, assetId: fileId, isFavorite: isStarred },
|
|
1778
|
+
{ operation: "addToStarred" }
|
|
1779
|
+
);
|
|
1780
|
+
return response.data;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Get all starred files in the vault.
|
|
1785
|
+
*
|
|
1786
|
+
* @param {string} vaultId - The vault ID
|
|
1787
|
+
* @returns {Promise<Object>} Starred files
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* const starred = await vault.getStarredFiles("your-vault-id");
|
|
1791
|
+
*/
|
|
1792
|
+
async getStarredFiles(vaultId) {
|
|
1793
|
+
validator.validate(
|
|
1794
|
+
{
|
|
1795
|
+
vaultId: { value: vaultId, type: "string" },
|
|
1796
|
+
},
|
|
1797
|
+
"getStarredFiles"
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
1801
|
+
const response = await this.request(
|
|
1802
|
+
"GET",
|
|
1803
|
+
`/v1/vault-sdk/favorites${queryString}`,
|
|
1804
|
+
undefined,
|
|
1805
|
+
{ operation: "getStarredFiles" }
|
|
1806
|
+
);
|
|
1807
|
+
return response.data;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// ─── Platform Operations ──────────────────────────────────────
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Create a vault for a user, or link an existing one to your client.
|
|
1814
|
+
*
|
|
1815
|
+
* Idempotent: if a user already exists for the email, they are linked to your
|
|
1816
|
+
* client API key and returned instead of erroring.
|
|
1817
|
+
*
|
|
1818
|
+
* @param {string} email - User's email address
|
|
1819
|
+
* @param {string} [platformId] - Optional platform ID to associate the user with
|
|
1820
|
+
* @returns {Promise<Object>} Created (or existing) user details, including vaultId
|
|
1821
|
+
*
|
|
515
1822
|
* @example
|
|
516
|
-
* const
|
|
1823
|
+
* const user = await vault.createVault("user@example.com", "platform-id");
|
|
1824
|
+
* const sdkUser = await vault.createVault("user@example.com"); // platform-less SDK user link
|
|
517
1825
|
*/
|
|
518
|
-
async
|
|
1826
|
+
async createVault(email, platformId) {
|
|
1827
|
+
const normalizedEmail =
|
|
1828
|
+
typeof email === "string" ? email.trim() : email;
|
|
1829
|
+
const normalizedPlatformId =
|
|
1830
|
+
typeof platformId === "string" ? platformId.trim() : platformId;
|
|
1831
|
+
|
|
519
1832
|
validator.validate(
|
|
520
1833
|
{
|
|
521
|
-
|
|
1834
|
+
email: {
|
|
1835
|
+
value: normalizedEmail,
|
|
1836
|
+
type: "string",
|
|
1837
|
+
message:
|
|
1838
|
+
"[Vault SDK] 'createVault' requires 'email' to be a non-empty string.",
|
|
1839
|
+
},
|
|
1840
|
+
platformId: {
|
|
1841
|
+
value: normalizedPlatformId || undefined,
|
|
1842
|
+
type: "string",
|
|
1843
|
+
required: false,
|
|
1844
|
+
},
|
|
522
1845
|
},
|
|
523
|
-
"
|
|
1846
|
+
"createVault"
|
|
524
1847
|
);
|
|
525
1848
|
|
|
526
|
-
const
|
|
1849
|
+
const payload = { email: normalizedEmail };
|
|
1850
|
+
if (normalizedPlatformId) {
|
|
1851
|
+
payload.platformId = normalizedPlatformId;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
527
1854
|
const response = await this.request(
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
{ operation: "
|
|
1855
|
+
"POST",
|
|
1856
|
+
"/v1/vault-sdk/create-user",
|
|
1857
|
+
payload,
|
|
1858
|
+
{ operation: "createVault" }
|
|
532
1859
|
);
|
|
533
1860
|
return response.data;
|
|
534
1861
|
}
|
|
535
1862
|
|
|
536
1863
|
/**
|
|
537
|
-
*
|
|
1864
|
+
* Import an existing vault into a platform.
|
|
538
1865
|
*
|
|
539
|
-
* @param {string} vaultId - The vault ID
|
|
540
|
-
* @param {string}
|
|
541
|
-
* @returns {Promise<Object>}
|
|
1866
|
+
* @param {string} vaultId - The vault ID to import
|
|
1867
|
+
* @param {string} [platformId] - Optional target platform ID
|
|
1868
|
+
* @returns {Promise<Object>} Import result
|
|
542
1869
|
*
|
|
543
1870
|
* @example
|
|
544
|
-
* const
|
|
1871
|
+
* const result = await vault.importVault("vault-id", "platform-id");
|
|
1872
|
+
* const result = await vault.importVault("vault-id"); // link client + enable SDK access without a platform
|
|
545
1873
|
*/
|
|
546
|
-
async
|
|
1874
|
+
async importVault(vaultId, platformId) {
|
|
1875
|
+
const normalizedPlatformId =
|
|
1876
|
+
typeof platformId === "string" ? platformId.trim() : platformId;
|
|
1877
|
+
|
|
547
1878
|
validator.validate(
|
|
548
1879
|
{
|
|
549
1880
|
vaultId: { value: vaultId, type: "string" },
|
|
550
|
-
|
|
1881
|
+
platformId: {
|
|
1882
|
+
value: normalizedPlatformId || undefined,
|
|
1883
|
+
type: "string",
|
|
1884
|
+
required: false,
|
|
1885
|
+
},
|
|
551
1886
|
},
|
|
552
|
-
"
|
|
1887
|
+
"importVault"
|
|
553
1888
|
);
|
|
554
1889
|
|
|
1890
|
+
const payload = { vaultId };
|
|
1891
|
+
if (normalizedPlatformId) {
|
|
1892
|
+
payload.platformId = normalizedPlatformId;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
555
1895
|
const response = await this.request(
|
|
556
1896
|
"POST",
|
|
557
|
-
"/v1/vault-sdk/
|
|
558
|
-
|
|
559
|
-
{ operation: "
|
|
1897
|
+
"/v1/vault-sdk/import-vault",
|
|
1898
|
+
payload,
|
|
1899
|
+
{ operation: "importVault" }
|
|
560
1900
|
);
|
|
561
1901
|
return response.data;
|
|
562
1902
|
}
|
|
563
1903
|
|
|
564
1904
|
/**
|
|
565
|
-
*
|
|
1905
|
+
* Create a bot for the given vault.
|
|
566
1906
|
*
|
|
567
|
-
* @param {string} vaultId - The vault ID
|
|
568
|
-
* @
|
|
1907
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
1908
|
+
* @param {Object} bot
|
|
1909
|
+
* @param {string} bot.name - Bot display name
|
|
1910
|
+
* @param {string} [bot.description] - Optional bot personality/description
|
|
1911
|
+
* @param {string} [bot.profession] - Optional profession label
|
|
1912
|
+
* @returns {Promise<Object>} Created bot details, including its dedicated folder
|
|
569
1913
|
*
|
|
570
1914
|
* @example
|
|
571
|
-
* const
|
|
1915
|
+
* const bot = await vault.createBot("your-vault-id", {
|
|
1916
|
+
* name: "Support Bot",
|
|
1917
|
+
* description: "Answers customer questions clearly",
|
|
1918
|
+
* profession: "Customer Support",
|
|
1919
|
+
* });
|
|
572
1920
|
*/
|
|
573
|
-
async
|
|
1921
|
+
async createBot(vaultId, bot) {
|
|
1922
|
+
const normalizedVaultId =
|
|
1923
|
+
typeof vaultId === "string" ? vaultId.trim() : vaultId;
|
|
1924
|
+
|
|
1925
|
+
const normalizedBot = {
|
|
1926
|
+
...bot,
|
|
1927
|
+
description: bot?.description?.trim() || undefined,
|
|
1928
|
+
profession: bot?.profession?.trim() || undefined,
|
|
1929
|
+
};
|
|
1930
|
+
|
|
574
1931
|
validator.validate(
|
|
575
1932
|
{
|
|
576
|
-
vaultId: {
|
|
1933
|
+
vaultId: {
|
|
1934
|
+
value: normalizedVaultId,
|
|
1935
|
+
type: "string",
|
|
1936
|
+
message:
|
|
1937
|
+
"[Vault SDK] 'createBot' requires 'vaultId' to be a non-empty string.",
|
|
1938
|
+
},
|
|
1939
|
+
bot: { value: normalizedBot, type: "object" },
|
|
1940
|
+
name: {
|
|
1941
|
+
value: normalizedBot.name,
|
|
1942
|
+
type: "string",
|
|
1943
|
+
message:
|
|
1944
|
+
"[Vault SDK] 'createBot' requires bot's name to be a non-empty string.",
|
|
1945
|
+
},
|
|
1946
|
+
description: {
|
|
1947
|
+
value: normalizedBot.description,
|
|
1948
|
+
type: "string",
|
|
1949
|
+
required: false,
|
|
1950
|
+
},
|
|
1951
|
+
profession: {
|
|
1952
|
+
value: normalizedBot.profession,
|
|
1953
|
+
type: "string",
|
|
1954
|
+
required: false,
|
|
1955
|
+
},
|
|
577
1956
|
},
|
|
578
|
-
"
|
|
1957
|
+
"createBot"
|
|
579
1958
|
);
|
|
580
1959
|
|
|
1960
|
+
const payload = {
|
|
1961
|
+
vaultId: normalizedVaultId,
|
|
1962
|
+
name: bot.name.trim(),
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
if (typeof bot.description === "string" && bot.description.trim()) {
|
|
1966
|
+
payload.description = bot.description.trim();
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (typeof bot.profession === "string" && bot.profession.trim()) {
|
|
1970
|
+
payload.profession = bot.profession.trim();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
581
1973
|
const response = await this.request(
|
|
582
1974
|
"POST",
|
|
583
|
-
"/v1/vault-sdk/
|
|
584
|
-
|
|
585
|
-
{ operation: "
|
|
1975
|
+
"/v1/vault-sdk/bots",
|
|
1976
|
+
payload,
|
|
1977
|
+
{ operation: "createBot" }
|
|
586
1978
|
);
|
|
587
1979
|
return response.data;
|
|
588
1980
|
}
|
|
589
1981
|
|
|
590
1982
|
/**
|
|
591
|
-
*
|
|
1983
|
+
* Update a bot through the Vault SDK.
|
|
592
1984
|
*
|
|
593
|
-
*
|
|
594
|
-
* @param {string} priceId - Stripe price ID for the upcoming plan
|
|
595
|
-
* @returns {Promise<Object>} Upcoming plan scheduling result
|
|
1985
|
+
* Any supported field may be omitted for a partial update.
|
|
596
1986
|
*
|
|
597
|
-
* @
|
|
598
|
-
*
|
|
1987
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
1988
|
+
* @param {string} botId - The bot ID to update
|
|
1989
|
+
* @param {Object} updates
|
|
1990
|
+
* @returns {Promise<Object>} Updated bot response
|
|
599
1991
|
*/
|
|
600
|
-
async
|
|
1992
|
+
async updateBot(vaultId, botId, updates) {
|
|
601
1993
|
validator.validate(
|
|
602
1994
|
{
|
|
603
1995
|
vaultId: { value: vaultId, type: "string" },
|
|
604
|
-
|
|
1996
|
+
botId: { value: botId, type: "string" },
|
|
1997
|
+
updates: { value: updates, type: "object" },
|
|
1998
|
+
name: { value: updates?.name, type: "string", required: false, rejectNull: true },
|
|
1999
|
+
description: { value: updates?.description, type: "string", required: false, rejectNull: true },
|
|
2000
|
+
profession: { value: updates?.profession, type: "string", required: false, rejectNull: true },
|
|
2001
|
+
useLLMFallback: { value: updates?.useLLMFallback, type: "boolean", required: false, rejectNull: true },
|
|
2002
|
+
wordLimit: { value: updates?.wordLimit, type: "integer", required: false, rejectNull: true },
|
|
605
2003
|
},
|
|
606
|
-
"
|
|
2004
|
+
"updateBot"
|
|
607
2005
|
);
|
|
608
2006
|
|
|
2007
|
+
const payload = { vaultId };
|
|
2008
|
+
|
|
2009
|
+
if (typeof updates?.name === "string") payload.name = updates.name.trim();
|
|
2010
|
+
if (typeof updates?.description === "string") {
|
|
2011
|
+
payload.description = updates.description;
|
|
2012
|
+
}
|
|
2013
|
+
if (typeof updates?.profession === "string") {
|
|
2014
|
+
payload.profession = updates.profession;
|
|
2015
|
+
}
|
|
2016
|
+
if (typeof updates?.useLLMFallback === "boolean") {
|
|
2017
|
+
payload.useLLMFallback = updates.useLLMFallback;
|
|
2018
|
+
}
|
|
2019
|
+
if (typeof updates?.wordLimit === "number") payload.wordLimit = updates.wordLimit;
|
|
2020
|
+
|
|
609
2021
|
const response = await this.request(
|
|
610
|
-
"
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
{ operation: "
|
|
2022
|
+
"PATCH",
|
|
2023
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}`,
|
|
2024
|
+
payload,
|
|
2025
|
+
{ operation: "updateBot" }
|
|
614
2026
|
);
|
|
615
2027
|
return response.data;
|
|
616
2028
|
}
|
|
617
2029
|
|
|
618
2030
|
/**
|
|
619
|
-
*
|
|
2031
|
+
* Delete a bot owned by the authenticated vault user.
|
|
620
2032
|
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
2033
|
+
* This mirrors the Twin Vault backend `DELETE /bots/:botId` behavior.
|
|
2034
|
+
*
|
|
2035
|
+
* @param {string} botId - The bot ID to delete
|
|
2036
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2037
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
623
2038
|
*
|
|
624
2039
|
* @example
|
|
625
|
-
*
|
|
2040
|
+
* await vault.deleteBot("bot-id", "your-vault-id");
|
|
626
2041
|
*/
|
|
627
|
-
async
|
|
2042
|
+
async deleteBot(botId, vaultId) {
|
|
628
2043
|
validator.validate(
|
|
629
2044
|
{
|
|
2045
|
+
botId: { value: botId, type: "string" },
|
|
630
2046
|
vaultId: { value: vaultId, type: "string" },
|
|
631
2047
|
},
|
|
632
|
-
"
|
|
2048
|
+
"deleteBot"
|
|
633
2049
|
);
|
|
634
2050
|
|
|
635
2051
|
const response = await this.request(
|
|
636
|
-
"
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
{ operation: "
|
|
2052
|
+
"DELETE",
|
|
2053
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}?vaultId=${encodeURIComponent(vaultId)}`,
|
|
2054
|
+
undefined,
|
|
2055
|
+
{ operation: "deleteBot" }
|
|
640
2056
|
);
|
|
2057
|
+
|
|
641
2058
|
return response.data;
|
|
642
2059
|
}
|
|
643
2060
|
|
|
644
2061
|
/**
|
|
645
|
-
* Get
|
|
2062
|
+
* Get the extracted text content for a bot file.
|
|
646
2063
|
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
2064
|
+
* This mirrors the Twin Vault backend `GET /bots/:botId/files/:fileId/text`
|
|
2065
|
+
* behavior through the Vault SDK route chain.
|
|
2066
|
+
*
|
|
2067
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2068
|
+
* @param {string} botId - The bot ID
|
|
2069
|
+
* @param {string} fileId - The bot file ID
|
|
2070
|
+
* @returns {Promise<string>} Extracted text content for the file
|
|
649
2071
|
*
|
|
650
2072
|
* @example
|
|
651
|
-
* const
|
|
2073
|
+
* const text = await vault.getBotFileText("your-vault-id", "bot-id", "file-id");
|
|
652
2074
|
*/
|
|
653
|
-
async
|
|
2075
|
+
async getBotFileText(vaultId, botId, fileId) {
|
|
654
2076
|
validator.validate(
|
|
655
2077
|
{
|
|
656
|
-
vaultId: {
|
|
2078
|
+
vaultId: {
|
|
2079
|
+
value: vaultId,
|
|
2080
|
+
type: "string",
|
|
2081
|
+
message: "[Vault SDK] 'getBotFileText' requires 'vaultId' to be a non-empty string.",
|
|
2082
|
+
},
|
|
2083
|
+
botId: {
|
|
2084
|
+
value: botId,
|
|
2085
|
+
type: "string",
|
|
2086
|
+
message: "[Vault SDK] 'getBotFileText' requires 'botId' to be a non-empty string.",
|
|
2087
|
+
},
|
|
2088
|
+
fileId: {
|
|
2089
|
+
value: fileId,
|
|
2090
|
+
type: "string",
|
|
2091
|
+
message: "[Vault SDK] 'getBotFileText' requires 'fileId' to be a non-empty string.",
|
|
2092
|
+
},
|
|
657
2093
|
},
|
|
658
|
-
"
|
|
2094
|
+
"getBotFileText"
|
|
659
2095
|
);
|
|
660
2096
|
|
|
661
|
-
const queryString = `?vaultId=${encodeURIComponent(vaultId)}`;
|
|
662
2097
|
const response = await this.request(
|
|
663
2098
|
"GET",
|
|
664
|
-
`/v1/vault-sdk/
|
|
2099
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/files/${encodeURIComponent(fileId)}/text?vaultId=${encodeURIComponent(vaultId)}`,
|
|
665
2100
|
undefined,
|
|
666
|
-
{ operation: "
|
|
2101
|
+
{ operation: "getBotFileText" }
|
|
667
2102
|
);
|
|
2103
|
+
|
|
668
2104
|
return response.data;
|
|
669
2105
|
}
|
|
670
2106
|
|
|
671
|
-
|
|
2107
|
+
/**
|
|
2108
|
+
* Internal helper for bot file actions that share the same route shape.
|
|
2109
|
+
*
|
|
2110
|
+
* @private
|
|
2111
|
+
*/
|
|
2112
|
+
async updateBotFileAction(vaultId, botId, fileId, action) {
|
|
2113
|
+
validator.validate(
|
|
2114
|
+
{
|
|
2115
|
+
vaultId: { value: vaultId, type: "string" },
|
|
2116
|
+
botId: { value: botId, type: "string" },
|
|
2117
|
+
fileId: { value: fileId, type: "string" },
|
|
2118
|
+
action: { value: action, type: "string" },
|
|
2119
|
+
},
|
|
2120
|
+
"updateBotFileAction"
|
|
2121
|
+
);
|
|
2122
|
+
|
|
2123
|
+
if (action !== "cancel" && action !== "retry") {
|
|
2124
|
+
throw new VaultError(
|
|
2125
|
+
`[Vault SDK] 'updateBotFileAction': Unsupported action "${action}".`,
|
|
2126
|
+
{ code: "INVALID_PARAMETER", operation: "updateBotFileAction" }
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const response = await this.request(
|
|
2131
|
+
"POST",
|
|
2132
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/files/${encodeURIComponent(fileId)}/${action}?vaultId=${encodeURIComponent(vaultId)}`,
|
|
2133
|
+
undefined,
|
|
2134
|
+
{ operation: action === "cancel" ? "cancelBotFile" : "retryBotFile" }
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
return response.data;
|
|
2138
|
+
}
|
|
672
2139
|
|
|
673
2140
|
/**
|
|
674
|
-
*
|
|
2141
|
+
* Cancel a processing bot file.
|
|
675
2142
|
*
|
|
676
|
-
* @param {string} vaultId - The vault ID
|
|
677
|
-
* @param {string}
|
|
678
|
-
* @param {string}
|
|
679
|
-
* @returns {Promise<Object>}
|
|
2143
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2144
|
+
* @param {string} botId - The bot ID
|
|
2145
|
+
* @param {string} fileId - The bot file ID
|
|
2146
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
2147
|
+
*/
|
|
2148
|
+
async cancelBotFile(vaultId, botId, fileId) {
|
|
2149
|
+
return this.updateBotFileAction(vaultId, botId, fileId, "cancel");
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* Retry a failed bot file.
|
|
680
2154
|
*
|
|
681
|
-
* @
|
|
682
|
-
*
|
|
683
|
-
*
|
|
2155
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2156
|
+
* @param {string} botId - The bot ID
|
|
2157
|
+
* @param {string} fileId - The bot file ID
|
|
2158
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
684
2159
|
*/
|
|
685
|
-
async
|
|
2160
|
+
async retryBotFile(vaultId, botId, fileId) {
|
|
2161
|
+
return this.updateBotFileAction(vaultId, botId, fileId, "retry");
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/**
|
|
2165
|
+
* Quote the Twin Points cost of transcribing media before uploading or linking it.
|
|
2166
|
+
*
|
|
2167
|
+
* Pass direct file metadata in `payload.files`, folder IDs in `payload.folderIds`,
|
|
2168
|
+
* or both. Each file accepts `{ name, size, fileId?, durationSeconds? }`.
|
|
2169
|
+
*
|
|
2170
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2171
|
+
* @param {string} botId - The target bot ID
|
|
2172
|
+
* @param {Object} [payload]
|
|
2173
|
+
* @param {Array<Object>} [payload.files] - Files to quote
|
|
2174
|
+
* @param {string[]} [payload.folderIds] - Existing storage folders to inspect
|
|
2175
|
+
* @returns {Promise<Object>} Quote response from the bot endpoint
|
|
2176
|
+
*/
|
|
2177
|
+
async quoteTranscription(vaultId, botId, payload = {}) {
|
|
686
2178
|
validator.validate(
|
|
687
2179
|
{
|
|
688
2180
|
vaultId: { value: vaultId, type: "string" },
|
|
689
|
-
|
|
2181
|
+
botId: { value: botId, type: "string" },
|
|
2182
|
+
payload: { value: payload, type: "object", required: false },
|
|
690
2183
|
},
|
|
691
|
-
"
|
|
2184
|
+
"quoteTranscription"
|
|
692
2185
|
);
|
|
693
2186
|
|
|
2187
|
+
const files = Array.isArray(payload.files)
|
|
2188
|
+
? payload.files
|
|
2189
|
+
.filter((file) => file && typeof file === "object")
|
|
2190
|
+
.map((file) => ({
|
|
2191
|
+
name: typeof file.name === "string" ? file.name.trim() : "",
|
|
2192
|
+
size: Number(file.size || 0),
|
|
2193
|
+
...(typeof file.fileId === "string" && file.fileId.trim()
|
|
2194
|
+
? { fileId: file.fileId.trim() }
|
|
2195
|
+
: {}),
|
|
2196
|
+
...(Number.isFinite(Number(file.durationSeconds))
|
|
2197
|
+
? { durationSeconds: Number(file.durationSeconds) }
|
|
2198
|
+
: {}),
|
|
2199
|
+
}))
|
|
2200
|
+
.filter((file) => file.name)
|
|
2201
|
+
: [];
|
|
2202
|
+
|
|
2203
|
+
const folderIds = Array.isArray(payload.folderIds)
|
|
2204
|
+
? [...new Set(payload.folderIds.map((id) => (typeof id === "string" ? id.trim() : "")).filter(Boolean))]
|
|
2205
|
+
: [];
|
|
2206
|
+
|
|
2207
|
+
if (!files.length && !folderIds.length) {
|
|
2208
|
+
throw new VaultError(
|
|
2209
|
+
"[Vault SDK] 'quoteTranscription': At least one file or folder ID is required.",
|
|
2210
|
+
{ code: "INVALID_PARAMETER", operation: "quoteTranscription" }
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
694
2214
|
const response = await this.request(
|
|
695
2215
|
"POST",
|
|
696
|
-
|
|
697
|
-
{
|
|
698
|
-
|
|
2216
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/files/quote`,
|
|
2217
|
+
{
|
|
2218
|
+
vaultId,
|
|
2219
|
+
files,
|
|
2220
|
+
folderIds,
|
|
2221
|
+
},
|
|
2222
|
+
{ operation: "quoteTranscription" }
|
|
699
2223
|
);
|
|
2224
|
+
|
|
700
2225
|
return response.data;
|
|
701
2226
|
}
|
|
702
2227
|
|
|
703
2228
|
/**
|
|
704
|
-
*
|
|
2229
|
+
* Internal helper for bot uploads that now use the same presign -> upload
|
|
2230
|
+
* -> register flow as regular drive uploads.
|
|
2231
|
+
*
|
|
2232
|
+
* @private
|
|
2233
|
+
*/
|
|
2234
|
+
async uploadSingleFileToBot(file, vaultId, botId) {
|
|
2235
|
+
const resolved = await resolveFile(file, "uploadFilesToBot", {
|
|
2236
|
+
uploadRoot: this.uploadRoot,
|
|
2237
|
+
});
|
|
2238
|
+
const fileSize = resolved.buffer.length;
|
|
2239
|
+
|
|
2240
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
2241
|
+
throw new VaultError(
|
|
2242
|
+
`[Vault SDK] 'uploadFilesToBot': "${resolved.name}" is ${formatFileSize(fileSize)}, ` +
|
|
2243
|
+
`which exceeds the maximum upload size of ${formatFileSize(MAX_FILE_SIZE)}.`,
|
|
2244
|
+
{ code: "FILE_TOO_LARGE", operation: "uploadFilesToBot" }
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const fileName = sanitizeFileName(resolved.name);
|
|
2249
|
+
const fileType = resolved.type || contentTypeFor(fileName);
|
|
2250
|
+
const contentHash = crypto
|
|
2251
|
+
.createHash("sha256")
|
|
2252
|
+
.update(resolved.buffer)
|
|
2253
|
+
.digest("hex");
|
|
2254
|
+
|
|
2255
|
+
let presign;
|
|
2256
|
+
try {
|
|
2257
|
+
const response = await this.request(
|
|
2258
|
+
"POST",
|
|
2259
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/files/presign`,
|
|
2260
|
+
{
|
|
2261
|
+
vaultId,
|
|
2262
|
+
fileName,
|
|
2263
|
+
fileType,
|
|
2264
|
+
fileSize,
|
|
2265
|
+
contentHash,
|
|
2266
|
+
},
|
|
2267
|
+
{ operation: "uploadFilesToBot" }
|
|
2268
|
+
);
|
|
2269
|
+
presign = response.data?.data ?? response.data;
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
if (error instanceof VaultError) throw error;
|
|
2272
|
+
throw new VaultError(
|
|
2273
|
+
`[Vault SDK] 'uploadFilesToBot': Failed to get an upload URL for "${fileName}" — ${error.message}`,
|
|
2274
|
+
{ code: "PRESIGN_FAILED", operation: "uploadFilesToBot" }
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
const { url, key, contentType, sanitizedName, userId, metadata } = presign || {};
|
|
2279
|
+
|
|
2280
|
+
if (!url || !key) {
|
|
2281
|
+
throw new VaultError(
|
|
2282
|
+
`[Vault SDK] 'uploadFilesToBot': The server did not return an upload URL for "${fileName}".`,
|
|
2283
|
+
{
|
|
2284
|
+
code: "PRESIGN_FAILED",
|
|
2285
|
+
operation: "uploadFilesToBot",
|
|
2286
|
+
data: safeErrorDetails(presign),
|
|
2287
|
+
}
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
const metaHeaders = metadata
|
|
2292
|
+
? Object.fromEntries(
|
|
2293
|
+
Object.entries(metadata).map(([metaKey, value]) => [
|
|
2294
|
+
`x-amz-meta-${metaKey}`,
|
|
2295
|
+
String(value),
|
|
2296
|
+
])
|
|
2297
|
+
)
|
|
2298
|
+
: {
|
|
2299
|
+
"x-amz-meta-original-filename": sanitizedName || fileName,
|
|
2300
|
+
"x-amz-meta-content-hash": contentHash,
|
|
2301
|
+
"x-amz-meta-user-id": String(userId ?? ""),
|
|
2302
|
+
"x-amz-meta-file-size": fileSize.toString(),
|
|
2303
|
+
};
|
|
2304
|
+
|
|
2305
|
+
const uploadUrl = this._checkUploadUrl(url, "uploadFilesToBot", fileName);
|
|
2306
|
+
|
|
2307
|
+
try {
|
|
2308
|
+
await axios.put(uploadUrl, resolved.buffer, {
|
|
2309
|
+
headers: {
|
|
2310
|
+
"Content-Type": contentType || fileType,
|
|
2311
|
+
...metaHeaders,
|
|
2312
|
+
},
|
|
2313
|
+
maxBodyLength: Infinity,
|
|
2314
|
+
maxContentLength: Infinity,
|
|
2315
|
+
maxRedirects: 0,
|
|
2316
|
+
timeout: this._uploadTimeoutFor(fileSize),
|
|
2317
|
+
});
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
const status = error.response?.status;
|
|
2320
|
+
let detail = error.message;
|
|
2321
|
+
if (status === 403) {
|
|
2322
|
+
detail =
|
|
2323
|
+
"The presigned URL has expired or required signing headers are missing. Please try uploading again.";
|
|
2324
|
+
}
|
|
2325
|
+
if (status === 413) {
|
|
2326
|
+
detail = `File "${fileName}" exceeds the maximum allowed upload size.`;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
throw new VaultError(
|
|
2330
|
+
`[Vault SDK] 'uploadFilesToBot': Failed to upload "${fileName}" to storage — ${detail}`,
|
|
2331
|
+
{
|
|
2332
|
+
status,
|
|
2333
|
+
code: "STORAGE_UPLOAD_FAILED",
|
|
2334
|
+
operation: "uploadFilesToBot",
|
|
2335
|
+
}
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const rawDurationSeconds =
|
|
2340
|
+
typeof file === "object" && file !== null ? file.durationSeconds : undefined;
|
|
2341
|
+
const durationSeconds = Number(rawDurationSeconds);
|
|
2342
|
+
|
|
2343
|
+
try {
|
|
2344
|
+
const response = await this.request(
|
|
2345
|
+
"POST",
|
|
2346
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/files/register`,
|
|
2347
|
+
{
|
|
2348
|
+
vaultId,
|
|
2349
|
+
fileName: sanitizedName || fileName,
|
|
2350
|
+
filebaseKey: key,
|
|
2351
|
+
fileSize,
|
|
2352
|
+
contentHash,
|
|
2353
|
+
...(Number.isFinite(durationSeconds) && durationSeconds >= 0
|
|
2354
|
+
? { durationSeconds }
|
|
2355
|
+
: {}),
|
|
2356
|
+
},
|
|
2357
|
+
{ operation: "uploadFilesToBot" }
|
|
2358
|
+
);
|
|
2359
|
+
return response.data;
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
if (error instanceof VaultError) throw error;
|
|
2362
|
+
throw new VaultError(
|
|
2363
|
+
`[Vault SDK] 'uploadFilesToBot': File "${fileName}" was uploaded to storage but failed to register. ` +
|
|
2364
|
+
`Please contact support if this persists — ${error.message}`,
|
|
2365
|
+
{ code: "REGISTER_FAILED", operation: "uploadFilesToBot" }
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Delete one or more bot chat sessions through the bulk-delete route.
|
|
705
2372
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* @param {string}
|
|
709
|
-
* @
|
|
2373
|
+
* A single session ID is accepted and normalized into a one-item array.
|
|
2374
|
+
*
|
|
2375
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2376
|
+
* @param {string} botId - The bot ID
|
|
2377
|
+
* @param {string|string[]} sessionIds - One session ID or multiple session IDs
|
|
2378
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
710
2379
|
*
|
|
711
2380
|
* @example
|
|
712
|
-
* await vault.
|
|
2381
|
+
* await vault.deleteBotSessions("your-vault-id", "bot-id", "session-id");
|
|
2382
|
+
* await vault.deleteBotSessions("your-vault-id", "bot-id", ["session-a", "session-b"]);
|
|
713
2383
|
*/
|
|
714
|
-
async
|
|
2384
|
+
async deleteBotSessions(vaultId, botId, sessionIds) {
|
|
715
2385
|
validator.validate(
|
|
716
2386
|
{
|
|
717
2387
|
vaultId: { value: vaultId, type: "string" },
|
|
718
|
-
|
|
719
|
-
newName: { value: newName, type: "string" },
|
|
2388
|
+
botId: { value: botId, type: "string" },
|
|
720
2389
|
},
|
|
721
|
-
"
|
|
2390
|
+
"deleteBotSessions"
|
|
722
2391
|
);
|
|
723
2392
|
|
|
2393
|
+
const normalizedSessionIds = Array.isArray(sessionIds)
|
|
2394
|
+
? [...new Set(sessionIds.map((id) => (typeof id === "string" ? id.trim() : "")).filter(Boolean))]
|
|
2395
|
+
: typeof sessionIds === "string" && sessionIds.trim()
|
|
2396
|
+
? [sessionIds.trim()]
|
|
2397
|
+
: [];
|
|
2398
|
+
|
|
2399
|
+
if (!normalizedSessionIds.length) {
|
|
2400
|
+
throw new VaultError(
|
|
2401
|
+
"[Vault SDK] 'deleteBotSessions': At least one session ID is required.",
|
|
2402
|
+
{ code: "INVALID_PARAMETER", operation: "deleteBotSessions" }
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
724
2406
|
const response = await this.request(
|
|
725
2407
|
"POST",
|
|
726
|
-
|
|
727
|
-
{
|
|
728
|
-
|
|
2408
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/sessions/bulk-delete`,
|
|
2409
|
+
{
|
|
2410
|
+
vaultId,
|
|
2411
|
+
sessionIds: normalizedSessionIds,
|
|
2412
|
+
},
|
|
2413
|
+
{ operation: "deleteBotSessions" }
|
|
729
2414
|
);
|
|
2415
|
+
|
|
730
2416
|
return response.data;
|
|
731
2417
|
}
|
|
732
2418
|
|
|
733
2419
|
/**
|
|
734
|
-
*
|
|
2420
|
+
* Export one or more bot chat sessions through the bulk-export route.
|
|
735
2421
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* @
|
|
2422
|
+
* A single session ID is accepted and normalized into a one-item array.
|
|
2423
|
+
*
|
|
2424
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2425
|
+
* @param {string} botId - The bot ID
|
|
2426
|
+
* @param {string|string[]} sessionIds - One session ID or multiple session IDs
|
|
2427
|
+
* @param {"drive"|"brain"} saveOption - Where to export the session data
|
|
2428
|
+
* @param {string} [targetBotId] - Optional target bot ID when exporting to brain
|
|
2429
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
739
2430
|
*
|
|
740
2431
|
* @example
|
|
741
|
-
* await vault.
|
|
2432
|
+
* await vault.exportBotSessions("your-vault-id", "bot-id", "session-id", "drive");
|
|
2433
|
+
* await vault.exportBotSessions("your-vault-id", "bot-id", ["session-a", "session-b"], "brain", "target-bot-id");
|
|
742
2434
|
*/
|
|
743
|
-
async
|
|
2435
|
+
async exportBotSessions(vaultId, botId, sessionIds, saveOption, targetBotId) {
|
|
744
2436
|
validator.validate(
|
|
745
2437
|
{
|
|
746
2438
|
vaultId: { value: vaultId, type: "string" },
|
|
747
|
-
|
|
2439
|
+
botId: { value: botId, type: "string" },
|
|
2440
|
+
saveOption: { value: saveOption, type: "string" },
|
|
2441
|
+
targetBotId: { value: targetBotId, type: "string", required: false },
|
|
748
2442
|
},
|
|
749
|
-
"
|
|
2443
|
+
"exportBotSessions"
|
|
750
2444
|
);
|
|
751
2445
|
|
|
2446
|
+
const normalizedSessionIds = Array.isArray(sessionIds)
|
|
2447
|
+
? [...new Set(sessionIds.map((id) => (typeof id === "string" ? id.trim() : "")).filter(Boolean))]
|
|
2448
|
+
: typeof sessionIds === "string" && sessionIds.trim()
|
|
2449
|
+
? [sessionIds.trim()]
|
|
2450
|
+
: [];
|
|
2451
|
+
|
|
2452
|
+
if (!normalizedSessionIds.length) {
|
|
2453
|
+
throw new VaultError(
|
|
2454
|
+
"[Vault SDK] 'exportBotSessions': At least one session ID is required.",
|
|
2455
|
+
{ code: "INVALID_PARAMETER", operation: "exportBotSessions" }
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
if (saveOption !== "drive" && saveOption !== "brain") {
|
|
2460
|
+
throw new VaultError(
|
|
2461
|
+
"[Vault SDK] 'exportBotSessions': saveOption must be either \"drive\" or \"brain\".",
|
|
2462
|
+
{ code: "INVALID_PARAMETER", operation: "exportBotSessions" }
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const payload = {
|
|
2467
|
+
vaultId,
|
|
2468
|
+
sessionIds: normalizedSessionIds,
|
|
2469
|
+
saveOption,
|
|
2470
|
+
};
|
|
2471
|
+
|
|
2472
|
+
if (typeof targetBotId === "string" && targetBotId.trim()) {
|
|
2473
|
+
payload.targetBotId = targetBotId.trim();
|
|
2474
|
+
}
|
|
2475
|
+
|
|
752
2476
|
const response = await this.request(
|
|
753
|
-
"
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
{ operation: "
|
|
2477
|
+
"POST",
|
|
2478
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/sessions/bulk-export`,
|
|
2479
|
+
payload,
|
|
2480
|
+
{ operation: "exportBotSessions" }
|
|
757
2481
|
);
|
|
2482
|
+
|
|
758
2483
|
return response.data;
|
|
759
2484
|
}
|
|
760
2485
|
|
|
761
2486
|
/**
|
|
762
|
-
*
|
|
2487
|
+
* Remove either a bot file or a linked storage folder from a bot.
|
|
763
2488
|
*
|
|
764
|
-
* @param {string} vaultId - The vault ID
|
|
765
|
-
* @param {string}
|
|
766
|
-
* @
|
|
2489
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2490
|
+
* @param {string} botId - The bot ID
|
|
2491
|
+
* @param {"file"|"folder"} assetType - The asset type to remove
|
|
2492
|
+
* @param {string} assetId - The file ID or folder ID to remove
|
|
2493
|
+
* @param {{ permanent?: boolean, keepTranscript?: boolean }} [options] - Optional file-removal flags
|
|
2494
|
+
* @returns {Promise<Object>} Standard API response from the backend
|
|
767
2495
|
*
|
|
768
2496
|
* @example
|
|
769
|
-
* await vault.
|
|
2497
|
+
* await vault.removeBotAsset("your-vault-id", "bot-id", "file", "file-id", {
|
|
2498
|
+
* permanent: true,
|
|
2499
|
+
* keepTranscript: false,
|
|
2500
|
+
* });
|
|
2501
|
+
* await vault.removeBotAsset("your-vault-id", "bot-id", "folder", "folder-id");
|
|
770
2502
|
*/
|
|
771
|
-
async
|
|
2503
|
+
async removeBotAsset(vaultId, botId, assetType, assetId, options = {}) {
|
|
772
2504
|
validator.validate(
|
|
773
2505
|
{
|
|
774
2506
|
vaultId: { value: vaultId, type: "string" },
|
|
775
|
-
|
|
2507
|
+
botId: { value: botId, type: "string" },
|
|
2508
|
+
assetType: { value: assetType, type: "string" },
|
|
2509
|
+
assetId: { value: assetId, type: "string" },
|
|
2510
|
+
options: { value: options, type: "object", required: false },
|
|
776
2511
|
},
|
|
777
|
-
"
|
|
2512
|
+
"removeBotAsset"
|
|
778
2513
|
);
|
|
779
2514
|
|
|
2515
|
+
const normalizedType =
|
|
2516
|
+
typeof assetType === "string" ? assetType.trim().toLowerCase() : "";
|
|
2517
|
+
|
|
2518
|
+
if (normalizedType !== "file" && normalizedType !== "folder") {
|
|
2519
|
+
throw new VaultError(
|
|
2520
|
+
"[Vault SDK] 'removeBotAsset': assetType must be either \"file\" or \"folder\".",
|
|
2521
|
+
{ code: "INVALID_PARAMETER", operation: "removeBotAsset" }
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const { permanent, keepTranscript } = options || {};
|
|
2526
|
+
const params = new URLSearchParams({
|
|
2527
|
+
vaultId: vaultId.trim(),
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
if (normalizedType === "folder" && (permanent !== undefined || keepTranscript !== undefined)) {
|
|
2531
|
+
throw new VaultError(
|
|
2532
|
+
"[Vault SDK] 'removeBotAsset': permanent and keepTranscript are supported only for assetType \"file\".",
|
|
2533
|
+
{ code: "INVALID_PARAMETER", operation: "removeBotAsset" }
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
if (permanent !== undefined) {
|
|
2538
|
+
if (typeof permanent !== "boolean") {
|
|
2539
|
+
throw new VaultError(
|
|
2540
|
+
"[Vault SDK] 'removeBotAsset': options.permanent must be a boolean when provided.",
|
|
2541
|
+
{ code: "INVALID_PARAMETER", operation: "removeBotAsset" }
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
params.set("permanent", String(permanent));
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
if (keepTranscript !== undefined) {
|
|
2548
|
+
if (typeof keepTranscript !== "boolean") {
|
|
2549
|
+
throw new VaultError(
|
|
2550
|
+
"[Vault SDK] 'removeBotAsset': options.keepTranscript must be a boolean when provided.",
|
|
2551
|
+
{ code: "INVALID_PARAMETER", operation: "removeBotAsset" }
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
params.set("keepTranscript", String(keepTranscript));
|
|
2555
|
+
}
|
|
2556
|
+
|
|
780
2557
|
const response = await this.request(
|
|
781
2558
|
"DELETE",
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
{
|
|
2559
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/assets/${encodeURIComponent(
|
|
2560
|
+
normalizedType
|
|
2561
|
+
)}/${encodeURIComponent(assetId)}?${params.toString()}`,
|
|
2562
|
+
undefined,
|
|
2563
|
+
{ operation: "removeBotAsset" }
|
|
785
2564
|
);
|
|
2565
|
+
|
|
786
2566
|
return response.data;
|
|
787
2567
|
}
|
|
788
2568
|
|
|
789
|
-
// ─── Starred Files ────────────────────────────────────────────
|
|
790
|
-
|
|
791
2569
|
/**
|
|
792
|
-
*
|
|
2570
|
+
* Fetch one bot's full details, or all bots with their associated files and folders.
|
|
793
2571
|
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* @
|
|
2572
|
+
* When `botId` is omitted, the SDK returns the detailed view for every bot
|
|
2573
|
+
* owned by the vault user.
|
|
2574
|
+
*
|
|
2575
|
+
* @param {string} vaultId - The vault ID that owns the bot(s)
|
|
2576
|
+
* @param {string} [botId] - Optional bot ID
|
|
2577
|
+
* @returns {Promise<Object|Object[]>} One detailed bot or an array of detailed bots
|
|
798
2578
|
*
|
|
799
2579
|
* @example
|
|
800
|
-
* await vault.
|
|
2580
|
+
* const oneBot = await vault.getBotDetails("your-vault-id", "bot-id");
|
|
2581
|
+
* const allBots = await vault.getBotDetails("your-vault-id");
|
|
801
2582
|
*/
|
|
802
|
-
async
|
|
2583
|
+
async getBotDetails(vaultId, botId) {
|
|
803
2584
|
validator.validate(
|
|
804
2585
|
{
|
|
805
2586
|
vaultId: { value: vaultId, type: "string" },
|
|
806
|
-
|
|
807
|
-
isStarred: { value: isStarred, type: "boolean" },
|
|
2587
|
+
botId: { value: botId, type: "string", required: false },
|
|
808
2588
|
},
|
|
809
|
-
"
|
|
2589
|
+
"getBotDetails"
|
|
810
2590
|
);
|
|
811
2591
|
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
2592
|
+
const encodedVaultId = encodeURIComponent(vaultId);
|
|
2593
|
+
const endpoint = botId
|
|
2594
|
+
? `/v1/vault-sdk/bots/${encodeURIComponent(botId)}?vaultId=${encodedVaultId}`
|
|
2595
|
+
: `/v1/vault-sdk/bots?vaultId=${encodedVaultId}`;
|
|
2596
|
+
|
|
2597
|
+
const response = await this.request("GET", endpoint, undefined, {
|
|
2598
|
+
operation: "getBotDetails",
|
|
2599
|
+
});
|
|
818
2600
|
return response.data;
|
|
819
2601
|
}
|
|
820
2602
|
|
|
821
2603
|
/**
|
|
822
|
-
*
|
|
2604
|
+
* Fetch all chat sessions for a bot, or all messages for one session.
|
|
823
2605
|
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
2606
|
+
* When `sessionId` is omitted, the SDK returns the bot's session list.
|
|
2607
|
+
* When `sessionId` is provided, it returns that session's full message history.
|
|
2608
|
+
*
|
|
2609
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2610
|
+
* @param {string} botId - The bot ID
|
|
2611
|
+
* @param {string} [sessionId] - Optional session ID
|
|
2612
|
+
* @returns {Promise<Object[]|Object>} Session list or session messages response
|
|
826
2613
|
*
|
|
827
2614
|
* @example
|
|
828
|
-
* const
|
|
2615
|
+
* const sessions = await vault.getBotSessions("your-vault-id", "bot-id");
|
|
2616
|
+
* const messages = await vault.getBotSessions("your-vault-id", "bot-id", "session-id");
|
|
829
2617
|
*/
|
|
830
|
-
async
|
|
2618
|
+
async getBotSessions(vaultId, botId, sessionId) {
|
|
831
2619
|
validator.validate(
|
|
832
2620
|
{
|
|
833
2621
|
vaultId: { value: vaultId, type: "string" },
|
|
2622
|
+
botId: { value: botId, type: "string" },
|
|
2623
|
+
sessionId: { value: sessionId, type: "string", required: false },
|
|
834
2624
|
},
|
|
835
|
-
"
|
|
2625
|
+
"getBotSessions"
|
|
836
2626
|
);
|
|
837
2627
|
|
|
838
|
-
const
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
2628
|
+
const encodedVaultId = encodeURIComponent(vaultId);
|
|
2629
|
+
const normalizedBotId = botId.trim();
|
|
2630
|
+
const normalizedSessionId =
|
|
2631
|
+
typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
2632
|
+
|
|
2633
|
+
const endpoint = normalizedSessionId
|
|
2634
|
+
? `/v1/vault-sdk/bots/${encodeURIComponent(normalizedBotId)}/sessions/${encodeURIComponent(
|
|
2635
|
+
normalizedSessionId
|
|
2636
|
+
)}/messages?vaultId=${encodedVaultId}`
|
|
2637
|
+
: `/v1/vault-sdk/bots/${encodeURIComponent(normalizedBotId)}/sessions?vaultId=${encodedVaultId}`;
|
|
2638
|
+
|
|
2639
|
+
const response = await this.request("GET", endpoint, undefined, {
|
|
2640
|
+
operation: "getBotSessions",
|
|
2641
|
+
});
|
|
845
2642
|
return response.data;
|
|
846
2643
|
}
|
|
847
2644
|
|
|
848
|
-
// ─── Platform Operations ──────────────────────────────────────
|
|
849
|
-
|
|
850
2645
|
/**
|
|
851
|
-
*
|
|
2646
|
+
* Attach an existing storage file to a bot without re-uploading it.
|
|
852
2647
|
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* @
|
|
2648
|
+
* The file stays in storage and is linked into the bot's knowledge set.
|
|
2649
|
+
*
|
|
2650
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2651
|
+
* @param {string} botId - The target bot ID
|
|
2652
|
+
* @param {string|string[]} fileIds - One file ID or multiple file IDs
|
|
2653
|
+
* @returns {Promise<Object>} Bulk link result from the bot endpoint
|
|
856
2654
|
*
|
|
857
2655
|
* @example
|
|
858
|
-
*
|
|
859
|
-
*
|
|
2656
|
+
* await vault.addDriveFilesToBot("your-vault-id", "bot-id", "file-id");
|
|
2657
|
+
* await vault.addDriveFilesToBot("your-vault-id", "bot-id", ["file-a", "file-b"]);
|
|
860
2658
|
*/
|
|
861
|
-
async
|
|
862
|
-
const normalizedPlatformId =
|
|
863
|
-
typeof platformId === "string" ? platformId.trim() : platformId;
|
|
864
|
-
|
|
2659
|
+
async addDriveFilesToBot(vaultId, botId, fileIds) {
|
|
865
2660
|
validator.validate(
|
|
866
2661
|
{
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
value: normalizedPlatformId || undefined,
|
|
870
|
-
type: "string",
|
|
871
|
-
required: false,
|
|
872
|
-
},
|
|
2662
|
+
vaultId: { value: vaultId, type: "string" },
|
|
2663
|
+
botId: { value: botId, type: "string" },
|
|
873
2664
|
},
|
|
874
|
-
"
|
|
2665
|
+
"addDriveFilesToBot"
|
|
875
2666
|
);
|
|
876
2667
|
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
2668
|
+
const normalizedFileIds = Array.isArray(fileIds)
|
|
2669
|
+
? [...new Set(fileIds.map((id) => (typeof id === "string" ? id.trim() : "")).filter(Boolean))]
|
|
2670
|
+
: typeof fileIds === "string" && fileIds.trim()
|
|
2671
|
+
? [fileIds.trim()]
|
|
2672
|
+
: [];
|
|
2673
|
+
|
|
2674
|
+
if (!normalizedFileIds.length) {
|
|
2675
|
+
throw new VaultError(
|
|
2676
|
+
"[Vault SDK] 'addDriveFilesToBot': At least one file ID is required.",
|
|
2677
|
+
{ code: "INVALID_PARAMETER", operation: "addDriveFilesToBot" }
|
|
2678
|
+
);
|
|
880
2679
|
}
|
|
881
2680
|
|
|
882
2681
|
const response = await this.request(
|
|
883
2682
|
"POST",
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
2683
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/add-drive-files`,
|
|
2684
|
+
{
|
|
2685
|
+
vaultId,
|
|
2686
|
+
fileIds: normalizedFileIds,
|
|
2687
|
+
},
|
|
2688
|
+
{ operation: "addDriveFilesToBot" }
|
|
887
2689
|
);
|
|
2690
|
+
this.assertNotAllItemsFailed(response.data, "addDriveFilesToBot", "files");
|
|
888
2691
|
return response.data;
|
|
889
2692
|
}
|
|
890
2693
|
|
|
891
2694
|
/**
|
|
892
|
-
*
|
|
2695
|
+
* Attach one or more existing storage folders to a bot without moving them.
|
|
893
2696
|
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
2697
|
+
* You can pass a single folder ID or an array of folder IDs. The SDK
|
|
2698
|
+
* normalizes the input and uses the bulk folder-link route.
|
|
2699
|
+
*
|
|
2700
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2701
|
+
* @param {string} botId - The target bot ID
|
|
2702
|
+
* @param {string|string[]} folderIds - One folder ID or multiple folder IDs
|
|
2703
|
+
* @returns {Promise<Object>} Bulk link result from the bot endpoint
|
|
897
2704
|
*
|
|
898
2705
|
* @example
|
|
899
|
-
*
|
|
900
|
-
*
|
|
2706
|
+
* await vault.addDriveFoldersToBot("your-vault-id", "bot-id", "folder-id");
|
|
2707
|
+
* await vault.addDriveFoldersToBot("your-vault-id", "bot-id", ["folder-a", "folder-b"]);
|
|
901
2708
|
*/
|
|
902
|
-
async
|
|
903
|
-
const normalizedPlatformId =
|
|
904
|
-
typeof platformId === "string" ? platformId.trim() : platformId;
|
|
905
|
-
|
|
2709
|
+
async addDriveFoldersToBot(vaultId, botId, folderIds) {
|
|
906
2710
|
validator.validate(
|
|
907
2711
|
{
|
|
908
2712
|
vaultId: { value: vaultId, type: "string" },
|
|
909
|
-
|
|
910
|
-
value: normalizedPlatformId || undefined,
|
|
911
|
-
type: "string",
|
|
912
|
-
required: false,
|
|
913
|
-
},
|
|
2713
|
+
botId: { value: botId, type: "string" },
|
|
914
2714
|
},
|
|
915
|
-
"
|
|
2715
|
+
"addDriveFoldersToBot"
|
|
916
2716
|
);
|
|
917
2717
|
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
2718
|
+
const normalizedFolderIds = Array.isArray(folderIds)
|
|
2719
|
+
? [...new Set(folderIds.map((id) => (typeof id === "string" ? id.trim() : "")).filter(Boolean))]
|
|
2720
|
+
: typeof folderIds === "string" && folderIds.trim()
|
|
2721
|
+
? [folderIds.trim()]
|
|
2722
|
+
: [];
|
|
2723
|
+
|
|
2724
|
+
if (!normalizedFolderIds.length) {
|
|
2725
|
+
throw new VaultError(
|
|
2726
|
+
"[Vault SDK] 'addDriveFoldersToBot': At least one folder ID is required.",
|
|
2727
|
+
{ code: "INVALID_PARAMETER", operation: "addDriveFoldersToBot" }
|
|
2728
|
+
);
|
|
921
2729
|
}
|
|
922
2730
|
|
|
923
2731
|
const response = await this.request(
|
|
924
2732
|
"POST",
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
2733
|
+
`/v1/vault-sdk/bots/${encodeURIComponent(botId)}/add-drive-folders`,
|
|
2734
|
+
{
|
|
2735
|
+
vaultId,
|
|
2736
|
+
folderIds: normalizedFolderIds,
|
|
2737
|
+
},
|
|
2738
|
+
{ operation: "addDriveFoldersToBot" }
|
|
2739
|
+
);
|
|
2740
|
+
this.assertNotAllItemsFailed(
|
|
2741
|
+
response.data,
|
|
2742
|
+
"addDriveFoldersToBot",
|
|
2743
|
+
"folders"
|
|
928
2744
|
);
|
|
929
2745
|
return response.data;
|
|
930
2746
|
}
|
|
931
2747
|
|
|
932
|
-
// ─── Media ────────────────────────────────────────────────────
|
|
933
|
-
|
|
934
2748
|
/**
|
|
935
|
-
*
|
|
2749
|
+
* Upload one or more files directly to a bot for ingestion.
|
|
936
2750
|
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
2751
|
+
* This stores the files in the bot's dedicated folder and starts bot
|
|
2752
|
+
* knowledge processing in the background.
|
|
2753
|
+
*
|
|
2754
|
+
* @param {string|Object|Blob|Array<string|Object|Blob>} files
|
|
2755
|
+
* @param {string} vaultId - The vault ID that owns the bot
|
|
2756
|
+
* @param {string} botId - The target bot ID
|
|
2757
|
+
* @returns {Promise<Object>} Upload result from the bot ingestion endpoint
|
|
939
2758
|
*
|
|
940
2759
|
* @example
|
|
941
|
-
*
|
|
2760
|
+
* await vault.uploadFilesToBot("./faq.pdf", "your-vault-id", "bot-id");
|
|
2761
|
+
* await vault.uploadFilesToBot(
|
|
2762
|
+
* ["./faq.pdf", { buffer: audioBuffer, name: "call.mp3" }],
|
|
2763
|
+
* "your-vault-id",
|
|
2764
|
+
* "bot-id"
|
|
2765
|
+
* );
|
|
942
2766
|
*/
|
|
943
|
-
async
|
|
2767
|
+
async uploadFilesToBot(files, vaultId, botId) {
|
|
944
2768
|
validator.validate(
|
|
945
2769
|
{
|
|
946
2770
|
vaultId: { value: vaultId, type: "string" },
|
|
2771
|
+
botId: { value: botId, type: "string" },
|
|
947
2772
|
},
|
|
948
|
-
"
|
|
2773
|
+
"uploadFilesToBot"
|
|
949
2774
|
);
|
|
950
2775
|
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
2776
|
+
const normalizedFiles = Array.isArray(files) ? files : [files];
|
|
2777
|
+
if (!normalizedFiles.length) {
|
|
2778
|
+
throw new VaultError(
|
|
2779
|
+
"[Vault SDK] 'uploadFilesToBot': At least one file is required.",
|
|
2780
|
+
{ code: "INVALID_PARAMETER", operation: "uploadFilesToBot" }
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
const results = await runWithConcurrency(
|
|
2785
|
+
normalizedFiles,
|
|
2786
|
+
this.uploadConcurrency,
|
|
2787
|
+
async (file, index) => {
|
|
2788
|
+
const label =
|
|
2789
|
+
baseName(
|
|
2790
|
+
typeof file === "string" ? file : file?.name || file?.path || ""
|
|
2791
|
+
) || `file[${index}]`;
|
|
2792
|
+
|
|
2793
|
+
try {
|
|
2794
|
+
const response = await this.uploadSingleFileToBot(file, vaultId, botId);
|
|
2795
|
+
return { status: "success", fileName: label, response };
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
return {
|
|
2798
|
+
status: "failed",
|
|
2799
|
+
fileName: label,
|
|
2800
|
+
error: error.message,
|
|
2801
|
+
code: error.code || "UPLOAD_FAILED",
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
957
2805
|
);
|
|
958
|
-
|
|
2806
|
+
|
|
2807
|
+
const successful = results.filter((result) => result.status === "success");
|
|
2808
|
+
if (!successful.length) {
|
|
2809
|
+
throw new VaultError(
|
|
2810
|
+
"[Vault SDK] 'uploadFilesToBot': All files failed to upload.",
|
|
2811
|
+
{ code: "UPLOAD_FAILED", operation: "uploadFilesToBot", data: { results } }
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
const filesOut = [];
|
|
2816
|
+
const skipped = [];
|
|
2817
|
+
|
|
2818
|
+
for (const result of successful) {
|
|
2819
|
+
const payload = result.response?.data || {};
|
|
2820
|
+
if (payload.file) filesOut.push(payload.file);
|
|
2821
|
+
if (Array.isArray(payload.skipped)) skipped.push(...payload.skipped);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
for (const result of results) {
|
|
2825
|
+
if (result.status === "failed") {
|
|
2826
|
+
skipped.push({
|
|
2827
|
+
name: result.fileName,
|
|
2828
|
+
reason: result.error,
|
|
2829
|
+
code: result.code,
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
const successCount = successful.length;
|
|
2835
|
+
const failureCount = results.length - successCount;
|
|
2836
|
+
const messageParts = [];
|
|
2837
|
+
if (filesOut.length) {
|
|
2838
|
+
messageParts.push(
|
|
2839
|
+
`${filesOut.length} file${filesOut.length === 1 ? "" : "s"} sent for ingestion`
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
if (skipped.length) {
|
|
2843
|
+
messageParts.push(`${skipped.length} skipped`);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
return {
|
|
2847
|
+
success: true,
|
|
2848
|
+
message: messageParts.length
|
|
2849
|
+
? `${messageParts.join(", ")}. Processing happens in the background.`
|
|
2850
|
+
: "Nothing to upload.",
|
|
2851
|
+
data: {
|
|
2852
|
+
files: filesOut,
|
|
2853
|
+
skipped,
|
|
2854
|
+
results,
|
|
2855
|
+
successCount,
|
|
2856
|
+
failureCount,
|
|
2857
|
+
},
|
|
2858
|
+
};
|
|
959
2859
|
}
|
|
960
2860
|
|
|
961
2861
|
/**
|
|
@@ -969,84 +2869,6 @@ class Vault extends EventEmitter {
|
|
|
969
2869
|
return this.renameItem(vaultId, itemId, newName);
|
|
970
2870
|
}
|
|
971
2871
|
|
|
972
|
-
// ─── Internal Helpers ─────────────────────────────────────────
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Internal: Get a presigned S3 URL for file upload.
|
|
976
|
-
* @private
|
|
977
|
-
*/
|
|
978
|
-
async getPresignedUrl({
|
|
979
|
-
vaultId,
|
|
980
|
-
fileName,
|
|
981
|
-
fileType,
|
|
982
|
-
fileSize,
|
|
983
|
-
contentHash,
|
|
984
|
-
folderId,
|
|
985
|
-
}) {
|
|
986
|
-
validator.validate(
|
|
987
|
-
{
|
|
988
|
-
vaultId: { value: vaultId, type: "string" },
|
|
989
|
-
fileName: { value: fileName, type: "string" },
|
|
990
|
-
fileSize: { value: fileSize, type: "number" },
|
|
991
|
-
contentHash: { value: contentHash, type: "string" },
|
|
992
|
-
},
|
|
993
|
-
"getPresignedUrl"
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
const response = await this.request(
|
|
997
|
-
"POST",
|
|
998
|
-
"/v1/vault-sdk/get-presigned-url",
|
|
999
|
-
{
|
|
1000
|
-
vaultId,
|
|
1001
|
-
fileName,
|
|
1002
|
-
fileType: fileType || "application/octet-stream",
|
|
1003
|
-
fileSize,
|
|
1004
|
-
contentHash,
|
|
1005
|
-
folderId,
|
|
1006
|
-
},
|
|
1007
|
-
{ operation: "getPresignedUrl" }
|
|
1008
|
-
);
|
|
1009
|
-
return response.data;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Internal: Register a completed file upload with the backend.
|
|
1014
|
-
* @private
|
|
1015
|
-
*/
|
|
1016
|
-
async registerUpload({
|
|
1017
|
-
vaultId,
|
|
1018
|
-
fileName,
|
|
1019
|
-
filebaseKey,
|
|
1020
|
-
fileSize,
|
|
1021
|
-
contentHash,
|
|
1022
|
-
folderId,
|
|
1023
|
-
}) {
|
|
1024
|
-
validator.validate(
|
|
1025
|
-
{
|
|
1026
|
-
vaultId: { value: vaultId, type: "string" },
|
|
1027
|
-
fileName: { value: fileName, type: "string" },
|
|
1028
|
-
filebaseKey: { value: filebaseKey, type: "string" },
|
|
1029
|
-
fileSize: { value: fileSize, type: "number" },
|
|
1030
|
-
contentHash: { value: contentHash, type: "string" },
|
|
1031
|
-
},
|
|
1032
|
-
"registerUpload"
|
|
1033
|
-
);
|
|
1034
|
-
|
|
1035
|
-
const response = await this.request(
|
|
1036
|
-
"POST",
|
|
1037
|
-
"/v1/vault-sdk/register-upload",
|
|
1038
|
-
{
|
|
1039
|
-
vaultId,
|
|
1040
|
-
fileName,
|
|
1041
|
-
filebaseKey,
|
|
1042
|
-
fileSize,
|
|
1043
|
-
contentHash,
|
|
1044
|
-
folderId,
|
|
1045
|
-
},
|
|
1046
|
-
{ operation: "registerUpload" }
|
|
1047
|
-
);
|
|
1048
|
-
return response.data;
|
|
1049
|
-
}
|
|
1050
2872
|
}
|
|
1051
2873
|
|
|
1052
2874
|
export default Vault;
|