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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File helpers for uploads: reading whatever the caller passed as a file,
|
|
3
|
+
* resolving its MIME type, and reporting sizes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { VaultError } from "./validationError.js";
|
|
8
|
+
|
|
9
|
+
export const MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/** Largest buffer this runtime can allocate; reading past it aborts the process. */
|
|
12
|
+
export const MAX_BUFFERABLE_SIZE =
|
|
13
|
+
Buffer?.constants?.MAX_LENGTH ?? Buffer?.kMaxLength ?? 2 ** 31 - 1;
|
|
14
|
+
|
|
15
|
+
/** Extension → MIME type */
|
|
16
|
+
const CONTENT_TYPES = {
|
|
17
|
+
pdf: "application/pdf",
|
|
18
|
+
doc: "application/msword",
|
|
19
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
20
|
+
txt: "text/plain",
|
|
21
|
+
jpg: "image/jpeg",
|
|
22
|
+
jpeg: "image/jpeg",
|
|
23
|
+
png: "image/png",
|
|
24
|
+
gif: "image/gif",
|
|
25
|
+
mp4: "video/mp4",
|
|
26
|
+
mp3: "audio/mpeg",
|
|
27
|
+
zip: "application/zip",
|
|
28
|
+
json: "application/json",
|
|
29
|
+
csv: "text/csv",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MIME type for a file name, falling back to "application/octet-stream".
|
|
34
|
+
*
|
|
35
|
+
* @param {string} fileName
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function contentTypeFor(fileName) {
|
|
39
|
+
const extension = String(fileName).split(".").pop()?.toLowerCase();
|
|
40
|
+
return CONTENT_TYPES[extension] || "application/octet-stream";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Last path segment of a path or name, for both "/" and "\" separators. */
|
|
44
|
+
export const baseName = (filePath) =>
|
|
45
|
+
String(filePath).split(/[\\/]/).pop() || "";
|
|
46
|
+
|
|
47
|
+
/** Human-readable byte count, e.g. "1.5 GB". Mirrors the backend formatter. */
|
|
48
|
+
export function formatFileSize(bytes) {
|
|
49
|
+
if (!bytes) return "0 Bytes";
|
|
50
|
+
const k = 1024;
|
|
51
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
52
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
53
|
+
return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Anything byte-shaped → a Buffer, or null when the value isn't bytes. */
|
|
57
|
+
export function toBuffer(value) {
|
|
58
|
+
if (Buffer.isBuffer(value)) return value;
|
|
59
|
+
if (value instanceof ArrayBuffer) return Buffer.from(value);
|
|
60
|
+
if (ArrayBuffer.isView(value)) {
|
|
61
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reject a file that is too large, before its bytes are read into memory.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} size - Size in bytes
|
|
70
|
+
* @param {string} name - File name, used in the message
|
|
71
|
+
* @param {string} operation - Calling method name
|
|
72
|
+
* @throws {VaultError} With code FILE_TOO_LARGE
|
|
73
|
+
*/
|
|
74
|
+
export function assertNotTooLarge(size, name, operation) {
|
|
75
|
+
if (!Number.isFinite(size)) return;
|
|
76
|
+
|
|
77
|
+
const limit = Math.min(MAX_FILE_SIZE, MAX_BUFFERABLE_SIZE);
|
|
78
|
+
if (size > limit) {
|
|
79
|
+
const cap =
|
|
80
|
+
limit === MAX_FILE_SIZE
|
|
81
|
+
? `the maximum upload size of ${formatFileSize(MAX_FILE_SIZE)}`
|
|
82
|
+
: `what this runtime can hold in memory (${formatFileSize(limit)})`;
|
|
83
|
+
throw new VaultError(
|
|
84
|
+
`[Vault SDK] '${operation}': "${name}" is ${formatFileSize(size)}, which exceeds ${cap}.`,
|
|
85
|
+
{ code: "FILE_TOO_LARGE", operation }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The working directory, when the runtime has one. */
|
|
91
|
+
const defaultUploadRoot = () =>
|
|
92
|
+
typeof process !== "undefined" && typeof process.cwd === "function"
|
|
93
|
+
? process.cwd()
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a caller-supplied path and confirm it stays inside `root`.
|
|
98
|
+
*
|
|
99
|
+
* Both sides go through realpath first, so a symlink cannot step out of the
|
|
100
|
+
* root. Messages name the file only, never the resolved path.
|
|
101
|
+
*/
|
|
102
|
+
async function resolveWithinRoot(filePath, root, fail) {
|
|
103
|
+
if (
|
|
104
|
+
typeof fs?.promises?.realpath !== "function" ||
|
|
105
|
+
typeof fs?.promises?.stat !== "function"
|
|
106
|
+
) {
|
|
107
|
+
fail(
|
|
108
|
+
"Reading files from disk is not available here. Pass a File/Blob or { buffer, name } instead of a path.",
|
|
109
|
+
"FILE_READ_FAILED"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const configuredRoot = root || defaultUploadRoot();
|
|
114
|
+
if (!configuredRoot) {
|
|
115
|
+
fail(
|
|
116
|
+
"No upload directory is configured, so paths on disk cannot be read. Set VAULT_UPLOAD_ROOT, or pass { buffer, name } instead.",
|
|
117
|
+
"PATH_NOT_ALLOWED"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let realRoot;
|
|
122
|
+
try {
|
|
123
|
+
realRoot = await fs.promises.realpath(configuredRoot);
|
|
124
|
+
} catch {
|
|
125
|
+
fail(
|
|
126
|
+
"The configured upload directory does not exist. Check VAULT_UPLOAD_ROOT.",
|
|
127
|
+
"PATH_NOT_ALLOWED"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let realPath;
|
|
132
|
+
try {
|
|
133
|
+
realPath = await fs.promises.realpath(filePath);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
fail(
|
|
136
|
+
`Could not read "${baseName(filePath)}" — ${error.code || error.message}.`,
|
|
137
|
+
"FILE_READ_FAILED"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const separator = realRoot.includes("\\") ? "\\" : "/";
|
|
142
|
+
const caseInsensitive = /^[A-Za-z]:/.test(realRoot);
|
|
143
|
+
const normalize = (value) => (caseInsensitive ? value.toLowerCase() : value);
|
|
144
|
+
const base = normalize(
|
|
145
|
+
realRoot.endsWith(separator)
|
|
146
|
+
? realRoot.slice(0, -separator.length)
|
|
147
|
+
: realRoot
|
|
148
|
+
);
|
|
149
|
+
const target = normalize(realPath);
|
|
150
|
+
|
|
151
|
+
if (target !== base && !target.startsWith(base + separator)) {
|
|
152
|
+
fail(
|
|
153
|
+
`"${baseName(filePath)}" is outside the allowed upload directory. ` +
|
|
154
|
+
`Pass a file inside it, set VAULT_UPLOAD_ROOT to the directory you upload from, or hand over { buffer, name } instead.`,
|
|
155
|
+
"PATH_NOT_ALLOWED"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return realPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Turn whatever the caller passed as `file` into { buffer, name, type }.
|
|
164
|
+
*
|
|
165
|
+
* Accepts a path on disk, a File/Blob, or an object carrying the bytes under
|
|
166
|
+
* any of the common property names, so callers only ever hand over a file.
|
|
167
|
+
*
|
|
168
|
+
* @param {string|Object|Blob} input - The file, in any supported form
|
|
169
|
+
* @param {string} operation - Calling method name, used in error messages
|
|
170
|
+
* @param {Object} [options] - { uploadRoot }: the directory paths must stay inside
|
|
171
|
+
* @returns {Promise<{buffer: Buffer, name: string, type: string|undefined}>}
|
|
172
|
+
* @throws {VaultError} If the file is unreadable, too large, or outside the upload root
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveFile(input, operation, options = {}) {
|
|
175
|
+
const fail = (message, code = "INVALID_PARAMETER") => {
|
|
176
|
+
throw new VaultError(`[Vault SDK] '${operation}': ${message}`, {
|
|
177
|
+
code,
|
|
178
|
+
operation,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (input === undefined || input === null || input === "") {
|
|
183
|
+
fail(
|
|
184
|
+
"The 'file' parameter is required. Pass a path on disk, a File/Blob, or an object like { buffer, name }."
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const readPath = async (filePath) => {
|
|
189
|
+
const realPath = await resolveWithinRoot(filePath, options.uploadRoot, fail);
|
|
190
|
+
|
|
191
|
+
let stats;
|
|
192
|
+
try {
|
|
193
|
+
stats = await fs.promises.stat(realPath);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
fail(
|
|
196
|
+
`Could not read "${baseName(filePath)}" — ${error.code || error.message}.`,
|
|
197
|
+
"FILE_READ_FAILED"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!stats.isFile()) {
|
|
202
|
+
fail(`"${baseName(filePath)}" is not a file.`, "FILE_READ_FAILED");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
assertNotTooLarge(stats.size, baseName(filePath), operation);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return await fs.promises.readFile(realPath);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
fail(
|
|
211
|
+
`Could not read "${baseName(filePath)}" — ${error.code || error.message}.`,
|
|
212
|
+
"FILE_READ_FAILED"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// uploadFile("./photo.jpg", vaultId)
|
|
218
|
+
if (typeof input === "string") {
|
|
219
|
+
const buffer = await readPath(input);
|
|
220
|
+
return { buffer, name: baseName(input), type: undefined };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A bare Buffer carries no name, so there is nothing to store it under.
|
|
224
|
+
if (toBuffer(input) && !input.name) {
|
|
225
|
+
fail(
|
|
226
|
+
"A raw buffer has no file name. Pass { buffer, name } instead, e.g. { buffer, name: 'document.pdf' }."
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof input !== "object") {
|
|
231
|
+
fail(
|
|
232
|
+
"The 'file' parameter must be a path, a File/Blob, or an object like { buffer, name }."
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const name =
|
|
237
|
+
input.name || input.fileName || input.filename || baseName(input.path || "");
|
|
238
|
+
const type = input.type || input.mimeType || input.contentType;
|
|
239
|
+
|
|
240
|
+
// File / Blob (browser, or Node 20+ globals)
|
|
241
|
+
if (typeof input.arrayBuffer === "function") {
|
|
242
|
+
assertNotTooLarge(input.size, name || "file", operation);
|
|
243
|
+
const buffer = Buffer.from(await input.arrayBuffer());
|
|
244
|
+
assertNotTooLarge(buffer.length, name || "file", operation);
|
|
245
|
+
return { buffer, name, type };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const raw = input.buffer ?? input.data ?? input.content ?? input.bytes;
|
|
249
|
+
const buffer = raw
|
|
250
|
+
? toBuffer(raw)
|
|
251
|
+
: input.path
|
|
252
|
+
? await readPath(input.path)
|
|
253
|
+
: null;
|
|
254
|
+
|
|
255
|
+
if (!buffer) {
|
|
256
|
+
fail(
|
|
257
|
+
"Could not read the file content. Provide file.buffer as a Buffer/Uint8Array, or file.path pointing at a file on disk."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!name) {
|
|
262
|
+
fail(
|
|
263
|
+
"file.name is required. Provide the file name as a non-empty string (e.g. 'document.pdf')."
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
assertNotTooLarge(buffer.length, name, operation);
|
|
268
|
+
|
|
269
|
+
return { buffer, name, type };
|
|
270
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File name sanitization — mirrors the vault backend and frontend sanitizer so
|
|
3
|
+
* a name the SDK sends is the same name the server would have derived itself.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const MAX_FILENAME_LENGTH = 255;
|
|
7
|
+
|
|
8
|
+
/** Windows rejects these as file names whatever the extension. */
|
|
9
|
+
const RESERVED_NAMES = new Set([
|
|
10
|
+
"CON",
|
|
11
|
+
"PRN",
|
|
12
|
+
"AUX",
|
|
13
|
+
"NUL",
|
|
14
|
+
...Array.from({ length: 9 }, (_, i) => `COM${i + 1}`),
|
|
15
|
+
...Array.from({ length: 9 }, (_, i) => `LPT${i + 1}`),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** Illegal in a Windows path, a URL path segment, or both. */
|
|
19
|
+
const ILLEGAL_CHARS = /[<>:"/\\|?*]/g;
|
|
20
|
+
|
|
21
|
+
const foldToAscii = (value) =>
|
|
22
|
+
value
|
|
23
|
+
.normalize("NFKD")
|
|
24
|
+
.replace(/\p{M}/gu, "")
|
|
25
|
+
.replace(/[^\x20-\x7E]/g, "");
|
|
26
|
+
|
|
27
|
+
/** Leading dots are stripped: hidden files and "../" are not names we store. */
|
|
28
|
+
const tidy = (value) =>
|
|
29
|
+
foldToAscii(value)
|
|
30
|
+
.replace(ILLEGAL_CHARS, "")
|
|
31
|
+
.replace(/\s+/g, " ")
|
|
32
|
+
.replace(/^[\s.]+|[\s.]+$/g, "");
|
|
33
|
+
|
|
34
|
+
/** Splits "report.final.pdf" into { base: "report.final", ext: "pdf" }. */
|
|
35
|
+
export const splitExtension = (name) => {
|
|
36
|
+
const dot = name.lastIndexOf(".");
|
|
37
|
+
if (dot <= 0) return { base: name, ext: "" };
|
|
38
|
+
return { base: name.slice(0, dot), ext: name.slice(dot + 1) };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} name - Raw name as the caller supplied it
|
|
43
|
+
* @param {string} [fallback="file"] - Used when nothing usable survives.
|
|
44
|
+
* @returns {string} ASCII-safe name, at most MAX_FILENAME_LENGTH characters
|
|
45
|
+
*
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeFileName(name, fallback = "file") {
|
|
48
|
+
if (typeof name !== "string" || !name) return fallback;
|
|
49
|
+
|
|
50
|
+
const segment = name.split(/[\\/]/).pop() ?? "";
|
|
51
|
+
const { base, ext } = splitExtension(segment);
|
|
52
|
+
|
|
53
|
+
let safeBase = tidy(base) || fallback;
|
|
54
|
+
if (RESERVED_NAMES.has(safeBase.toUpperCase())) {
|
|
55
|
+
safeBase = `_${safeBase}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const safeExt = tidy(ext).slice(0, 32);
|
|
59
|
+
const suffix = safeExt ? `.${safeExt}` : "";
|
|
60
|
+
|
|
61
|
+
const room = Math.max(1, MAX_FILENAME_LENGTH - suffix.length);
|
|
62
|
+
const truncated = safeBase.slice(0, room).replace(/[\s.]+$/, "") || fallback;
|
|
63
|
+
|
|
64
|
+
if (!truncated) return "";
|
|
65
|
+
|
|
66
|
+
return `${truncated}${suffix}`;
|
|
67
|
+
}
|
|
@@ -14,7 +14,10 @@ export class ValidationError extends Error {
|
|
|
14
14
|
this.param = param;
|
|
15
15
|
this.expectedType = expectedType;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Chrome and Node only: Safari and older Firefox throw on it.
|
|
18
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
19
|
+
Error.captureStackTrace(this, ValidationError);
|
|
20
|
+
}
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -23,21 +26,53 @@ export class ValidationError extends Error {
|
|
|
23
26
|
* Wraps HTTP errors with status codes and structured error data.
|
|
24
27
|
*/
|
|
25
28
|
export class VaultError extends Error {
|
|
26
|
-
constructor(message, { status, code, operation, data } = {}) {
|
|
29
|
+
constructor(message, { status, code, operation, data, requestId } = {}) {
|
|
27
30
|
super(message);
|
|
28
31
|
this.name = "VaultError";
|
|
29
32
|
this.status = status || null;
|
|
30
33
|
this.code = code || "VAULT_ERROR";
|
|
31
34
|
this.operation = operation || null;
|
|
32
35
|
this.data = data || null;
|
|
36
|
+
this.requestId = requestId || null;
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
// Chrome and Node only: Safari and older Firefox throw on it.
|
|
39
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
40
|
+
Error.captureStackTrace(this, VaultError);
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* Maps HTTP status codes to user-friendly error descriptions.
|
|
40
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Reduce a server error body to what an integrator may safely see.
|
|
50
|
+
*
|
|
51
|
+
* Upstream bodies can carry stack traces, database table names and internal
|
|
52
|
+
* hostnames, so only the message, code and request id cross the boundary.
|
|
53
|
+
*
|
|
54
|
+
* @param {*} body - Response body from the server
|
|
55
|
+
* @returns {{message?: string, code?: string, requestId?: string}|null}
|
|
56
|
+
*/
|
|
57
|
+
export function safeErrorDetails(body) {
|
|
58
|
+
if (!body || typeof body !== "object") return null;
|
|
59
|
+
|
|
60
|
+
const text = (value) =>
|
|
61
|
+
typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
62
|
+
|
|
63
|
+
const details = {
|
|
64
|
+
message: text(body.message) ?? text(body.error),
|
|
65
|
+
code: text(body.code),
|
|
66
|
+
requestId: text(body.requestId) ?? text(body.request_id),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const key of Object.keys(details)) {
|
|
70
|
+
if (details[key] === undefined) delete details[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Object.keys(details).length ? details : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
export const HTTP_ERROR_MAP = {
|
|
42
77
|
400: { code: "BAD_REQUEST", description: "The request was invalid. Check your parameters." },
|
|
43
78
|
401: { code: "UNAUTHORIZED", description: "Authentication failed. Verify your VAULT_ACCESS_KEY and VAULT_SECRET_KEY." },
|
|
@@ -51,12 +86,33 @@ export const HTTP_ERROR_MAP = {
|
|
|
51
86
|
503: { code: "SERVICE_UNAVAILABLE", description: "The service is temporarily unavailable. Please try again later." },
|
|
52
87
|
};
|
|
53
88
|
|
|
89
|
+
// `typeof ""` and `typeof "abc"` both read as "string", which turns a rejected
|
|
90
|
+
// empty field into "must be a valid string. Received: string." Name what was
|
|
91
|
+
// actually wrong with the value instead.
|
|
92
|
+
const describe = (value) => {
|
|
93
|
+
if (typeof value === "string") {
|
|
94
|
+
if (value === "") return "an empty string";
|
|
95
|
+
if (value.trim() === "") return "a blank string";
|
|
96
|
+
return "string";
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) return value.length === 0 ? "an empty array" : "array";
|
|
99
|
+
if (value === null) return "null";
|
|
100
|
+
if (typeof value === "number") {
|
|
101
|
+
if (Number.isNaN(value)) return "NaN";
|
|
102
|
+
if (!Number.isFinite(value)) return String(value);
|
|
103
|
+
if (value < 0) return `a negative number (${value})`;
|
|
104
|
+
if (!Number.isInteger(value)) return `a decimal (${value})`;
|
|
105
|
+
}
|
|
106
|
+
return typeof value;
|
|
107
|
+
};
|
|
108
|
+
|
|
54
109
|
export const validator = {
|
|
55
110
|
types: {
|
|
56
111
|
string: (value) => typeof value === "string" && value.trim() !== "",
|
|
57
112
|
object: (value) => typeof value === "object" && value !== null,
|
|
58
113
|
array: (value) => Array.isArray(value) && value.length > 0,
|
|
59
|
-
number: (value) =>
|
|
114
|
+
number: (value) => Number.isFinite(value) && value >= 0,
|
|
115
|
+
integer: (value) => Number.isInteger(value) && value >= 0,
|
|
60
116
|
boolean: (value) => typeof value === "boolean",
|
|
61
117
|
function: (value) => typeof value === "function",
|
|
62
118
|
buffer: (value) => Buffer.isBuffer(value) || (value instanceof Uint8Array),
|
|
@@ -71,9 +127,26 @@ export const validator = {
|
|
|
71
127
|
}
|
|
72
128
|
|
|
73
129
|
Object.entries(params).forEach(([param, config]) => {
|
|
74
|
-
const { value, type, required = true, custom, message } = config;
|
|
130
|
+
const { value, type, required = true, custom, message, rejectNull } = config;
|
|
131
|
+
|
|
132
|
+
if (value === null && rejectNull) {
|
|
133
|
+
throw new ValidationError(
|
|
134
|
+
operation,
|
|
135
|
+
param,
|
|
136
|
+
type,
|
|
137
|
+
message ||
|
|
138
|
+
`[Vault SDK] '${operation}': Parameter '${param}' must be a valid ${type}, or be left out entirely. Received: null.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// An optional string left empty means "not supplied"; the string type
|
|
143
|
+
// rejects "", so without this `getFiles(vaultId)` throws on its own default.
|
|
144
|
+
const omitted =
|
|
145
|
+
value === undefined ||
|
|
146
|
+
value === null ||
|
|
147
|
+
(type === "string" && typeof value === "string" && value.trim() === "");
|
|
75
148
|
|
|
76
|
-
if (
|
|
149
|
+
if (omitted && !required) {
|
|
77
150
|
return;
|
|
78
151
|
}
|
|
79
152
|
|
|
@@ -88,11 +161,16 @@ export const validator = {
|
|
|
88
161
|
|
|
89
162
|
const typeValidator = this.types[type];
|
|
90
163
|
if (typeValidator && !typeValidator(value)) {
|
|
164
|
+
const blank =
|
|
165
|
+
type === "string" && typeof value === "string" && value.trim() === "";
|
|
91
166
|
throw new ValidationError(
|
|
92
167
|
operation,
|
|
93
168
|
param,
|
|
94
169
|
type,
|
|
95
|
-
message ||
|
|
170
|
+
message ||
|
|
171
|
+
(blank
|
|
172
|
+
? `[Vault SDK] '${operation}' requires a non-empty '${param}'.`
|
|
173
|
+
: `[Vault SDK] '${operation}': Parameter '${param}' must be a valid ${type}. Received: ${describe(value)}.`)
|
|
96
174
|
);
|
|
97
175
|
}
|
|
98
176
|
|