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.
@@ -1,3 +1,5 @@
1
+ import { JobHandler } from 'hazo_jobs';
2
+ import { Readable } from 'node:stream';
1
3
  import { OAuth2Client } from 'google-auth-library';
2
4
 
3
5
  /**
@@ -187,6 +189,10 @@ interface FileMetadataInput {
187
189
  original_filename?: string;
188
190
  /** Content tag classifying the document type (V3) */
189
191
  content_tag?: string;
192
+ /** Actor UUID who performed this mutation (V4) */
193
+ changed_by?: string;
194
+ /** Source URL when file was imported via importFromUrl (V4) */
195
+ source_url?: string;
190
196
  }
191
197
  /**
192
198
  * Input for updating an existing metadata record
@@ -433,6 +439,10 @@ interface FileMetadataRecordV2 extends FileMetadataRecord {
433
439
  deleted_at?: string | null;
434
440
  /** Content tag classifying the document type (V3) */
435
441
  content_tag?: string | null;
442
+ /** Actor UUID who last mutated this record (V4 — migration 004) */
443
+ changed_by?: string | null;
444
+ /** Source URL when file was imported via importFromUrl (V4 — migration 005) */
445
+ source_url?: string | null;
436
446
  }
437
447
  /**
438
448
  * Options for adding a reference to a file
@@ -770,8 +780,9 @@ declare class FileMetadataService {
770
780
  recordAccess(path: string, storageType: StorageProvider): Promise<boolean>;
771
781
  /**
772
782
  * Record a file deletion
783
+ * changedBy is accepted for API consistency but not written (record is deleted)
773
784
  */
774
- recordDelete(path: string, storageType: StorageProvider): Promise<boolean>;
785
+ recordDelete(path: string, storageType: StorageProvider, _changedBy?: string): Promise<boolean>;
775
786
  /**
776
787
  * Record a directory deletion (recursive)
777
788
  */
