hazo_files 1.5.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -187,6 +187,10 @@ interface FileMetadataInput {
187
187
  original_filename?: string;
188
188
  /** Content tag classifying the document type (V3) */
189
189
  content_tag?: string;
190
+ /** Actor UUID who performed this mutation (V4) */
191
+ changed_by?: string;
192
+ /** Source URL when file was imported via importFromUrl (V4) */
193
+ source_url?: string;
190
194
  }
191
195
  /**
192
196
  * Input for updating an existing metadata record
@@ -433,6 +437,10 @@ interface FileMetadataRecordV2 extends FileMetadataRecord {
433
437
  deleted_at?: string | null;
434
438
  /** Content tag classifying the document type (V3) */
435
439
  content_tag?: string | null;
440
+ /** Actor UUID who last mutated this record (V4 — migration 004) */
441
+ changed_by?: string | null;
442
+ /** Source URL when file was imported via importFromUrl (V4 — migration 005) */
443
+ source_url?: string | null;
436
444
  }
437
445
  /**
438
446
  * Options for adding a reference to a file
@@ -770,8 +778,9 @@ declare class FileMetadataService {
770
778
  recordAccess(path: string, storageType: StorageProvider): Promise<boolean>;
771
779
  /**
772
780
  * Record a file deletion
781
+ * changedBy is accepted for API consistency but not written (record is deleted)
773
782
  */
774
- recordDelete(path: string, storageType: StorageProvider): Promise<boolean>;
783
+ recordDelete(path: string, storageType: StorageProvider, _changedBy?: string): Promise<boolean>;
775
784
  /**
776
785
  * Record a directory deletion (recursive)
777
786
  */
