@myned-ai/gsplat-flame-avatar-renderer 1.0.4 → 1.0.6

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/src/utils/Util.js CHANGED
@@ -1,27 +1,39 @@
1
1
  /**
2
2
  * Core utility functions for Gaussian Splat rendering
3
- *
3
+ *
4
4
  * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
5
5
  * https://github.com/mkkellogg/GaussianSplats3D
6
- *
6
+ *
7
7
  * Import paths adjusted for gsplat-flame-avatar package structure.
8
8
  */
9
9
 
10
10
  import { DataUtils } from 'three';
11
+ import { NetworkError } from '../errors/index.js';
12
+ import { getLogger } from './Logger.js';
13
+
14
+ const logger = getLogger('Util');
11
15
 
12
16
  /**
13
17
  * Custom error for aborted operations
18
+ * @deprecated Use NetworkError instead for consistency
14
19
  */
15
20
  export class AbortedPromiseError extends Error {
16
21
  constructor(msg) {
17
22
  super(msg);
18
23
  this.name = 'AbortedPromiseError';
24
+ this.code = 'ABORTED';
19
25
  }
20
26
  }
21
27
 
22
28
  /**
23
29
  * Fetch with progress tracking using standard AbortController
24
- * Returns a Promise with an attached `abort()` method and `abortController`
30
+ *
31
+ * @param {string} path - URL to fetch
32
+ * @param {Function} [onProgress] - Progress callback (percent, percentLabel, chunk, fileSize)
33
+ * @param {boolean} [saveChunks=true] - Whether to save and return downloaded chunks
34
+ * @param {object} [headers] - Optional HTTP headers
35
+ * @returns {Promise<ArrayBuffer>} Promise that resolves with downloaded data
36
+ * @throws {NetworkError} If fetch fails or is aborted
25
37
  */