@@ -779,11 +790,11 @@ declare class FileMetadataService {
779
790
  /**
780
791
  * Record a file or folder move
781
792
  */
782
- recordMove(sourcePath: string, destinationPath: string, storageType: StorageProvider): Promise<boolean>;
793
+ recordMove(sourcePath: string, destinationPath: string, storageType: StorageProvider, changedBy?: string): Promise<boolean>;
783
794
  /**
784
795
  * Record a file or folder rename
785
796
  */
786
- recordRename(path: string, newName: string, storageType: StorageProvider): Promise<boolean>;
797
+ recordRename(path: string, newName: string, storageType: StorageProvider, changedBy?: string): Promise<boolean>;
787
798
  /**
788
799
  * Find a record by path and storage type
789
800
  */
@@ -887,9 +898,9 @@ declare class FileMetadataService {
887
898
  */
888
899
  softDelete(fileId: string): Promise<boolean>;
889
900
  /**
890
- * Update specific V2 fields on a record
901
+ * Update specific V2/V4 fields on a record
891
902
  */
892
- updateFields(fileId: string, fields: Partial<Pick<FileMetadataRecordV2, 'scope_id' | 'uploaded_by' | 'original_filename' | 'storage_verified_at' | 'status' | 'content_tag'>>): Promise<boolean>;
903
+ 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
904
  /**
894
905
  * Find orphaned files (zero references)
895
906
  */
@@ -1053,6 +1064,142 @@ declare function createFileManager(options?: FileManagerOptions): FileManager;
1053
1064
  */
1054
1065
  declare function createInitializedFileManager(options?: FileManagerOptions): Promise<FileManager>;
1055
1066
 
1067
+ /**
1068
+ * Quota Service
1069
+ * Per-scope opt-in quota tracking with threshold callbacks.
1070
+ *
1071
+ * A scope without a hazo_file_quotas row has no quota and uploads succeed (fail-open).
1072
+ */
1073
+ /**
1074
+ * Quota status for a scope
1075
+ */
1076
+ interface QuotaStatus {
1077
+ scopeId: string;
1078
+ byteLimit: number;
1079
+ byteUsed: number;
1080
+ percentUsed: number;
1081
+ }
1082
+ /**
1083
+ * Event emitted when usage crosses a configured threshold band
1084
+ */
1085
+ interface QuotaThresholdEvent {
1086
+ scopeId: string;
1087
+ /** Fractional threshold crossed, e.g. 0.80 or 0.95 */
1088
+ percent: number;
1089
+ bytesUsed: number;
1090
+ byteLimit: number;
1091
+ }
1092
+ /**
1093
+ * Raw row shape from hazo_file_quotas table
1094
+ */
1095
+ interface QuotaRow {
1096
+ scope_id: string;
1097
+ byte_limit: number;
1098
+ byte_used: number;
1099
+ updated_at: string;
1100
+ [key: string]: unknown;
1101
+ }
1102
+ /**
1103
+ * Minimal interface for the quota CRUD service (hazo_connect compatible)
1104
+ */
1105
+ interface QuotaCrudServiceLike {
1106
+ findBy(criteria: Record<string, unknown>): Promise<QuotaRow[]>;
1107
+ findOneBy(criteria: Record<string, unknown>): Promise<QuotaRow | null>;
1108
+ insert(data: Partial<QuotaRow> | Partial<QuotaRow>[]): Promise<QuotaRow[]>;
1109
+ updateById(id: unknown, patch: Partial<QuotaRow>): Promise<QuotaRow[]>;
1110
+ list(configure?: (qb: unknown) => unknown): Promise<QuotaRow[]>;
1111
+ }
1112
+ /**
1113
+ * Options for QuotaService
1114
+ */
1115
+ interface QuotaServiceOptions {
1116
+ /** hazo_connect CRUD service pointed at hazo_file_quotas table */
1117
+ crudService: QuotaCrudServiceLike;
1118
+ /** Callback fired when usage crosses a threshold band */
1119
+ onThreshold?: (event: QuotaThresholdEvent) => void;
1120
+ /** Fractional threshold bands (default: [0.80, 0.95]) */
1121
+ bands?: number[];
1122
+ /** Logger for diagnostics */
1123
+ logger?: {
1124
+ debug?(message: string, data?: Record<string, unknown>): void;
1125
+ warn?(message: string, data?: Record<string, unknown>): void;
1126
+ error?(message: string, data?: Record<string, unknown>): void;
1127
+ };
1128
+ }
1129
+ /**
1130
+ * Per-scope opt-in quota tracking.
1131
+ *
1132
+ * Fail-open: a scope without a quota row has no quota and all uploads succeed.
1133
+ */
1134
+ declare class QuotaService {
1135
+ private crud;
1136
+ private onThreshold?;
1137
+ private bands;
1138
+ private logger?;
1139
+ constructor(opts: QuotaServiceOptions);
1140
+ /**
1141
+ * Get quota status for a scope.
1142
+ * Returns null if no quota row exists (fail-open = no quota set).
1143
+ */
1144
+ getQuota(scopeId: string): Promise<QuotaStatus | null>;
1145
+ /**
1146
+ * Set (or update) the byte limit for a scope.
1147
+ * Creates a quota row if one does not exist (with byte_used = 0).
1148
+ * Returns the current stored status after upsert.
1149
+ *
1150
+ * @note This method does NOT auto-reconcile byte_used via a SUM query —
1151
+ * it simply upserts the limit and returns the stored row. To reconcile
1152
+ * byte_used against actual file sizes, call recomputeQuota() separately
1153
+ * after a SUM(file_size) query on hazo_files for the scope.
1154
+ */
1155
+ setQuotaLimit(scopeId: string, bytes: number): Promise<QuotaStatus>;
1156
+ /**
1157
+ * Recompute byteUsed by reading the current row.
1158
+ * (Full reconciliation against actual file sizes should be done externally
1159
+ * via a SUM query on hazo_files; this method just returns the stored state.)
1160
+ */
1161
+ recomputeQuota(scopeId: string): Promise<QuotaStatus>;
1162
+ /**
1163
+ * Pre-upload check ONLY — does NOT increment.
1164
+ * Throws QuotaExceededError if the upload would exceed the limit.
1165
+ * If no quota row exists for the scope, succeeds silently (fail-open).
1166
+ *
1167
+ * Use this before the upload, then call incrementUsage after confirmed success.
1168
+ * This prevents quota inflation when an upload fails mid-stream.
1169
+ */
1170
+ checkQuota(scopeId: string, deltaBytes: number): Promise<void>;
1171
+ /**
1172
+ * Pre-upload check and increment (atomic). Throws QuotaExceededError if the upload
1173
+ * would exceed the limit. If no quota row exists, succeeds silently (fail-open).
1174
+ * Also fires threshold callbacks for any bands crossed by the new usage.
1175
+ *
1176
+ * @deprecated Prefer checkQuota() before upload + incrementUsage() after success.
1177
+ * checkAndIncrement() increments before the upload completes; if the upload
1178
+ * subsequently fails the quota is inflated with no rollback.
1179
+ */
1180
+ checkAndIncrement(scopeId: string, deltaBytes: number): Promise<void>;
1181
+ /**
1182
+ * Decrement usage (call after soft-delete or hard-delete).
1183
+ * Clamps to zero; no-ops if no quota row.
1184
+ */
1185
+ decrementUsage(scopeId: string, deltaBytes: number): Promise<void>;
1186
+ /**
1187
+ * Increment usage manually (admin override).
1188
+ * Does NOT throw on exceeded quota — admin is explicitly bypassing.
1189
+ */
1190
+ incrementUsage(scopeId: string, deltaBytes: number): Promise<void>;
1191
+ /**
1192
+ * Fire threshold callbacks for all bands crossed going from prevUsed → newUsed.
1193
+ * Bands are sorted ascending so callbacks fire in order (80% before 95%).
1194
+ */
1195
+ private fireThresholdCallbacks;
1196
+ private rowToStatus;
1197
+ }
1198
+ /**
1199
+ * Create a QuotaService instance
1200
+ */
1201
+ declare function createQuotaService(opts: QuotaServiceOptions): QuotaService;
1202
+
1056
1203
  /**
1057
1204
  * Tracked File Manager
1058
1205
  * Extends FileManager to add database tracking of file operations
@@ -1068,6 +1215,10 @@ interface TrackedFileManagerFullOptions extends FileManagerOptions {
1068
1215
  tracking?: DatabaseTrackingConfig;
1069
1216
  /** Logger for structured file operation logging */
1070
1217
  logger?: MetadataLogger;
1218
+ /** Optional quota service for per-scope upload limits */
1219
+ quotaService?: QuotaService;
1220
+ /** SSRF allowlist passed to importFromUrl (host strings) */
1221
+ ssrfAllowlist?: string[];
1071
1222
  }
1072
1223
  /**
1073
1224
  * Extended upload options with hash tracking
@@ -1081,6 +1232,10 @@ interface TrackedUploadOptions extends UploadOptions {
1081
1232
  * Set to true when you need to immediately query/update the file record.
1082
1233
  */
1083
1234
  awaitRecording?: boolean;
1235
+ /** Actor ID (UUID) to record in uploaded_by and changed_by columns */
1236
+ actor_id?: string;
1237
+ /** Scope ID for quota tracking and organizational grouping */
1238
+ scope_id?: string;
1084
1239
  }
