hazo_files 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -165,63 +165,63 @@ var HazoFilesError = class extends Error {
165
165
  }
166
166
  };
167
167
  var FileNotFoundError = class extends HazoFilesError {
168
- constructor(path3) {
169
- super(`File not found: ${path3}`, "FILE_NOT_FOUND", { path: path3 });
168
+ constructor(path4) {
169
+ super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
170
170
  this.name = "FileNotFoundError";
171
171
  }
172
172
  };
173
173
  var DirectoryNotFoundError = class extends HazoFilesError {
174
- constructor(path3) {
175
- super(`Directory not found: ${path3}`, "DIRECTORY_NOT_FOUND", { path: path3 });
174
+ constructor(path4) {
175
+ super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
176
176
  this.name = "DirectoryNotFoundError";
177
177
  }
178
178
  };
179
179
  var FileExistsError = class extends HazoFilesError {
180
- constructor(path3) {
181
- super(`File already exists: ${path3}`, "FILE_EXISTS", { path: path3 });
180
+ constructor(path4) {
181
+ super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
182
182
  this.name = "FileExistsError";
183
183
  }
184
184
  };
185
185
  var DirectoryExistsError = class extends HazoFilesError {
186
- constructor(path3) {
187
- super(`Directory already exists: ${path3}`, "DIRECTORY_EXISTS", { path: path3 });
186
+ constructor(path4) {
187
+ super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
188
188
  this.name = "DirectoryExistsError";
189
189
  }
190
190
  };
191
191
  var DirectoryNotEmptyError = class extends HazoFilesError {
192
- constructor(path3) {
193
- super(`Directory is not empty: ${path3}`, "DIRECTORY_NOT_EMPTY", { path: path3 });
192
+ constructor(path4) {
193
+ super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
194
194
  this.name = "DirectoryNotEmptyError";
195
195
  }
196
196
  };
197
197
  var PermissionDeniedError = class extends HazoFilesError {
198
- constructor(path3, operation) {
199
- super(`Permission denied for ${operation} on: ${path3}`, "PERMISSION_DENIED", { path: path3, operation });
198
+ constructor(path4, operation) {
199
+ super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
200
200
  this.name = "PermissionDeniedError";
201
201
  }
202
202
  };
203
203
  var InvalidPathError = class extends HazoFilesError {
204
- constructor(path3, reason) {
205
- super(`Invalid path "${path3}": ${reason}`, "INVALID_PATH", { path: path3, reason });
204
+ constructor(path4, reason) {
205
+ super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
206
206
  this.name = "InvalidPathError";
207
207
  }
208
208
  };
209
209
  var FileTooLargeError = class extends HazoFilesError {
210
- constructor(path3, size, maxSize) {
210
+ constructor(path4, size, maxSize) {
211
211
  super(
212
- `File "${path3}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
212
+ `File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
213
213
  "FILE_TOO_LARGE",
214
- { path: path3, size, maxSize }
214
+ { path: path4, size, maxSize }
215
215
  );
216
216
  this.name = "FileTooLargeError";
217
217
  }
218
218
  };
219
219
  var InvalidExtensionError = class extends HazoFilesError {
220
- constructor(path3, extension, allowedExtensions) {
220
+ constructor(path4, extension, allowedExtensions) {
221
221
  super(
222
222
  `File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
223
223
  "INVALID_EXTENSION",
224
- { path: path3, extension, allowedExtensions }
224
+ { path: path4, extension, allowedExtensions }
225
225
  );
226
226
  this.name = "InvalidExtensionError";
227
227
  }
@@ -244,6 +244,22 @@ var OperationError = class extends HazoFilesError {
244
244
  this.name = "OperationError";
245
245
  }
246
246
  };
247
+ var SSRFError = class extends HazoFilesError {
248
+ constructor(url, reason) {
249
+ super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
250
+ this.name = "SSRFError";
251
+ }
252
+ };
253
+ var ImportSizeCapError = class extends HazoFilesError {
254
+ constructor(url, capBytes) {
255
+ super(
256
+ `Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
257
+ "IMPORT_SIZE_CAP",
258
+ { url, capBytes }
259
+ );
260
+ this.name = "ImportSizeCapError";
261
+ }
262
+ };
247
263
 
248
264
  // src/common/utils.ts
249
265
  function successResult(data) {
@@ -487,10 +503,10 @@ var BaseStorageModule = class {
487
503
  * Get folder tree structure.
488
504
  * Default implementation that can be overridden by subclasses for optimization.
489
505
  */
490
- async getFolderTree(path3 = "/", depth = 3) {
506
+ async getFolderTree(path4 = "/", depth = 3) {
491
507
  this.ensureInitialized();
492
508
  try {
493
- const result = await this.buildTree(path3, depth, 0);
509
+ const result = await this.buildTree(path4, depth, 0);
494
510
  return successResult(result);
495
511
  } catch (error) {
496
512
  return errorResult(`Failed to get folder tree: ${error.message}`);
@@ -499,11 +515,11 @@ var BaseStorageModule = class {
499
515
  /**
500
516
  * Recursively build folder tree
501
517
  */
502
- async buildTree(path3, maxDepth, currentDepth) {
518
+ async buildTree(path4, maxDepth, currentDepth) {
503
519
  if (currentDepth >= maxDepth) {
504
520
  return [];
505
521
  }
506
- const listResult = await this.listDirectory(path3, { recursive: false });
522
+ const listResult = await this.listDirectory(path4, { recursive: false });
507
523
  if (!listResult.success || !listResult.data) {
508
524
  return [];
509
525
  }
@@ -1322,12 +1338,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
1322
1338
  */
1323
1339
  driveFileToItem(file, virtualPath) {
1324
1340
  const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
1325
- const path3 = virtualPath || "";
1341
+ const path4 = virtualPath || "";
1326
1342
  if (isFolder2) {
1327
1343
  return createFolderItem({
1328
1344
  id: file.id,
1329
1345
  name: file.name,
1330
- path: path3,
1346
+ path: path4,
1331
1347
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
1332
1348
  modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
1333
1349
  metadata: {
@@ -1339,7 +1355,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
1339
1355
  return createFileItem({
1340
1356
  id: file.id,
1341
1357
  name: file.name,
1342
- path: path3,
1358
+ path: path4,
1343
1359
  size: parseInt(file.size || "0", 10),
1344
1360
  mimeType: file.mimeType || "application/octet-stream",
1345
1361
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
@@ -1438,10 +1454,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1438
1454
  }
1439
1455
  let media;
1440
1456
  if (typeof source === "string") {
1441
- const fs3 = await import("fs");
1457
+ const fs4 = await import("fs");
1442
1458
  media = {
1443
1459
  mimeType: "application/octet-stream",
1444
- body: fs3.createReadStream(source)
1460
+ body: fs4.createReadStream(source)
1445
1461
  };
1446
1462
  } else if (Buffer.isBuffer(source)) {
1447
1463
  media = {
@@ -1490,10 +1506,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1490
1506
  options.onProgress(100, buffer.length, buffer.length);
1491
1507
  }
1492
1508
  if (localPath) {
1493
- const fs3 = await import("fs");
1494
- const path3 = await import("path");
1495
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
1496
- await fs3.promises.writeFile(localPath, buffer);
1509
+ const fs4 = await import("fs");
1510
+ const path4 = await import("path");
1511
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
1512
+ await fs4.promises.writeFile(localPath, buffer);
1497
1513
  return this.successResult(localPath);
1498
1514
  }
1499
1515
  return this.successResult(buffer);
@@ -1681,10 +1697,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1681
1697
  return false;
1682
1698
  }
1683
1699
  }
1684
- async getFolderTree(path3 = "/", depth = 3) {
1700
+ async getFolderTree(path4 = "/", depth = 3) {
1685
1701
  try {
1686
1702
  await this.ensureAuthenticated();
1687
- return super.getFolderTree(path3, depth);
1703
+ return super.getFolderTree(path4, depth);
1688
1704
  } catch (error) {
1689
1705
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
1690
1706
  }
@@ -1975,12 +1991,12 @@ var DropboxModule = class extends BaseStorageModule {
1975
1991
  */
1976
1992
  metadataToItem(entry, virtualPath) {
1977
1993
  const isFolder2 = entry[".tag"] === "folder";
1978
- const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
1994
+ const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
1979
1995
  if (isFolder2) {
1980
1996
  return createFolderItem({
1981
1997
  id: entry.id,
1982
1998
  name: entry.name,
1983
- path: path3,
1999
+ path: path4,
1984
2000
  createdAt: /* @__PURE__ */ new Date(),
1985
2001
  modifiedAt: /* @__PURE__ */ new Date(),
1986
2002
  metadata: {
@@ -1993,7 +2009,7 @@ var DropboxModule = class extends BaseStorageModule {
1993
2009
  return createFileItem({
1994
2010
  id: fileEntry.id,
1995
2011
  name: fileEntry.name,
1996
- path: path3,
2012
+ path: path4,
1997
2013
  size: fileEntry.size,
1998
2014
  mimeType: getMimeType(fileEntry.name),
1999
2015
  createdAt: new Date(fileEntry.client_modified),
@@ -2069,8 +2085,8 @@ var DropboxModule = class extends BaseStorageModule {
2069
2085
  const dbxPath = this.toDropboxPath(remotePath);
2070
2086
  let contents;
2071
2087
  if (typeof source === "string") {
2072
- const fs3 = await import("fs");
2073
- contents = await fs3.promises.readFile(source);
2088
+ const fs4 = await import("fs");
2089
+ contents = await fs4.promises.readFile(source);
2074
2090
  } else if (Buffer.isBuffer(source)) {
2075
2091
  contents = source;
2076
2092
  } else {
@@ -2139,10 +2155,10 @@ var DropboxModule = class extends BaseStorageModule {
2139
2155
  options.onProgress(100, buffer.length, buffer.length);
2140
2156
  }
2141
2157
  if (localPath) {
2142
- const fs3 = await import("fs");
2143
- const path3 = await import("path");
2144
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2145
- await fs3.promises.writeFile(localPath, buffer);
2158
+ const fs4 = await import("fs");
2159
+ const path4 = await import("path");
2160
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
2161
+ await fs4.promises.writeFile(localPath, buffer);
2146
2162
  return this.successResult(localPath);
2147
2163
  }
2148
2164
  return this.successResult(buffer);
@@ -2311,10 +2327,10 @@ var DropboxModule = class extends BaseStorageModule {
2311
2327
  return false;
2312
2328
  }
2313
2329
  }
2314
- async getFolderTree(path3 = "/", depth = 3) {
2330
+ async getFolderTree(path4 = "/", depth = 3) {
2315
2331
  try {
2316
2332
  await this.ensureAuthenticated();
2317
- return super.getFolderTree(path3, depth);
2333
+ return super.getFolderTree(path4, depth);
2318
2334
  } catch (error) {
2319
2335
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
2320
2336
  }
@@ -2433,13 +2449,13 @@ var FileManager = class {
2433
2449
  /**
2434
2450
  * Create a directory at the specified path
2435
2451
  */
2436
- async createDirectory(path3) {
2452
+ async createDirectory(path4) {
2437
2453
  this.ensureInitialized();
2438
2454
  const start = Date.now();
2439
- const result = await this.module.createDirectory(path3);
2455
+ const result = await this.module.createDirectory(path4);
2440
2456
  this.logFileOp(result.success ? "info" : "error", {
2441
2457
  operation: "upload",
2442
- file_path: path3,
2458
+ file_path: path4,
2443
2459
  mime_type: "folder",
2444
2460
  storage: this.config?.provider,
2445
2461
  duration_ms: Date.now() - start,
@@ -2454,13 +2470,13 @@ var FileManager = class {
2454
2470
  * @param path - Directory path
2455
2471
  * @param recursive - If true, remove directory and all contents
2456
2472
  */
2457
- async removeDirectory(path3, recursive = false) {
2473
+ async removeDirectory(path4, recursive = false) {
2458
2474
  this.ensureInitialized();
2459
2475
  const start = Date.now();
2460
- const result = await this.module.removeDirectory(path3, recursive);
2476
+ const result = await this.module.removeDirectory(path4, recursive);
2461
2477
  this.logFileOp(result.success ? "info" : "error", {
2462
2478
  operation: "delete",
2463
- file_path: path3,
2479
+ file_path: path4,
2464
2480
  mime_type: "folder",
2465
2481
  storage: this.config?.provider,
2466
2482
  duration_ms: Date.now() - start,
@@ -2539,13 +2555,13 @@ var FileManager = class {
2539
2555
  /**
2540
2556
  * Delete a file
2541
2557
  */
2542
- async deleteFile(path3) {
2558
+ async deleteFile(path4) {
2543
2559
  this.ensureInitialized();
2544
2560
  const start = Date.now();
2545
- const result = await this.module.deleteFile(path3);
2561
+ const result = await this.module.deleteFile(path4);
2546
2562
  this.logFileOp(result.success ? "info" : "error", {
2547
2563
  operation: "delete",
2548
- file_path: path3,
2564
+ file_path: path4,
2549
2565
  storage: this.config?.provider,
2550
2566
  duration_ms: Date.now() - start,
2551
2567
  success: result.success,
@@ -2559,14 +2575,14 @@ var FileManager = class {
2559
2575
  * @param newName - New filename (not full path)
2560
2576
  * @param options - Rename options
2561
2577
  */
2562
- async renameFile(path3, newName, options) {
2578
+ async renameFile(path4, newName, options) {
2563
2579
  this.ensureInitialized();
2564
2580
  const start = Date.now();
2565
- const result = await this.module.renameFile(path3, newName, options);
2581
+ const result = await this.module.renameFile(path4, newName, options);
2566
2582
  this.logFileOp(result.success ? "info" : "error", {
2567
2583
  operation: "move",
2568
2584
  file_name: result.data?.name,
2569
- file_path: path3,
2585
+ file_path: path4,
2570
2586
  storage: this.config?.provider,
2571
2587
  duration_ms: Date.now() - start,
2572
2588
  success: result.success,
@@ -2581,14 +2597,14 @@ var FileManager = class {
2581
2597
  * @param newName - New folder name (not full path)
2582
2598
  * @param options - Rename options
2583
2599
  */
2584
- async renameFolder(path3, newName, options) {
2600
+ async renameFolder(path4, newName, options) {
2585
2601
  this.ensureInitialized();
2586
2602
  const start = Date.now();
2587
- const result = await this.module.renameFolder(path3, newName, options);
2603
+ const result = await this.module.renameFolder(path4, newName, options);
2588
2604
  this.logFileOp(result.success ? "info" : "error", {
2589
2605
  operation: "move",
2590
2606
  file_name: result.data?.name,
2591
- file_path: path3,
2607
+ file_path: path4,
2592
2608
  storage: this.config?.provider,
2593
2609
  duration_ms: Date.now() - start,
2594
2610
  success: result.success,
@@ -2603,54 +2619,54 @@ var FileManager = class {
2603
2619
  * @param path - Directory path
2604
2620
  * @param options - List options
2605
2621
  */
2606
- async listDirectory(path3, options) {
2622
+ async listDirectory(path4, options) {
2607
2623
  this.ensureInitialized();
2608
- return this.module.listDirectory(path3, options);
2624
+ return this.module.listDirectory(path4, options);
2609
2625
  }
2610
2626
  /**
2611
2627
  * Get information about a file or folder
2612
2628
  */
2613
- async getItem(path3) {
2629
+ async getItem(path4) {
2614
2630
  this.ensureInitialized();
2615
- return this.module.getItem(path3);
2631
+ return this.module.getItem(path4);
2616
2632
  }
2617
2633
  /**
2618
2634
  * Check if a file or folder exists
2619
2635
  */
2620
- async exists(path3) {
2636
+ async exists(path4) {
2621
2637
  this.ensureInitialized();
2622
- return this.module.exists(path3);
2638
+ return this.module.exists(path4);
2623
2639
  }
2624
2640
  /**
2625
2641
  * Get folder tree structure
2626
2642
  * @param path - Starting path (default: root)
2627
2643
  * @param depth - Maximum depth to traverse
2628
2644
  */
2629
- async getFolderTree(path3 = "/", depth = 3) {
2645
+ async getFolderTree(path4 = "/", depth = 3) {
2630
2646
  this.ensureInitialized();
2631
- return this.module.getFolderTree(path3, depth);
2647
+ return this.module.getFolderTree(path4, depth);
2632
2648
  }
2633
2649
  // ============ Convenience Methods ============
2634
2650
  /**
2635
2651
  * Create a file with string content
2636
2652
  */
2637
- async writeFile(path3, content, options) {
2653
+ async writeFile(path4, content, options) {
2638
2654
  const buffer = Buffer.from(content, "utf-8");
2639
- return this.uploadFile(buffer, path3, options);
2655
+ return this.uploadFile(buffer, path4, options);
2640
2656
  }
2641
2657
  /**
2642
2658
  * Read a file as string
2643
2659
  */
2644
- async readFile(path3) {
2645
- const result = await this.downloadFile(path3);
2660
+ async readFile(path4) {
2661
+ const result = await this.downloadFile(path4);
2646
2662
  if (!result.success) {
2647
2663
  return { success: false, error: result.error };
2648
2664
  }
2649
2665
  if (Buffer.isBuffer(result.data)) {
2650
2666
  return { success: true, data: result.data.toString("utf-8") };
2651
2667
  }
2652
- const fs3 = await import("fs");
2653
- const content = await fs3.promises.readFile(result.data, "utf-8");
2668
+ const fs4 = await import("fs");
2669
+ const content = await fs4.promises.readFile(result.data, "utf-8");
2654
2670
  return { success: true, data: content };
2655
2671
  }
2656
2672
  /**
@@ -2661,22 +2677,22 @@ var FileManager = class {
2661
2677
  if (!downloadResult.success) {
2662
2678
  return { success: false, error: downloadResult.error };
2663
2679
  }
2664
- const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs3) => fs3.promises.readFile(downloadResult.data));
2680
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
2665
2681
  return this.uploadFile(buffer, destinationPath, options);
2666
2682
  }
2667
2683
  /**
2668
2684
  * Ensure a directory exists (creates if needed)
2669
2685
  */
2670
- async ensureDirectory(path3) {
2671
- const exists = await this.exists(path3);
2686
+ async ensureDirectory(path4) {
2687
+ const exists = await this.exists(path4);
2672
2688
  if (exists) {
2673
- const item = await this.getItem(path3);
2689
+ const item = await this.getItem(path4);
2674
2690
  if (item.success && item.data?.isDirectory) {
2675
2691
  return { success: true, data: item.data };
2676
2692
  }
2677
2693
  return { success: false, error: "Path exists but is not a directory" };
2678
2694
  }
2679
- return this.createDirectory(path3);
2695
+ return this.createDirectory(path4);
2680
2696
  }
2681
2697
  };
2682
2698
  function createFileManager(options) {
@@ -2688,6 +2704,11 @@ async function createInitializedFileManager(options) {
2688
2704
  return manager;
2689
2705
  }
2690
2706
 
2707
+ // src/services/tracked-file-manager.ts
2708
+ import * as fs3 from "fs";
2709
+ import * as os from "os";
2710
+ import * as path3 from "path";
2711
+
2691
2712
  // src/common/file-data-utils.ts
2692
2713
  function generateExtractionId() {
2693
2714
  return `ext_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -2989,6 +3010,8 @@ var FileMetadataService = class {
2989
3010
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
2990
3011
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
2991
3012
  if (input.content_tag !== void 0) record.content_tag = input.content_tag;
3013
+ if (input.changed_by !== void 0) record.changed_by = input.changed_by;
3014
+ if (input.source_url !== void 0) record.source_url = input.source_url;
2992
3015
  const results = await this.crud.insert(record);
2993
3016
  const duration_ms = Date.now() - start;
2994
3017
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
@@ -3021,32 +3044,32 @@ var FileMetadataService = class {
3021
3044
  /**
3022
3045
  * Record a directory creation
3023
3046
  */
3024
- async recordDirectoryCreation(path3, storageType, metadata) {
3047
+ async recordDirectoryCreation(path4, storageType, metadata) {
3025
3048
  return this.recordUpload({
3026
- filename: getBaseName(path3),
3049
+ filename: getBaseName(path4),
3027
3050
  file_type: "folder",
3028
3051
  file_data: metadata,
3029
- file_path: path3,
3052
+ file_path: path4,
3030
3053
  storage_type: storageType
3031
3054
  });
3032
3055
  }
3033
3056
  /**
3034
3057
  * Record a file access (download)
3035
3058
  */
3036
- async recordAccess(path3, storageType) {
3059
+ async recordAccess(path4, storageType) {
3037
3060
  const start = Date.now();
3038
3061
  try {
3039
- const existing = await this.findByPath(path3, storageType);
3062
+ const existing = await this.findByPath(path4, storageType);
3040
3063
  if (existing) {
3041
3064
  await this.crud.updateById(existing.id, {
3042
3065
  changed_at: this.now()
3043
3066
  });
3044
3067
  const duration_ms = Date.now() - start;
3045
- this.logger?.debug?.("Recorded file access", { path: path3 });
3068
+ this.logger?.debug?.("Recorded file access", { path: path4 });
3046
3069
  this.logger?.info?.("file_operation", {
3047
3070
  operation: "download",
3048
3071
  file_name: existing.filename,
3049
- file_path: path3,
3072
+ file_path: path4,
3050
3073
  mime_type: existing.file_type,
3051
3074
  size_bytes: existing.file_size,
3052
3075
  storage: storageType,
@@ -3060,7 +3083,7 @@ var FileMetadataService = class {
3060
3083
  const duration_ms = Date.now() - start;
3061
3084
  this.logger?.error?.("file_operation", {
3062
3085
  operation: "download",
3063
- file_path: path3,
3086
+ file_path: path4,
3064
3087
  storage: storageType,
3065
3088
  duration_ms,
3066
3089
  success: false,
@@ -3072,19 +3095,20 @@ var FileMetadataService = class {
3072
3095
  }
3073
3096
  /**
3074
3097
  * Record a file deletion
3098
+ * changedBy is accepted for API consistency but not written (record is deleted)
3075
3099
  */
3076
- async recordDelete(path3, storageType) {
3100
+ async recordDelete(path4, storageType, _changedBy) {
3077
3101
  const start = Date.now();
3078
3102
  try {
3079
- const existing = await this.findByPath(path3, storageType);
3103
+ const existing = await this.findByPath(path4, storageType);
3080
3104
  if (existing) {
3081
3105
  await this.crud.deleteById(existing.id);
3082
3106
  const duration_ms = Date.now() - start;
3083
- this.logger?.debug?.("Recorded file deletion", { path: path3 });
3107
+ this.logger?.debug?.("Recorded file deletion", { path: path4 });
3084
3108
  this.logger?.info?.("file_operation", {
3085
3109
  operation: "delete",
3086
3110
  file_name: existing.filename,
3087
- file_path: path3,
3111
+ file_path: path4,
3088
3112
  mime_type: existing.file_type,
3089
3113
  size_bytes: existing.file_size,
3090
3114
  storage: storageType,
@@ -3098,7 +3122,7 @@ var FileMetadataService = class {
3098
3122
  const duration_ms = Date.now() - start;
3099
3123
  this.logger?.error?.("file_operation", {
3100
3124
  operation: "delete",
3101
- file_path: path3,
3125
+ file_path: path4,
3102
3126
  storage: storageType,
3103
3127
  duration_ms,
3104
3128
  success: false,
@@ -3111,23 +3135,23 @@ var FileMetadataService = class {
3111
3135
  /**
3112
3136
  * Record a directory deletion (recursive)
3113
3137
  */
3114
- async recordDirectoryDelete(path3, storageType, recursive) {
3138
+ async recordDirectoryDelete(path4, storageType, recursive) {
3115
3139
  try {
3116
3140
  if (recursive) {
3117
3141
  const records = await this.crud.findBy({ storage_type: storageType });
3118
3142
  const toDelete = records.filter(
3119
- (r) => r.file_path === path3 || r.file_path.startsWith(path3 + "/")
3143
+ (r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
3120
3144
  );
3121
3145
  for (const record of toDelete) {
3122
3146
  await this.crud.deleteById(record.id);
3123
3147
  }
3124
3148
  this.logger?.debug?.("Recorded recursive directory deletion", {
3125
- path: path3,
3149
+ path: path4,
3126
3150
  count: toDelete.length
3127
3151
  });
3128
3152
  return true;
3129
3153
  } else {
3130
- return this.recordDelete(path3, storageType);
3154
+ return this.recordDelete(path4, storageType);
3131
3155
  }
3132
3156
  } catch (error) {
3133
3157
  this.logError("recordDirectoryDelete", error);
@@ -3137,16 +3161,18 @@ var FileMetadataService = class {
3137
3161
  /**
3138
3162
  * Record a file or folder move
3139
3163
  */
3140
- async recordMove(sourcePath, destinationPath, storageType) {
3164
+ async recordMove(sourcePath, destinationPath, storageType, changedBy) {
3141
3165
  const start = Date.now();
3142
3166
  try {
3143
3167
  const existing = await this.findByPath(sourcePath, storageType);
3144
3168
  if (existing) {
3145
- await this.crud.updateById(existing.id, {
3169
+ const patch = {
3146
3170
  file_path: destinationPath,
3147
3171
  filename: getBaseName(destinationPath),
3148
3172
  changed_at: this.now()
3149
- });
3173
+ };
3174
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3175
+ await this.crud.updateById(existing.id, patch);
3150
3176
  const duration_ms = Date.now() - start;
3151
3177
  this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
3152
3178
  this.logger?.info?.("file_operation", {
@@ -3180,24 +3206,26 @@ var FileMetadataService = class {
3180
3206
  /**
3181
3207
  * Record a file or folder rename
3182
3208
  */
3183
- async recordRename(path3, newName, storageType) {
3209
+ async recordRename(path4, newName, storageType, changedBy) {
3184
3210
  const start = Date.now();
3185
3211
  try {
3186
- const existing = await this.findByPath(path3, storageType);
3212
+ const existing = await this.findByPath(path4, storageType);
3187
3213
  if (existing) {
3188
- const parentPath = getDirName(path3);
3214
+ const parentPath = getDirName(path4);
3189
3215
  const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
3190
- await this.crud.updateById(existing.id, {
3216
+ const patch = {
3191
3217
  filename: newName,
3192
3218
  file_path: newPath,
3193
3219
  changed_at: this.now()
3194
- });
3220
+ };
3221
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3222
+ await this.crud.updateById(existing.id, patch);
3195
3223
  const duration_ms = Date.now() - start;
3196
- this.logger?.debug?.("Recorded file rename", { path: path3, newName });
3224
+ this.logger?.debug?.("Recorded file rename", { path: path4, newName });
3197
3225
  this.logger?.info?.("file_operation", {
3198
3226
  operation: "move",
3199
3227
  file_name: existing.filename,
3200
- file_path: path3,
3228
+ file_path: path4,
3201
3229
  mime_type: existing.file_type,
3202
3230
  size_bytes: existing.file_size,
3203
3231
  storage: storageType,
@@ -3212,7 +3240,7 @@ var FileMetadataService = class {
3212
3240
  const duration_ms = Date.now() - start;
3213
3241
  this.logger?.error?.("file_operation", {
3214
3242
  operation: "move",
3215
- file_path: path3,
3243
+ file_path: path4,
3216
3244
  storage: storageType,
3217
3245
  duration_ms,
3218
3246
  success: false,
@@ -3226,10 +3254,10 @@ var FileMetadataService = class {
3226
3254
  /**
3227
3255
  * Find a record by path and storage type
3228
3256
  */
3229
- async findByPath(path3, storageType) {
3257
+ async findByPath(path4, storageType) {
3230
3258
  try {
3231
3259
  return await this.crud.findOneBy({
3232
- file_path: path3,
3260
+ file_path: path4,
3233
3261
  storage_type: storageType
3234
3262
  });
3235
3263
  } catch (error) {
@@ -3272,9 +3300,9 @@ var FileMetadataService = class {
3272
3300
  /**
3273
3301
  * Update custom metadata for a file
3274
3302
  */
3275
- async updateMetadata(path3, storageType, metadata) {
3303
+ async updateMetadata(path4, storageType, metadata) {
3276
3304
  try {
3277
- const existing = await this.findByPath(path3, storageType);
3305
+ const existing = await this.findByPath(path4, storageType);
3278
3306
  if (existing) {
3279
3307
  const currentData = JSON.parse(existing.file_data || "{}");
3280
3308
  const newData = { ...currentData, ...metadata };
@@ -3282,7 +3310,7 @@ var FileMetadataService = class {
3282
3310
  file_data: JSON.stringify(newData),
3283
3311
  changed_at: this.now()
3284
3312
  });
3285
- this.logger?.debug?.("Updated file metadata", { path: path3 });
3313
+ this.logger?.debug?.("Updated file metadata", { path: path4 });
3286
3314
  return true;
3287
3315
  }
3288
3316
  return false;
@@ -3298,9 +3326,9 @@ var FileMetadataService = class {
3298
3326
  * Get parsed file_data structure for a file
3299
3327
  * Automatically migrates old format to new extraction structure
3300
3328
  */
3301
- async getFileData(path3, storageType) {
3329
+ async getFileData(path4, storageType) {
3302
3330
  try {
3303
- const existing = await this.findByPath(path3, storageType);
3331
+ const existing = await this.findByPath(path4, storageType);
3304
3332
  if (!existing) {
3305
3333
  return null;
3306
3334
  }
@@ -3313,9 +3341,9 @@ var FileMetadataService = class {
3313
3341
  /**
3314
3342
  * Get merged extraction data for a file
3315
3343
  */
3316
- async getMergedData(path3, storageType) {
3344
+ async getMergedData(path4, storageType) {
3317
3345
  try {
3318
- const fileData = await this.getFileData(path3, storageType);
3346
+ const fileData = await this.getFileData(path4, storageType);
3319
3347
  if (!fileData) {
3320
3348
  return null;
3321
3349
  }
@@ -3328,12 +3356,12 @@ var FileMetadataService = class {
3328
3356
  /**
3329
3357
  * Add an extraction to a file's data
3330
3358
  */
3331
- async addExtraction(path3, storageType, data, options) {
3359
+ async addExtraction(path4, storageType, data, options) {
3332
3360
  const start = Date.now();
3333
3361
  try {
3334
- const existing = await this.findByPath(path3, storageType);
3362
+ const existing = await this.findByPath(path4, storageType);
3335
3363
  if (!existing) {
3336
- this.logger?.warn?.("Cannot add extraction: file not found", { path: path3 });
3364
+ this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
3337
3365
  return null;
3338
3366
  }
3339
3367
  const currentFileData = parseFileData(existing.file_data);
@@ -3348,11 +3376,11 @@ var FileMetadataService = class {
3348
3376
  });
3349
3377
  const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
3350
3378
  const duration_ms = Date.now() - start;
3351
- this.logger?.debug?.("Added extraction", { path: path3, extractionId: newExtraction.id });
3379
+ this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
3352
3380
  this.logger?.info?.("file_operation", {
3353
3381
  operation: "extract",
3354
3382
  file_name: existing.filename,
3355
- file_path: path3,
3383
+ file_path: path4,
3356
3384
  mime_type: existing.file_type,
3357
3385
  storage: storageType,
3358
3386
  duration_ms,
@@ -3364,7 +3392,7 @@ var FileMetadataService = class {
3364
3392
  const duration_ms = Date.now() - start;
3365
3393
  this.logger?.error?.("file_operation", {
3366
3394
  operation: "extract",
3367
- file_path: path3,
3395
+ file_path: path4,
3368
3396
  storage: storageType,
3369
3397
  duration_ms,
3370
3398
  success: false,
@@ -3377,9 +3405,9 @@ var FileMetadataService = class {
3377
3405
  /**
3378
3406
  * Remove an extraction by ID
3379
3407
  */
3380
- async removeExtractionById(path3, storageType, id, options) {
3408
+ async removeExtractionById(path4, storageType, id, options) {
3381
3409
  try {
3382
- const existing = await this.findByPath(path3, storageType);
3410
+ const existing = await this.findByPath(path4, storageType);
3383
3411
  if (!existing) {
3384
3412
  return false;
3385
3413
  }
@@ -3393,7 +3421,7 @@ var FileMetadataService = class {
3393
3421
  file_data: stringifyFileData(result.data),
3394
3422
  changed_at: this.now()
3395
3423
  });
3396
- this.logger?.debug?.("Removed extraction by ID", { path: path3, extractionId: id });
3424
+ this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
3397
3425
  return true;
3398
3426
  } catch (error) {
3399
3427
  this.logError("removeExtractionById", error);
@@ -3403,9 +3431,9 @@ var FileMetadataService = class {
3403
3431
  /**
3404
3432
  * Remove an extraction by index
3405
3433
  */
3406
- async removeExtractionByIndex(path3, storageType, index, options) {
3434
+ async removeExtractionByIndex(path4, storageType, index, options) {
3407
3435
  try {
3408
- const existing = await this.findByPath(path3, storageType);
3436
+ const existing = await this.findByPath(path4, storageType);
3409
3437
  if (!existing) {
3410
3438
  return false;
3411
3439
  }
@@ -3419,7 +3447,7 @@ var FileMetadataService = class {
3419
3447
  file_data: stringifyFileData(result.data),
3420
3448
  changed_at: this.now()
3421
3449
  });
3422
- this.logger?.debug?.("Removed extraction by index", { path: path3, index });
3450
+ this.logger?.debug?.("Removed extraction by index", { path: path4, index });
3423
3451
  return true;
3424
3452
  } catch (error) {
3425
3453
  this.logError("removeExtractionByIndex", error);
@@ -3429,9 +3457,9 @@ var FileMetadataService = class {
3429
3457
  /**
3430
3458
  * Get all extractions for a file
3431
3459
  */
3432
- async getExtractions(path3, storageType) {
3460
+ async getExtractions(path4, storageType) {
3433
3461
  try {
3434
- const fileData = await this.getFileData(path3, storageType);
3462
+ const fileData = await this.getFileData(path4, storageType);
3435
3463
  if (!fileData) {
3436
3464
  return null;
3437
3465
  }
@@ -3444,9 +3472,9 @@ var FileMetadataService = class {
3444
3472
  /**
3445
3473
  * Get a specific extraction by ID
3446
3474
  */
3447
- async getExtractionById(path3, storageType, id) {
3475
+ async getExtractionById(path4, storageType, id) {
3448
3476
  try {
3449
- const fileData = await this.getFileData(path3, storageType);
3477
+ const fileData = await this.getFileData(path4, storageType);
3450
3478
  if (!fileData) {
3451
3479
  return null;
3452
3480
  }
@@ -3459,9 +3487,9 @@ var FileMetadataService = class {
3459
3487
  /**
3460
3488
  * Clear all extractions for a file
3461
3489
  */
3462
- async clearExtractions(path3, storageType) {
3490
+ async clearExtractions(path4, storageType) {
3463
3491
  try {
3464
- const existing = await this.findByPath(path3, storageType);
3492
+ const existing = await this.findByPath(path4, storageType);
3465
3493
  if (!existing) {
3466
3494
  return false;
3467
3495
  }
@@ -3469,7 +3497,7 @@ var FileMetadataService = class {
3469
3497
  file_data: stringifyFileData(createEmptyFileDataStructure()),
3470
3498
  changed_at: this.now()
3471
3499
  });
3472
- this.logger?.debug?.("Cleared all extractions", { path: path3 });
3500
+ this.logger?.debug?.("Cleared all extractions", { path: path4 });
3473
3501
  return true;
3474
3502
  } catch (error) {
3475
3503
  this.logError("clearExtractions", error);
@@ -3679,7 +3707,7 @@ var FileMetadataService = class {
3679
3707
  return this.updateStatus(fileId, "soft_deleted");
3680
3708
  }
3681
3709
  /**
3682
- * Update specific V2 fields on a record
3710
+ * Update specific V2/V4 fields on a record
3683
3711
  */
3684
3712
  async updateFields(fileId, fields) {
3685
3713
  try {
@@ -3810,6 +3838,7 @@ var TrackedFileManager = class extends FileManager {
3810
3838
  constructor(options = {}) {
3811
3839
  super(options);
3812
3840
  this.metadataService = null;
3841
+ this.quotaService = null;
3813
3842
  this.trackingConfig = {
3814
3843
  enabled: options.tracking?.enabled ?? false,
3815
3844
  tableName: options.tracking?.tableName ?? "hazo_files",
@@ -3823,6 +3852,8 @@ var TrackedFileManager = class extends FileManager {
3823
3852
  logErrors: this.trackingConfig.logErrors
3824
3853
  });
3825
3854
  }
3855
+ this.quotaService = options.quotaService ?? null;
3856
+ this.ssrfAllowlist = options.ssrfAllowlist ?? [];
3826
3857
  }
3827
3858
  /**
3828
3859
  * Check if tracking is enabled and service is available
@@ -3840,11 +3871,11 @@ var TrackedFileManager = class extends FileManager {
3840
3871
  /**
3841
3872
  * Create a directory and record it in the database
3842
3873
  */
3843
- async createDirectory(path3) {
3844
- const result = await super.createDirectory(path3);
3874
+ async createDirectory(path4) {
3875
+ const result = await super.createDirectory(path4);
3845
3876
  if (result.success && this.isTrackingEnabled()) {
3846
3877
  this.metadataService.recordDirectoryCreation(
3847
- path3,
3878
+ path4,
3848
3879
  this.getStorageType(),
3849
3880
  result.data?.metadata
3850
3881
  ).catch(() => {
@@ -3855,11 +3886,11 @@ var TrackedFileManager = class extends FileManager {
3855
3886
  /**
3856
3887
  * Remove a directory and delete its record from the database
3857
3888
  */
3858
- async removeDirectory(path3, recursive = false) {
3859
- const result = await super.removeDirectory(path3, recursive);
3889
+ async removeDirectory(path4, recursive = false) {
3890
+ const result = await super.removeDirectory(path4, recursive);
3860
3891
  if (result.success && this.isTrackingEnabled()) {
3861
3892
  this.metadataService.recordDirectoryDelete(
3862
- path3,
3893
+ path4,
3863
3894
  this.getStorageType(),
3864
3895
  recursive
3865
3896
  ).catch(() => {
@@ -3877,7 +3908,16 @@ var TrackedFileManager = class extends FileManager {
3877
3908
  if (source instanceof Buffer) {
3878
3909
  fileBuffer = source;
3879
3910
  }
3911
+ const scope_id = options?.scope_id;
3912
+ const preCheckSize = fileBuffer?.length ?? 0;
3913
+ if (this.quotaService && scope_id && preCheckSize > 0) {
3914
+ await this.quotaService.checkQuota(scope_id, preCheckSize);
3915
+ }
3880
3916
  const result = await super.uploadFile(source, remotePath, options);
3917
+ if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
3918
+ await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
3919
+ });
3920
+ }
3881
3921
  if (result.success && this.isTrackingEnabled() && result.data) {
3882
3922
  const fileItem = result.data;
3883
3923
  const skipHash = options?.skipHash ?? false;
@@ -3901,7 +3941,10 @@ var TrackedFileManager = class extends FileManager {
3901
3941
  file_path: remotePath,
3902
3942
  storage_type: this.getStorageType(),
3903
3943
  file_hash: fileHash,
3904
- file_size: fileSize
3944
+ file_size: fileSize,
3945
+ uploaded_by: options?.actor_id,
3946
+ changed_by: options?.actor_id,
3947
+ scope_id: options?.scope_id
3905
3948
  });
3906
3949
  if (awaitRecording) {
3907
3950
  await recordPromise;
@@ -3935,7 +3978,8 @@ var TrackedFileManager = class extends FileManager {
3935
3978
  this.metadataService.recordMove(
3936
3979
  sourcePath,
3937
3980
  destinationPath,
3938
- this.getStorageType()
3981
+ this.getStorageType(),
3982
+ options?.actor_id
3939
3983
  ).catch(() => {
3940
3984
  });
3941
3985
  }
@@ -3944,12 +3988,13 @@ var TrackedFileManager = class extends FileManager {
3944
3988
  /**
3945
3989
  * Delete a file and remove its record from the database
3946
3990
  */
3947
- async deleteFile(path3) {
3948
- const result = await super.deleteFile(path3);
3991
+ async deleteFile(path4, opts) {
3992
+ const result = await super.deleteFile(path4);
3949
3993
  if (result.success && this.isTrackingEnabled()) {
3950
3994
  this.metadataService.recordDelete(
3951
- path3,
3952
- this.getStorageType()
3995
+ path4,
3996
+ this.getStorageType(),
3997
+ opts?.actor_id
3953
3998
  ).catch(() => {
3954
3999
  });
3955
4000
  }
@@ -3958,13 +4003,14 @@ var TrackedFileManager = class extends FileManager {
3958
4003
  /**
3959
4004
  * Rename a file and update its record in the database
3960
4005
  */
3961
- async renameFile(path3, newName, options) {
3962
- const result = await super.renameFile(path3, newName, options);
4006
+ async renameFile(path4, newName, options) {
4007
+ const result = await super.renameFile(path4, newName, options);
3963
4008
  if (result.success && this.isTrackingEnabled()) {
3964
4009
  this.metadataService.recordRename(
3965
- path3,
4010
+ path4,
3966
4011
  newName,
3967
- this.getStorageType()
4012
+ this.getStorageType(),
4013
+ options?.actor_id
3968
4014
  ).catch(() => {
3969
4015
  });
3970
4016
  }
@@ -3973,13 +4019,14 @@ var TrackedFileManager = class extends FileManager {
3973
4019
  /**
3974
4020
  * Rename a folder and update its record in the database
3975
4021
  */
3976
- async renameFolder(path3, newName, options) {
3977
- const result = await super.renameFolder(path3, newName, options);
4022
+ async renameFolder(path4, newName, options) {
4023
+ const result = await super.renameFolder(path4, newName, options);
3978
4024
  if (result.success && this.isTrackingEnabled()) {
3979
4025
  this.metadataService.recordRename(
3980
- path3,
4026
+ path4,
3981
4027
  newName,
3982
- this.getStorageType()
4028
+ this.getStorageType(),
4029
+ options?.actor_id
3983
4030
  ).catch(() => {
3984
4031
  });
3985
4032
  }
@@ -3989,15 +4036,15 @@ var TrackedFileManager = class extends FileManager {
3989
4036
  /**
3990
4037
  * Write a file with string content and track it
3991
4038
  */
3992
- async writeFile(path3, content, options) {
4039
+ async writeFile(path4, content, options) {
3993
4040
  const buffer = Buffer.from(content, "utf-8");
3994
- return this.uploadFile(buffer, path3, options);
4041
+ return this.uploadFile(buffer, path4, options);
3995
4042
  }
3996
4043
  /**
3997
4044
  * Read a file and optionally track access
3998
4045
  */
3999
- async readFile(path3) {
4000
- return super.readFile(path3);
4046
+ async readFile(path4) {
4047
+ return super.readFile(path4);
4001
4048
  }
4002
4049
  /**
4003
4050
  * Copy a file and track the new file
@@ -4042,11 +4089,11 @@ var TrackedFileManager = class extends FileManager {
4042
4089
  * }
4043
4090
  * ```
4044
4091
  */
4045
- async hasFileChanged(path3) {
4092
+ async hasFileChanged(path4) {
4046
4093
  if (!this.isTrackingEnabled()) {
4047
4094
  return null;
4048
4095
  }
4049
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4096
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4050
4097
  if (!record) {
4051
4098
  return null;
4052
4099
  }
@@ -4054,7 +4101,7 @@ var TrackedFileManager = class extends FileManager {
4054
4101
  if (!storedHash) {
4055
4102
  return true;
4056
4103
  }
4057
- const downloadResult = await super.downloadFile(path3);
4104
+ const downloadResult = await super.downloadFile(path4);
4058
4105
  if (!downloadResult.success || !downloadResult.data) {
4059
4106
  return null;
4060
4107
  }
@@ -4074,11 +4121,11 @@ var TrackedFileManager = class extends FileManager {
4074
4121
  * @param path - Virtual path to the file
4075
4122
  * @returns Stored hash or null if not found/not tracked
4076
4123
  */
4077
- async getStoredHash(path3) {
4124
+ async getStoredHash(path4) {
4078
4125
  if (!this.isTrackingEnabled()) {
4079
4126
  return null;
4080
4127
  }
4081
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4128
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4082
4129
  return record?.file_hash || null;
4083
4130
  }
4084
4131
  /**
@@ -4087,11 +4134,11 @@ var TrackedFileManager = class extends FileManager {
4087
4134
  * @param path - Virtual path to the file
4088
4135
  * @returns Stored size in bytes or null if not found/not tracked
4089
4136
  */
4090
- async getStoredSize(path3) {
4137
+ async getStoredSize(path4) {
4091
4138
  if (!this.isTrackingEnabled()) {
4092
4139
  return null;
4093
4140
  }
4094
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4141
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4095
4142
  return record?.file_size ?? null;
4096
4143
  }
4097
4144
  // ============ Reference Tracking Methods (V2) ============
@@ -4125,10 +4172,67 @@ var TrackedFileManager = class extends FileManager {
4125
4172
  }
4126
4173
  /**
4127
4174
  * Soft-delete a file (marks as soft_deleted, does not remove physical file)
4175
+ * Also decrements quota usage if quotaService is configured.
4128
4176
  */
4129
- async softDeleteFile(fileId) {
4177
+ async softDeleteFile(fileId, opts) {
4130
4178
  if (!this.isTrackingEnabled()) return false;
4131
- return this.metadataService.softDelete(fileId);
4179
+ let fileSizeForQuota;
4180
+ let scopeIdForQuota;
4181
+ if (this.quotaService) {
4182
+ const record = await this.metadataService.findById(fileId);
4183
+ if (record) {
4184
+ fileSizeForQuota = record.file_size ?? void 0;
4185
+ scopeIdForQuota = record.scope_id;
4186
+ }
4187
+ }
4188
+ const ok = await this.metadataService.softDelete(fileId);
4189
+ if (ok) {
4190
+ if (opts?.actor_id) {
4191
+ await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
4192
+ }
4193
+ if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
4194
+ await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
4195
+ }
4196
+ }
4197
+ return ok;
4198
+ }
4199
+ // ============ Quota Pass-through Methods ============
4200
+ /**
4201
+ * Get quota status for a scope.
4202
+ * Returns null if no quota is configured (fail-open).
4203
+ */
4204
+ async getQuota(scopeId) {
4205
+ if (!this.quotaService) return null;
4206
+ return this.quotaService.getQuota(scopeId);
4207
+ }
4208
+ /**
4209
+ * Set or update the byte limit for a scope.
4210
+ * Creates a quota row if one does not exist.
4211
+ */
4212
+ async setQuotaLimit(scopeId, bytes) {
4213
+ if (!this.quotaService) return null;
4214
+ return this.quotaService.setQuotaLimit(scopeId, bytes);
4215
+ }
4216
+ /**
4217
+ * Recompute and return the quota status for a scope.
4218
+ */
4219
+ async recomputeQuota(scopeId) {
4220
+ if (!this.quotaService) return null;
4221
+ return this.quotaService.recomputeQuota(scopeId);
4222
+ }
4223
+ /**
4224
+ * Increment usage for a scope (admin override — does not throw on exceeded quota).
4225
+ */
4226
+ async incrementQuotaUsage(scopeId, deltaBytes) {
4227
+ if (!this.quotaService) return;
4228
+ return this.quotaService.incrementUsage(scopeId, deltaBytes);
4229
+ }
4230
+ /**
4231
+ * Decrement usage for a scope (e.g. after manual cleanup).
4232
+ */
4233
+ async decrementQuotaUsage(scopeId, deltaBytes) {
4234
+ if (!this.quotaService) return;
4235
+ return this.quotaService.decrementUsage(scopeId, deltaBytes);
4132
4236
  }
4133
4237
  /**
4134
4238
  * Find orphaned files (files with zero references)
@@ -4220,6 +4324,139 @@ var TrackedFileManager = class extends FileManager {
4220
4324
  }
4221
4325
  };
4222
4326
  }
4327
+ /**
4328
+ * Import a file from a URL into virtual storage.
4329
+ *
4330
+ * Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
4331
+ * Streams the response to a temp file, counting bytes live.
4332
+ * On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
4333
+ * On success: uploads to virtualPath, sets source_url in DB record.
4334
+ *
4335
+ * @param url - URL to fetch
4336
+ * @param virtualPath - Destination virtual path in storage
4337
+ * @param opts.referrer - Optional Referer header to send
4338
+ * @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
4339
+ * @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
4340
+ */
4341
+ async importFromUrl(url, virtualPath, opts) {
4342
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
4343
+ const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
4344
+ let hazoSecureMod = null;
4345
+ try {
4346
+ hazoSecureMod = await import("hazo_secure/fetch");
4347
+ } catch {
4348
+ return {
4349
+ success: false,
4350
+ error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
4351
+ };
4352
+ }
4353
+ const safeFetch = hazoSecureMod.safeFetch;
4354
+ const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
4355
+ const policy = {
4356
+ blockPrivateIps: true,
4357
+ allowedProtocols: ["https:", "http:"]
4358
+ };
4359
+ if (this.ssrfAllowlist.length > 0) {
4360
+ policy.allowedHosts = this.ssrfAllowlist;
4361
+ }
4362
+ const headers = {};
4363
+ if (opts?.referrer) {
4364
+ headers["Referer"] = opts.referrer;
4365
+ }
4366
+ const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
4367
+ let tmpWriteStream = null;
4368
+ try {
4369
+ const controller = new AbortController();
4370
+ let response;
4371
+ try {
4372
+ response = await safeFetch(url, {
4373
+ policy,
4374
+ headers,
4375
+ signal: controller.signal
4376
+ });
4377
+ } catch (err) {
4378
+ if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
4379
+ const ssrfErr = err;
4380
+ throw new SSRFError(url, ssrfErr.message);
4381
+ }
4382
+ throw err;
4383
+ }
4384
+ if (!response) {
4385
+ return { success: false, error: `No response from ${url}` };
4386
+ }
4387
+ if (!response.ok) {
4388
+ return { success: false, error: `HTTP ${response.status} from ${url}` };
4389
+ }
4390
+ if (!response.body) {
4391
+ return { success: false, error: `No response body from ${url}` };
4392
+ }
4393
+ tmpWriteStream = fs3.createWriteStream(tmpFile);
4394
+ let totalBytes = 0;
4395
+ let capExceeded = false;
4396
+ const reader = response.body.getReader();
4397
+ try {
4398
+ while (true) {
4399
+ const { done, value } = await reader.read();
4400
+ if (done) break;
4401
+ totalBytes += value.length;
4402
+ if (totalBytes > maxBytes) {
4403
+ capExceeded = true;
4404
+ controller.abort();
4405
+ reader.cancel().catch(() => {
4406
+ });
4407
+ break;
4408
+ }
4409
+ await new Promise((resolve2, reject) => {
4410
+ tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
4411
+ });
4412
+ }
4413
+ } finally {
4414
+ await new Promise((resolve2) => tmpWriteStream.end(resolve2));
4415
+ }
4416
+ if (capExceeded) {
4417
+ try {
4418
+ fs3.unlinkSync(tmpFile);
4419
+ } catch {
4420
+ }
4421
+ throw new ImportSizeCapError(url, maxBytes);
4422
+ }
4423
+ if (this.quotaService && opts?.scope_id && totalBytes > 0) {
4424
+ await this.quotaService.checkQuota(opts.scope_id, totalBytes);
4425
+ }
4426
+ const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
4427
+ actor_id: opts?.actor_id,
4428
+ scope_id: opts?.scope_id,
4429
+ awaitRecording: true
4430
+ });
4431
+ try {
4432
+ fs3.unlinkSync(tmpFile);
4433
+ } catch {
4434
+ }
4435
+ if (!uploadResult.success) {
4436
+ return { success: false, error: uploadResult.error };
4437
+ }
4438
+ if (this.isTrackingEnabled()) {
4439
+ const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
4440
+ if (record) {
4441
+ await this.metadataService.updateFields(record.id, { source_url: url });
4442
+ }
4443
+ }
4444
+ return {
4445
+ success: true,
4446
+ data: { virtualPath, size: totalBytes, sourceUrl: url }
4447
+ };
4448
+ } catch (err) {
4449
+ try {
4450
+ fs3.unlinkSync(tmpFile);
4451
+ } catch {
4452
+ }
4453
+ if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
4454
+ throw err;
4455
+ }
4456
+ const msg = err instanceof Error ? err.message : String(err);
4457
+ return { success: false, error: `importFromUrl failed: ${msg}` };
4458
+ }
4459
+ }
4223
4460
  };
4224
4461
  function createTrackedFileManager(options) {
4225
4462
  return new TrackedFileManager(options);