26
38
  export const fetchWithProgress = function(path, onProgress, saveChunks = true, headers) {
27
39
 
@@ -32,9 +44,14 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
32
44
  let onProgressCalledAtComplete = false;
33
45
  const localOnProgress = (percent, percentLabel, chunk, fileSize) => {
34
46
  if (onProgress && !onProgressCalledAtComplete) {
35
- onProgress(percent, percentLabel, chunk, fileSize);
36
- if (percent === 100) {
37
- onProgressCalledAtComplete = true;
47
+ try {
48
+ onProgress(percent, percentLabel, chunk, fileSize);
49
+ if (percent === 100) {
50
+ onProgressCalledAtComplete = true;
51
+ }
52
+ } catch (error) {
53
+ // Don't let progress callback errors break the download
54
+ logger.error('Error in progress callback:', error);
38
55
  }
39
56
  }
40
57
  };
@@ -42,20 +59,33 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
42
59
  const promise = new Promise((resolve, reject) => {
43
60
  const fetchOptions = { signal };
44
61
  if (headers) fetchOptions.headers = headers;
45
-
62
+
46
63
  fetch(path, fetchOptions)
47
64
  .then(async (data) => {
48
- // Handle error conditions where data is still returned
65
+ // Handle HTTP error responses
49
66
  if (!data.ok) {
50
- const errorText = await data.text();
51
- reject(new Error(`Fetch failed: ${data.status} ${data.statusText} ${errorText}`));
67
+ let errorText = '';
68
+ try {
69
+ errorText = await data.text();
70
+ } catch {
71
+ // Ignore if we can't read error text
72
+ }
73
+ reject(new NetworkError(
74
+ `Fetch failed: ${data.statusText}${errorText ? ' - ' + errorText : ''}`,
75
+ data.status
76
+ ));
77
+ return;
78
+ }
79
+
80
+ const reader = data.body?.getReader();
81
+ if (!reader) {
82
+ reject(new NetworkError('Response body is not readable', 0));
52
83
  return;
53
84
  }
54
85
 
55
- const reader = data.body.getReader();
56
86
  let bytesDownloaded = 0;
57
- let _fileSize = data.headers.get('Content-Length');
58
- let fileSize = _fileSize ? parseInt(_fileSize) : undefined;
87
+ const _fileSize = data.headers.get('Content-Length');
88
+ const fileSize = _fileSize ? parseInt(_fileSize, 10) : undefined;
59
89
 
60
90
  const chunks = [];
61
91
 
@@ -76,7 +106,7 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
76
106
  let percent;
77
107
  let percentLabel;
78
108
  if (fileSize !== undefined) {
79
- percent = bytesDownloaded / fileSize * 100;
109
+ percent = (bytesDownloaded / fileSize) * 100;
80
110
  percentLabel = `${percent.toFixed(2)}%`;
81
111
  }
82
112
  if (saveChunks) {
@@ -84,16 +114,27 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
84
114
  }
85
115
  localOnProgress(percent, percentLabel, chunk, fileSize);
86
116
  } catch (error) {
87
- reject(error);
117
+ reject(new NetworkError(
118
+ `Error reading response stream: ${error.message}`,
119
+ 0,
120
+ error
121
+ ));
88
122
  return;
89
123
  }
90
124
  }
91
125
  })
92
126
  .catch((error) => {
93
- if (error.name === 'AbortError') {
94
- reject(new AbortedPromiseError('Fetch aborted'));
127
+ // Don't wrap if already a NetworkError
128
+ if (error instanceof NetworkError) {
129
+ reject(error);
130
+ } else if (error.name === 'AbortError') {
131
+ reject(new NetworkError('Fetch aborted by user', 0, error));
95
132
  } else {
96
- reject(new AbortedPromiseError(error.message || error));
133
+ reject(new NetworkError(
134
+ `Fetch failed: ${error.message || 'Unknown error'}`,
135
+ 0,
136
+ error
137
+ ));
97
138
  }
98
139
  });
99
140
  });
@@ -0,0 +1,331 @@
1
+ /**
2
+ * ValidationUtils - Security-focused validation utilities
3
+ *
4
+ * CRITICAL: All external inputs MUST be validated before use.
5
+ * This module provides allowlist-based validation following security best practices.
6
+ */
7
+
8
+ import { ValidationError } from '../errors/index.js';
9
+
10
+ /**
11
+ * Allowed URL protocols (allowlist approach)
12
+ */
13
+ const ALLOWED_PROTOCOLS = Object.freeze(['https:', 'http:', 'blob:', 'data:']);
14
+
15
+ /**
16
+ * Validate and sanitize a URL
17
+ *
18
+ * @param {string} url - URL to validate
19
+ * @param {string} [baseURL] - Base URL for relative URLs (defaults to window.location.href)
20
+ * @returns {string} Sanitized absolute URL
21
+ * @throws {ValidationError} If URL is invalid or uses disallowed protocol
22
+ */
23
+ export function validateUrl(url, baseURL) {
24
+ if (typeof url !== 'string' || url.length === 0) {
25
+ throw new ValidationError('URL must be a non-empty string', 'url');
26
+ }
27
+
28
+ let parsed;
29
+ try {
30
+ // Use base URL if provided, otherwise use window location (browser only)
31
+ const base = baseURL || (typeof window !== 'undefined' ? window.location.href : undefined);
32
+ parsed = new URL(url, base);
33
+ } catch (error) {
34
+ throw new ValidationError(
35
+ `Invalid URL format: ${url}`,
36
+ 'url',
37
+ error
38
+ );
39
+ }
40
+
41
+ // Validate protocol against allowlist
42
+ if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
43
+ throw new ValidationError(
44
+ `Disallowed protocol: ${parsed.protocol}. Allowed protocols: ${ALLOWED_PROTOCOLS.join(', ')}`,
45
+ 'url.protocol'
46
+ );
47
+ }
48
+
49
+ return parsed.href;
50
+ }
51
+
52
+ /**
53
+ * Validate asset path to prevent path traversal attacks
54
+ *
55
+ * @param {string} path - File path to validate
56
+ * @returns {string} Validated path
57
+ * @throws {ValidationError} If path contains traversal sequences
58
+ */
59
+ export function validateAssetPath(path) {
60
+ if (typeof path !== 'string' || path.length === 0) {
61
+ throw new ValidationError('Asset path must be a non-empty string', 'path');
62
+ }
63
+
64
+ // Check for path traversal sequences
65
+ const dangerousPatterns = ['../', '..\\', '%2e%2e/', '%2e%2e\\'];
66
+ const normalizedPath = path.toLowerCase();
67
+
68
+ for (const pattern of dangerousPatterns) {
69
+ if (normalizedPath.includes(pattern)) {
70
+ throw new ValidationError(
71
+ `Path traversal detected in asset path: ${path}`,
72
+ 'path'
73
+ );
74
+ }
75
+ }
76
+
77
+ return path;
78
+ }
79
+
80
+ /**
81
+ * Validate file extension against allowlist
82
+ *
83
+ * @param {string} filename - Filename to validate
84
+ * @param {string[]} allowedExtensions - Array of allowed extensions (e.g., ['.ply', '.glb'])
85
+ * @returns {string} Validated filename
86
+ * @throws {ValidationError} If extension is not allowed
87
+ */
88
+ export function validateFileExtension(filename, allowedExtensions) {
89
+ if (typeof filename !== 'string' || filename.length === 0) {
90
+ throw new ValidationError('Filename must be a non-empty string', 'filename');
91
+ }
92
+
93
+ if (!Array.isArray(allowedExtensions) || allowedExtensions.length === 0) {
94
+ throw new ValidationError(
95
+ 'allowedExtensions must be a non-empty array',
96
+ 'allowedExtensions'
97
+ );
98
+ }
99
+
100
+ const extension = filename.slice(filename.lastIndexOf('.')).toLowerCase();
101
+ const normalizedAllowed = allowedExtensions.map(ext => ext.toLowerCase());
102
+
103
+ if (!normalizedAllowed.includes(extension)) {
104
+ throw new ValidationError(
105
+ `File extension ${extension} not allowed. Allowed: ${allowedExtensions.join(', ')}`,
106
+ 'filename'
107
+ );
108
+ }
109
+
110
+ return filename;
111
+ }
112
+
113
+ /**
114
+ * Validate numeric value is within range
115
+ *
116
+ * @param {number} value - Value to validate
117
+ * @param {number} min - Minimum allowed value (inclusive)
118
+ * @param {number} max - Maximum allowed value (inclusive)
119
+ * @param {string} fieldName - Name of field for error messages
120
+ * @returns {number} Validated value
121
+ * @throws {ValidationError} If value is not a number or out of range
122
+ */
123
+ export function validateNumberInRange(value, min, max, fieldName) {
124
+ if (typeof value !== 'number' || isNaN(value)) {
125
+ throw new ValidationError(
126
+ `${fieldName} must be a valid number`,
127
+ fieldName
128
+ );
129
+ }
130
+
131
+ if (value < min || value > max) {
132
+ throw new ValidationError(
133
+ `${fieldName} must be between ${min} and ${max}, got ${value}`,
134
+ fieldName
135
+ );
136
+ }
137
+
138
+ return value;
139
+ }
140
+
141
+ /**
142
+ * Validate integer value
143
+ *
144
+ * @param {number} value - Value to validate
145
+ * @param {string} fieldName - Name of field for error messages
146
+ * @returns {number} Validated integer
147
+ * @throws {ValidationError} If value is not an integer
148
+ */
149
+ export function validateInteger(value, fieldName) {
150
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
151
+ throw new ValidationError(
152
+ `${fieldName} must be an integer`,
153
+ fieldName
154
+ );
155
+ }
156
+
157
+ return value;
158
+ }
159
+
160
+ /**
161
+ * Validate positive integer
162
+ *
163
+ * @param {number} value - Value to validate
164
+ * @param {string} fieldName - Name of field for error messages
165
+ * @returns {number} Validated positive integer
166
+ * @throws {ValidationError} If value is not a positive integer
167
+ */
168
+ export function validatePositiveInteger(value, fieldName) {
169
+ validateInteger(value, fieldName);
170
+
171
+ if (value <= 0) {
172
+ throw new ValidationError(
173
+ `${fieldName} must be positive, got ${value}`,
174
+ fieldName
175
+ );
176
+ }
177
+
178
+ return value;
179
+ }
180
+
181
+ /**
182
+ * Validate object has required properties
183
+ *
184
+ * @param {object} obj - Object to validate
185
+ * @param {string[]} requiredProps - Array of required property names
186
+ * @param {string} objectName - Name of object for error messages
187
+ * @returns {object} Validated object
188
+ * @throws {ValidationError} If object is invalid or missing required properties
189
+ */
190
+ export function validateRequiredProperties(obj, requiredProps, objectName) {
191
+ if (obj === null || typeof obj !== 'object') {
192
+ throw new ValidationError(
193
+ `${objectName} must be an object`,
194
+ objectName
195
+ );
196
+ }
197
+
198
+ for (const prop of requiredProps) {
199
+ if (!(prop in obj) || obj[prop] === undefined) {
200
+ throw new ValidationError(
201
+ `${objectName} missing required property: ${prop}`,
202
+ `${objectName}.${prop}`
203
+ );
204
+ }
205
+ }
206
+
207
+ return obj;
208
+ }
209
+
210
+ /**
211
+ * Validate array buffer
212
+ *
213
+ * @param {ArrayBuffer} buffer - Buffer to validate
214
+ * @param {string} fieldName - Name of field for error messages
215
+ * @param {number} [minSize=0] - Minimum size in bytes
216
+ * @returns {ArrayBuffer} Validated buffer
217
+ * @throws {ValidationError} If buffer is invalid or too small
218
+ */
219
+ export function validateArrayBuffer(buffer, fieldName, minSize = 0) {
220
+ if (!(buffer instanceof ArrayBuffer)) {
221
+ throw new ValidationError(
222
+ `${fieldName} must be an ArrayBuffer`,
223
+ fieldName
224
+ );
225
+ }
226
+
227
+ if (buffer.byteLength < minSize) {
228
+ throw new ValidationError(
229
+ `${fieldName} must be at least ${minSize} bytes, got ${buffer.byteLength}`,
230
+ fieldName
231
+ );
232
+ }
233
+
234
+ return buffer;
235
+ }
236
+
237
+ /**
238
+ * Validate enum value against allowed values
239
+ *
240
+ * @param {*} value - Value to validate
241
+ * @param {Array} allowedValues - Array of allowed values
242
+ * @param {string} fieldName - Name of field for error messages
243
+ * @returns {*} Validated value
244
+ * @throws {ValidationError} If value is not in allowed values
245
+ */
246
+ export function validateEnum(value, allowedValues, fieldName) {
247
+ if (!allowedValues.includes(value)) {
248
+ throw new ValidationError(
249
+ `${fieldName} must be one of: ${allowedValues.join(', ')}. Got: ${value}`,
250
+ fieldName
251
+ );
252
+ }
253
+
254
+ return value;
255
+ }
256
+
257
+ /**
258
+ * Validate callback function
259
+ *
260
+ * @param {Function} callback - Callback to validate
261
+ * @param {string} fieldName - Name of field for error messages
262
+ * @param {boolean} [required=true] - Whether callback is required
263
+ * @returns {Function|null} Validated callback or null if not required and not provided
264
+ * @throws {ValidationError} If callback is required but not a function
265
+ */
266
+ export function validateCallback(callback, fieldName, required = true) {
267
+ if (callback === null || callback === undefined) {
268
+ if (required) {
269
+ throw new ValidationError(
270
+ `${fieldName} is required`,
271
+ fieldName
272
+ );
273
+ }
274
+ return null;
275
+ }
276
+
277
+ if (typeof callback !== 'function') {
278
+ throw new ValidationError(
279
+ `${fieldName} must be a function`,
280
+ fieldName
281
+ );
282
+ }
283
+
284
+ return callback;
285
+ }
286
+
287
+ /**
288
+ * Validate hex color string
289
+ *
290
+ * @param {string} value - Color string to validate
291
+ * @param {string} fieldName - Name of field for error messages
292
+ * @returns {string} Validated color string
293
+ * @throws {ValidationError} If color format is invalid
294
+ */
295
+ export function validateHexColor(value, fieldName) {
296
+ if (typeof value !== 'string') {
297
+ throw new ValidationError(
298
+ `${fieldName} must be a string`,
299
+ fieldName
300
+ );
301
+ }
302
+
303
+ const hexColorRegex = /^(#|0x)[0-9A-Fa-f]{6}$/i;
304
+ if (!hexColorRegex.test(value)) {
305
+ throw new ValidationError(
306
+ `${fieldName} must be a valid hex color (e.g., #FFFFFF or 0xFFFFFF)`,
307
+ fieldName
308
+ );
309
+ }
310
+
311
+ return value;
312
+ }
313
+
314
+ /**
315
+ * Validate DOM element
316
+ *
317
+ * @param {HTMLElement} element - Element to validate
318
+ * @param {string} fieldName - Name of field for error messages
319
+ * @returns {HTMLElement} Validated element
320
+ * @throws {ValidationError} If element is not a valid DOM element
321
+ */
322
+ export function validateDOMElement(element, fieldName) {
323
+ if (typeof HTMLElement !== 'undefined' && !(element instanceof HTMLElement)) {
324
+ throw new ValidationError(
325
+ `${fieldName} must be a valid HTML element`,
326
+ fieldName
327
+ );
328
+ }
329
+
330
+ return element;
331
+ }
@@ -1,9 +1,18 @@
1
1
  /**
2
2
  * gsplat-flame-avatar - Utils Module
3
3
  * Shared utility functions.
4
- *
4
+ *
5
5
  * Derived from @mkkellogg/gaussian-splats-3d (MIT License)
6
6
  */
7
7
 
8
+ // Original utilities
8
9
  export * from './LoaderUtils.js';
9
10
  export * from './Util.js';
11
+
12
+ // New refactored utilities
13
+ export * from './ValidationUtils.js';
14
+ export * from './Logger.js';
15
+ export * from './ObjectPool.js';
16
+ export * from './BlobUrlManager.js';
17
+ export * from './RenderLoop.js';
18
+ export * from './EventEmitter.js';