1085
1240
  /**
1086
1241
  * TrackedFileManager - File manager with database tracking
@@ -1091,6 +1246,8 @@ interface TrackedUploadOptions extends UploadOptions {
1091
1246
  declare class TrackedFileManager extends FileManager {
1092
1247
  private metadataService;
1093
1248
  private trackingConfig;
1249
+ private quotaService;
1250
+ private ssrfAllowlist;
1094
1251
  constructor(options?: TrackedFileManagerFullOptions);
1095
1252
  /**
1096
1253
  * Check if tracking is enabled and service is available
@@ -1120,19 +1277,27 @@ declare class TrackedFileManager extends FileManager {
1120
1277
  /**
1121
1278
  * Move a file or folder and update its path in the database
1122
1279
  */
1123
- moveItem(sourcePath: string, destinationPath: string, options?: MoveOptions): Promise<OperationResult<FileSystemItem>>;
1280
+ moveItem(sourcePath: string, destinationPath: string, options?: MoveOptions & {
1281
+ actor_id?: string;
1282
+ }): Promise<OperationResult<FileSystemItem>>;
1124
1283
  /**
1125
1284
  * Delete a file and remove its record from the database
1126
1285
  */
1127
- deleteFile(path: string): Promise<OperationResult>;
1286
+ deleteFile(path: string, opts?: {
1287
+ actor_id?: string;
1288
+ }): Promise<OperationResult>;
1128
1289
  /**
1129
1290
  * Rename a file and update its record in the database
1130
1291
  */
1131
- renameFile(path: string, newName: string, options?: RenameOptions): Promise<OperationResult<FileItem>>;
1292
+ renameFile(path: string, newName: string, options?: RenameOptions & {
1293
+ actor_id?: string;
1294
+ }): Promise<OperationResult<FileItem>>;
1132
1295
  /**
1133
1296
  * Rename a folder and update its record in the database
1134
1297
  */
1135
- renameFolder(path: string, newName: string, options?: RenameOptions): Promise<OperationResult<FolderItem>>;
1298
+ renameFolder(path: string, newName: string, options?: RenameOptions & {
1299
+ actor_id?: string;
1300
+ }): Promise<OperationResult<FolderItem>>;
1136
1301
  /**
1137
1302
  * Write a file with string content and track it
1138
1303
  */
@@ -1211,8 +1376,33 @@ declare class TrackedFileManager extends FileManager {
1211
1376
  getFilesById(fileIds: string[]): Promise<FileWithStatus[]>;
1212
1377
  /**
1213
1378
  * Soft-delete a file (marks as soft_deleted, does not remove physical file)
1379
+ * Also decrements quota usage if quotaService is configured.
1380
+ */
1381
+ softDeleteFile(fileId: string, opts?: {
1382
+ actor_id?: string;
1383
+ }): Promise<boolean>;
1384
+ /**
1385
+ * Get quota status for a scope.
1386
+ * Returns null if no quota is configured (fail-open).
1387
+ */
1388
+ getQuota(scopeId: string): Promise<QuotaStatus | null>;
1389
+ /**
1390
+ * Set or update the byte limit for a scope.
1391
+ * Creates a quota row if one does not exist.
1392
+ */
1393
+ setQuotaLimit(scopeId: string, bytes: number): Promise<QuotaStatus | null>;
1394
+ /**
1395
+ * Recompute and return the quota status for a scope.
1214
1396
  */
1215
- softDeleteFile(fileId: string): Promise<boolean>;
1397
+ recomputeQuota(scopeId: string): Promise<QuotaStatus | null>;
1398
+ /**
1399
+ * Increment usage for a scope (admin override — does not throw on exceeded quota).
1400
+ */
1401
+ incrementQuotaUsage(scopeId: string, deltaBytes: number): Promise<void>;
1402
+ /**
1403
+ * Decrement usage for a scope (e.g. after manual cleanup).
1404
+ */
1405
+ decrementQuotaUsage(scopeId: string, deltaBytes: number): Promise<void>;
1216
1406
  /**
1217
1407
  * Find orphaned files (files with zero references)
1218
1408
  */
@@ -1235,6 +1425,31 @@ declare class TrackedFileManager extends FileManager {
1235
1425
  file_id?: string;
1236
1426
  ref_id?: string;
1237
1427
  }>>;
1428
+ /**
1429
+ * Import a file from a URL into virtual storage.
1430
+ *
1431
+ * Uses hazo_secure/fetch for SSRF protection (optional peer dependency).
1432
+ * Streams the response to a temp file, counting bytes live.
1433
+ * On cap exceeded: aborts, deletes temp file, throws ImportSizeCapError.
1434
+ * On success: uploads to virtualPath, sets source_url in DB record.
1435
+ *
1436
+ * @param url - URL to fetch
1437
+ * @param virtualPath - Destination virtual path in storage
1438
+ * @param opts.referrer - Optional Referer header to send
1439
+ * @param opts.maxBytes - Maximum response size in bytes (default: 50MB)
1440
+ * @param opts.actor_id - Actor UUID to record in uploaded_by / changed_by
1441
+ */
1442
+ importFromUrl(url: string, virtualPath: string, opts?: {
1443
+ referrer?: string;
1444
+ maxBytes?: number;
1445
+ actor_id?: string;
1446
+ /** Scope ID for quota checking and tracking. If provided, quota is checked before upload. */
1447
+ scope_id?: string;
1448
+ }): Promise<OperationResult<{
1449
+ virtualPath: string;
1450
+ size: number;
1451
+ sourceUrl: string;
1452
+ }>>;
1238
1453
  }
1239
1454
  /**
1240
1455
  * Create a new TrackedFileManager instance
@@ -1804,6 +2019,29 @@ interface HazoFilesServerOptions {
1804
2019
  * When set, uploads can automatically classify document content.
1805
2020
  */
1806
2021
  defaultContentTagConfig?: ContentTagConfig;