@@ -779,11 +788,11 @@ declare class FileMetadataService {
779
788
  /**
780
789
  * Record a file or folder move
781
790
  */
782
- recordMove(sourcePath: string, destinationPath: string, storageType: StorageProvider): Promise<boolean>;
791
+ recordMove(sourcePath: string, destinationPath: string, storageType: StorageProvider, changedBy?: string): Promise<boolean>;
783
792
  /**
784
793
  * Record a file or folder rename
785
794
  */
786
- recordRename(path: string, newName: string, storageType: StorageProvider): Promise<boolean>;
795
+ recordRename(path: string, newName: string, storageType: StorageProvider, changedBy?: string): Promise<boolean>;
787
796
  /**
788
797
  * Find a record by path and storage type
789
798
  */
@@ -887,9 +896,9 @@ declare class FileMetadataService {
887
896
  */
888
897
  softDelete(fileId: string): Promise<boolean>;
889
898
  /**
890
- * Update specific V2 fields on a record
899
+ * Update specific V2/V4 fields on a record
891
900
  */
892
- updateFields(fileId: string, fields: Partial<Pick<FileMetadataRecordV2, 'scope_id' | 'uploaded_by' | 'original_filename' | 'storage_verified_at' | 'status' | 'content_tag'>>): Promise<boolean>;
901
+ updateFields(fileId: string, fields: Partial<Pick<FileMetadataRecordV2, 'scope_id' | 'uploaded_by' | 'original_filename' | 'storage_verified_at' | 'status' | 'content_tag' | 'changed_by' | 'source_url'>>): Promise<boolean>;
893
902
  /**
894
903
  * Find orphaned files (zero references)
895
904
  */
@@ -1053,6 +1062,138 @@ declare function createFileManager(options?: FileManagerOptions): FileManager;
1053
1062
  */
1054
1063
  declare function createInitializedFileManager(options?: FileManagerOptions): Promise<FileManager>;
1055
1064
 
1065
+ /**
1066
+ * Quota Service
1067
+ * Per-scope opt-in quota tracking with threshold callbacks.
1068
+ *
1069
+ * A scope without a hazo_file_quotas row has no quota and uploads succeed (fail-open).
1070
+ */
1071
+ /**
1072
+ * Quota status for a scope
1073
+ */
1074
+ interface QuotaStatus {
1075
+ scopeId: string;
1076
+ byteLimit: number;
1077
+ byteUsed: number;
1078
+ percentUsed: number;
1079
+ }
1080
+ /**
1081
+ * Event emitted when usage crosses a configured threshold band
1082
+ */
1083
+ interface QuotaThresholdEvent {
1084
+ scopeId: string;
1085
+ /** Fractional threshold crossed, e.g. 0.80 or 0.95 */
1086
+ percent: number;
1087
+ bytesUsed: number;
1088
+ byteLimit: number;
1089
+ }
1090
+ /**
1091
+ * Raw row shape from hazo_file_quotas table
1092
+ */
1093
+ interface QuotaRow {
1094
+ scope_id: string;
1095
+ byte_limit: number;
1096
+ byte_used: number;
1097
+ updated_at: string;
1098
+ [key: string]: unknown;
1099
+ }
1100
+ /**
1101
+ * Minimal interface for the quota CRUD service (hazo_connect compatible)
1102
+ */
1103
+ interface QuotaCrudServiceLike {
1104
+ findBy(criteria: Record<string, unknown>): Promise<QuotaRow[]>;
1105
+ findOneBy(criteria: Record<string, unknown>): Promise<QuotaRow | null>;
1106
+ insert(data: Partial<QuotaRow> | Partial<QuotaRow>[]): Promise<QuotaRow[]>;
1107
+ updateById(id: unknown, patch: Partial<QuotaRow>): Promise<QuotaRow[]>;
1108
+ list(configure?: (qb: unknown) => unknown): Promise<QuotaRow[]>;
1109
+ }
1110
+ /**
1111
+ * Options for QuotaService
1112
+ */
1113
+ interface QuotaServiceOptions {
1114
+ /** hazo_connect CRUD service pointed at hazo_file_quotas table */
1115
+ crudService: QuotaCrudServiceLike;
1116
+ /** Callback fired when usage crosses a threshold band */
1117
+ onThreshold?: (event: QuotaThresholdEvent) => void;
1118
+ /** Fractional threshold bands (default: [0.80, 0.95]) */
1119
+ bands?: number[];
1120
+ /** Logger for diagnostics */
1121
+ logger?: {
1122
+ debug?(message: string, data?: Record<string, unknown>): void;
1123
+ warn?(message: string, data?: Record<string, unknown>): void;
1124
+ error?(message: string, data?: Record<string, unknown>): void;
1125
+ };
1126
+ }
1127
+ /**
1128
+ * Per-scope opt-in quota tracking.
1129
+ *
1130
+ * Fail-open: a scope without a quota row has no quota and all uploads succeed.
1131
+ */
1132
+ declare class QuotaService {
1133
+ private crud;
1134
+ private onThreshold?;
1135
+ private bands;
1136
+ private logger?;
1137
+ constructor(opts: QuotaServiceOptions);
1138
+ /**
1139
+ * Get quota status for a scope.
1140
+ * Returns null if no quota row exists (fail-open = no quota set).
1141
+ */
1142
+ getQuota(scopeId: string): Promise<QuotaStatus | null>;
1143
+ /**
1144
+ * Set (or update) the byte limit for a scope.
1145
+ * Creates a quota row if one does not exist (with byte_used = 0).
1146
+ * Returns the current stored status after upsert.
1147
+ *
1148
+ * @note This method does NOT auto-reconcile byte_used via a SUM query —
1149
+ * it simply upserts the limit and returns the stored row. To reconcile
1150
+ * byte_used against actual file sizes, call recomputeQuota() separately
1151
+ * after a SUM(file_size) query on hazo_files for the scope.
1152
+ */
1153
+ setQuotaLimit(scopeId: string, bytes: number): Promise<QuotaStatus>;
1154
+ /**
1155
+ * Recompute byteUsed by reading the current row.
1156
+ * (Full reconciliation against actual file sizes should be done externally
1157
+ * via a SUM query on hazo_files; this method just returns the stored state.)
1158
+ */
1159
+ recomputeQuota(scopeId: string): Promise<QuotaStatus>;
1160
+ /**
1161
+ * Pre-upload check ONLY — does NOT increment.
1162
+ * Throws QuotaExceededError if the upload would exceed the limit.
1163
+ * If no quota row exists for the scope, succeeds silently (fail-open).
1164
+ *
1165
+ * Use this before the upload, then call incrementUsage after confirmed success.
1166
+ * This prevents quota inflation when an upload fails mid-stream.
1167
+ */
1168
+ checkQuota(scopeId: string, deltaBytes: number): Promise<void>;
1169
+ /**
1170
+ * Pre-upload check and increment (atomic). Throws QuotaExceededError if the upload
1171
+ * would exceed the limit. If no quota row exists, succeeds silently (fail-open).
1172
+ * Also fires threshold callbacks for any bands crossed by the new usage.
1173
+ *
1174
+ * @deprecated Prefer checkQuota() before upload + incrementUsage() after success.
1175
+ * checkAndIncrement() increments before the upload completes; if the upload
1176
+ * subsequently fails the quota is inflated with no rollback.
1177
+ */
1178
+ checkAndIncrement(scopeId: string, deltaBytes: number): Promise<void>;
1179
+ /**
1180
+ * Decrement usage (call after soft-delete or hard-delete).
1181
+ * Clamps to zero; no-ops if no quota row.
1182
+ */
1183
+ decrementUsage(scopeId: string, deltaBytes: number): Promise<void>;
1184
+ /**
1185
+ * Increment usage manually (admin override).
1186
+ * Does NOT throw on exceeded quota — admin is explicitly bypassing.
1187
+ */
1188
+ incrementUsage(scopeId: string, deltaBytes: number): Promise<void>;
1189
+ /**
1190
+ * Fire threshold callbacks for all bands crossed going from prevUsed → newUsed.
1191
+ * Bands are sorted ascending so callbacks fire in order (80% before 95%).
1192
+ */
1193
+ private fireThresholdCallbacks;
1194
+ private rowToStatus;
1195
+ }
1196
+
1056
1197
  /**
1057
1198
  * Tracked File Manager
1058
1199
  * Extends FileManager to add database tracking of file operations
@@ -1068,6 +1209,10 @@ interface TrackedFileManagerFullOptions extends FileManagerOptions {
1068
1209
  tracking?: DatabaseTrackingConfig;
1069
1210
  /** Logger for structured file operation logging */
1070
1211
  logger?: MetadataLogger;
1212
+ /** Optional quota service for per-scope upload limits */
1213
+ quotaService?: QuotaService;
1214
+ /** SSRF allowlist passed to importFromUrl (host strings) */
1215
+ ssrfAllowlist?: string[];
1071
1216
  }
1072
1217
  /**
1073
1218
  * Extended upload options with hash tracking
@@ -1081,6 +1226,10 @@ interface TrackedUploadOptions extends UploadOptions {
1081
1226
  * Set to true when you need to immediately query/update the file record.
1082
1227
  */
1083
1228
  awaitRecording?: boolean;
1229
+ /** Actor ID (UUID) to record in uploaded_by and changed_by columns */
1230
+ actor_id?: string;
1231
+ /** Scope ID for quota tracking and organizational grouping */
1232
+ scope_id?: string;
1084
1233
  }
