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.
@@ -5,6 +5,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/services/tracked-file-manager.ts
9
+ import * as fs3 from "fs";
10
+ import * as os from "os";
11
+ import * as path3 from "path";
12
+
8
13
  // src/config/index.ts
9
14
  import * as ini from "ini";
10
15
  import * as fs from "fs";
@@ -172,63 +177,63 @@ var HazoFilesError = class extends Error {
172
177
  }
173
178
  };
174
179
  var FileNotFoundError = class extends HazoFilesError {
175
- constructor(path3) {
176
- super(`File not found: ${path3}`, "FILE_NOT_FOUND", { path: path3 });
180
+ constructor(path4) {
181
+ super(`File not found: ${path4}`, "FILE_NOT_FOUND", { path: path4 });
177
182
  this.name = "FileNotFoundError";
178
183
  }
179
184
  };
180
185
  var DirectoryNotFoundError = class extends HazoFilesError {
181
- constructor(path3) {
182
- super(`Directory not found: ${path3}`, "DIRECTORY_NOT_FOUND", { path: path3 });
186
+ constructor(path4) {
187
+ super(`Directory not found: ${path4}`, "DIRECTORY_NOT_FOUND", { path: path4 });
183
188
  this.name = "DirectoryNotFoundError";
184
189
  }
185
190
  };
186
191
  var FileExistsError = class extends HazoFilesError {
187
- constructor(path3) {
188
- super(`File already exists: ${path3}`, "FILE_EXISTS", { path: path3 });
192
+ constructor(path4) {
193
+ super(`File already exists: ${path4}`, "FILE_EXISTS", { path: path4 });
189
194
  this.name = "FileExistsError";
190
195
  }
191
196
  };
192
197
  var DirectoryExistsError = class extends HazoFilesError {
193
- constructor(path3) {
194
- super(`Directory already exists: ${path3}`, "DIRECTORY_EXISTS", { path: path3 });
198
+ constructor(path4) {
199
+ super(`Directory already exists: ${path4}`, "DIRECTORY_EXISTS", { path: path4 });
195
200
  this.name = "DirectoryExistsError";
196
201
  }
197
202
  };
198
203
  var DirectoryNotEmptyError = class extends HazoFilesError {
199
- constructor(path3) {
200
- super(`Directory is not empty: ${path3}`, "DIRECTORY_NOT_EMPTY", { path: path3 });
204
+ constructor(path4) {
205
+ super(`Directory is not empty: ${path4}`, "DIRECTORY_NOT_EMPTY", { path: path4 });
201
206
  this.name = "DirectoryNotEmptyError";
202
207
  }
203
208
  };
204
209
  var PermissionDeniedError = class extends HazoFilesError {
205
- constructor(path3, operation) {
206
- super(`Permission denied for ${operation} on: ${path3}`, "PERMISSION_DENIED", { path: path3, operation });
210
+ constructor(path4, operation) {
211
+ super(`Permission denied for ${operation} on: ${path4}`, "PERMISSION_DENIED", { path: path4, operation });
207
212
  this.name = "PermissionDeniedError";
208
213
  }
209
214
  };
210
215
  var InvalidPathError = class extends HazoFilesError {
211
- constructor(path3, reason) {
212
- super(`Invalid path "${path3}": ${reason}`, "INVALID_PATH", { path: path3, reason });
216
+ constructor(path4, reason) {
217
+ super(`Invalid path "${path4}": ${reason}`, "INVALID_PATH", { path: path4, reason });
213
218
  this.name = "InvalidPathError";
214
219
  }
215
220
  };
216
221
  var FileTooLargeError = class extends HazoFilesError {
217
- constructor(path3, size, maxSize) {
222
+ constructor(path4, size, maxSize) {
218
223
  super(
219
- `File "${path3}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
224
+ `File "${path4}" is too large (${size} bytes). Maximum allowed: ${maxSize} bytes`,
220
225
  "FILE_TOO_LARGE",
221
- { path: path3, size, maxSize }
226
+ { path: path4, size, maxSize }
222
227
  );
223
228
  this.name = "FileTooLargeError";
224
229
  }
225
230
  };
226
231
  var InvalidExtensionError = class extends HazoFilesError {
227
- constructor(path3, extension, allowedExtensions) {
232
+ constructor(path4, extension, allowedExtensions) {
228
233
  super(
229
234
  `File extension "${extension}" is not allowed. Allowed: ${allowedExtensions.join(", ")}`,
230
235
  "INVALID_EXTENSION",
231
- { path: path3, extension, allowedExtensions }
236
+ { path: path4, extension, allowedExtensions }
232
237
  );
233
238
  this.name = "InvalidExtensionError";
234
239
  }
@@ -251,6 +256,32 @@ var OperationError = class extends HazoFilesError {
251
256
  this.name = "OperationError";
252
257
  }
253
258
  };
259
+ var QuotaExceededError = class extends HazoFilesError {
260
+ constructor(scopeId, byteUsed, byteLimit, deltaBytes) {
261
+ super(
262
+ `Quota exceeded for scope ${scopeId}: would use ${byteUsed + deltaBytes} of ${byteLimit} bytes`,
263
+ "QUOTA_EXCEEDED",
264
+ { scopeId, byteUsed, byteLimit, deltaBytes }
265
+ );
266
+ this.name = "QuotaExceededError";
267
+ }
268
+ };
269
+ var SSRFError = class extends HazoFilesError {
270
+ constructor(url, reason) {
271
+ super(`SSRF check failed for URL "${url}": ${reason}`, "SSRF_ERROR", { url, reason });
272
+ this.name = "SSRFError";
273
+ }
274
+ };
275
+ var ImportSizeCapError = class extends HazoFilesError {
276
+ constructor(url, capBytes) {
277
+ super(
278
+ `Import from "${url}" aborted: response exceeds ${capBytes} byte limit`,
279
+ "IMPORT_SIZE_CAP",
280
+ { url, capBytes }
281
+ );
282
+ this.name = "ImportSizeCapError";
283
+ }
284
+ };
254
285
 
255
286
  // src/common/utils.ts
256
287
  function successResult(data) {
@@ -494,10 +525,10 @@ var BaseStorageModule = class {
494
525
  * Get folder tree structure.
495
526
  * Default implementation that can be overridden by subclasses for optimization.
496
527
  */
497
- async getFolderTree(path3 = "/", depth = 3) {
528
+ async getFolderTree(path4 = "/", depth = 3) {
498
529
  this.ensureInitialized();
499
530
  try {
500
- const result = await this.buildTree(path3, depth, 0);
531
+ const result = await this.buildTree(path4, depth, 0);
501
532
  return successResult(result);
502
533
  } catch (error) {
503
534
  return errorResult(`Failed to get folder tree: ${error.message}`);
@@ -506,11 +537,11 @@ var BaseStorageModule = class {
506
537
  /**
507
538
  * Recursively build folder tree
508
539
  */
509
- async buildTree(path3, maxDepth, currentDepth) {
540
+ async buildTree(path4, maxDepth, currentDepth) {
510
541
  if (currentDepth >= maxDepth) {
511
542
  return [];
512
543
  }
513
- const listResult = await this.listDirectory(path3, { recursive: false });
544
+ const listResult = await this.listDirectory(path4, { recursive: false });
514
545
  if (!listResult.success || !listResult.data) {
515
546
  return [];
516
547
  }
@@ -1329,12 +1360,12 @@ var GoogleDriveModule = class extends BaseStorageModule {
1329
1360
  */
1330
1361
  driveFileToItem(file, virtualPath) {
1331
1362
  const isFolder2 = file.mimeType === FOLDER_MIME_TYPE;
1332
- const path3 = virtualPath || "";
1363
+ const path4 = virtualPath || "";
1333
1364
  if (isFolder2) {
1334
1365
  return createFolderItem({
1335
1366
  id: file.id,
1336
1367
  name: file.name,
1337
- path: path3,
1368
+ path: path4,
1338
1369
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
1339
1370
  modifiedAt: file.modifiedTime ? new Date(file.modifiedTime) : /* @__PURE__ */ new Date(),
1340
1371
  metadata: {
@@ -1346,7 +1377,7 @@ var GoogleDriveModule = class extends BaseStorageModule {
1346
1377
  return createFileItem({
1347
1378
  id: file.id,
1348
1379
  name: file.name,
1349
- path: path3,
1380
+ path: path4,
1350
1381
  size: parseInt(file.size || "0", 10),
1351
1382
  mimeType: file.mimeType || "application/octet-stream",
1352
1383
  createdAt: file.createdTime ? new Date(file.createdTime) : /* @__PURE__ */ new Date(),
@@ -1445,10 +1476,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1445
1476
  }
1446
1477
  let media;
1447
1478
  if (typeof source === "string") {
1448
- const fs3 = await import("fs");
1479
+ const fs4 = await import("fs");
1449
1480
  media = {
1450
1481
  mimeType: "application/octet-stream",
1451
- body: fs3.createReadStream(source)
1482
+ body: fs4.createReadStream(source)
1452
1483
  };
1453
1484
  } else if (Buffer.isBuffer(source)) {
1454
1485
  media = {
@@ -1497,10 +1528,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1497
1528
  options.onProgress(100, buffer.length, buffer.length);
1498
1529
  }
1499
1530
  if (localPath) {
1500
- const fs3 = await import("fs");
1501
- const path3 = await import("path");
1502
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
1503
- await fs3.promises.writeFile(localPath, buffer);
1531
+ const fs4 = await import("fs");
1532
+ const path4 = await import("path");
1533
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
1534
+ await fs4.promises.writeFile(localPath, buffer);
1504
1535
  return this.successResult(localPath);
1505
1536
  }
1506
1537
  return this.successResult(buffer);
@@ -1688,10 +1719,10 @@ var GoogleDriveModule = class extends BaseStorageModule {
1688
1719
  return false;
1689
1720
  }
1690
1721
  }
1691
- async getFolderTree(path3 = "/", depth = 3) {
1722
+ async getFolderTree(path4 = "/", depth = 3) {
1692
1723
  try {
1693
1724
  await this.ensureAuthenticated();
1694
- return super.getFolderTree(path3, depth);
1725
+ return super.getFolderTree(path4, depth);
1695
1726
  } catch (error) {
1696
1727
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
1697
1728
  }
@@ -1982,12 +2013,12 @@ var DropboxModule = class extends BaseStorageModule {
1982
2013
  */
1983
2014
  metadataToItem(entry, virtualPath) {
1984
2015
  const isFolder2 = entry[".tag"] === "folder";
1985
- const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
2016
+ const path4 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
1986
2017
  if (isFolder2) {
1987
2018
  return createFolderItem({
1988
2019
  id: entry.id,
1989
2020
  name: entry.name,
1990
- path: path3,
2021
+ path: path4,
1991
2022
  createdAt: /* @__PURE__ */ new Date(),
1992
2023
  modifiedAt: /* @__PURE__ */ new Date(),
1993
2024
  metadata: {
@@ -2000,7 +2031,7 @@ var DropboxModule = class extends BaseStorageModule {
2000
2031
  return createFileItem({
2001
2032
  id: fileEntry.id,
2002
2033
  name: fileEntry.name,
2003
- path: path3,
2034
+ path: path4,
2004
2035
  size: fileEntry.size,
2005
2036
  mimeType: getMimeType(fileEntry.name),
2006
2037
  createdAt: new Date(fileEntry.client_modified),
@@ -2076,8 +2107,8 @@ var DropboxModule = class extends BaseStorageModule {
2076
2107
  const dbxPath = this.toDropboxPath(remotePath);
2077
2108
  let contents;
2078
2109
  if (typeof source === "string") {
2079
- const fs3 = await import("fs");
2080
- contents = await fs3.promises.readFile(source);
2110
+ const fs4 = await import("fs");
2111
+ contents = await fs4.promises.readFile(source);
2081
2112
  } else if (Buffer.isBuffer(source)) {
2082
2113
  contents = source;
2083
2114
  } else {
@@ -2146,10 +2177,10 @@ var DropboxModule = class extends BaseStorageModule {
2146
2177
  options.onProgress(100, buffer.length, buffer.length);
2147
2178
  }
2148
2179
  if (localPath) {
2149
- const fs3 = await import("fs");
2150
- const path3 = await import("path");
2151
- await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2152
- await fs3.promises.writeFile(localPath, buffer);
2180
+ const fs4 = await import("fs");
2181
+ const path4 = await import("path");
2182
+ await fs4.promises.mkdir(path4.dirname(localPath), { recursive: true });
2183
+ await fs4.promises.writeFile(localPath, buffer);
2153
2184
  return this.successResult(localPath);
2154
2185
  }
2155
2186
  return this.successResult(buffer);
@@ -2318,10 +2349,10 @@ var DropboxModule = class extends BaseStorageModule {
2318
2349
  return false;
2319
2350
  }
2320
2351
  }
2321
- async getFolderTree(path3 = "/", depth = 3) {
2352
+ async getFolderTree(path4 = "/", depth = 3) {
2322
2353
  try {
2323
2354
  await this.ensureAuthenticated();
2324
- return super.getFolderTree(path3, depth);
2355
+ return super.getFolderTree(path4, depth);
2325
2356
  } catch (error) {
2326
2357
  return this.errorResult(`Failed to get folder tree: ${error.message}`);
2327
2358
  }
@@ -2440,13 +2471,13 @@ var FileManager = class {
2440
2471
  /**
2441
2472
  * Create a directory at the specified path
2442
2473
  */
2443
- async createDirectory(path3) {
2474
+ async createDirectory(path4) {
2444
2475
  this.ensureInitialized();
2445
2476
  const start = Date.now();
2446
- const result = await this.module.createDirectory(path3);
2477
+ const result = await this.module.createDirectory(path4);
2447
2478
  this.logFileOp(result.success ? "info" : "error", {
2448
2479
  operation: "upload",
2449
- file_path: path3,
2480
+ file_path: path4,
2450
2481
  mime_type: "folder",
2451
2482
  storage: this.config?.provider,
2452
2483
  duration_ms: Date.now() - start,
@@ -2461,13 +2492,13 @@ var FileManager = class {
2461
2492
  * @param path - Directory path
2462
2493
  * @param recursive - If true, remove directory and all contents
2463
2494
  */
2464
- async removeDirectory(path3, recursive = false) {
2495
+ async removeDirectory(path4, recursive = false) {
2465
2496
  this.ensureInitialized();
2466
2497
  const start = Date.now();
2467
- const result = await this.module.removeDirectory(path3, recursive);
2498
+ const result = await this.module.removeDirectory(path4, recursive);
2468
2499
  this.logFileOp(result.success ? "info" : "error", {
2469
2500
  operation: "delete",
2470
- file_path: path3,
2501
+ file_path: path4,
2471
2502
  mime_type: "folder",
2472
2503
  storage: this.config?.provider,
2473
2504
  duration_ms: Date.now() - start,
@@ -2546,13 +2577,13 @@ var FileManager = class {
2546
2577
  /**
2547
2578
  * Delete a file
2548
2579
  */
2549
- async deleteFile(path3) {
2580
+ async deleteFile(path4) {
2550
2581
  this.ensureInitialized();
2551
2582
  const start = Date.now();
2552
- const result = await this.module.deleteFile(path3);
2583
+ const result = await this.module.deleteFile(path4);
2553
2584
  this.logFileOp(result.success ? "info" : "error", {
2554
2585
  operation: "delete",
2555
- file_path: path3,
2586
+ file_path: path4,
2556
2587
  storage: this.config?.provider,
2557
2588
  duration_ms: Date.now() - start,
2558
2589
  success: result.success,
@@ -2566,14 +2597,14 @@ var FileManager = class {
2566
2597
  * @param newName - New filename (not full path)
2567
2598
  * @param options - Rename options
2568
2599
  */
2569
- async renameFile(path3, newName, options) {
2600
+ async renameFile(path4, newName, options) {
2570
2601
  this.ensureInitialized();
2571
2602
  const start = Date.now();
2572
- const result = await this.module.renameFile(path3, newName, options);
2603
+ const result = await this.module.renameFile(path4, newName, options);
2573
2604
  this.logFileOp(result.success ? "info" : "error", {
2574
2605
  operation: "move",
2575
2606
  file_name: result.data?.name,
2576
- file_path: path3,
2607
+ file_path: path4,
2577
2608
  storage: this.config?.provider,
2578
2609
  duration_ms: Date.now() - start,
2579
2610
  success: result.success,
@@ -2588,14 +2619,14 @@ var FileManager = class {
2588
2619
  * @param newName - New folder name (not full path)
2589
2620
  * @param options - Rename options
2590
2621
  */
2591
- async renameFolder(path3, newName, options) {
2622
+ async renameFolder(path4, newName, options) {
2592
2623
  this.ensureInitialized();
2593
2624
  const start = Date.now();
2594
- const result = await this.module.renameFolder(path3, newName, options);
2625
+ const result = await this.module.renameFolder(path4, newName, options);
2595
2626
  this.logFileOp(result.success ? "info" : "error", {
2596
2627
  operation: "move",
2597
2628
  file_name: result.data?.name,
2598
- file_path: path3,
2629
+ file_path: path4,
2599
2630
  storage: this.config?.provider,
2600
2631
  duration_ms: Date.now() - start,
2601
2632
  success: result.success,
@@ -2610,54 +2641,54 @@ var FileManager = class {
2610
2641
  * @param path - Directory path
2611
2642
  * @param options - List options
2612
2643
  */
2613
- async listDirectory(path3, options) {
2644
+ async listDirectory(path4, options) {
2614
2645
  this.ensureInitialized();
2615
- return this.module.listDirectory(path3, options);
2646
+ return this.module.listDirectory(path4, options);
2616
2647
  }
2617
2648
  /**
2618
2649
  * Get information about a file or folder
2619
2650
  */
2620
- async getItem(path3) {
2651
+ async getItem(path4) {
2621
2652
  this.ensureInitialized();
2622
- return this.module.getItem(path3);
2653
+ return this.module.getItem(path4);
2623
2654
  }
2624
2655
  /**
2625
2656
  * Check if a file or folder exists
2626
2657
  */
2627
- async exists(path3) {
2658
+ async exists(path4) {
2628
2659
  this.ensureInitialized();
2629
- return this.module.exists(path3);
2660
+ return this.module.exists(path4);
2630
2661
  }
2631
2662
  /**
2632
2663
  * Get folder tree structure
2633
2664
  * @param path - Starting path (default: root)
2634
2665
  * @param depth - Maximum depth to traverse
2635
2666
  */
2636
- async getFolderTree(path3 = "/", depth = 3) {
2667
+ async getFolderTree(path4 = "/", depth = 3) {
2637
2668
  this.ensureInitialized();
2638
- return this.module.getFolderTree(path3, depth);
2669
+ return this.module.getFolderTree(path4, depth);
2639
2670
  }
2640
2671
  // ============ Convenience Methods ============
2641
2672
  /**
2642
2673
  * Create a file with string content
2643
2674
  */
2644
- async writeFile(path3, content, options) {
2675
+ async writeFile(path4, content, options) {
2645
2676
  const buffer = Buffer.from(content, "utf-8");
2646
- return this.uploadFile(buffer, path3, options);
2677
+ return this.uploadFile(buffer, path4, options);
2647
2678
  }
2648
2679
  /**
2649
2680
  * Read a file as string
2650
2681
  */
2651
- async readFile(path3) {
2652
- const result = await this.downloadFile(path3);
2682
+ async readFile(path4) {
2683
+ const result = await this.downloadFile(path4);
2653
2684
  if (!result.success) {
2654
2685
  return { success: false, error: result.error };
2655
2686
  }
2656
2687
  if (Buffer.isBuffer(result.data)) {
2657
2688
  return { success: true, data: result.data.toString("utf-8") };
2658
2689
  }
2659
- const fs3 = await import("fs");
2660
- const content = await fs3.promises.readFile(result.data, "utf-8");
2690
+ const fs4 = await import("fs");
2691
+ const content = await fs4.promises.readFile(result.data, "utf-8");
2661
2692
  return { success: true, data: content };
2662
2693
  }
2663
2694
  /**
@@ -2668,22 +2699,22 @@ var FileManager = class {
2668
2699
  if (!downloadResult.success) {
2669
2700
  return { success: false, error: downloadResult.error };
2670
2701
  }
2671
- const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs3) => fs3.promises.readFile(downloadResult.data));
2702
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : await import("fs").then((fs4) => fs4.promises.readFile(downloadResult.data));
2672
2703
  return this.uploadFile(buffer, destinationPath, options);
2673
2704
  }
2674
2705
  /**
2675
2706
  * Ensure a directory exists (creates if needed)
2676
2707
  */
2677
- async ensureDirectory(path3) {
2678
- const exists = await this.exists(path3);
2708
+ async ensureDirectory(path4) {
2709
+ const exists = await this.exists(path4);
2679
2710
  if (exists) {
2680
- const item = await this.getItem(path3);
2711
+ const item = await this.getItem(path4);
2681
2712
  if (item.success && item.data?.isDirectory) {
2682
2713
  return { success: true, data: item.data };
2683
2714
  }
2684
2715
  return { success: false, error: "Path exists but is not a directory" };
2685
2716
  }
2686
- return this.createDirectory(path3);
2717
+ return this.createDirectory(path4);
2687
2718
  }
2688
2719
  };
2689
2720
  function createFileManager(options) {
@@ -2996,6 +3027,8 @@ var FileMetadataService = class {
2996
3027
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
2997
3028
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
2998
3029
  if (input.content_tag !== void 0) record.content_tag = input.content_tag;
3030
+ if (input.changed_by !== void 0) record.changed_by = input.changed_by;
3031
+ if (input.source_url !== void 0) record.source_url = input.source_url;
2999
3032
  const results = await this.crud.insert(record);
3000
3033
  const duration_ms = Date.now() - start;
3001
3034
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
@@ -3028,32 +3061,32 @@ var FileMetadataService = class {
3028
3061
  /**
3029
3062
  * Record a directory creation
3030
3063
  */
3031
- async recordDirectoryCreation(path3, storageType, metadata) {
3064
+ async recordDirectoryCreation(path4, storageType, metadata) {
3032
3065
  return this.recordUpload({
3033
- filename: getBaseName(path3),
3066
+ filename: getBaseName(path4),
3034
3067
  file_type: "folder",
3035
3068
  file_data: metadata,
3036
- file_path: path3,
3069
+ file_path: path4,
3037
3070
  storage_type: storageType
3038
3071
  });
3039
3072
  }
3040
3073
  /**
3041
3074
  * Record a file access (download)
3042
3075
  */
3043
- async recordAccess(path3, storageType) {
3076
+ async recordAccess(path4, storageType) {
3044
3077
  const start = Date.now();
3045
3078
  try {
3046
- const existing = await this.findByPath(path3, storageType);
3079
+ const existing = await this.findByPath(path4, storageType);
3047
3080
  if (existing) {
3048
3081
  await this.crud.updateById(existing.id, {
3049
3082
  changed_at: this.now()
3050
3083
  });
3051
3084
  const duration_ms = Date.now() - start;
3052
- this.logger?.debug?.("Recorded file access", { path: path3 });
3085
+ this.logger?.debug?.("Recorded file access", { path: path4 });
3053
3086
  this.logger?.info?.("file_operation", {
3054
3087
  operation: "download",
3055
3088
  file_name: existing.filename,
3056
- file_path: path3,
3089
+ file_path: path4,
3057
3090
  mime_type: existing.file_type,
3058
3091
  size_bytes: existing.file_size,
3059
3092
  storage: storageType,
@@ -3067,7 +3100,7 @@ var FileMetadataService = class {
3067
3100
  const duration_ms = Date.now() - start;
3068
3101
  this.logger?.error?.("file_operation", {
3069
3102
  operation: "download",
3070
- file_path: path3,
3103
+ file_path: path4,
3071
3104
  storage: storageType,
3072
3105
  duration_ms,
3073
3106
  success: false,
@@ -3079,19 +3112,20 @@ var FileMetadataService = class {
3079
3112
  }
3080
3113
  /**
3081
3114
  * Record a file deletion
3115
+ * changedBy is accepted for API consistency but not written (record is deleted)
3082
3116
  */
3083
- async recordDelete(path3, storageType) {
3117
+ async recordDelete(path4, storageType, _changedBy) {
3084
3118
  const start = Date.now();
3085
3119
  try {
3086
- const existing = await this.findByPath(path3, storageType);
3120
+ const existing = await this.findByPath(path4, storageType);
3087
3121
  if (existing) {
3088
3122
  await this.crud.deleteById(existing.id);
3089
3123
  const duration_ms = Date.now() - start;
3090
- this.logger?.debug?.("Recorded file deletion", { path: path3 });
3124
+ this.logger?.debug?.("Recorded file deletion", { path: path4 });
3091
3125
  this.logger?.info?.("file_operation", {
3092
3126
  operation: "delete",
3093
3127
  file_name: existing.filename,
3094
- file_path: path3,
3128
+ file_path: path4,
3095
3129
  mime_type: existing.file_type,
3096
3130
  size_bytes: existing.file_size,
3097
3131
  storage: storageType,
@@ -3105,7 +3139,7 @@ var FileMetadataService = class {
3105
3139
  const duration_ms = Date.now() - start;
3106
3140
  this.logger?.error?.("file_operation", {
3107
3141
  operation: "delete",
3108
- file_path: path3,
3142
+ file_path: path4,
3109
3143
  storage: storageType,
3110
3144
  duration_ms,
3111
3145
  success: false,
@@ -3118,23 +3152,23 @@ var FileMetadataService = class {
3118
3152
  /**
3119
3153
  * Record a directory deletion (recursive)
3120
3154
  */
3121
- async recordDirectoryDelete(path3, storageType, recursive) {
3155
+ async recordDirectoryDelete(path4, storageType, recursive) {
3122
3156
  try {
3123
3157
  if (recursive) {
3124
3158
  const records = await this.crud.findBy({ storage_type: storageType });
3125
3159
  const toDelete = records.filter(
3126
- (r) => r.file_path === path3 || r.file_path.startsWith(path3 + "/")
3160
+ (r) => r.file_path === path4 || r.file_path.startsWith(path4 + "/")
3127
3161
  );
3128
3162
  for (const record of toDelete) {
3129
3163
  await this.crud.deleteById(record.id);
3130
3164
  }
3131
3165
  this.logger?.debug?.("Recorded recursive directory deletion", {
3132
- path: path3,
3166
+ path: path4,
3133
3167
  count: toDelete.length
3134
3168
  });
3135
3169
  return true;
3136
3170
  } else {
3137
- return this.recordDelete(path3, storageType);
3171
+ return this.recordDelete(path4, storageType);
3138
3172
  }
3139
3173
  } catch (error) {
3140
3174
  this.logError("recordDirectoryDelete", error);
@@ -3144,16 +3178,18 @@ var FileMetadataService = class {
3144
3178
  /**
3145
3179
  * Record a file or folder move
3146
3180
  */
3147
- async recordMove(sourcePath, destinationPath, storageType) {
3181
+ async recordMove(sourcePath, destinationPath, storageType, changedBy) {
3148
3182
  const start = Date.now();
3149
3183
  try {
3150
3184
  const existing = await this.findByPath(sourcePath, storageType);
3151
3185
  if (existing) {
3152
- await this.crud.updateById(existing.id, {
3186
+ const patch = {
3153
3187
  file_path: destinationPath,
3154
3188
  filename: getBaseName(destinationPath),
3155
3189
  changed_at: this.now()
3156
- });
3190
+ };
3191
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3192
+ await this.crud.updateById(existing.id, patch);
3157
3193
  const duration_ms = Date.now() - start;
3158
3194
  this.logger?.debug?.("Recorded file move", { from: sourcePath, to: destinationPath });
3159
3195
  this.logger?.info?.("file_operation", {
@@ -3187,24 +3223,26 @@ var FileMetadataService = class {
3187
3223
  /**
3188
3224
  * Record a file or folder rename
3189
3225
  */
3190
- async recordRename(path3, newName, storageType) {
3226
+ async recordRename(path4, newName, storageType, changedBy) {
3191
3227
  const start = Date.now();
3192
3228
  try {
3193
- const existing = await this.findByPath(path3, storageType);
3229
+ const existing = await this.findByPath(path4, storageType);
3194
3230
  if (existing) {
3195
- const parentPath = getDirName(path3);
3231
+ const parentPath = getDirName(path4);
3196
3232
  const newPath = parentPath === "/" ? `/${newName}` : `${parentPath}/${newName}`;
3197
- await this.crud.updateById(existing.id, {
3233
+ const patch = {
3198
3234
  filename: newName,
3199
3235
  file_path: newPath,
3200
3236
  changed_at: this.now()
3201
- });
3237
+ };
3238
+ if (changedBy !== void 0) patch.changed_by = changedBy;
3239
+ await this.crud.updateById(existing.id, patch);
3202
3240
  const duration_ms = Date.now() - start;
3203
- this.logger?.debug?.("Recorded file rename", { path: path3, newName });
3241
+ this.logger?.debug?.("Recorded file rename", { path: path4, newName });
3204
3242
  this.logger?.info?.("file_operation", {
3205
3243
  operation: "move",
3206
3244
  file_name: existing.filename,
3207
- file_path: path3,
3245
+ file_path: path4,
3208
3246
  mime_type: existing.file_type,
3209
3247
  size_bytes: existing.file_size,
3210
3248
  storage: storageType,
@@ -3219,7 +3257,7 @@ var FileMetadataService = class {
3219
3257
  const duration_ms = Date.now() - start;
3220
3258
  this.logger?.error?.("file_operation", {
3221
3259
  operation: "move",
3222
- file_path: path3,
3260
+ file_path: path4,
3223
3261
  storage: storageType,
3224
3262
  duration_ms,
3225
3263
  success: false,
@@ -3233,10 +3271,10 @@ var FileMetadataService = class {
3233
3271
  /**
3234
3272
  * Find a record by path and storage type
3235
3273
  */
3236
- async findByPath(path3, storageType) {
3274
+ async findByPath(path4, storageType) {
3237
3275
  try {
3238
3276
  return await this.crud.findOneBy({
3239
- file_path: path3,
3277
+ file_path: path4,
3240
3278
  storage_type: storageType
3241
3279
  });
3242
3280
  } catch (error) {
@@ -3279,9 +3317,9 @@ var FileMetadataService = class {
3279
3317
  /**
3280
3318
  * Update custom metadata for a file
3281
3319
  */
3282
- async updateMetadata(path3, storageType, metadata) {
3320
+ async updateMetadata(path4, storageType, metadata) {
3283
3321
  try {
3284
- const existing = await this.findByPath(path3, storageType);
3322
+ const existing = await this.findByPath(path4, storageType);
3285
3323
  if (existing) {
3286
3324
  const currentData = JSON.parse(existing.file_data || "{}");
3287
3325
  const newData = { ...currentData, ...metadata };
@@ -3289,7 +3327,7 @@ var FileMetadataService = class {
3289
3327
  file_data: JSON.stringify(newData),
3290
3328
  changed_at: this.now()
3291
3329
  });
3292
- this.logger?.debug?.("Updated file metadata", { path: path3 });
3330
+ this.logger?.debug?.("Updated file metadata", { path: path4 });
3293
3331
  return true;
3294
3332
  }
3295
3333
  return false;
@@ -3305,9 +3343,9 @@ var FileMetadataService = class {
3305
3343
  * Get parsed file_data structure for a file
3306
3344
  * Automatically migrates old format to new extraction structure
3307
3345
  */
3308
- async getFileData(path3, storageType) {
3346
+ async getFileData(path4, storageType) {
3309
3347
  try {
3310
- const existing = await this.findByPath(path3, storageType);
3348
+ const existing = await this.findByPath(path4, storageType);
3311
3349
  if (!existing) {
3312
3350
  return null;
3313
3351
  }
@@ -3320,9 +3358,9 @@ var FileMetadataService = class {
3320
3358
  /**
3321
3359
  * Get merged extraction data for a file
3322
3360
  */
3323
- async getMergedData(path3, storageType) {
3361
+ async getMergedData(path4, storageType) {
3324
3362
  try {
3325
- const fileData = await this.getFileData(path3, storageType);
3363
+ const fileData = await this.getFileData(path4, storageType);
3326
3364
  if (!fileData) {
3327
3365
  return null;
3328
3366
  }
@@ -3335,12 +3373,12 @@ var FileMetadataService = class {
3335
3373
  /**
3336
3374
  * Add an extraction to a file's data
3337
3375
  */
3338
- async addExtraction(path3, storageType, data, options) {
3376
+ async addExtraction(path4, storageType, data, options) {
3339
3377
  const start = Date.now();
3340
3378
  try {
3341
- const existing = await this.findByPath(path3, storageType);
3379
+ const existing = await this.findByPath(path4, storageType);
3342
3380
  if (!existing) {
3343
- this.logger?.warn?.("Cannot add extraction: file not found", { path: path3 });
3381
+ this.logger?.warn?.("Cannot add extraction: file not found", { path: path4 });
3344
3382
  return null;
3345
3383
  }
3346
3384
  const currentFileData = parseFileData(existing.file_data);
@@ -3355,11 +3393,11 @@ var FileMetadataService = class {
3355
3393
  });
3356
3394
  const newExtraction = result.data.raw_data[result.data.raw_data.length - 1];
3357
3395
  const duration_ms = Date.now() - start;
3358
- this.logger?.debug?.("Added extraction", { path: path3, extractionId: newExtraction.id });
3396
+ this.logger?.debug?.("Added extraction", { path: path4, extractionId: newExtraction.id });
3359
3397
  this.logger?.info?.("file_operation", {
3360
3398
  operation: "extract",
3361
3399
  file_name: existing.filename,
3362
- file_path: path3,
3400
+ file_path: path4,
3363
3401
  mime_type: existing.file_type,
3364
3402
  storage: storageType,
3365
3403
  duration_ms,
@@ -3371,7 +3409,7 @@ var FileMetadataService = class {
3371
3409
  const duration_ms = Date.now() - start;
3372
3410
  this.logger?.error?.("file_operation", {
3373
3411
  operation: "extract",
3374
- file_path: path3,
3412
+ file_path: path4,
3375
3413
  storage: storageType,
3376
3414
  duration_ms,
3377
3415
  success: false,
@@ -3384,9 +3422,9 @@ var FileMetadataService = class {
3384
3422
  /**
3385
3423
  * Remove an extraction by ID
3386
3424
  */
3387
- async removeExtractionById(path3, storageType, id, options) {
3425
+ async removeExtractionById(path4, storageType, id, options) {
3388
3426
  try {
3389
- const existing = await this.findByPath(path3, storageType);
3427
+ const existing = await this.findByPath(path4, storageType);
3390
3428
  if (!existing) {
3391
3429
  return false;
3392
3430
  }
@@ -3400,7 +3438,7 @@ var FileMetadataService = class {
3400
3438
  file_data: stringifyFileData(result.data),
3401
3439
  changed_at: this.now()
3402
3440
  });
3403
- this.logger?.debug?.("Removed extraction by ID", { path: path3, extractionId: id });
3441
+ this.logger?.debug?.("Removed extraction by ID", { path: path4, extractionId: id });
3404
3442
  return true;
3405
3443
  } catch (error) {
3406
3444
  this.logError("removeExtractionById", error);
@@ -3410,9 +3448,9 @@ var FileMetadataService = class {
3410
3448
  /**
3411
3449
  * Remove an extraction by index
3412
3450
  */
3413
- async removeExtractionByIndex(path3, storageType, index, options) {
3451
+ async removeExtractionByIndex(path4, storageType, index, options) {
3414
3452
  try {
3415
- const existing = await this.findByPath(path3, storageType);
3453
+ const existing = await this.findByPath(path4, storageType);
3416
3454
  if (!existing) {
3417
3455
  return false;
3418
3456
  }
@@ -3426,7 +3464,7 @@ var FileMetadataService = class {
3426
3464
  file_data: stringifyFileData(result.data),
3427
3465
  changed_at: this.now()
3428
3466
  });
3429
- this.logger?.debug?.("Removed extraction by index", { path: path3, index });
3467
+ this.logger?.debug?.("Removed extraction by index", { path: path4, index });
3430
3468
  return true;
3431
3469
  } catch (error) {
3432
3470
  this.logError("removeExtractionByIndex", error);
@@ -3436,9 +3474,9 @@ var FileMetadataService = class {
3436
3474
  /**
3437
3475
  * Get all extractions for a file
3438
3476
  */
3439
- async getExtractions(path3, storageType) {
3477
+ async getExtractions(path4, storageType) {
3440
3478
  try {
3441
- const fileData = await this.getFileData(path3, storageType);
3479
+ const fileData = await this.getFileData(path4, storageType);
3442
3480
  if (!fileData) {
3443
3481
  return null;
3444
3482
  }
@@ -3451,9 +3489,9 @@ var FileMetadataService = class {
3451
3489
  /**
3452
3490
  * Get a specific extraction by ID
3453
3491
  */
3454
- async getExtractionById(path3, storageType, id) {
3492
+ async getExtractionById(path4, storageType, id) {
3455
3493
  try {
3456
- const fileData = await this.getFileData(path3, storageType);
3494
+ const fileData = await this.getFileData(path4, storageType);
3457
3495
  if (!fileData) {
3458
3496
  return null;
3459
3497
  }
@@ -3466,9 +3504,9 @@ var FileMetadataService = class {
3466
3504
  /**
3467
3505
  * Clear all extractions for a file
3468
3506
  */
3469
- async clearExtractions(path3, storageType) {
3507
+ async clearExtractions(path4, storageType) {
3470
3508
  try {
3471
- const existing = await this.findByPath(path3, storageType);
3509
+ const existing = await this.findByPath(path4, storageType);
3472
3510
  if (!existing) {
3473
3511
  return false;
3474
3512
  }
@@ -3476,7 +3514,7 @@ var FileMetadataService = class {
3476
3514
  file_data: stringifyFileData(createEmptyFileDataStructure()),
3477
3515
  changed_at: this.now()
3478
3516
  });
3479
- this.logger?.debug?.("Cleared all extractions", { path: path3 });
3517
+ this.logger?.debug?.("Cleared all extractions", { path: path4 });
3480
3518
  return true;
3481
3519
  } catch (error) {
3482
3520
  this.logError("clearExtractions", error);
@@ -3686,7 +3724,7 @@ var FileMetadataService = class {
3686
3724
  return this.updateStatus(fileId, "soft_deleted");
3687
3725
  }
3688
3726
  /**
3689
- * Update specific V2 fields on a record
3727
+ * Update specific V2/V4 fields on a record
3690
3728
  */
3691
3729
  async updateFields(fileId, fields) {
3692
3730
  try {
@@ -3817,6 +3855,7 @@ var TrackedFileManager = class extends FileManager {
3817
3855
  constructor(options = {}) {
3818
3856
  super(options);
3819
3857
  this.metadataService = null;
3858
+ this.quotaService = null;
3820
3859
  this.trackingConfig = {
3821
3860
  enabled: options.tracking?.enabled ?? false,
3822
3861
  tableName: options.tracking?.tableName ?? "hazo_files",
@@ -3830,6 +3869,8 @@ var TrackedFileManager = class extends FileManager {
3830
3869
  logErrors: this.trackingConfig.logErrors
3831
3870
  });
3832
3871
  }
3872
+ this.quotaService = options.quotaService ?? null;
3873
+ this.ssrfAllowlist = options.ssrfAllowlist ?? [];
3833
3874
  }
3834
3875
  /**
3835
3876
  * Check if tracking is enabled and service is available
@@ -3847,11 +3888,11 @@ var TrackedFileManager = class extends FileManager {
3847
3888
  /**
3848
3889
  * Create a directory and record it in the database
3849
3890
  */
3850
- async createDirectory(path3) {
3851
- const result = await super.createDirectory(path3);
3891
+ async createDirectory(path4) {
3892
+ const result = await super.createDirectory(path4);
3852
3893
  if (result.success && this.isTrackingEnabled()) {
3853
3894
  this.metadataService.recordDirectoryCreation(
3854
- path3,
3895
+ path4,
3855
3896
  this.getStorageType(),
3856
3897
  result.data?.metadata
3857
3898
  ).catch(() => {
@@ -3862,11 +3903,11 @@ var TrackedFileManager = class extends FileManager {
3862
3903
  /**
3863
3904
  * Remove a directory and delete its record from the database
3864
3905
  */
3865
- async removeDirectory(path3, recursive = false) {
3866
- const result = await super.removeDirectory(path3, recursive);
3906
+ async removeDirectory(path4, recursive = false) {
3907
+ const result = await super.removeDirectory(path4, recursive);
3867
3908
  if (result.success && this.isTrackingEnabled()) {
3868
3909
  this.metadataService.recordDirectoryDelete(
3869
- path3,
3910
+ path4,
3870
3911
  this.getStorageType(),
3871
3912
  recursive
3872
3913
  ).catch(() => {
@@ -3884,7 +3925,16 @@ var TrackedFileManager = class extends FileManager {
3884
3925
  if (source instanceof Buffer) {
3885
3926
  fileBuffer = source;
3886
3927
  }
3928
+ const scope_id = options?.scope_id;
3929
+ const preCheckSize = fileBuffer?.length ?? 0;
3930
+ if (this.quotaService && scope_id && preCheckSize > 0) {
3931
+ await this.quotaService.checkQuota(scope_id, preCheckSize);
3932
+ }
3887
3933
  const result = await super.uploadFile(source, remotePath, options);
3934
+ if (result.success && this.quotaService && scope_id && preCheckSize > 0) {
3935
+ await this.quotaService.incrementUsage(scope_id, preCheckSize).catch(() => {
3936
+ });
3937
+ }
3888
3938
  if (result.success && this.isTrackingEnabled() && result.data) {
3889
3939
  const fileItem = result.data;
3890
3940
  const skipHash = options?.skipHash ?? false;
@@ -3908,7 +3958,10 @@ var TrackedFileManager = class extends FileManager {
3908
3958
  file_path: remotePath,
3909
3959
  storage_type: this.getStorageType(),
3910
3960
  file_hash: fileHash,
3911
- file_size: fileSize
3961
+ file_size: fileSize,
3962
+ uploaded_by: options?.actor_id,
3963
+ changed_by: options?.actor_id,
3964
+ scope_id: options?.scope_id
3912
3965
  });
3913
3966
  if (awaitRecording) {
3914
3967
  await recordPromise;
@@ -3942,7 +3995,8 @@ var TrackedFileManager = class extends FileManager {
3942
3995
  this.metadataService.recordMove(
3943
3996
  sourcePath,
3944
3997
  destinationPath,
3945
- this.getStorageType()
3998
+ this.getStorageType(),
3999
+ options?.actor_id
3946
4000
  ).catch(() => {
3947
4001
  });
3948
4002
  }
@@ -3951,12 +4005,13 @@ var TrackedFileManager = class extends FileManager {
3951
4005
  /**
3952
4006
  * Delete a file and remove its record from the database
3953
4007
  */
3954
- async deleteFile(path3) {
3955
- const result = await super.deleteFile(path3);
4008
+ async deleteFile(path4, opts) {
4009
+ const result = await super.deleteFile(path4);
3956
4010
  if (result.success && this.isTrackingEnabled()) {
3957
4011
  this.metadataService.recordDelete(
3958
- path3,
3959
- this.getStorageType()
4012
+ path4,
4013
+ this.getStorageType(),
4014
+ opts?.actor_id
3960
4015
  ).catch(() => {
3961
4016
  });
3962
4017
  }
@@ -3965,13 +4020,14 @@ var TrackedFileManager = class extends FileManager {
3965
4020
  /**
3966
4021
  * Rename a file and update its record in the database
3967
4022
  */
3968
- async renameFile(path3, newName, options) {
3969
- const result = await super.renameFile(path3, newName, options);
4023
+ async renameFile(path4, newName, options) {
4024
+ const result = await super.renameFile(path4, newName, options);
3970
4025
  if (result.success && this.isTrackingEnabled()) {
3971
4026
  this.metadataService.recordRename(
3972
- path3,
4027
+ path4,
3973
4028
  newName,
3974
- this.getStorageType()
4029
+ this.getStorageType(),
4030
+ options?.actor_id
3975
4031
  ).catch(() => {
3976
4032
  });
3977
4033
  }
@@ -3980,13 +4036,14 @@ var TrackedFileManager = class extends FileManager {
3980
4036
  /**
3981
4037
  * Rename a folder and update its record in the database
3982
4038
  */
3983
- async renameFolder(path3, newName, options) {
3984
- const result = await super.renameFolder(path3, newName, options);
4039
+ async renameFolder(path4, newName, options) {
4040
+ const result = await super.renameFolder(path4, newName, options);
3985
4041
  if (result.success && this.isTrackingEnabled()) {
3986
4042
  this.metadataService.recordRename(
3987
- path3,
4043
+ path4,
3988
4044
  newName,
3989
- this.getStorageType()
4045
+ this.getStorageType(),
4046
+ options?.actor_id
3990
4047
  ).catch(() => {
3991
4048
  });
3992
4049
  }
@@ -3996,15 +4053,15 @@ var TrackedFileManager = class extends FileManager {
3996
4053
  /**
3997
4054
  * Write a file with string content and track it
3998
4055
  */
3999
- async writeFile(path3, content, options) {
4056
+ async writeFile(path4, content, options) {
4000
4057
  const buffer = Buffer.from(content, "utf-8");
4001
- return this.uploadFile(buffer, path3, options);
4058
+ return this.uploadFile(buffer, path4, options);
4002
4059
  }
4003
4060
  /**
4004
4061
  * Read a file and optionally track access
4005
4062
  */
4006
- async readFile(path3) {
4007
- return super.readFile(path3);
4063
+ async readFile(path4) {
4064
+ return super.readFile(path4);
4008
4065
  }
4009
4066
  /**
4010
4067
  * Copy a file and track the new file
@@ -4049,11 +4106,11 @@ var TrackedFileManager = class extends FileManager {
4049
4106
  * }
4050
4107
  * ```
4051
4108
  */
4052
- async hasFileChanged(path3) {
4109
+ async hasFileChanged(path4) {
4053
4110
  if (!this.isTrackingEnabled()) {
4054
4111
  return null;
4055
4112
  }
4056
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4113
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4057
4114
  if (!record) {
4058
4115
  return null;
4059
4116
  }
@@ -4061,7 +4118,7 @@ var TrackedFileManager = class extends FileManager {
4061
4118
  if (!storedHash) {
4062
4119
  return true;
4063
4120
  }
4064
- const downloadResult = await super.downloadFile(path3);
4121
+ const downloadResult = await super.downloadFile(path4);
4065
4122
  if (!downloadResult.success || !downloadResult.data) {
4066
4123
  return null;
4067
4124
  }
@@ -4081,11 +4138,11 @@ var TrackedFileManager = class extends FileManager {
4081
4138
  * @param path - Virtual path to the file
4082
4139
  * @returns Stored hash or null if not found/not tracked
4083
4140
  */
4084
- async getStoredHash(path3) {
4141
+ async getStoredHash(path4) {
4085
4142
  if (!this.isTrackingEnabled()) {
4086
4143
  return null;
4087
4144
  }
4088
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4145
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4089
4146
  return record?.file_hash || null;
4090
4147
  }
4091
4148
  /**
@@ -4094,11 +4151,11 @@ var TrackedFileManager = class extends FileManager {
4094
4151
  * @param path - Virtual path to the file
4095
4152
  * @returns Stored size in bytes or null if not found/not tracked
4096
4153
  */
4097
- async getStoredSize(path3) {
4154
+ async getStoredSize(path4) {
4098
4155
  if (!this.isTrackingEnabled()) {
4099
4156
  return null;
4100
4157
  }
4101
- const record = await this.metadataService.findByPath(path3, this.getStorageType());
4158
+ const record = await this.metadataService.findByPath(path4, this.getStorageType());
4102
4159
  return record?.file_size ?? null;
4103
4160
  }
4104
4161
  // ============ Reference Tracking Methods (V2) ============
@@ -4132,10 +4189,67 @@ var TrackedFileManager = class extends FileManager {
4132
4189
  }
4133
4190
  /**
4134
4191
  * Soft-delete a file (marks as soft_deleted, does not remove physical file)
4192
+ * Also decrements quota usage if quotaService is configured.
4135
4193
  */
4136
- async softDeleteFile(fileId) {
4194
+ async softDeleteFile(fileId, opts) {
4137
4195
  if (!this.isTrackingEnabled()) return false;
4138
- return this.metadataService.softDelete(fileId);
4196
+ let fileSizeForQuota;
4197
+ let scopeIdForQuota;
4198
+ if (this.quotaService) {
4199
+ const record = await this.metadataService.findById(fileId);
4200
+ if (record) {
4201
+ fileSizeForQuota = record.file_size ?? void 0;
4202
+ scopeIdForQuota = record.scope_id;
4203
+ }
4204
+ }
4205
+ const ok = await this.metadataService.softDelete(fileId);
4206
+ if (ok) {
4207
+ if (opts?.actor_id) {
4208
+ await this.metadataService.updateFields(fileId, { changed_by: opts.actor_id });
4209
+ }
4210
+ if (this.quotaService && scopeIdForQuota && fileSizeForQuota !== void 0) {
4211
+ await this.quotaService.decrementUsage(scopeIdForQuota, fileSizeForQuota);
4212
+ }
4213
+ }
4214
+ return ok;
4215
+ }
4216
+ // ============ Quota Pass-through Methods ============
4217
+ /**
4218
+ * Get quota status for a scope.
4219
+ * Returns null if no quota is configured (fail-open).
4220
+ */
4221
+ async getQuota(scopeId) {
4222
+ if (!this.quotaService) return null;
4223
+ return this.quotaService.getQuota(scopeId);
4224
+ }
4225
+ /**
4226
+ * Set or update the byte limit for a scope.
4227
+ * Creates a quota row if one does not exist.
4228
+ */
4229
+ async setQuotaLimit(scopeId, bytes) {
4230
+ if (!this.quotaService) return null;
4231
+ return this.quotaService.setQuotaLimit(scopeId, bytes);
4232
+ }
4233
+ /**
4234
+ * Recompute and return the quota status for a scope.
4235
+ */
4236
+ async recomputeQuota(scopeId) {
4237
+ if (!this.quotaService) return null;
4238
+ return this.quotaService.recomputeQuota(scopeId);
4239
+ }
4240
+ /**
4241
+ * Increment usage for a scope (admin override — does not throw on exceeded quota).
4242
+ */
4243
+ async incrementQuotaUsage(scopeId, deltaBytes) {
4244
+ if (!this.quotaService) return;
4245
+ return this.quotaService.incrementUsage(scopeId, deltaBytes);
4246
+ }
4247
+ /**
4248
+ * Decrement usage for a scope (e.g. after manual cleanup).
4249
+ */
4250
+ async decrementQuotaUsage(scopeId, deltaBytes) {
4251
+ if (!this.quotaService) return;
4252
+ return this.quotaService.decrementUsage(scopeId, deltaBytes);
4139
4253
  }
4140
4254
  /**
4141
4255
  * Find orphaned files (files with zero references)
@@ -4227,6 +4341,139 @@ var TrackedFileManager = class extends FileManager {
4227
4341
  }
4228
4342
  };
4229
4343
  }
4344
+ /**
4345
+ * Import a file from a URL into virtual storage.
4346
+ *
4347
+ * Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
4348
+ * Streams the response to a temp file, counting bytes live.
4349
+ * On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
4350
+ * On success: uploads to virtualPath, sets source_url in DB record.
4351
+ *
4352
+ * @param url - URL to fetch
4353
+ * @param virtualPath - Destination virtual path in storage
4354
+ * @param opts.referrer - Optional Referer header to send
4355
+ * @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
4356
+ * @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
4357
+ */
4358
+ async importFromUrl(url, virtualPath, opts) {
4359
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
4360
+ const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
4361
+ let hazoSecureMod = null;
4362
+ try {
4363
+ hazoSecureMod = await import("hazo_secure/fetch");
4364
+ } catch {
4365
+ return {
4366
+ success: false,
4367
+ error: 'importFromUrl requires the optional peer dependency "hazo_secure" to be installed. Run: npm install hazo_secure'
4368
+ };
4369
+ }
4370
+ const safeFetch = hazoSecureMod.safeFetch;
4371
+ const SafeFetchErrorClass = hazoSecureMod.SafeFetchError;
4372
+ const policy = {
4373
+ blockPrivateIps: true,
4374
+ allowedProtocols: ["https:", "http:"]
4375
+ };
4376
+ if (this.ssrfAllowlist.length > 0) {
4377
+ policy.allowedHosts = this.ssrfAllowlist;
4378
+ }
4379
+ const headers = {};
4380
+ if (opts?.referrer) {
4381
+ headers["Referer"] = opts.referrer;
4382
+ }
4383
+ const tmpFile = path3.join(os.tmpdir(), `hazo_import_${Date.now()}_${Math.random().toString(36).slice(2)}.tmp`);
4384
+ let tmpWriteStream = null;
4385
+ try {
4386
+ const controller = new AbortController();
4387
+ let response;
4388
+ try {
4389
+ response = await safeFetch(url, {
4390
+ policy,
4391
+ headers,
4392
+ signal: controller.signal
4393
+ });
4394
+ } catch (err) {
4395
+ if (SafeFetchErrorClass && err instanceof SafeFetchErrorClass) {
4396
+ const ssrfErr = err;
4397
+ throw new SSRFError(url, ssrfErr.message);
4398
+ }
4399
+ throw err;
4400
+ }
4401
+ if (!response) {
4402
+ return { success: false, error: `No response from ${url}` };
4403
+ }
4404
+ if (!response.ok) {
4405
+ return { success: false, error: `HTTP ${response.status} from ${url}` };
4406
+ }
4407
+ if (!response.body) {
4408
+ return { success: false, error: `No response body from ${url}` };
4409
+ }
4410
+ tmpWriteStream = fs3.createWriteStream(tmpFile);
4411
+ let totalBytes = 0;
4412
+ let capExceeded = false;
4413
+ const reader = response.body.getReader();
4414
+ try {
4415
+ while (true) {
4416
+ const { done, value } = await reader.read();
4417
+ if (done) break;
4418
+ totalBytes += value.length;
4419
+ if (totalBytes > maxBytes) {
4420
+ capExceeded = true;
4421
+ controller.abort();
4422
+ reader.cancel().catch(() => {
4423
+ });
4424
+ break;
4425
+ }
4426
+ await new Promise((resolve2, reject) => {
4427
+ tmpWriteStream.write(value, (err) => err ? reject(err) : resolve2());
4428
+ });
4429
+ }
4430
+ } finally {
4431
+ await new Promise((resolve2) => tmpWriteStream.end(resolve2));
4432
+ }
4433
+ if (capExceeded) {
4434
+ try {
4435
+ fs3.unlinkSync(tmpFile);
4436
+ } catch {
4437
+ }
4438
+ throw new ImportSizeCapError(url, maxBytes);
4439
+ }
4440
+ if (this.quotaService && opts?.scope_id && totalBytes > 0) {
4441
+ await this.quotaService.checkQuota(opts.scope_id, totalBytes);
4442
+ }
4443
+ const uploadResult = await this.uploadFile(tmpFile, virtualPath, {
4444
+ actor_id: opts?.actor_id,
4445
+ scope_id: opts?.scope_id,
4446
+ awaitRecording: true
4447
+ });
4448
+ try {
4449
+ fs3.unlinkSync(tmpFile);
4450
+ } catch {
4451
+ }
4452
+ if (!uploadResult.success) {
4453
+ return { success: false, error: uploadResult.error };
4454
+ }
4455
+ if (this.isTrackingEnabled()) {
4456
+ const record = await this.metadataService.findByPath(virtualPath, this.getStorageType());
4457
+ if (record) {
4458
+ await this.metadataService.updateFields(record.id, { source_url: url });
4459
+ }
4460
+ }
4461
+ return {
4462
+ success: true,
4463
+ data: { virtualPath, size: totalBytes, sourceUrl: url }
4464
+ };
4465
+ } catch (err) {
4466
+ try {
4467
+ fs3.unlinkSync(tmpFile);
4468
+ } catch {
4469
+ }
4470
+ if (err instanceof SSRFError || err instanceof ImportSizeCapError) {
4471
+ throw err;
4472
+ }
4473
+ const msg = err instanceof Error ? err.message : String(err);
4474
+ return { success: false, error: `importFromUrl failed: ${msg}` };
4475
+ }
4476
+ }
4230
4477
  };
4231
4478
  function createTrackedFileManager(options) {
4232
4479
  return new TrackedFileManager(options);
@@ -5285,6 +5532,206 @@ function createUploadExtractService(fileManager, namingService, extractionServic
5285
5532
  return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
5286
5533
  }
5287
5534
 
5535
+ // src/services/quota-service.ts
5536
+ var QuotaService = class {
5537
+ constructor(opts) {
5538
+ this.crud = opts.crudService;
5539
+ this.onThreshold = opts.onThreshold;
5540
+ this.bands = (opts.bands ?? [0.8, 0.95]).slice().sort((a, b) => a - b);
5541
+ this.logger = opts.logger;
5542
+ }
5543
+ /**
5544
+ * Get quota status for a scope.
5545
+ * Returns null if no quota row exists (fail-open = no quota set).
5546
+ */
5547
+ async getQuota(scopeId) {
5548
+ try {
5549
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5550
+ if (!row) return null;
5551
+ return this.rowToStatus(row);
5552
+ } catch (error) {
5553
+ this.logger?.error?.("QuotaService.getQuota failed", {
5554
+ scopeId,
5555
+ error: error instanceof Error ? error.message : String(error)
5556
+ });
5557
+ return null;
5558
+ }
5559
+ }
5560
+ /**
5561
+ * Set (or update) the byte limit for a scope.
5562
+ * Creates a quota row if one does not exist (with byte_used = 0).
5563
+ * Returns the current stored status after upsert.
5564
+ *
5565
+ * @note This method does NOT auto-reconcile byte_used via a SUM query —
5566
+ * it simply upserts the limit and returns the stored row. To reconcile
5567
+ * byte_used against actual file sizes, call recomputeQuota() separately
5568
+ * after a SUM(file_size) query on hazo_files for the scope.
5569
+ */
5570
+ async setQuotaLimit(scopeId, bytes) {
5571
+ try {
5572
+ const existing = await this.crud.findOneBy({ scope_id: scopeId });
5573
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5574
+ if (existing) {
5575
+ await this.crud.updateById(scopeId, {
5576
+ byte_limit: bytes,
5577
+ updated_at: now
5578
+ });
5579
+ } else {
5580
+ await this.crud.insert({
5581
+ scope_id: scopeId,
5582
+ byte_limit: bytes,
5583
+ byte_used: 0,
5584
+ updated_at: now
5585
+ });
5586
+ }
5587
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5588
+ if (!row) {
5589
+ return { scopeId, byteLimit: bytes, byteUsed: 0, percentUsed: 0 };
5590
+ }
5591
+ return this.rowToStatus(row);
5592
+ } catch (error) {
5593
+ this.logger?.error?.("QuotaService.setQuotaLimit failed", {
5594
+ scopeId,
5595
+ error: error instanceof Error ? error.message : String(error)
5596
+ });
5597
+ throw error;
5598
+ }
5599
+ }
5600
+ /**
5601
+ * Recompute byteUsed by reading the current row.
5602
+ * (Full reconciliation against actual file sizes should be done externally
5603
+ * via a SUM query on hazo_files; this method just returns the stored state.)
5604
+ */
5605
+ async recomputeQuota(scopeId) {
5606
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5607
+ if (!row) {
5608
+ throw new Error(`No quota row found for scope ${scopeId}`);
5609
+ }
5610
+ return this.rowToStatus(row);
5611
+ }
5612
+ /**
5613
+ * Pre-upload check ONLY — does NOT increment.
5614
+ * Throws QuotaExceededError if the upload would exceed the limit.
5615
+ * If no quota row exists for the scope, succeeds silently (fail-open).
5616
+ *
5617
+ * Use this before the upload, then call incrementUsage after confirmed success.
5618
+ * This prevents quota inflation when an upload fails mid-stream.
5619
+ */
5620
+ async checkQuota(scopeId, deltaBytes) {
5621
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5622
+ if (!row) {
5623
+ return;
5624
+ }
5625
+ const prevUsed = row.byte_used;
5626
+ const newUsed = prevUsed + deltaBytes;
5627
+ if (newUsed > row.byte_limit) {
5628
+ throw new QuotaExceededError(scopeId, prevUsed, row.byte_limit, deltaBytes);
5629
+ }
5630
+ }
5631
+ /**
5632
+ * Pre-upload check and increment (atomic). Throws QuotaExceededError if the upload
5633
+ * would exceed the limit. If no quota row exists, succeeds silently (fail-open).
5634
+ * Also fires threshold callbacks for any bands crossed by the new usage.
5635
+ *
5636
+ * @deprecated Prefer checkQuota() before upload + incrementUsage() after success.
5637
+ * checkAndIncrement() increments before the upload completes; if the upload
5638
+ * subsequently fails the quota is inflated with no rollback.
5639
+ */
5640
+ async checkAndIncrement(scopeId, deltaBytes) {
5641
+ await this.checkQuota(scopeId, deltaBytes);
5642
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5643
+ if (!row) return;
5644
+ const prevUsed = row.byte_used;
5645
+ const newUsed = prevUsed + deltaBytes;
5646
+ await this.crud.updateById(scopeId, {
5647
+ byte_used: newUsed,
5648
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
5649
+ });
5650
+ this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
5651
+ }
5652
+ /**
5653
+ * Decrement usage (call after soft-delete or hard-delete).
5654
+ * Clamps to zero; no-ops if no quota row.
5655
+ */
5656
+ async decrementUsage(scopeId, deltaBytes) {
5657
+ try {
5658
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5659
+ if (!row) return;
5660
+ const newUsed = Math.max(0, row.byte_used - deltaBytes);
5661
+ await this.crud.updateById(scopeId, {
5662
+ byte_used: newUsed,
5663
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
5664
+ });
5665
+ } catch (error) {
5666
+ this.logger?.error?.("QuotaService.decrementUsage failed", {
5667
+ scopeId,
5668
+ error: error instanceof Error ? error.message : String(error)
5669
+ });
5670
+ }
5671
+ }
5672
+ /**
5673
+ * Increment usage manually (admin override).
5674
+ * Does NOT throw on exceeded quota — admin is explicitly bypassing.
5675
+ */
5676
+ async incrementUsage(scopeId, deltaBytes) {
5677
+ try {
5678
+ const row = await this.crud.findOneBy({ scope_id: scopeId });
5679
+ if (!row) return;
5680
+ const prevUsed = row.byte_used;
5681
+ const newUsed = prevUsed + deltaBytes;
5682
+ await this.crud.updateById(scopeId, {
5683
+ byte_used: newUsed,
5684
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
5685
+ });
5686
+ this.fireThresholdCallbacks(scopeId, prevUsed, newUsed, row.byte_limit);
5687
+ } catch (error) {
5688
+ this.logger?.error?.("QuotaService.incrementUsage failed", {
5689
+ scopeId,
5690
+ error: error instanceof Error ? error.message : String(error)
5691
+ });
5692
+ }
5693
+ }
5694
+ /**
5695
+ * Fire threshold callbacks for all bands crossed going from prevUsed → newUsed.
5696
+ * Bands are sorted ascending so callbacks fire in order (80% before 95%).
5697
+ */
5698
+ fireThresholdCallbacks(scopeId, prevUsed, newUsed, byteLimit) {
5699
+ if (!this.onThreshold || byteLimit <= 0) return;
5700
+ const prevPercent = prevUsed / byteLimit;
5701
+ const newPercent = newUsed / byteLimit;
5702
+ for (const band of this.bands) {
5703
+ if (prevPercent < band && newPercent >= band) {
5704
+ try {
5705
+ this.onThreshold({
5706
+ scopeId,
5707
+ percent: band,
5708
+ bytesUsed: newUsed,
5709
+ byteLimit
5710
+ });
5711
+ } catch (err) {
5712
+ this.logger?.error?.("QuotaService threshold callback threw", {
5713
+ band,
5714
+ error: err instanceof Error ? err.message : String(err)
5715
+ });
5716
+ }
5717
+ }
5718
+ }
5719
+ }
5720
+ rowToStatus(row) {
5721
+ const byteLimit = row.byte_limit;
5722
+ const byteUsed = row.byte_used;
5723
+ return {
5724
+ scopeId: row.scope_id,
5725
+ byteLimit,
5726
+ byteUsed,
5727
+ percentUsed: byteLimit > 0 ? byteUsed / byteLimit : 0
5728
+ };
5729
+ }
5730
+ };
5731
+ function createQuotaService(opts) {
5732
+ return new QuotaService(opts);
5733
+ }
5734
+
5288
5735
  // src/server/factory.ts
5289
5736
  async function createHazoFilesServer(options = {}) {
5290
5737
  const {
@@ -5299,8 +5746,20 @@ async function createHazoFilesServer(options = {}) {
5299
5746
  namingTableName = "hazo_files_naming",
5300
5747
  enableTracking = !!crudService,
5301
5748
  trackDownloads = true,
5302
- defaultContentTagConfig
5749
+ defaultContentTagConfig,
5750
+ ssrf,
5751
+ quotaCrudService,
5752
+ onQuotaThreshold,
5753
+ quotaBands
5303
5754
  } = options;
5755
+ let quotaService;
5756
+ if (quotaCrudService) {
5757
+ quotaService = new QuotaService({
5758
+ crudService: quotaCrudService,
5759
+ onThreshold: onQuotaThreshold,
5760
+ bands: quotaBands
5761
+ });
5762
+ }
5304
5763
  const fileManagerOptions = {
5305
5764
  config,
5306
5765
  crudService,
@@ -5310,7 +5769,9 @@ async function createHazoFilesServer(options = {}) {
5310
5769
  tableName: metadataTableName,
5311
5770
  trackDownloads,
5312
5771
  logErrors: true
5313
- }
5772
+ },
5773
+ quotaService,
5774
+ ssrfAllowlist: ssrf?.allowlist
5314
5775
  };
5315
5776
  const fileManager = await createInitializedTrackedFileManager(fileManagerOptions);
5316
5777
  const metadataService = fileManager.getMetadataService();
@@ -5354,6 +5815,124 @@ async function createBasicFileManager(config, crudService) {
5354
5815
  });
5355
5816
  }
5356
5817
 
5818
+ // src/services/purge-handlers.ts
5819
+ var HAZO_FILES_JOB_TYPES = {
5820
+ PURGE_PLAN: "hazo_files.purge_plan",
5821
+ PURGE_ONE: "hazo_files.purge_one"
5822
+ };
5823
+ function createPurgeJobHandlers(fm, opts) {
5824
+ const { submitJob, logger } = opts ?? {};
5825
+ const purgePlanHandler = async (job) => {
5826
+ const { retentionDays = 30, dryRun = false } = job.payload ?? {};
5827
+ const cutoffMs = retentionDays * 24 * 60 * 60 * 1e3;
5828
+ const cutoffDate = new Date(Date.now() - cutoffMs);
5829
+ const metadataService = fm.getMetadataService();
5830
+ if (!metadataService) {
5831
+ logger?.warn?.("purge_plan: no metadata service available (tracking disabled)");
5832
+ return { purgedCount: 0 };
5833
+ }
5834
+ let allRecords = [];
5835
+ try {
5836
+ const crud = metadataService.crud;
5837
+ if (crud && typeof crud.findBy === "function") {
5838
+ allRecords = await crud.findBy({ status: "soft_deleted" });
5839
+ }
5840
+ } catch (err) {
5841
+ logger?.error?.("purge_plan: failed to query soft-deleted records", {
5842
+ error: err instanceof Error ? err.message : String(err)
5843
+ });
5844
+ return { purgedCount: 0 };
5845
+ }
5846
+ const eligible = allRecords.filter((r) => {
5847
+ if (!r.deleted_at) return false;
5848
+ const deletedAt = new Date(r.deleted_at);
5849
+ return deletedAt < cutoffDate;
5850
+ });
5851
+ logger?.info?.("purge_plan: found eligible records", {
5852
+ total: allRecords.length,
5853
+ eligible: eligible.length,
5854
+ retentionDays,
5855
+ dryRun
5856
+ });
5857
+ if (dryRun) {
5858
+ return {
5859
+ purgedCount: 0,
5860
+ wouldPurge: eligible.map((r) => r.id)
5861
+ };
5862
+ }
5863
+ let purgedCount = 0;
5864
+ if (submitJob) {
5865
+ for (const record of eligible) {
5866
+ try {
5867
+ await submitJob(HAZO_FILES_JOB_TYPES.PURGE_ONE, { fileId: record.id });
5868
+ purgedCount++;
5869
+ } catch (err) {
5870
+ logger?.error?.("purge_plan: failed to submit purge_one job", {
5871
+ fileId: record.id,
5872
+ error: err instanceof Error ? err.message : String(err)
5873
+ });
5874
+ }
5875
+ }
5876
+ } else {
5877
+ for (const record of eligible) {
5878
+ try {
5879
+ await hardDeleteRecord(fm, record, metadataService, logger);
5880
+ purgedCount++;
5881
+ } catch (err) {
5882
+ logger?.error?.("purge_plan: failed to inline-purge record", {
5883
+ fileId: record.id,
5884
+ error: err instanceof Error ? err.message : String(err)
5885
+ });
5886
+ }
5887
+ }
5888
+ }
5889
+ return { purgedCount };
5890
+ };
5891
+ const purgeOneHandler = async (job) => {
5892
+ const { fileId } = job.payload;
5893
+ const metadataService = fm.getMetadataService();
5894
+ if (!metadataService) {
5895
+ logger?.warn?.("purge_one: no metadata service (tracking disabled)", { fileId });
5896
+ return;
5897
+ }
5898
+ const record = await metadataService.findById(fileId);
5899
+ if (!record) {
5900
+ logger?.info?.("purge_one: record not found (already purged)", { fileId });
5901
+ return;
5902
+ }
5903
+ if (record.status !== "soft_deleted") {
5904
+ logger?.warn?.("[hazo_files] purge_one skipping non-soft-deleted file", {
5905
+ fileId,
5906
+ status: record.status
5907
+ });
5908
+ return;
5909
+ }
5910
+ await hardDeleteRecord(fm, record, metadataService, logger);
5911
+ logger?.info?.("purge_one: completed", { fileId, file_path: record.file_path });
5912
+ };
5913
+ return {
5914
+ [HAZO_FILES_JOB_TYPES.PURGE_PLAN]: purgePlanHandler,
5915
+ [HAZO_FILES_JOB_TYPES.PURGE_ONE]: purgeOneHandler
5916
+ };
5917
+ }
5918
+ async function hardDeleteRecord(fm, record, _metadataService, logger) {
5919
+ try {
5920
+ const deleteResult = await fm.deleteFile(record.file_path);
5921
+ if (!deleteResult.success) {
5922
+ logger?.warn?.("purge: physical delete returned error (continuing)", {
5923
+ fileId: record.id,
5924
+ file_path: record.file_path,
5925
+ error: deleteResult.error
5926
+ });
5927
+ }
5928
+ } catch (err) {
5929
+ logger?.warn?.("purge: physical delete threw (continuing)", {
5930
+ fileId: record.id,
5931
+ error: err instanceof Error ? err.message : String(err)
5932
+ });
5933
+ }
5934
+ }
5935
+
5357
5936
  // src/schema/index.ts
5358
5937
  var HAZO_FILES_DEFAULT_TABLE_NAME = "hazo_files";
5359
5938
  var HAZO_FILES_TABLE_SCHEMA = {
@@ -5637,6 +6216,77 @@ function getMigrationV3ForTable(tableName, dbType) {
5637
6216
  backfill: migration.backfill
5638
6217
  };
5639
6218
  }
6219
+ var HAZO_FILES_MIGRATION_V4 = {
6220
+ tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
6221
+ sqlite: {
6222
+ // NOT IDEMPOTENT — SQLite ALTER TABLE does not support IF NOT EXISTS.
6223
+ // Wrap each statement in try/catch when running on SQLite.
6224
+ alterStatements: [
6225
+ "ALTER TABLE hazo_files ADD COLUMN changed_by TEXT",
6226
+ "ALTER TABLE hazo_files ADD COLUMN source_url TEXT"
6227
+ ],
6228
+ indexes: [
6229
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
6230
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
6231
+ ],
6232
+ backfill: ""
6233
+ // No backfill needed — columns are nullable
6234
+ },
6235
+ postgres: {
6236
+ alterStatements: [
6237
+ "ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS changed_by UUID",
6238
+ "ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS source_url TEXT"
6239
+ ],
6240
+ indexes: [
6241
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_changed_by ON hazo_files (changed_by)",
6242
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_source_url ON hazo_files (source_url)"
6243
+ ],
6244
+ backfill: ""
6245
+ // No backfill needed — columns are nullable
6246
+ },
6247
+ newColumns: ["changed_by", "source_url"]
6248
+ };
6249
+ function getMigrationV4ForTable(tableName, dbType) {
6250
+ validateTableName(tableName);
6251
+ const migration = HAZO_FILES_MIGRATION_V4[dbType];
6252
+ const defaultName = HAZO_FILES_MIGRATION_V4.tableName;
6253
+ return {
6254
+ alterStatements: migration.alterStatements.map(
6255
+ (stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
6256
+ ),
6257
+ indexes: migration.indexes.map(
6258
+ (idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
6259
+ ),
6260
+ backfill: migration.backfill
6261
+ };
6262
+ }
6263
+ var HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME = "hazo_file_quotas";
6264
+ var HAZO_FILE_QUOTAS_TABLE_SCHEMA = {
6265
+ tableName: HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
6266
+ sqlite: {
6267
+ ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
6268
+ scope_id TEXT PRIMARY KEY,
6269
+ byte_limit INTEGER NOT NULL,
6270
+ byte_used INTEGER NOT NULL DEFAULT 0,
6271
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
6272
+ )`,
6273
+ indexes: [
6274
+ "CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
6275
+ ]
6276
+ },
6277
+ postgres: {
6278
+ ddl: `CREATE TABLE IF NOT EXISTS hazo_file_quotas (
6279
+ scope_id UUID PRIMARY KEY,
6280
+ byte_limit BIGINT NOT NULL,
6281
+ byte_used BIGINT NOT NULL DEFAULT 0,
6282
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6283
+ )`,
6284
+ indexes: [
6285
+ "CREATE INDEX IF NOT EXISTS idx_hazo_file_quotas_used ON hazo_file_quotas (byte_used)"
6286
+ ]
6287
+ },
6288
+ columns: ["scope_id", "byte_limit", "byte_used", "updated_at"]
6289
+ };
5640
6290
 
5641
6291
  // src/migrations/add-reference-tracking.ts
5642
6292
  async function migrateToV2(executor, dbType, tableName) {
@@ -5696,12 +6346,17 @@ export {
5696
6346
  GoogleDriveAuth,
5697
6347
  GoogleDriveModule,
5698
6348
  HAZO_FILES_DEFAULT_TABLE_NAME,
6349
+ HAZO_FILES_JOB_TYPES,
5699
6350
  HAZO_FILES_MIGRATION_V2,
5700
6351
  HAZO_FILES_MIGRATION_V3,
6352
+ HAZO_FILES_MIGRATION_V4,
5701
6353
  HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
5702
6354
  HAZO_FILES_NAMING_TABLE_SCHEMA,
5703
6355
  HAZO_FILES_TABLE_SCHEMA,
6356
+ HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME,
6357
+ HAZO_FILE_QUOTAS_TABLE_SCHEMA,
5704
6358
  HazoFilesError,
6359
+ ImportSizeCapError,
5705
6360
  InvalidExtensionError,
5706
6361
  InvalidPathError,
5707
6362
  LLMExtractionService,
@@ -5709,6 +6364,9 @@ export {
5709
6364
  NamingConventionService,
5710
6365
  OperationError,
5711
6366
  PermissionDeniedError,
6367
+ QuotaExceededError,
6368
+ QuotaService,
6369
+ SSRFError,
5712
6370
  SYSTEM_COUNTER_VARIABLES,
5713
6371
  SYSTEM_DATE_VARIABLES,
5714
6372
  SYSTEM_FILE_VARIABLES,
@@ -5744,6 +6402,8 @@ export {
5744
6402
  createLocalModule,
5745
6403
  createModule,
5746
6404
  createNamingConventionService,
6405
+ createPurgeJobHandlers,
6406
+ createQuotaService,
5747
6407
  createTrackedFileManager,
5748
6408
  createUploadExtractService,
5749
6409
  createVariableSegment,
@@ -5772,6 +6432,7 @@ export {
5772
6432
  getMergedData,
5773
6433
  getMigrationForTable,
5774
6434
  getMigrationV3ForTable,
6435
+ getMigrationV4ForTable,
5775
6436
  getMimeType,
5776
6437
  getNameWithoutExtension,
5777
6438
  getNamingSchemaForTable,