2022
+ /**
2023
+ * SSRF protection configuration for importFromUrl.
2024
+ * When provided, only URLs matching the allowlist will be permitted.
2025
+ */
2026
+ ssrf?: {
2027
+ /** Allowed host strings (e.g. ['example.com', 'cdn.myapp.com']) */
2028
+ allowlist: string[];
2029
+ };
2030
+ /**
2031
+ * CRUD service for the hazo_file_quotas table.
2032
+ * Required if you want to use per-scope quota tracking.
2033
+ */
2034
+ quotaCrudService?: QuotaCrudServiceLike;
2035
+ /**
2036
+ * Callback fired when a quota threshold band is crossed.
2037
+ * Receives scopeId, percent crossed, bytesUsed, and byteLimit.
2038
+ */
2039
+ onQuotaThreshold?: (event: QuotaThresholdEvent) => void;
2040
+ /**
2041
+ * Fractional threshold bands that trigger onQuotaThreshold.
2042
+ * Default: [0.80, 0.95]
2043
+ */
2044
+ quotaBands?: number[];
1807
2045
  }
1808
2046
  /**
1809
2047
  * Result of createHazoFilesServer
@@ -1881,6 +2119,95 @@ declare function createHazoFilesServer(options?: HazoFilesServerOptions): Promis
1881
2119
  */
1882
2120
  declare function createBasicFileManager(config: HazoFilesConfig, crudService?: CrudServiceLike<FileMetadataRecord>): Promise<TrackedFileManager>;
1883
2121
 
2122
+ /**
2123
+ * Purge Job Handlers
2124
+ *
2125
+ * Factory for hazo_jobs-compatible job handlers that perform file lifecycle purge operations.
2126
+ * Uses types-only import from hazo_jobs — no runtime dependency.
2127
+ *
2128
+ * @example
2129
+ * ```typescript
2130
+ * import { createPurgeJobHandlers, HAZO_FILES_JOB_TYPES } from 'hazo_files/server';
2131
+ *
2132
+ * const handlers = createPurgeJobHandlers(fm, {
2133
+ * submitJob: (type, payload) => jobs.submit({ type, payload }),
2134
+ * });
2135
+ *
2136
+ * worker.register(HAZO_FILES_JOB_TYPES.PURGE_PLAN, handlers[HAZO_FILES_JOB_TYPES.PURGE_PLAN]);
2137
+ * worker.register(HAZO_FILES_JOB_TYPES.PURGE_ONE, handlers[HAZO_FILES_JOB_TYPES.PURGE_ONE]);
2138
+ * ```
2139
+ */
2140
+
2141
+ /**
2142
+ * Job type constants for hazo_files purge operations
2143
+ */
2144
+ declare const HAZO_FILES_JOB_TYPES: {
2145
+ readonly PURGE_PLAN: "hazo_files.purge_plan";
2146
+ readonly PURGE_ONE: "hazo_files.purge_one";
2147
+ };
2148
+ type HazoFilesJobType = typeof HAZO_FILES_JOB_TYPES[keyof typeof HAZO_FILES_JOB_TYPES];
2149
+ /**
2150
+ * Payload for the purge_plan job.
2151
+ * Finds soft-deleted files older than retentionDays and schedules purge_one jobs.
2152
+ */
2153
+ interface PurgePlanPayload {
2154
+ /** Number of days after soft-delete before a file is eligible for purge. Default: 30 */
2155
+ retentionDays?: number;
2156
+ /**
2157
+ * Dry-run mode: return wouldPurge list but do not submit any purge_one jobs
2158
+ * and do not delete anything.
2159
+ */
2160
+ dryRun?: boolean;
2161
+ }
2162
+ /**
2163
+ * Payload for the purge_one job.
2164
+ * Hard-deletes a single soft-deleted file record and its physical storage.
2165
+ */
2166
+ interface PurgeOnePayload {
2167
+ fileId: string;
2168
+ }
2169
+ /**
2170
+ * Result returned by the purge_plan handler
2171
+ */
2172
+ interface PurgePlanResult {
2173
+ /** Number of files purged (or queued for purge) */
2174
+ purgedCount: number;
2175
+ /**
2176
+ * Populated only when dryRun: true.
2177
+ * Array of fileIds that would be purged.
2178
+ */
2179
+ wouldPurge?: string[];
2180
+ }
2181
+ /**
2182
+ * Options for the purge job handler factory
2183
+ */
2184
+ interface PurgeJobHandlerOptions {
2185
+ /**
2186
+ * Function to submit a child job.
2187
+ * When provided, purge_plan submits individual purge_one jobs via this function.
2188
+ * When omitted, purge_plan returns the fileIds in the result and does not submit jobs.
2189
+ */
2190
+ submitJob?: (type: string, payload: unknown) => Promise<void>;
2191
+ /** Logger for diagnostics */
2192
+ logger?: {
2193
+ info?(message: string, data?: Record<string, unknown>): void;
2194
+ warn?(message: string, data?: Record<string, unknown>): void;
2195
+ error?(message: string, data?: Record<string, unknown>): void;
2196
+ };
2197
+ }
2198
+ /**
2199
+ * Create purge job handlers for hazo_files lifecycle management.
2200
+ *
2201
+ * The handlers are compatible with hazo_jobs JobHandler<TPayload, TResult>.
2202
+ *
2203
+ * @param fm - TrackedFileManager instance
2204
+ * @param opts - Optional submitJob function and logger
2205
+ */
2206
+ declare function createPurgeJobHandlers(fm: TrackedFileManager, opts?: PurgeJobHandlerOptions): {
2207
+ [HAZO_FILES_JOB_TYPES.PURGE_PLAN]: JobHandler<PurgePlanPayload, PurgePlanResult>;
2208
+ [HAZO_FILES_JOB_TYPES.PURGE_ONE]: JobHandler<PurgeOnePayload>;
2209
+ };
2210
+
1884
2211
  /**
1885
2212
  * Database Schema Exports for hazo_files
1886
2213
  *
@@ -1948,6 +2275,10 @@ interface HazoFilesColumnDefinitions {
1948
2275
  original_filename: 'TEXT';
1949
2276
  /** Content tag classifying the document type (V3) */