1085
1234
  /**
1086
1235
  * TrackedFileManager - File manager with database tracking
@@ -1091,6 +1240,8 @@ interface TrackedUploadOptions extends UploadOptions {
1091
1240
  declare class TrackedFileManager extends FileManager {
1092
1241
  private metadataService;
1093
1242
  private trackingConfig;
1243
+ private quotaService;
1244
+ private ssrfAllowlist;
1094
1245
  constructor(options?: TrackedFileManagerFullOptions);
1095
1246
  /**
1096
1247
  * Check if tracking is enabled and service is available
@@ -1120,19 +1271,27 @@ declare class TrackedFileManager extends FileManager {
1120
1271
  /**
1121
1272
  * Move a file or folder and update its path in the database
1122
1273
  */
1123
- moveItem(sourcePath: string, destinationPath: string, options?: MoveOptions): Promise<OperationResult<FileSystemItem>>;
1274
+ moveItem(sourcePath: string, destinationPath: string, options?: MoveOptions & {
1275
+ actor_id?: string;
1276
+ }): Promise<OperationResult<FileSystemItem>>;
1124
1277
  /**
1125
1278
  * Delete a file and remove its record from the database
1126
1279
  */
1127
- deleteFile(path: string): Promise<OperationResult>;
1280
+ deleteFile(path: string, opts?: {
1281
+ actor_id?: string;
1282
+ }): Promise<OperationResult>;
1128
1283
  /**
1129
1284
  * Rename a file and update its record in the database
1130
1285
  */
1131
- renameFile(path: string, newName: string, options?: RenameOptions): Promise<OperationResult<FileItem>>;
1286
+ renameFile(path: string, newName: string, options?: RenameOptions & {
1287
+ actor_id?: string;
1288
+ }): Promise<OperationResult<FileItem>>;
1132
1289
  /**
1133
1290
  * Rename a folder and update its record in the database
1134
1291
  */
