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/CHANGE_LOG.md +16 -0
- package/README.md +185 -0
- package/SETUP_CHECKLIST.md +96 -0
- package/dist/index.d.mts +223 -10
- package/dist/index.d.ts +223 -10
- package/dist/index.js +406 -169
- package/dist/index.mjs +406 -169
- package/dist/server/index.d.mts +514 -11
- package/dist/server/index.d.ts +514 -11
- package/dist/server/index.js +947 -171
- package/dist/server/index.mjs +939 -171
- package/docs/superpowers/plans/2026-05-23-test-app-v2-providers.md +968 -0
- package/migrations/004_changed_by.sql +10 -0
- package/migrations/005_source_url.sql +10 -0
- package/migrations/006_quota_tracking.sql +20 -0
- package/package.json +14 -3
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|