1950
2277
  content_tag: 'TEXT';
2278
+ /** UUID of the actor who last mutated this record (V4 — migration 004) */
2279
+ changed_by: 'TEXT' | 'UUID';
2280
+ /** Source URL when file was imported via importFromUrl (V4 — migration 005) */
2281
+ source_url: 'TEXT';
1951
2282
  }
1952
2283
  /**
1953
2284
  * Schema definition for a specific database type
@@ -2170,6 +2501,169 @@ declare const HAZO_FILES_MIGRATION_V3: HazoFilesMigrationV3;
2170
2501
  * Get V3 migration statements for a custom table name
2171
2502
  */
2172
2503
  declare function getMigrationV3ForTable(tableName: string, dbType: 'sqlite' | 'postgres'): MigrationSchemaDefinition;
2504
+ /**
2505
+ * Migration schema for adding V4 actor-tracking and source_url columns.
2506
+ *
2507
+ * @note PostgreSQL ALTER statements use `IF NOT EXISTS` and are idempotent.
2508
+ * SQLite `ALTER TABLE ADD COLUMN` does NOT support `IF NOT EXISTS` —
2509
+ * wrap each statement in a try/catch or check `PRAGMA table_info(hazo_files)`
2510
+ * before running on SQLite to avoid "duplicate column name" errors.
2511
+ *
2512
+ * @example
2513
+ * ```typescript
2514
+ * import { HAZO_FILES_MIGRATION_V4 } from 'hazo_files';
2515
+ *
2516
+ * // SQLite — wrap in try/catch (NOT idempotent)
2517
+ * for (const stmt of HAZO_FILES_MIGRATION_V4.sqlite.alterStatements) {
2518
+ * try { await db.run(stmt); } catch { /* column already exists *\/ }
2519
+ * }
2520
+ *
2521
+ * // PostgreSQL — idempotent (uses IF NOT EXISTS)
2522
+ * for (const stmt of HAZO_FILES_MIGRATION_V4.postgres.alterStatements) {
2523
+ * await client.query(stmt);
2524
+ * }
2525
+ * for (const idx of HAZO_FILES_MIGRATION_V4.postgres.indexes) {
2526
+ * await client.query(idx);
2527
+ * }
2528
+ * ```
2529
+ */
2530
+ interface HazoFilesMigrationV4 {
2531
+ /** Default table name */
2532
+ tableName: string;
2533
+ /** SQLite migration statements */
2534
+ sqlite: MigrationSchemaDefinition;
2535
+ /** PostgreSQL migration statements */
2536
+ postgres: MigrationSchemaDefinition;
2537
+ /** New column names added in V4 */
2538
+ newColumns: readonly string[];
2539
+ }
2540
+ declare const HAZO_FILES_MIGRATION_V4: HazoFilesMigrationV4;
2541
+ /**
2542
+ * Get V4 migration statements for a custom table name
2543
+ */
2544
+ declare function getMigrationV4ForTable(tableName: string, dbType: 'sqlite' | 'postgres'): MigrationSchemaDefinition;
2545
+ /**
2546
+ * Default table name for quota tracking
2547
+ */
2548
+ declare const HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME = "hazo_file_quotas";
2549
+ /**
2550
+ * Column definitions for the hazo_file_quotas table
2551
+ */
2552
+ interface HazoFileQuotasColumnDefinitions {
2553
+ /** Scope ID — primary key, links to organizational unit */
2554
+ scope_id: 'TEXT' | 'UUID';
2555
+ /** Maximum allowed bytes for this scope */
2556
+ byte_limit: 'INTEGER' | 'BIGINT';
2557
+ /** Current bytes used (reconciled against actual file records) */
2558
+ byte_used: 'INTEGER' | 'BIGINT';
2559
+ /** ISO timestamp of last update */
2560
+ updated_at: 'TEXT' | 'TIMESTAMP';
2561
+ }
2562
+ /**
2563
+ * Schema definition for hazo_file_quotas table.
2564
+ *
2565
+ * Opt-in per-scope quota. A scope without a row has no quota (fail-open).
2566
+ *
2567
+ * @example
2568
+ * ```typescript
2569
+ * import { HAZO_FILE_QUOTAS_TABLE_SCHEMA } from 'hazo_files';
2570
+ *
2571
+ * // PostgreSQL
2572
+ * await client.query(HAZO_FILE_QUOTAS_TABLE_SCHEMA.postgres.ddl);
2573
+ * for (const idx of HAZO_FILE_QUOTAS_TABLE_SCHEMA.postgres.indexes) {
2574
+ * await client.query(idx);
2575
+ * }
2576
+ * ```
2577
+ */
2578
+ declare const HAZO_FILE_QUOTAS_TABLE_SCHEMA: {
2579
+ tableName: string;
2580
+ sqlite: DatabaseSchemaDefinition;
2581
+ postgres: DatabaseSchemaDefinition;
2582
+ columns: readonly ["scope_id", "byte_limit", "byte_used", "updated_at"];
2583
+ };
2584
+
2585
+ type StoragePath = string;
2586
+ interface PutOpts {
2587
+ /** Reject if the target path already exists (atomic). */
2588
+ ifNotExists?: boolean;
2589
+ /** Hint for content-type; provider may sniff if absent. */
2590
+ contentType?: string;
2591
+ /** Free-form key/value metadata persisted with the file when supported. */
2592
+ metadata?: Record<string, string>;
2593
+ }
2594
+ interface PutResult {
2595
+ /** Provider tag — `"app_file_server"`, `"gdrive"`, `"in_memory"`. */
2596
+ provider: string;
2597
+ /** Provider-native identifier (path for app-server; file ID for GDrive). */
2598
+ native_id: string;
2599
+ /** Size in bytes of the persisted body. */
2600
+ size: number;
2601
+ }
2602
+ interface SignedUrlOpts {
2603
+ /** Seconds the URL is valid for. */
2604
+ ttl_seconds?: number;
2605
+ /** Suggested download filename (Content-Disposition). */
2606
+ filename_hint?: string;
2607
+ }
2608
+ interface ProbeResult {
2609
+ ok: boolean;
2610
+ /** Machine-readable error tag when ok=false. */
2611
+ error?: "drive_not_shared" | "write_denied" | "invalid_id" | "transient" | "config_missing";
2612
+ /** Free-form detail for logging. */
2613
+ message?: string;
2614
+ }
2615
+ /**
2616
+ * Storage provider abstraction. Every method MUST be idempotent at the
2617
+ * data-content level — re-invoking put with identical body is allowed.
2618
+ *
2619
+ * Paths are logical; providers translate to native identifiers internally.
2620
+ */
2621
+ interface FileStorageProvider {
2622
+ put(path: StoragePath, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
2623
+ get(path: StoragePath): Promise<Buffer | Readable>;
2624
+ delete(path: StoragePath): Promise<void>;
2625
+ exists(path: StoragePath): Promise<boolean>;
2626
+ getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
2627
+ /** Used by validation cron + onboarding step 2. */
2628
+ probe(): Promise<ProbeResult>;
2629
+ }
2630
+ declare class StorageCollisionExhausted extends Error {
2631
+ attempts: number;
2632
+ lastPath: StoragePath;
2633
+ constructor(attempts: number, lastPath: StoragePath);
2634
+ }
2635
+ declare class StorageNotConfigured extends Error {
2636
+ constructor();
2637
+ }
2638
+ declare class StorageUnavailable extends Error {
2639
+ reason: ProbeResult["error"];
2640
+ constructor(reason: ProbeResult["error"], message: string);
2641
+ }
2642
+
2643
+ interface AppFileServerOpts {
2644
+ /** Filesystem root. All paths are resolved relative to this. */
2645
+ root: string;
2646
+ /** HMAC secret used to sign download URLs. */
2647
+ hmac_secret: string;
2648
+ /** Default signed-URL TTL in seconds. */
2649
+ default_ttl_seconds?: number;
2650
+ }
2651
+ declare class AppFileServerProvider implements FileStorageProvider {
2652
+ readonly provider_tag: "app_file_server";
2653
+ private readonly root;
2654
+ private readonly secret;
2655
+ private readonly default_ttl;
2656
+ constructor(opts: AppFileServerOpts);
2657
+ private resolve;
2658
+ put(path: string, body: Buffer | Readable, opts?: PutOpts): Promise<PutResult>;
2659
+ get(path: string): Promise<Buffer>;
2660
+ delete(path: string): Promise<void>;
2661
+ exists(path: string): Promise<boolean>;
2662
+ getSignedUrl(path: string, opts?: SignedUrlOpts): Promise<string>;
2663
+ /** Verify a token produced by `getSignedUrl`. Used by the `/api/files/serve` route. */
2664
+ verifySignedUrl(token: string, path: string): boolean;
2665
+ probe(): Promise<ProbeResult>;
2666
+ }
2173
2667
 
2174
2668
  /**
2175
2669
  * Migration: Add Reference Tracking (V2)
@@ -2853,6 +3347,15 @@ declare class ConfigurationError extends HazoFilesError {
2853
3347
  declare class OperationError extends HazoFilesError {
2854
3348
  constructor(operation: string, message: string, details?: Record<string, unknown>);
2855
3349
  }
3350
+ declare class QuotaExceededError extends HazoFilesError {
3351
+ constructor(scopeId: string, byteUsed: number, byteLimit: number, deltaBytes: number);
3352
+ }
3353
+ declare class SSRFError extends HazoFilesError {
3354
+ constructor(url: string, reason: string);
3355
+ }
3356
+ declare class ImportSizeCapError extends HazoFilesError {
3357
+ constructor(url: string, capBytes: number);
3358
+ }
2856
3359
 
2857
3360
  /**
2858
3361
  * MIME type utilities
@@ -3225,4 +3728,4 @@ declare function toV2Record(record: FileMetadataRecord): FileMetadataRecordV2;
3225
3728
  */
3226
3729
  declare function buildFileWithStatus(record: FileMetadataRecord): FileWithStatus;
3227
3730
 
3228
- export { ALL_SYSTEM_VARIABLES, type AddExtractionOptions, type AddRefOptions, type AuthCallbacks, AuthenticationError, type CleanupOrphanedOptions, ConfigurationError, type ContentTagConfig, type CreateFolderOptions, type CrudServiceLike, DEFAULT_DATE_FORMATS, type DatabaseSchemaDefinition, type DatabaseTrackingConfig, DirectoryExistsError, DirectoryNotEmptyError, DirectoryNotFoundError, type DownloadOptions, DropboxAuth, type DropboxAuthCallbacks, type DropboxAuthConfig, type DropboxConfig, DropboxModule, type DropboxTokenData, type ExtractionData, type ExtractionOptions, type ExtractionResult, type FileBrowserState, type FileDataStructure, FileExistsError, type FileInfo, type FileItem, FileManager, type FileManagerOptions, type FileMetadataInput, type FileMetadataRecord, type FileMetadataRecordV2, FileMetadataService, type FileMetadataServiceOptions, type FileMetadataUpdate, FileNotFoundError, type FileRef, type FileRefVisibility, type FileStatus, type FileSystemItem, FileTooLargeError, type FileWithStatus, type FindOrphanedOptions, type FolderItem, type GeneratedNameResult, type GoogleAuthConfig, GoogleDriveAuth, type GoogleDriveConfig, GoogleDriveModule, HAZO_FILES_DEFAULT_TABLE_NAME, HAZO_FILES_MIGRATION_V2, HAZO_FILES_MIGRATION_V3, HAZO_FILES_NAMING_DEFAULT_TABLE_NAME, HAZO_FILES_NAMING_TABLE_SCHEMA, HAZO_FILES_TABLE_SCHEMA, type HazoFilesColumnDefinitions, type HazoFilesConfig, HazoFilesError, type HazoFilesMigrationV2, type HazoFilesMigrationV3, type HazoFilesNamingColumnDefinitions, type HazoFilesNamingTableSchema, type HazoFilesServerInstance, type HazoFilesServerOptions, type HazoFilesTableSchema, type HazoLLMInstance, type HazoLogger, InvalidExtensionError, InvalidPathError, LLMExtractionService, type LLMFactory, type LLMFactoryConfig, type LLMProvider, type ListNamingConventionsOptions, type ListOptions, type LocalStorageConfig, LocalStorageModule, type MetadataLogger, type MigrationExecutor, type MigrationSchemaDefinition, type MoveOptions, type NameGenerationOptions, type NamingConventionInput, type NamingConventionRecord, NamingConventionService, type NamingConventionServiceOptions, type NamingConventionType, type NamingConventionUpdate, type NamingRuleConfiguratorProps, type NamingRuleHistoryEntry, type NamingRuleSchema, type NamingVariable, OperationError, type OperationResult, type ParsedNamingConvention, type PatternSegment, PermissionDeniedError, type ProgressCallback, type RemoveExtractionOptions, type RemoveRefsCriteria, type RenameOptions, SYSTEM_COUNTER_VARIABLES, SYSTEM_DATE_VARIABLES, SYSTEM_FILE_VARIABLES, type StorageModule, type StorageProvider, type TokenData, TrackedFileManager, type TrackedFileManagerFullOptions, type TrackedFileManagerOptions, type TrackedUploadOptions, type TreeNode, type UploadExtractOptions, type UploadExtractResult, UploadExtractService, type UploadOptions, type UploadWithRefOptions, type UseNamingRuleActions, type UseNamingRuleReturn, type UseNamingRuleState, type VariableCategory, addExtractionToFileData, backfillV2Defaults, buildFileWithStatus, clearExtractions, clonePattern, computeFileHash, computeFileHashFromStream, computeFileHashSync, computeFileInfo, createAndInitializeModule, createBasicFileManager, createDropboxAuth, createDropboxModule, createEmptyFileDataStructure, createEmptyNamingRuleSchema, createFileItem, createFileManager, createFileMetadataService, createFileRef, createFolderItem, createGoogleDriveAuth, createGoogleDriveModule, createHazoFilesServer, createInitializedFileManager, createInitializedTrackedFileManager, createLLMExtractionService, createLiteralSegment, createLocalModule, createModule, createNamingConventionService, createTrackedFileManager, createUploadExtractService, createVariableSegment, deepMerge, errorResult, filterItems, formatBytes, formatCounter, formatDateToken, generateExtractionId, generateId, generatePreviewName, generateRefId, generateSampleConfig, generateSegmentId, getBaseName, getBreadcrumbs, getDirName, getExtension, getExtensionFromMime, getExtractionById, getExtractionCount, getExtractions, getFileCategory, getFileMetadataValues, getMergedData, getMigrationForTable, getMigrationV3ForTable, getMimeType, getNameWithoutExtension, getNamingSchemaForTable, getParentPath, getPathSegments, getRegisteredProviders, getRelativePath, getSchemaForTable, getSystemVariablePreviewValues, hasExtension, hasExtractionStructure, hasFileContentChanged, hashesEqual, hazo_files_generate_file_name, hazo_files_generate_folder_name, isAudio, isChildPath, isCounterVariable, isDateVariable, isDocument, isFile, isFileMetadataVariable, isFolder, isImage, isPreviewable, isProviderRegistered, isText, isVideo, joinPath, loadConfig, loadConfigAsync, migrateToV2, migrateToV3, normalizePath, parseConfig, parseFileData, parseFileRefs, parsePatternString, patternToString, recalculateMergedData, registerModule, removeExtractionById, removeExtractionByIndex, removeRefFromArray, removeRefsByCriteriaFromArray, sanitizeFilename, saveConfig, sortItems, stringifyFileData, stringifyFileRefs, successResult, toV2Record, updateExtractionById, validateExtractionData, validateFileDataStructure, validateNamingRuleSchema, validatePath };
3731
+ export { ALL_SYSTEM_VARIABLES, type AddExtractionOptions, type AddRefOptions, type AppFileServerOpts, AppFileServerProvider, type AuthCallbacks, AuthenticationError, type CleanupOrphanedOptions, ConfigurationError, type ContentTagConfig, type CreateFolderOptions, type CrudServiceLike, DEFAULT_DATE_FORMATS, type DatabaseSchemaDefinition, type DatabaseTrackingConfig, DirectoryExistsError, DirectoryNotEmptyError, DirectoryNotFoundError, type DownloadOptions, DropboxAuth, type DropboxAuthCallbacks, type DropboxAuthConfig, type DropboxConfig, DropboxModule, type DropboxTokenData, type ExtractionData, type ExtractionOptions, type ExtractionResult, type FileBrowserState, type FileDataStructure, FileExistsError, type FileInfo, type FileItem, FileManager, type FileManagerOptions, type FileMetadataInput, type FileMetadataRecord, type FileMetadataRecordV2, FileMetadataService, type FileMetadataServiceOptions, type FileMetadataUpdate, FileNotFoundError, type FileRef, type FileRefVisibility, type FileStatus, type FileStorageProvider, type FileSystemItem, FileTooLargeError, type FileWithStatus, type FindOrphanedOptions, type FolderItem, type GeneratedNameResult, type GoogleAuthConfig, GoogleDriveAuth, type GoogleDriveConfig, GoogleDriveModule, HAZO_FILES_DEFAULT_TABLE_NAME, HAZO_FILES_JOB_TYPES, HAZO_FILES_MIGRATION_V2, HAZO_FILES_MIGRATION_V3, HAZO_FILES_MIGRATION_V4, HAZO_FILES_NAMING_DEFAULT_TABLE_NAME, HAZO_FILES_NAMING_TABLE_SCHEMA, HAZO_FILES_TABLE_SCHEMA, HAZO_FILE_QUOTAS_DEFAULT_TABLE_NAME, HAZO_FILE_QUOTAS_TABLE_SCHEMA, type HazoFileQuotasColumnDefinitions, type HazoFilesColumnDefinitions, type HazoFilesConfig, HazoFilesError, type HazoFilesJobType, type HazoFilesMigrationV2, type HazoFilesMigrationV3, type HazoFilesMigrationV4, type HazoFilesNamingColumnDefinitions, type HazoFilesNamingTableSchema, type HazoFilesServerInstance, type HazoFilesServerOptions, type HazoFilesTableSchema, type HazoLLMInstance, type HazoLogger, ImportSizeCapError, InvalidExtensionError, InvalidPathError, LLMExtractionService, type LLMFactory, type LLMFactoryConfig, type LLMProvider, type ListNamingConventionsOptions, type ListOptions, type LocalStorageConfig, LocalStorageModule, type MetadataLogger, type MigrationExecutor, type MigrationSchemaDefinition, type MoveOptions, type NameGenerationOptions, type NamingConventionInput, type NamingConventionRecord, NamingConventionService, type NamingConventionServiceOptions, type NamingConventionType, type NamingConventionUpdate, type NamingRuleConfiguratorProps, type NamingRuleHistoryEntry, type NamingRuleSchema, type NamingVariable, OperationError, type OperationResult, type ParsedNamingConvention, type PatternSegment, PermissionDeniedError, type ProbeResult, type ProgressCallback, type PurgeJobHandlerOptions, type PurgeOnePayload, type PurgePlanPayload, type PurgePlanResult, type PutOpts, type PutResult, type QuotaCrudServiceLike, QuotaExceededError, QuotaService, type QuotaServiceOptions, type QuotaStatus, type QuotaThresholdEvent, type RemoveExtractionOptions, type RemoveRefsCriteria, type RenameOptions, SSRFError, SYSTEM_COUNTER_VARIABLES, SYSTEM_DATE_VARIABLES, SYSTEM_FILE_VARIABLES, type SignedUrlOpts, StorageCollisionExhausted, type StorageModule, StorageNotConfigured, type StoragePath, type StorageProvider, StorageUnavailable, type TokenData, TrackedFileManager, type TrackedFileManagerFullOptions, type TrackedFileManagerOptions, type TrackedUploadOptions, type TreeNode, type UploadExtractOptions, type UploadExtractResult, UploadExtractService, type UploadOptions, type UploadWithRefOptions, type UseNamingRuleActions, type UseNamingRuleReturn, type UseNamingRuleState, type VariableCategory, addExtractionToFileData, backfillV2Defaults, buildFileWithStatus, clearExtractions, clonePattern, computeFileHash, computeFileHashFromStream, computeFileHashSync, computeFileInfo, createAndInitializeModule, createBasicFileManager, createDropboxAuth, createDropboxModule, createEmptyFileDataStructure, createEmptyNamingRuleSchema, createFileItem, createFileManager, createFileMetadataService, createFileRef, createFolderItem, createGoogleDriveAuth, createGoogleDriveModule, createHazoFilesServer, createInitializedFileManager, createInitializedTrackedFileManager, createLLMExtractionService, createLiteralSegment, createLocalModule, createModule, createNamingConventionService, createPurgeJobHandlers, createQuotaService, createTrackedFileManager, createUploadExtractService, createVariableSegment, deepMerge, errorResult, filterItems, formatBytes, formatCounter, formatDateToken, generateExtractionId, generateId, generatePreviewName, generateRefId, generateSampleConfig, generateSegmentId, getBaseName, getBreadcrumbs, getDirName, getExtension, getExtensionFromMime, getExtractionById, getExtractionCount, getExtractions, getFileCategory, getFileMetadataValues, getMergedData, getMigrationForTable, getMigrationV3ForTable, getMigrationV4ForTable, getMimeType, getNameWithoutExtension, getNamingSchemaForTable, getParentPath, getPathSegments, getRegisteredProviders, getRelativePath, getSchemaForTable, getSystemVariablePreviewValues, hasExtension, hasExtractionStructure, hasFileContentChanged, hashesEqual, hazo_files_generate_file_name, hazo_files_generate_folder_name, isAudio, isChildPath, isCounterVariable, isDateVariable, isDocument, isFile, isFileMetadataVariable, isFolder, isImage, isPreviewable, isProviderRegistered, isText, isVideo, joinPath, loadConfig, loadConfigAsync, migrateToV2, migrateToV3, normalizePath, parseConfig, parseFileData, parseFileRefs, parsePatternString, patternToString, recalculateMergedData, registerModule, removeExtractionById, removeExtractionByIndex, removeRefFromArray, removeRefsByCriteriaFromArray, sanitizeFilename, saveConfig, sortItems, stringifyFileData, stringifyFileRefs, successResult, toV2Record, updateExtractionById, validateExtractionData, validateFileDataStructure, validateNamingRuleSchema, validatePath };