1135
- renameFolder(path: string, newName: string, options?: RenameOptions): Promise<OperationResult<FolderItem>>;
1292
+ renameFolder(path: string, newName: string, options?: RenameOptions & {
1293
+ actor_id?: string;
1294
+ }): Promise<OperationResult<FolderItem>>;
1136
1295
  /**
1137
1296
  * Write a file with string content and track it
1138
1297
  */
@@ -1211,8 +1370,33 @@ declare class TrackedFileManager extends FileManager {
1211
1370
  getFilesById(fileIds: string[]): Promise<FileWithStatus[]>;
1212
1371
  /**
1213
1372
  * Soft-delete a file (marks as soft_deleted, does not remove physical file)
1373
+ * Also decrements quota usage if quotaService is configured.
1374
+ */
1375
+ softDeleteFile(fileId: string, opts?: {
1376
+ actor_id?: string;
1377
+ }): Promise<boolean>;
1378
+ /**
1379
+ * Get quota status for a scope.
1380
+ * Returns null if no quota is configured (fail-open).
1381
+ */
1382
+ getQuota(scopeId: string): Promise<QuotaStatus | null>;
1383
+ /**
1384
+ * Set or update the byte limit for a scope.
1385
+ * Creates a quota row if one does not exist.
1386
+ */
1387
+ setQuotaLimit(scopeId: string, bytes: number): Promise<QuotaStatus | null>;
1388
+ /**
1389
+ * Recompute and return the quota status for a scope.
1214
1390
  */
1215
- softDeleteFile(fileId: string): Promise<boolean>;
1391
+ recomputeQuota(scopeId: string): Promise<QuotaStatus | null>;
1392
+ /**
1393
+ * Increment usage for a scope (admin override — does not throw on exceeded quota).
1394
+ */
1395
+ incrementQuotaUsage(scopeId: string, deltaBytes: number): Promise<void>;
1396
+ /**
1397
+ * Decrement usage for a scope (e.g. after manual cleanup).
1398
+ */
1399
+ decrementQuotaUsage(scopeId: string, deltaBytes: number): Promise<void>;
1216
1400
  /**
1217
1401
  * Find orphaned files (files with zero references)
1218
1402
  */
@@ -1235,6 +1419,31 @@ declare class TrackedFileManager extends FileManager {
1235
1419
  file_id?: string;
1236
1420
  ref_id?: string;
1237
1421
  }>>;
1422
+ /**
1423
+ * Import a file from a URL into virtual storage.
1424
+ *
1425
+ * Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
1426
+ * Streams the response to a temp file, counting bytes live.
1427
+ * On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
1428
+ * On success: uploads to virtualPath, sets source_url in DB record.
1429
+ *
1430
+ * @param url - URL to fetch
1431
+ * @param virtualPath - Destination virtual path in storage
1432
+ * @param opts.referrer - Optional Referer header to send
1433
+ * @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
1434
+ * @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
1435
+ */
1436
+ importFromUrl(url: string, virtualPath: string, opts?: {
1437
+ referrer?: string;
1438
+ maxBytes?: number;
1439
+ actor_id?: string;
1440
+ /** Scope ID for quota checking and tracking. If provided, quota is checked before upload. */
1441
+ scope_id?: string;
1442
+ }): Promise<OperationResult<{
1443
+ virtualPath: string;
1444
+ size: number;
1445
+ sourceUrl: string;
1446
+ }>>;
1238
1447
  }
1239
1448
  /**
1240
1449
  * Create a new TrackedFileManager instance
@@ -1821,6 +2030,10 @@ interface HazoFilesColumnDefinitions {
1821
2030
  original_filename: 'TEXT';
1822
2031
  /** Content tag classifying the document type (V3) */
1823
2032
  content_tag: 'TEXT';
2033
+ /** UUID of the actor who last mutated this record (V4 — migration 004) */
2034
+ changed_by: 'TEXT' | 'UUID';
2035
+ /** Source URL when file was imported via importFromUrl (V4 — migration 005) */
2036
+ source_url: 'TEXT';
1824
2037
  }
1825
2038
  /**
1826
2039
  * Schema definition for a specific database type