hazo_files 1.5.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -348,63 +348,63 @@ var HazoFilesError = class extends Error {
348
348
  }
349
349
  };
350
350
  var FileNotFoundError = class extends HazoFilesError {
351
- constructor(path3) {
352
- super(`File not found: ${path3}`, "FILE_NOT_FOUND", { path: path3 });
351
+ constructor(path4) {
352
+ super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
353
353
  this.name = "FileNotFoundError";
354
354
  }
355
355
  };
356
356
  var DirectoryNotFoundError = class extends HazoFilesError {
357
- constructor(path3) {
358
- super(`Directory not found: ${path3}`, "DIRECTORY_NOT_FOUND", { path: path3 });
357
+ constructor(path4) {
358
+ super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
359
359
  this.name = "DirectoryNotFoundError";
360
360
  }
361
361
  };
362
362
  var FileExistsError = class extends HazoFilesError {
363
- constructor(path3) {
364
- super(`File already exists: ${path3}`, "FILE_EXISTS", { path: path3 });
363
+ constructor(path4) {
364
+ super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
365
365
  this.name = "FileExistsError";
366
366
  }
367
367
  };
368
368
  var DirectoryExistsError = class extends HazoFilesError {
369
- constructor(path3) {
370
- super(`Directory already exists: ${path3}`, "DIRECTORY_EXISTS", { path: path3 });
369
+ constructor(path4) {
370
+ super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
371
371
  this.name = "DirectoryExistsError";
372
372
  }
373
373
  };
374
374
  var DirectoryNotEmptyError = class extends HazoFilesError {
375
- constructor(path3) {
376
- super(`Directory is not empty: ${path3}`, "DIRECTORY_NOT_EMPTY", { path: path3 });
375
+ constructor(path4) {
376
+ super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
377
377
  this.name = "DirectoryNotEmptyError";
378
378
  }
379
379
  };
380
380
  var PermissionDeniedError = class extends HazoFilesError {
381
- constructor(path3, operation) {
382
- super(`Permission denied for ${operation} on: ${path3}`, "PERMISSION_DENIED", { path: path3, operation });
381
+ constructor(path4, operation) {
382
+ super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
383
383
  this.name = "PermissionDeniedError";
384
384
  }
385
385
  };
386
386
  var InvalidPathError = class extends HazoFilesError {
387
- constructor(path3, reason) {
388
- super(`Invalid path "${path3}": ${reason}`, "INVALID_PATH", { path: path3, reason });
387
+ constructor(path4, reason) {
388
+ super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
389
389
  this.name = "InvalidPathError";
390
390
  }
391
391
  };
392
392
  var FileTooLargeError = class extends HazoFilesError {
393
- constructor(path3, size, maxSize) {
393
+ constructor(path4, size, maxSize) {
394
394
  super(
395
- `File "${path3}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
395
+ `File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
396
396
  "FILE_TOO_LARGE",
397
- { path: path3, size, maxSize }
397
+ { path: path4, size, maxSize }
398
398
  );
399
399
  this.name = "FileTooLargeError";
400
400
  }
401
401
  };
402
402
  var InvalidExtensionError = class extends HazoFilesError {
403
- constructor(path3, extension, allowedExtensions) {
403
+ constructor(path4, extension, allowedExtensions) {
404
404
  super(
405
405
  `File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
406
406
  "INVALID_EXTENSION",
407
- { path: path3, extension, allowedExtensions }
407
+ { path: path4, extension, allowedExtensions }
408
408
  );
409
409
  this.name = "InvalidExtensionError";
410
410
  }
@@ -427,6 +427,22 @@ var OperationError = class extends HazoFilesError {
427
427
  this.name = "OperationError";
428
428
  }
429
429
  };
430
+ var SSRFError = class extends HazoFilesError {
431
+ constructor(url, reason) {
432
+ super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
433
+ this.name = "SSRFError";
434
+ }
435
+ };
436
+ var ImportSizeCapError = class extends HazoFilesError {
437
+ constructor(url, capBytes) {
438
+ super(
439
+ `Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
440
+ "IMPORT_SIZE_CAP",
441
+ { url, capBytes }
442
+ );
443
+ this.name = "ImportSizeCapError";
444
+ }
445
+ };
430
446
 
431
447
  // src/common/utils.ts
432
448
  function successResult(data) {
@@ -670,10 +686,10 @@ var BaseStorageModule = class {
670
686
  * Get folder tree structure.
671
687
  * Default implementation that can be overridden by subclasses for optimization.
672
688
  */
673
- async getFolderTree(path3 = "/", depth = 3) {
689
+ async getFolderTree(path4 = "/", depth = 3) {
674
690
  this.ensureInitialized();
675
691
  try {
676
- const result = await this.buildTree(path3, depth, 0);
692
+ const result = await this.buildTree(path4, depth, 0);
677
693
  return successResult(result);
678
694
  } catch (error) {
679
695
  return errorResult(`Failed to get folder tree: ${error.message}`);
@@ -682,11 +698,11 @@ var BaseStorageModule = class {
682
698
  /**
683
699
  * Recursively build folder tree
684
700
  */
685
- async buildTree(path3, maxDepth, currentDepth) {
701
+ async buildTree(path4, maxDepth, currentDepth) {
686
702
  if (currentDepth >= maxDepth) {
687
703
  return [];
688
704
  }
689
- const listResult = await this.listDirectory(path3, { recursive: false });
705
+ const listResult = await this.listDirectory(path4, { recursive: false });
690
706
  if (!listResult.success || !listResult.data) {
691
707
  return [];
692
708
  }
@@ -1505,12 +1521,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
1505
1521
  */
1506
1522
  driveFileToItem(file, virtualPath) {
1507
1523
  const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
1508
- const path3 = virtualPath || "";
1524
+ const path4 = virtualPath || "";
1509
1525
  if (isFolder2) {
1510
1526
  return createFolderItem({
1511
1527
  id: file.id,
1512
1528
  name: file.name,
1513
- path: path3,
1529
+ path: path4,
1514
1530
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
1515
1531
  modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
1516
1532
  metadata: {
@@ -1522,7 +1538,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
1522
1538
  return createFileItem({
1523
1539
  id: file.id,
1524
1540
  name: file.name,
1525
- path: path3,
1541
+ path: path4,
1526
1542
  size: parseInt(file.size || "0", 10),
1527
1543
  mimeType: file.mimeType || "application/octet-stream",
1528
1544
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
@@ -1621,10 +1637,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1621
1637
  }
1622
1638
  let media;
1623
1639
  if (typeof source === "string") {
1624
- const fs3 = await import("fs");
1640
+ const fs4 = await import("fs");
1625
1641
  media = {
1626
1642
  mimeType: "application/octet-stream",
1627
- body: fs3.createReadStream(source)
1643
+ body: fs4.createReadStream(source)
1628
1644
  };
1629
1645
  } else if (Buffer.isBuffer(source)) {
1630
1646
  media = {
@@ -1673,10 +1689,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1673
1689
  options.onProgress(100, buffer.length, buffer.length);
1674
1690
  }
1675
1691
  if (localPath) {
1676
- const fs3 = await import("fs");
1677
- const path3 = await import("path");
1678
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
1679
- await fs3.promises.writeFile(localPath, buffer);
1692
+ const fs4 = await import("fs");
1693
+ const path4 = await import("path");
1694
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
1695
+ await fs4.promises.writeFile(localPath, buffer);
1680
1696
  return this.successResult(localPath);
1681
1697
  }
1682
1698
  return this.successResult(buffer);
@@ -1864,10 +1880,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1864
1880
  return false;
1865
1881
  }
1866
1882
  }
1867
- async getFolderTree(path3 = "/", depth = 3) {
1883
+ async getFolderTree(path4 = "/", depth = 3) {
1868
1884
  try {
1869
1885
  await this.ensureAuthenticated();
1870
- return super.getFolderTree(path3, depth);
1886
+ return super.getFolderTree(path4, depth);
1871
1887
  } catch (error) {
1872
1888
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
1873
1889
  }
@@ -2158,12 +2174,12 @@ var DropboxModule = class extends BaseStorageModule {
2158
2174
  */
2159
2175
  metadataToItem(entry, virtualPath) {
2160
2176
  const isFolder2 = entry[".tag"] === "folder";
2161
- const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
2177
+ const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
2162
2178
  if (isFolder2) {
2163
2179
  return createFolderItem({
2164
2180
  id: entry.id,
2165
2181
  name: entry.name,
2166
- path: path3,
2182
+ path: path4,
2167
2183
  createdAt: /* @__PURE__ */ new Date(),
2168
2184
  modifiedAt: /* @__PURE__ */ new Date(),
2169
2185
  metadata: {
@@ -2176,7 +2192,7 @@ var DropboxModule = class extends BaseStorageModule {
2176
2192
  return createFileItem({
2177
2193
  id: fileEntry.id,
2178
2194
  name: fileEntry.name,
2179
- path: path3,
2195
+ path: path4,
2180
2196
  size: fileEntry.size,
2181
2197
  mimeType: getMimeType(fileEntry.name),
2182
2198
  createdAt: new Date(fileEntry.client_modified),
@@ -2252,8 +2268,8 @@ var DropboxModule = class extends BaseStorageModule {
2252
2268
  const dbxPath = this.toDropboxPath(remotePath);
2253
2269
  let contents;
2254
2270
  if (typeof source === "string") {
2255
- const fs3 = await import("fs");
2256
- contents = await fs3.promises.readFile(source);
2271
+ const fs4 = await import("fs");
2272
+ contents = await fs4.promises.readFile(source);
2257
2273
  } else if (Buffer.isBuffer(source)) {
2258
2274
  contents = source;
2259
2275
  } else {
@@ -2322,10 +2338,10 @@ var DropboxModule = class extends BaseStorageModule {
2322
2338
  options.onProgress(100, buffer.length, buffer.length);
2323
2339
  }
2324
2340
  if (localPath) {
2325
- const fs3 = await import("fs");
2326
- const path3 = await import("path");
2327
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2328
- await fs3.promises.writeFile(localPath, buffer);
2341
+ const fs4 = await import("fs");
2342
+ const path4 = await import("path");
2343
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
2344
+ await fs4.promises.writeFile(localPath, buffer);
2329
2345
  return this.successResult(localPath);
2330
2346
  }
2331
2347
  return this.successResult(buffer);
@@ -2494,10 +2510,10 @@ var DropboxModule = class extends BaseStorageModule {
2494
2510
  return false;
2495
2511
  }
2496
2512
  }
2497
- async getFolderTree(path3 = "/", depth = 3) {
2513
+ async getFolderTree(path4 = "/", depth = 3) {
2498
2514
  try {
2499
2515
  await this.ensureAuthenticated();
2500
- return super.getFolderTree(path3, depth);
2516
+ return super.getFolderTree(path4, depth);
2501
2517
  } catch (error) {
2502
2518
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
2503
2519
  }
@@ -2616,13 +2632,13 @@ var FileManager = class {
2616
2632
  /**
2617
2633
  * Create a directory at the specified path
2618
2634
  */
2619
- async createDirectory(path3) {
2635
+ async createDirectory(path4) {
2620
2636
  this.ensureInitialized();
2621
2637
  const start = Date.now();
2622
- const result = await this.module.createDirectory(path3);
2638
+ const result = await this.module.createDirectory(path4);
2623
2639
  this.logFileOp(result.success ? "info" : "error", {
2624
2640
  operation: "upload",
2625
- file_path: path3,
2641
+ file_path: path4,
2626
2642
  mime_type: "folder",
2627
2643
  storage: this.config?.provider,
2628
2644
  duration_ms: Date.now() - start,
@@ -2637,13 +2653,13 @@ var FileManager = class {
2637
2653
  * @param path - Directory path
2638
2654
  * @param recursive - If true, remove directory and all contents
2639
2655
  */
2640
- async removeDirectory(path3, recursive = false) {
2656
+ async removeDirectory(path4, recursive = false) {
2641
2657
  this.ensureInitialized();
2642
2658
  const start = Date.now();
2643
- const result = await this.module.removeDirectory(path3, recursive);
2659
+ const result = await this.module.removeDirectory(path4, recursive);
2644
2660
  this.logFileOp(result.success ? "info" : "error", {
2645
2661
  operation: "delete",
2646
- file_path: path3,
2662
+ file_path: path4,
2647
2663
  mime_type: "folder",
2648
2664
  storage: this.config?.provider,
2649
2665
  duration_ms: Date.now() - start,
@@ -2722,13 +2738,13 @@ var FileManager = class {
2722
2738
  /**
2723
2739
  * Delete a file
2724
2740
  */
2725
- async deleteFile(path3) {
2741
+ async deleteFile(path4) {
2726
2742
  this.ensureInitialized();
2727
2743
  const start = Date.now();
2728
- const result = await this.module.deleteFile(path3);
2744
+ const result = await this.module.deleteFile(path4);
2729
2745
  this.logFileOp(result.success ? "info" : "error", {
2730
2746
  operation: "delete",
2731
- file_path: path3,
2747
+ file_path: path4,
2732
2748
  storage: this.config?.provider,
2733
2749
  duration_ms: Date.now() - start,
2734
2750
  success: result.success,
@@ -2742,14 +2758,14 @@ var FileManager = class {
2742
2758
  * @param newName - New filename (not full path)
2743
2759
  * @param options - Rename options
2744
2760
  */
2745
- async renameFile(path3, newName, options) {
2761
+ async renameFile(path4, newName, options) {
2746
2762
  this.ensureInitialized();
2747
2763
  const start = Date.now();
2748
- const result = await this.module.renameFile(path3, newName, options);
2764
+ const result = await this.module.renameFile(path4, newName, options);
2749
2765
  this.logFileOp(result.success ? "info" : "error", {
2750
2766
  operation: "move",
2751
2767
  file_name: result.data?.name,
2752
- file_path: path3,
2768
+ file_path: path4,
2753
2769
  storage: this.config?.provider,
2754
2770
  duration_ms: Date.now() - start,
2755
2771
  success: result.success,
@@ -2764,14 +2780,14 @@ var FileManager = class {
2764
2780
  * @param newName - New folder name (not full path)
2765
2781
  * @param options - Rename options
2766
2782
  */
2767
- async renameFolder(path3, newName, options) {
2783
+ async renameFolder(path4, newName, options) {
2768
2784
  this.ensureInitialized();
2769
2785
  const start = Date.now();
2770
- const result = await this.module.renameFolder(path3, newName, options);
2786
+ const result = await this.module.renameFolder(path4, newName, options);
2771
2787
  this.logFileOp(result.success ? "info" : "error", {
2772
2788
  operation: "move",
2773
2789
  file_name: result.data?.name,
2774
- file_path: path3,
2790
+ file_path: path4,
2775
2791
  storage: this.config?.provider,
2776
2792
  duration_ms: Date.now() - start,
2777
2793
  success: result.success,
@@ -2786,54 +2802,54 @@ var FileManager = class {
2786
2802
  * @param path - Directory path
2787
2803
  * @param options - List options
2788
2804
  */
2789
- async listDirectory(path3, options) {
2805
+ async listDirectory(path4, options) {
2790
2806
  this.ensureInitialized();
2791
- return this.module.listDirectory(path3, options);
2807
+ return this.module.listDirectory(path4, options);
2792
2808
  }
2793
2809
  /**
2794
2810
  * Get information about a file or folder
2795
2811
  */
2796
- async getItem(path3) {
2812
+ async getItem(path4) {
2797
2813
  this.ensureInitialized();
2798
- return this.module.getItem(path3);
2814
+ return this.module.getItem(path4);
2799
2815
  }
2800
2816
  /**
2801
2817
  * Check if a file or folder exists
2802
2818
  */
2803
- async exists(path3) {
2819
+ async exists(path4) {
2804
2820
  this.ensureInitialized();
2805
- return this.module.exists(path3);
2821
+ return this.module.exists(path4);
2806
2822
  }
2807
2823
  /**
2808
2824
  * Get folder tree structure
2809
2825
  * @param path - Starting path (default: root)
2810
2826
  * @param depth - Maximum depth to traverse
2811
2827
  */
2812
- async getFolderTree(path3 = "/", depth = 3) {
2828
+ async getFolderTree(path4 = "/", depth = 3) {
2813
2829
  this.ensureInitialized();
2814
- return this.module.getFolderTree(path3, depth);
2830
+ return this.module.getFolderTree(path4, depth);
2815
2831
  }
2816
2832
  // ============ Convenience Methods ============
2817
2833
  /**
2818
2834
  * Create a file with string content
2819
2835
  */
2820
- async writeFile(path3, content, options) {
2836
+ async writeFile(path4, content, options) {
2821
2837
  const buffer = Buffer.from(content, "utf-8");
2822
- return this.uploadFile(buffer, path3, options);
2838
+ return this.uploadFile(buffer, path4, options);
2823
2839
  }
2824
2840
  /**
2825
2841
  * Read a file as string
2826
2842
  */
2827
- async readFile(path3) {
2828
- const result = await this.downloadFile(path3);
2843
+ async readFile(path4) {
2844
+ const result = await this.downloadFile(path4);
2829
2845
  if (!result.success) {
2830
2846
  return { success: false, error: result.error };
2831
2847
  }
2832
2848
  if (Buffer.isBuffer(result.data)) {
2833
2849
  return { success: true, data: result.data.toString("utf-8") };
2834
2850
  }
2835
- const fs3 = await import("fs");
2836
- const content = await fs3.promises.readFile(result.data, "utf-8");
2851
+ const fs4 = await import("fs");
2852
+ const content = await fs4.promises.readFile(result.data, "utf-8");
2837
2853
  return { success: true, data: content };
2838
2854
  }
2839
2855
  /**
@@ -2844,22 +2860,22 @@ var FileManager = class {
2844
2860
  if (!downloadResult.success) {
2845
2861
  return { success: false, error: downloadResult.error };
2846
2862
  }
2847
- const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs3) => fs3.promises.readFile(downloadResult.data));
2863
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
2848
2864
  return this.uploadFile(buffer, destinationPath, options);
2849
2865
  }
2850
2866
  /**
2851
2867
  * Ensure a directory exists (creates if needed)
2852
2868
  */
2853
- async ensureDirectory(path3) {
2854
- const exists = await this.exists(path3);
2869
+ async ensureDirectory(path4) {
2870
+ const exists = await this.exists(path4);
2855
2871
  if (exists) {
2856
- const item = await this.getItem(path3);
2872
+ const item = await this.getItem(path4);
2857
2873
  if (item.success && item.data?.isDirectory) {
2858
2874
  return { success: true, data: item.data };
2859
2875
  }
2860
2876
  return { success: false, error: "Path exists but is not a directory" };
2861
2877
  }
2862
- return this.createDirectory(path3);
2878
+ return this.createDirectory(path4);
2863
2879
  }
2864
2880
  };
2865
2881
  function createFileManager(options) {
@@ -2871,6 +2887,11 @@ async function createInitializedFileManager(options) {
2871
2887
  return manager;
2872
2888
  }
2873
2889
 
2890
+ // src/services/tracked-file-manager.ts
2891
+ var fs3 = __toESM(require("fs"));
2892
+ var os = __toESM(require("os"));
2893
+ var path3 = __toESM(require("path"));
2894
+
2874
2895
  // src/common/file-data-utils.ts
2875
2896
  function generateExtractionId() {
2876
2897
  return `ext_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -3172,6 +3193,8 @@ var FileMetadataService = class {
3172
3193
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
3173
3194
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
3174
3195
  if (input.content_tag !== void 0) record.content_tag = input.content_tag;
3196
+ if (input.changed_by !== void 0) record.changed_by = input.changed_by;
3197
+ if (input.source_url !== void 0) record.source_url = input.source_url;
3175
3198
  const results = await this.crud.insert(record);
3176
3199
  const duration_ms = Date.now() - start;
3177
3200
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
@@ -3204,32 +3227,32 @@ var FileMetadataService = class {
3204
3227
  /**
3205
3228
  * Record a directory creation
3206
3229
  */
3207
- async recordDirectoryCreation(path3, storageType, metadata) {
3230
+ async recordDirectoryCreation(path4, storageType, metadata) {
3208
3231
  return this.recordUpload({
3209
- filename: getBaseName(path3),
3232
+ filename: getBaseName(path4),
3210
3233
  file_type: "folder",
3211
3234
  file_data: metadata,
3212
- file_path: path3,
3235
+ file_path: path4,
3213
3236
  storage_type: storageType
3214
3237
  });
3215
3238
  }
3216
3239
  /**
3217
3240
  * Record a file access (download)
3218
3241
  */
3219
- async recordAccess(path3, storageType) {
3242
+ async recordAccess(path4, storageType) {
3220
3243
  const start = Date.now();
3221
3244
  try {
3222
- const existing = await this.findByPath(path3, storageType);
3245
+ const existing = await this.findByPath(path4, storageType);
3223
3246
  if (existing) {
3224
3247
  await this.crud.updateById(existing.id, {
3225
3248
  changed_at: this.now()
3226
3249
  });
3227
3250
  const duration_ms = Date.now() - start;
3228
- this.logger?.debug?.("Recorded file access", { path: path3 });
3251
+ this.logger?.debug?.("Recorded file access", { path: path4 });
3229
3252
  this.logger?.info?.("file_operation", {
3230
3253
  operation: "download",
3231
3254
  file_name: existing.filename,
3232
- file_path: path3,
3255
+ file_path: path4,
3233
3256
  mime_type: existing.file_type,
3234
3257
  size_bytes: existing.file_size,
3235
3258
  storage: storageType,
@@ -3243,7 +3266,7 @@ var FileMetadataService = class {
3243
3266
  const duration_ms = Date.now() - start;
3244
3267
  this.logger?.error?.("file_operation", {
3245
3268
  operation: "download",
3246
- file_path: path3,
3269
+ file_path: path4,
3247
3270
  storage: storageType,
3248
3271
  duration_ms,
3249
3272
  success: false,
@@ -3255,19 +3278,20 @@ var FileMetadataService = class {
3255
3278
  }
3256
3279
  /**
3257
3280
  * Record a file deletion
3281
+ * changedBy is accepted for API consistency but not written (record is deleted)
3258
3282
  */
3259
- async recordDelete(path3, storageType) {
3283
+ async recordDelete(path4, storageType, _changedBy) {
3260
3284
  const start = Date.now();
3261
3285
  try {
3262
- const existing = await this.findByPath(path3, storageType);
3286
+ const existing = await this.findByPath(path4, storageType);
3263
3287
  if (existing) {
3264
3288
  await this.crud.deleteById(existing.id);
3265
3289
  const duration_ms = Date.now() - start;
3266
- this.logger?.debug?.("Recorded file deletion", { path: path3 });
3290
+ this.logger?.debug?.("Recorded file deletion", { path: path4 });
3267
3291
  this.logger?.info?.("file_operation", {
3268
3292
  operation: "delete",
3269
3293
  file_name: existing.filename,
3270
- file_path: path3,
3294
+ file_path: path4,
3271
3295
  mime_type: existing.file_type,
3272
3296
  size_bytes: existing.file_size,
3273
3297
  storage: storageType,
@@ -3281,7 +3305,7 @@ var FileMetadataService = class {
3281
3305
  const duration_ms = Date.now() - start;
3282
3306
  this.logger?.error?.("file_operation", {
3283
3307
  operation: "delete",
3284
- file_path: path3,
3308
+ file_path: path4,
3285
3309
  storage: storageType,
3286
3310
  duration_ms,
3287
3311
  success: false,
@@ -3294,23 +3318,23 @@ var FileMetadataService = class {
3294
3318
  /**
3295
3319
  * Record a directory deletion (recursive)
3296
3320
  */
3297
- async recordDirectoryDelete(path3, storageType, recursive) {
3321
+ async recordDirectoryDelete(path4, storageType, recursive) {
3298
3322
  try {
3299
3323
  if (recursive) {
3300
3324
  const records = await this.crud.findBy({ storage_type: storageType });
3301
3325
  const toDelete = records.filter(
3302
- (r) => r.file_path === path3 || r.file_path.startsWith(path3 + "/")
3326
+ (r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
3303
3327
  );
3304
3328
  for (const record of toDelete) {
3305
3329
  await this.crud.deleteById(record.id);
3306
3330
  }
3307
3331
  this.logger?.debug?.("Recorded recursive directory deletion", {
3308
- path: path3,
3332
+ path: path4,
3309
3333
  count: toDelete.length
3310
3334
  });
3311
3335
  return true;
3312
3336
  } else {
3313
- return this.recordDelete(path3, storageType);
3337
+ return this.recordDelete(path4, storageType);
3314
3338
  }
3315
3339
  } catch (error) {
3316
3340
  this.logError("recordDirectoryDelete", error);
@@ -3320,16 +3344,18 @@ var FileMetadataService = class {
3320
3344
  /**
3321
3345
  * Record a file or folder move
3322
3346
  */
3323
- async recordMove(sourcePath, destinationPath, storageType) {
3347
+ async recordMove(sourcePath, destinationPath, storageType, changedBy) {
3324
3348
  const start = Date.now();
3325
3349
  try {
3326
3350
  const existing = await this.findByPath(sourcePath, storageType);
3327
3351
  if (existing) {
3328
- await this.crud.updateById(existing.id, {
3352
+ const patch = {
3329
3353
  file_path: destinationPath,
3330
3354
  filename: getBaseName(destinationPath),
3331
3355
  changed_at: this.now()
3332
- });
3356
+ };
3357
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3358
+ await this.crud.updateById(existing.id, patch);
3333
3359
  const duration_ms = Date.now() - start;
3334
3360
  this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
3335
3361
  this.logger?.info?.("file_operation", {
@@ -3363,24 +3389,26 @@ var FileMetadataService = class {
3363
3389
  /**
3364
3390
  * Record a file or folder rename
3365
3391
  */
3366
- async recordRename(path3, newName, storageType) {
3392
+ async recordRename(path4, newName, storageType, changedBy) {
3367
3393
  const start = Date.now();
3368
3394
  try {
3369
- const existing = await this.findByPath(path3, storageType);
3395
+ const existing = await this.findByPath(path4, storageType);
3370
3396
  if (existing) {
3371
- const parentPath = getDirName(path3);
3397
+ const parentPath = getDirName(path4);
3372
3398
  const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
3373
- await this.crud.updateById(existing.id, {
3399
+ const patch = {
3374
3400
  filename: newName,
3375
3401
  file_path: newPath,
3376
3402
  changed_at: this.now()
3377
- });
3403
+ };
3404
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3405
+ await this.crud.updateById(existing.id, patch);
3378
3406
  const duration_ms = Date.now() - start;
3379
- this.logger?.debug?.("Recorded file rename", { path: path3, newName });
3407
+ this.logger?.debug?.("Recorded file rename", { path: path4, newName });
3380
3408
  this.logger?.info?.("file_operation", {
3381
3409
  operation: "move",
3382
3410
  file_name: existing.filename,
3383
- file_path: path3,
3411
+ file_path: path4,
3384
3412
  mime_type: existing.file_type,
3385
3413
  size_bytes: existing.file_size,
3386
3414
  storage: storageType,
@@ -3395,7 +3423,7 @@ var FileMetadataService = class {
3395
3423
  const duration_ms = Date.now() - start;
3396
3424
  this.logger?.error?.("file_operation", {
3397
3425
  operation: "move",
3398
- file_path: path3,
3426
+ file_path: path4,
3399
3427
  storage: storageType,
3400
3428
  duration_ms,
3401
3429
  success: false,
@@ -3409,10 +3437,10 @@ var FileMetadataService = class {
3409
3437
  /**
3410
3438
  * Find a record by path and storage type
3411
3439
  */
3412
- async findByPath(path3, storageType) {
3440
+ async findByPath(path4, storageType) {
3413
3441
  try {
3414
3442
  return await this.crud.findOneBy({
3415
- file_path: path3,
3443
+ file_path: path4,
3416
3444
  storage_type: storageType
3417
3445
  });
3418
3446
  } catch (error) {
@@ -3455,9 +3483,9 @@ var FileMetadataService = class {
3455
3483
  /**
3456
3484
  * Update custom metadata for a file
3457
3485
  */
3458
- async updateMetadata(path3, storageType, metadata) {
3486
+ async updateMetadata(path4, storageType, metadata) {
3459
3487
  try {
3460
- const existing = await this.findByPath(path3, storageType);
3488
+ const existing = await this.findByPath(path4, storageType);
3461
3489
  if (existing) {
3462
3490
  const currentData = JSON.parse(existing.file_data || "{}");
3463
3491
  const newData = { ...currentData, ...metadata };
@@ -3465,7 +3493,7 @@ var FileMetadataService = class {
3465
3493
  file_data: JSON.stringify(newData),
3466
3494
  changed_at: this.now()
3467
3495
  });
3468
- this.logger?.debug?.("Updated file metadata", { path: path3 });
3496
+ this.logger?.debug?.("Updated file metadata", { path: path4 });
3469
3497
  return true;
3470
3498
  }
3471
3499
  return false;
@@ -3481,9 +3509,9 @@ var FileMetadataService = class {
3481
3509
  * Get parsed file_data structure for a file
3482
3510
  * Automatically migrates old format to new extraction structure
3483
3511
  */
3484
- async getFileData(path3, storageType) {
3512
+ async getFileData(path4, storageType) {
3485
3513
  try {
3486
- const existing = await this.findByPath(path3, storageType);
3514
+ const existing = await this.findByPath(path4, storageType);
3487
3515
  if (!existing) {
3488
3516
  return null;
3489
3517
  }
@@ -3496,9 +3524,9 @@ var FileMetadataService = class {
3496
3524
  /**
3497
3525
  * Get merged extraction data for a file
3498
3526
  */
3499
- async getMergedData(path3, storageType) {
3527
+ async getMergedData(path4, storageType) {
3500
3528
  try {
3501
- const fileData = await this.getFileData(path3, storageType);
3529
+ const fileData = await this.getFileData(path4, storageType);
3502
3530
  if (!fileData) {
3503
3531
  return null;
3504
3532
  }
@@ -3511,12 +3539,12 @@ var FileMetadataService = class {
3511
3539
  /**
3512
3540
  * Add an extraction to a file's data
3513
3541
  */
3514
- async addExtraction(path3, storageType, data, options) {
3542
+ async addExtraction(path4, storageType, data, options) {
3515
3543
  const start = Date.now();
3516
3544
  try {
3517
- const existing = await this.findByPath(path3, storageType);
3545
+ const existing = await this.findByPath(path4, storageType);
3518
3546
  if (!existing) {
3519
- this.logger?.warn?.("Cannot add extraction: file not found", { path: path3 });
3547
+ this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
3520
3548
  return null;
3521
3549
  }
3522
3550
  const currentFileData = parseFileData(existing.file_data);
@@ -3531,11 +3559,11 @@ var FileMetadataService = class {
3531
3559
  });
3532
3560
  const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
3533
3561
  const duration_ms = Date.now() - start;
3534
- this.logger?.debug?.("Added extraction", { path: path3, extractionId: newExtraction.id });
3562
+ this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
3535
3563
  this.logger?.info?.("file_operation", {
3536
3564
  operation: "extract",
3537
3565
  file_name: existing.filename,
3538
- file_path: path3,
3566
+ file_path: path4,
3539
3567
  mime_type: existing.file_type,
3540
3568
  storage: storageType,
3541
3569
  duration_ms,
@@ -3547,7 +3575,7 @@ var FileMetadataService = class {
3547
3575
  const duration_ms = Date.now() - start;
3548
3576
  this.logger?.error?.("file_operation", {
3549
3577
  operation: "extract",
3550
- file_path: path3,
3578
+ file_path: path4,
3551
3579
  storage: storageType,
3552
3580
  duration_ms,
3553
3581
  success: false,
@@ -3560,9 +3588,9 @@ var FileMetadataService = class {
3560
3588
  /**
3561
3589
  * Remove an extraction by ID
3562
3590
  */
3563
- async removeExtractionById(path3, storageType, id, options) {
3591
+ async removeExtractionById(path4, storageType, id, options) {
3564
3592
  try {
3565
- const existing = await this.findByPath(path3, storageType);
3593
+ const existing = await this.findByPath(path4, storageType);
3566
3594
  if (!existing) {
3567
3595
  return false;
3568
3596
  }
@@ -3576,7 +3604,7 @@ var FileMetadataService = class {
3576
3604
  file_data: stringifyFileData(result.data),
3577
3605
  changed_at: this.now()
3578
3606
  });
3579
- this.logger?.debug?.("Removed extraction by ID", { path: path3, extractionId: id });
3607
+ this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
3580
3608
  return true;
3581
3609
  } catch (error) {
3582
3610
  this.logError("removeExtractionById", error);
@@ -3586,9 +3614,9 @@ var FileMetadataService = class {
3586
3614
  /**
3587
3615
  * Remove an extraction by index
3588
3616
  */
3589
- async removeExtractionByIndex(path3, storageType, index, options) {
3617
+ async removeExtractionByIndex(path4, storageType, index, options) {
3590
3618
  try {
3591
- const existing = await this.findByPath(path3, storageType);
3619
+ const existing = await this.findByPath(path4, storageType);
3592
3620
  if (!existing) {
3593
3621
  return false;
3594
3622
  }
@@ -3602,7 +3630,7 @@ var FileMetadataService = class {
3602
3630
  file_data: stringifyFileData(result.data),
3603
3631
  changed_at: this.now()
3604
3632
  });
3605
- this.logger?.debug?.("Removed extraction by index", { path: path3, index });
3633
+ this.logger?.debug?.("Removed extraction by index", { path: path4, index });
3606
3634
  return true;
3607
3635
  } catch (error) {
3608
3636
  this.logError("removeExtractionByIndex", error);
@@ -3612,9 +3640,9 @@ var FileMetadataService = class {
3612
3640
  /**
3613
3641
  * Get all extractions for a file
3614
3642
  */
3615
- async getExtractions(path3, storageType) {
3643
+ async getExtractions(path4, storageType) {
3616
3644
  try {
3617
- const fileData = await this.getFileData(path3, storageType);
3645
+ const fileData = await this.getFileData(path4, storageType);
3618
3646
  if (!fileData) {
3619
3647
  return null;
3620
3648
  }
@@ -3627,9 +3655,9 @@ var FileMetadataService = class {
3627
3655
  /**
3628
3656
  * Get a specific extraction by ID
3629
3657
  */
3630
- async getExtractionById(path3, storageType, id) {
3658
+ async getExtractionById(path4, storageType, id) {
3631
3659
  try {
3632
- const fileData = await this.getFileData(path3, storageType);
3660
+ const fileData = await this.getFileData(path4, storageType);
3633
3661
  if (!fileData) {
3634
3662
  return null;
3635
3663
  }
@@ -3642,9 +3670,9 @@ var FileMetadataService = class {
3642
3670
  /**
3643
3671
  * Clear all extractions for a file
3644
3672
  */
3645
- async clearExtractions(path3, storageType) {
3673
+ async clearExtractions(path4, storageType) {
3646
3674
  try {
3647
- const existing = await this.findByPath(path3, storageType);
3675
+ const existing = await this.findByPath(path4, storageType);
3648
3676
  if (!existing) {
3649
3677
  return false;
3650
3678
  }
@@ -3652,7 +3680,7 @@ var FileMetadataService = class {
3652
3680
  file_data: stringifyFileData(createEmptyFileDataStructure()),
3653
3681
  changed_at: this.now()
3654
3682
  });
3655
- this.logger?.debug?.("Cleared all extractions", { path: path3 });
3683
+ this.logger?.debug?.("Cleared all extractions", { path: path4 });
3656
3684
  return true;
3657
3685
  } catch (error) {
3658
3686
  this.logError("clearExtractions", error);
@@ -3862,7 +3890,7 @@ var FileMetadataService = class {
3862
3890
  return this.updateStatus(fileId, "soft_deleted");
3863
3891
  }
3864
3892
  /**
3865
- * Update specific V2 fields on a record
3893
+ * Update specific V2/V4 fields on a record
3866
3894
  */
3867
3895
  async updateFields(fileId, fields) {
3868
3896
  try {
@@ -3993,6 +4021,7 @@ var TrackedFileManager = class extends FileManager {
3993
4021
  constructor(options = {}) {
3994
4022
  super(options);
3995
4023
  this.metadataService = null;
4024
+ this.quotaService = null;
3996
4025
  this.trackingConfig = {
3997
4026
  enabled: options.tracking?.enabled ?? false,
3998
4027
  tableName: options.tracking?.tableName ?? "hazo_files",
@@ -4006,6 +4035,8 @@ var TrackedFileManager = class extends FileManager {
4006
4035
  logErrors: this.trackingConfig.logErrors
4007
4036
  });
4008
4037
  }
4038
+ this.quotaService = options.quotaService ?? null;
4039
+ this.ssrfAllowlist = options.ssrfAllowlist ?? [];
4009
4040
  }
4010
4041
  /**
4011
4042
  * Check if tracking is enabled and service is available
@@ -4023,11 +4054,11 @@ var TrackedFileManager = class extends FileManager {
4023
4054
  /**
4024
4055
  * Create a directory and record it in the database
4025
4056
  */
4026
- async createDirectory(path3) {
4027
- const result = await super.createDirectory(path3);
4057
+ async createDirectory(path4) {
4058
+ const result = await super.createDirectory(path4);
4028
4059
  if (result.success && this.isTrackingEnabled()) {
4029
4060
  this.metadataService.recordDirectoryCreation(
4030
- path3,
4061
+ path4,
4031
4062
  this.getStorageType(),
4032
4063
  result.data?.metadata
4033
4064
  ).catch(() => {
@@ -4038,11 +4069,11 @@ var TrackedFileManager = class extends FileManager {
4038
4069
  /**
4039
4070
  * Remove a directory and delete its record from the database
4040
4071
  */
4041
- async removeDirectory(path3, recursive = false) {
4042
- const result = await super.removeDirectory(path3, recursive);
4072
+ async removeDirectory(path4, recursive = false) {
4073
+ const result = await super.removeDirectory(path4, recursive);
4043
4074
  if (result.success && this.isTrackingEnabled()) {
4044
4075
  this.metadataService.recordDirectoryDelete(
4045
- path3,
4076
+ path4,
4046
4077
  this.getStorageType(),
4047
4078
  recursive
4048
4079
  ).catch(() => {
@@ -4060,7 +4091,16 @@ var TrackedFileManager = class extends FileManager {
4060
4091
  if (source instanceof Buffer) {
4061
4092
  fileBuffer = source;
4062
4093
  }
4094
+ const scope_id = options?.scope_id;
4095
+ const preCheckSize = fileBuffer?.length ?? 0;
4096
+ if (this.quotaService && scope_id && preCheckSize > 0) {
4097
+ await this.quotaService.checkQuota(scope_id, preCheckSize);
4098
+ }
4063
4099
  const result = await super.uploadFile(source, remotePath, options);
4100
+ if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
4101
+ await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
4102
+ });
4103
+ }
4064
4104
  if (result.success && this.isTrackingEnabled() && result.data) {
4065
4105
  const fileItem = result.data;
4066
4106
  const skipHash = options?.skipHash ?? false;
@@ -4084,7 +4124,10 @@ var TrackedFileManager = class extends FileManager {
4084
4124
  file_path: remotePath,
4085
4125
  storage_type: this.getStorageType(),
4086
4126
  file_hash: fileHash,
4087
- file_size: fileSize
4127
+ file_size: fileSize,
4128
+ uploaded_by: options?.actor_id,
4129
+ changed_by: options?.actor_id,
4130
+ scope_id: options?.scope_id
4088
4131
  });
4089
4132
  if (awaitRecording) {
4090
4133
  await recordPromise;
@@ -4118,7 +4161,8 @@ var TrackedFileManager = class extends FileManager {
4118
4161
  this.metadataService.recordMove(
4119
4162
  sourcePath,
4120
4163
  destinationPath,
4121
- this.getStorageType()
4164
+ this.getStorageType(),
4165
+ options?.actor_id
4122
4166
  ).catch(() => {
4123
4167
  });
4124
4168
  }
@@ -4127,12 +4171,13 @@ var TrackedFileManager = class extends FileManager {
4127
4171
  /**
4128
4172
  * Delete a file and remove its record from the database
4129
4173
  */
4130
- async deleteFile(path3) {
4131
- const result = await super.deleteFile(path3);
4174
+ async deleteFile(path4, opts) {
4175
+ const result = await super.deleteFile(path4);
4132
4176
  if (result.success && this.isTrackingEnabled()) {
4133
4177
  this.metadataService.recordDelete(
4134
- path3,
4135
- this.getStorageType()
4178
+ path4,
4179
+ this.getStorageType(),
4180
+ opts?.actor_id
4136
4181
  ).catch(() => {
4137
4182
  });
4138
4183
  }
@@ -4141,13 +4186,14 @@ var TrackedFileManager = class extends FileManager {
4141
4186
  /**
4142
4187
  * Rename a file and update its record in the database
4143
4188
  */
4144
- async renameFile(path3, newName, options) {
4145
- const result = await super.renameFile(path3, newName, options);
4189
+ async renameFile(path4, newName, options) {
4190
+ const result = await super.renameFile(path4, newName, options);
4146
4191
  if (result.success && this.isTrackingEnabled()) {
4147
4192
  this.metadataService.recordRename(
4148
- path3,
4193
+ path4,
4149
4194
  newName,
4150
- this.getStorageType()
4195
+ this.getStorageType(),
4196
+ options?.actor_id
4151
4197
  ).catch(() => {
4152
4198
  });
4153
4199
  }
@@ -4156,13 +4202,14 @@ var TrackedFileManager = class extends FileManager {
4156
4202
  /**
4157
4203
  * Rename a folder and update its record in the database
4158
4204
  */
4159
- async renameFolder(path3, newName, options) {
4160
- const result = await super.renameFolder(path3, newName, options);
4205
+ async renameFolder(path4, newName, options) {
4206
+ const result = await super.renameFolder(path4, newName, options);
4161
4207
  if (result.success && this.isTrackingEnabled()) {
4162
4208
  this.metadataService.recordRename(
4163
- path3,
4209
+ path4,
4164
4210
  newName,
4165
- this.getStorageType()
4211
+ this.getStorageType(),
4212
+ options?.actor_id
4166
4213
  ).catch(() => {
4167
4214
  });
4168
4215
  }
@@ -4172,15 +4219,15 @@ var TrackedFileManager = class extends FileManager {
4172
4219
  /**
4173
4220
  * Write a file with string content and track it
4174
4221
  */
4175
- async writeFile(path3, content, options) {
4222
+ async writeFile(path4, content, options) {
4176
4223
  const buffer = Buffer.from(content, "utf-8");
4177
- return this.uploadFile(buffer, path3, options);
4224
+ return this.uploadFile(buffer, path4, options);
4178
4225
  }
4179
4226
  /**
4180
4227
  * Read a file and optionally track access
4181
4228
  */
4182
- async readFile(path3) {
4183
- return super.readFile(path3);
4229
+ async readFile(path4) {
4230
+ return super.readFile(path4);
4184
4231
  }
4185
4232
  /**
4186
4233
  * Copy a file and track the new file
@@ -4225,11 +4272,11 @@ var TrackedFileManager = class extends FileManager {
4225
4272
  * }
4226
4273
  * ```
4227
4274
  */
4228
- async hasFileChanged(path3) {
4275
+ async hasFileChanged(path4) {
4229
4276
  if (!this.isTrackingEnabled()) {
4230
4277
  return null;
4231
4278
  }
4232
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4279
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4233
4280
  if (!record) {
4234
4281
  return null;
4235
4282
  }
@@ -4237,7 +4284,7 @@ var TrackedFileManager = class extends FileManager {
4237
4284
  if (!storedHash) {
4238
4285
  return true;
4239
4286
  }
4240
- const downloadResult = await super.downloadFile(path3);
4287
+ const downloadResult = await super.downloadFile(path4);
4241
4288
  if (!downloadResult.success || !downloadResult.data) {
4242
4289
  return null;
4243
4290
  }
@@ -4257,11 +4304,11 @@ var TrackedFileManager = class extends FileManager {
4257
4304
  * @param path - Virtual path to the file
4258
4305
  * @returns Stored hash or null if not found/not tracked
4259
4306
  */
4260
- async getStoredHash(path3) {
4307
+ async getStoredHash(path4) {
4261
4308
  if (!this.isTrackingEnabled()) {
4262
4309
  return null;
4263
4310
  }
4264
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4311
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4265
4312
  return record?.file_hash || null;
4266
4313
  }
4267
4314
  /**
@@ -4270,11 +4317,11 @@ var TrackedFileManager = class extends FileManager {
4270
4317
  * @param path - Virtual path to the file
4271
4318
  * @returns Stored size in bytes or null if not found/not tracked
4272
4319
  */
4273
- async getStoredSize(path3) {
4320
+ async getStoredSize(path4) {
4274
4321
  if (!this.isTrackingEnabled()) {
4275
4322
  return null;
4276
4323
  }
4277
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4324
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4278
4325
  return record?.file_size ?? null;
4279
4326
  }
4280
4327
  // ============ Reference Tracking Methods (V2) ============
@@ -4308,10 +4355,67 @@ var TrackedFileManager = class extends FileManager {
4308
4355
  }
4309
4356
  /**
4310
4357
  * Soft-delete a file (marks as soft_deleted, does not remove physical file)
4358
+ * Also decrements quota usage if quotaService is configured.
4311
4359
  */
4312
- async softDeleteFile(fileId) {
4360
+ async softDeleteFile(fileId, opts) {
4313
4361
  if (!this.isTrackingEnabled()) return false;
4314
- return this.metadataService.softDelete(fileId);
4362
+ let fileSizeForQuota;
4363
+ let scopeIdForQuota;
4364
+ if (this.quotaService) {
4365
+ const record = await this.metadataService.findById(fileId);
4366
+ if (record) {
4367
+ fileSizeForQuota = record.file_size ?? void 0;
4368
+ scopeIdForQuota = record.scope_id;
4369
+ }
4370
+ }
4371
+ const ok = await this.metadataService.softDelete(fileId);
4372
+ if (ok) {
4373
+ if (opts?.actor_id) {
4374
+ await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
4375
+ }
4376
+ if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
4377
+ await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
4378
+ }
4379
+ }
4380
+ return ok;
4381
+ }
4382
+ // ============ Quota Pass-through Methods ============
4383
+ /**
4384
+ * Get quota status for a scope.
4385
+ * Returns null if no quota is configured (fail-open).
4386
+ */
4387
+ async getQuota(scopeId) {
4388
+ if (!this.quotaService) return null;
4389
+ return this.quotaService.getQuota(scopeId);
4390
+ }
4391
+ /**
4392
+ * Set or update the byte limit for a scope.
4393
+ * Creates a quota row if one does not exist.
4394
+ */
4395
+ async setQuotaLimit(scopeId, bytes) {
4396
+ if (!this.quotaService) return null;
4397
+ return this.quotaService.setQuotaLimit(scopeId, bytes);
4398
+ }
4399
+ /**
4400
+ * Recompute and return the quota status for a scope.
4401
+ */
4402
+ async recomputeQuota(scopeId) {
4403
+ if (!this.quotaService) return null;
4404
+ return this.quotaService.recomputeQuota(scopeId);
4405
+ }
4406
+ /**
4407
+ * Increment usage for a scope (admin override — does not throw on exceeded quota).
4408
+ */
4409
+ async incrementQuotaUsage(scopeId, deltaBytes) {
4410
+ if (!this.quotaService) return;
4411
+ return this.quotaService.incrementUsage(scopeId, deltaBytes);
4412
+ }
4413
+ /**
4414
+ * Decrement usage for a scope (e.g. after manual cleanup).
4415
+ */
4416
+ async decrementQuotaUsage(scopeId, deltaBytes) {
4417
+ if (!this.quotaService) return;
4418
+ return this.quotaService.decrementUsage(scopeId, deltaBytes);
4315
4419
  }
4316
4420
  /**
4317
4421
  * Find orphaned files (files with zero references)
@@ -4403,6 +4507,139 @@ var TrackedFileManager = class extends FileManager {
4403
4507
  }
4404
4508
  };
4405
4509
  }
4510
+ /**
4511
+ * Import a file from a URL into virtual storage.
4512
+ *
4513
+ * Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
4514
+ * Streams the response to a temp file, counting bytes live.
4515
+ * On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
4516
+ * On success: uploads to virtualPath, sets source_url in DB record.
4517
+ *
4518
+ * @param url - URL to fetch
4519
+ * @param virtualPath - Destination virtual path in storage
4520
+ * @param opts.referrer - Optional Referer header to send
4521
+ * @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
4522
+ * @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
4523
+ */
4524
+ async importFromUrl(url, virtualPath, opts) {
4525
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
4526
+ const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
4527
+ let hazoSecureMod = null;
4528
+ try {
4529
+ hazoSecureMod = await import("hazo_secure/fetch");
4530
+ } catch {
4531
+ return {
4532
+ success: false,
4533
+ error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
4534
+ };
4535
+ }
4536
+ const safeFetch = hazoSecureMod.safeFetch;
4537
+ const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
4538
+ const policy = {
4539
+ blockPrivateIps: true,
4540
+ allowedProtocols: ["https:", "http:"]
4541
+ };
4542
+ if (this.ssrfAllowlist.length > 0) {
4543
+ policy.allowedHosts = this.ssrfAllowlist;
4544
+ }
4545
+ const headers = {};
4546
+ if (opts?.referrer) {
4547
+ headers["Referer"] = opts.referrer;
4548
+ }
4549
+ const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
4550
+ let tmpWriteStream = null;
4551
+ try {
4552
+ const controller = new AbortController();
4553
+ let response;
4554
+ try {
4555
+ response = await safeFetch(url, {
4556
+ policy,
4557
+ headers,
4558
+ signal: controller.signal
4559
+ });
4560
+ } catch (err) {
4561
+ if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
4562
+ const ssrfErr = err;
4563
+ throw new SSRFError(url, ssrfErr.message);
4564
+ }
4565
+ throw err;
4566
+ }
4567
+ if (!response) {
4568
+ return { success: false, error: `No response from ${url}` };
4569
+ }
4570
+ if (!response.ok) {
4571
+ return { success: false, error: `HTTP ${response.status} from ${url}` };
4572
+ }
4573
+ if (!response.body) {
4574
+ return { success: false, error: `No response body from ${url}` };
4575
+ }
4576
+ tmpWriteStream = fs3.createWriteStream(tmpFile);
4577
+ let totalBytes = 0;
4578
+ let capExceeded = false;
4579
+ const reader = response.body.getReader();
4580
+ try {
4581
+ while (true) {
4582
+ const { done, value } = await reader.read();
4583
+ if (done) break;
4584
+ totalBytes += value.length;
4585
+ if (totalBytes > maxBytes) {
4586
+ capExceeded = true;
4587
+ controller.abort();
4588
+ reader.cancel().catch(() => {
4589
+ });
4590
+ break;
4591
+ }
4592
+ await new Promise((resolve2, reject) => {
4593
+ tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
4594
+ });
4595
+ }
4596
+ } finally {
4597
+ await new Promise((resolve2) => tmpWriteStream.end(resolve2));
4598
+ }
4599
+ if (capExceeded) {
4600
+ try {
4601
+ fs3.unlinkSync(tmpFile);
4602
+ } catch {
4603
+ }
4604
+ throw new ImportSizeCapError(url, maxBytes);
4605
+ }
4606
+ if (this.quotaService && opts?.scope_id && totalBytes > 0) {
4607
+ await this.quotaService.checkQuota(opts.scope_id, totalBytes);
4608
+ }
4609
+ const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
4610
+ actor_id: opts?.actor_id,
4611
+ scope_id: opts?.scope_id,
4612
+ awaitRecording: true
4613
+ });
4614
+ try {
4615
+ fs3.unlinkSync(tmpFile);
4616
+ } catch {
4617
+ }
4618
+ if (!uploadResult.success) {
4619
+ return { success: false, error: uploadResult.error };
4620
+ }
4621
+ if (this.isTrackingEnabled()) {
4622
+ const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
4623
+ if (record) {
4624
+ await this.metadataService.updateFields(record.id, { source_url: url });
4625
+ }
4626
+ }
4627
+ return {
4628
+ success: true,
4629
+ data: { virtualPath, size: totalBytes, sourceUrl: url }
4630
+ };
4631
+ } catch (err) {
4632
+ try {
4633
+ fs3.unlinkSync(tmpFile);
4634
+ } catch {
4635
+ }
4636
+ if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
4637
+ throw err;
4638
+ }
4639
+ const msg = err instanceof Error ? err.message : String(err);
4640
+ return { success: false, error: `importFromUrl failed: ${msg}` };
4641
+ }
4642
+ }
4406
4643
  };
4407
4644
  function createTrackedFileManager(options) {
4408
4645
  return new TrackedFileManager(options);