hazo_files 3.0.0 → 3.1.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.
@@ -2614,6 +2614,22 @@ interface ProbeResult {
2614
2614
  /** Free-form detail for logging. */
2615
2615
  message?: string;
2616
2616
  }
2617
+ /**
2618
+ * A single entry returned by `FileStorageProvider.list()`.
2619
+ *
2620
+ * `path` is the same logical namespace used by `put`/`get` — callers can pass
2621
+ * it directly back to those methods without any transformation.
2622
+ */
2623
+ interface FileEntry {
2624
+ /** Logical path within the provider (same namespace as put/get). */
2625
+ path: StoragePath;
2626
+ /** Bare filename or directory name (the last segment of `path`). */
2627
+ name: string;
2628
+ /** Size in bytes. 0 for directories. */
2629
+ size: number;
2630
+ /** True when this entry represents a directory. */
2631
+ isDirectory: boolean;
2632
+ }
2617
2633
  /**
2618
2634
  * Storage provider abstraction. Every method MUST be idempotent at the
2619
2635
  * data-content level — re-invoking put with identical body is allowed.
@@ -2628,6 +2644,24 @@ interface FileStorageProvider {
2628
2644
  getSignedUrl(path: StoragePath, opts?: SignedUrlOpts): Promise<string>;
2629
2645
  /** Used by validation cron + onboarding step 2. */
2630
2646
  probe(): Promise<ProbeResult>;
2647
+ /**
2648
+ * List all entries (files and sub-directories) under a logical path prefix.
2649
+ * Results are recursive — all descendants are included.
2650
+ * Returns an empty array when the prefix does not exist.
2651
+ */
2652
+ list(prefix: StoragePath): Promise<FileEntry[]>;
2653
+ /**
2654
+ * Move bytes from `from` to `to`, creating any intermediate directories.
2655
+ * The source is removed on success.
2656
+ * Throws with a message containing "Not found" when `from` does not exist.
2657
+ */
2658
+ move(from: StoragePath, to: StoragePath): Promise<void>;
2659
+ /**
2660
+ * Rename a file or directory. `to` is the full new logical path.
2661
+ * Implementations may delegate to `move`.
2662
+ * Throws with a message containing "Not found" when `from` does not exist.
2663
+ */
2664
+ rename(from: StoragePath, to: StoragePath): Promise<void>;
2631
2665
  }
2632
2666
  declare class StorageCollisionExhausted extends Error {
2633
2667
  attempts: number;
@@ -2665,6 +2699,10 @@ declare class AppFileServerProvider implements FileStorageProvider {
2665
2699
  /** Verify a token produced by `getSignedUrl`. Used by the `/api/files/serve` route. */
2666
2700
  verifySignedUrl(token: string, path: string): boolean;
2667
2701
  probe(): Promise<ProbeResult>;
2702
+ list(prefix: string): Promise<FileEntry[]>;
2703
+ private walkDir;
2704
+ move(from: string, to: string): Promise<void>;
2705
+ rename(from: string, to: string): Promise<void>;
2668
2706
  }
2669
2707
 
2670
2708
  declare class InMemoryProvider implements FileStorageProvider {
@@ -2676,6 +2714,9 @@ declare class InMemoryProvider implements FileStorageProvider {
2676
2714
  exists(path: string): Promise<boolean>;
2677
2715
  getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
2678
2716
  probe(): Promise<ProbeResult>;
2717
+ list(prefix: string): Promise<FileEntry[]>;
2718
+ move(from: string, to: string): Promise<void>;
2719
+ rename(from: string, to: string): Promise<void>;
2679
2720
  /** Test-only escape hatch — exposes the internal store for assertions. */
2680
2721
  snapshot(): Map<string, Buffer>;
2681
2722
  }
@@ -2705,6 +2746,9 @@ declare class GoogleDriveProvider implements FileStorageProvider {
2705
2746
  delete(path: string): Promise<void>;
2706
2747
  exists(path: string): Promise<boolean>;
2707
2748
  getSignedUrl(path: string, _opts?: SignedUrlOpts): Promise<string>;
2749
+ list(prefix: string): Promise<FileEntry[]>;
2750
+ move(from: string, to: string): Promise<void>;
2751
+ rename(from: string, to: string): Promise<void>;
2708
2752
  private resolvePath;
2709
2753
  private findChild;
2710
2754
  private lookupFileId;
@@ -3770,4 +3814,43 @@ declare function toV2Record(record: FileMetadataRecord): FileMetadataRecordV2;
3770
3814
  */
3771
3815
  declare function buildFileWithStatus(record: FileMetadataRecord): FileWithStatus;
3772
3816
 
3773
- 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, type DrivePathCache, 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, type GoogleDriveOpts, GoogleDriveProvider, 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, type HazoFilesJobType, type HazoFilesMigrationV2, type HazoFilesMigrationV3, type HazoFilesMigrationV4, type HazoFilesNamingColumnDefinitions, type HazoFilesNamingTableSchema, type HazoFilesServerInstance, type HazoFilesServerOptions, type HazoFilesTableSchema, type HazoLLMInstance, type HazoLogger, ImportSizeCapError, InMemoryProvider, 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 };
3817
+ interface ExternalFileLinkOptions {
3818
+ serve_base?: string;
3819
+ serve_path?: string;
3820
+ metadata?: FileMetadataRecord;
3821
+ }
3822
+ interface ExternalFileLink {
3823
+ url: string;
3824
+ link_type: 'provider_managed' | 'app_gated';
3825
+ storage_type: StorageProvider | string;
3826
+ }
3827
+ interface ExternalFileLinkDeps {
3828
+ metadataService: Pick<FileMetadataService, 'findById'>;
3829
+ }
3830
+ declare function external_file_link(file_id: string, deps: ExternalFileLinkDeps, opts?: ExternalFileLinkOptions): Promise<ExternalFileLink>;
3831
+ type ExternalFileLinkResult = ExternalFileLink | {
3832
+ error: string;
3833
+ };
3834
+ declare function external_file_links(file_ids: string[], deps: ExternalFileLinkDeps, opts?: ExternalFileLinkOptions): Promise<Record<string, ExternalFileLinkResult>>;
3835
+
3836
+ interface FileServeAuthContext {
3837
+ request: Request;
3838
+ file_id: string;
3839
+ }
3840
+ interface FileServeUser {
3841
+ user_id?: string;
3842
+ [k: string]: unknown;
3843
+ }
3844
+ interface FileServeHandlerOptions {
3845
+ provider: Pick<FileStorageProvider, 'get'>;
3846
+ metadataService: Pick<FileMetadataService, 'findById'>;
3847
+ authenticate?: (ctx: FileServeAuthContext) => Promise<FileServeUser | null>;
3848
+ authorize?: (ctx: FileServeAuthContext & {
3849
+ user: FileServeUser;
3850
+ metadata: FileMetadataRecord;
3851
+ }) => Promise<boolean>;
3852
+ get_file_id?: (request: Request) => string | null;
3853
+ }
3854
+ declare function createFileServeHandler(opts: FileServeHandlerOptions): (request: Request) => Promise<Response>;
3855
+
3856
+ 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, type DrivePathCache, DropboxAuth, type DropboxAuthCallbacks, type DropboxAuthConfig, type DropboxConfig, DropboxModule, type DropboxTokenData, type ExternalFileLink, type ExternalFileLinkDeps, type ExternalFileLinkOptions, type ExternalFileLinkResult, type ExtractionData, type ExtractionOptions, type ExtractionResult, type FileBrowserState, type FileDataStructure, type FileEntry, 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 FileServeAuthContext, type FileServeHandlerOptions, type FileServeUser, type FileStatus, type FileStorageProvider, type FileSystemItem, FileTooLargeError, type FileWithStatus, type FindOrphanedOptions, type FolderItem, type GeneratedNameResult, type GoogleAuthConfig, GoogleDriveAuth, type GoogleDriveConfig, GoogleDriveModule, type GoogleDriveOpts, GoogleDriveProvider, 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, type HazoFilesJobType, type HazoFilesMigrationV2, type HazoFilesMigrationV3, type HazoFilesMigrationV4, type HazoFilesNamingColumnDefinitions, type HazoFilesNamingTableSchema, type HazoFilesServerInstance, type HazoFilesServerOptions, type HazoFilesTableSchema, type HazoLLMInstance, type HazoLogger, ImportSizeCapError, InMemoryProvider, 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, createFileServeHandler, createFolderItem, createGoogleDriveAuth, createGoogleDriveModule, createHazoFilesServer, createInitializedFileManager, createInitializedTrackedFileManager, createLLMExtractionService, createLiteralSegment, createLocalModule, createModule, createNamingConventionService, createPurgeJobHandlers, createQuotaService, createTrackedFileManager, createUploadExtractService, createVariableSegment, deepMerge, errorResult, external_file_link, external_file_links, 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 };
@@ -98,6 +98,7 @@ __export(index_exports, {
98
98
  createFileManager: () => createFileManager,
99
99
  createFileMetadataService: () => createFileMetadataService,
100
100
  createFileRef: () => createFileRef,
101
+ createFileServeHandler: () => createFileServeHandler,
101
102
  createFolderItem: () => createFolderItem,
102
103
  createGoogleDriveAuth: () => createGoogleDriveAuth,
103
104
  createGoogleDriveModule: () => createGoogleDriveModule,
@@ -116,6 +117,8 @@ __export(index_exports, {
116
117
  createVariableSegment: () => createVariableSegment,
117
118
  deepMerge: () => deepMerge,
118
119
  errorResult: () => errorResult,
120
+ external_file_link: () => external_file_link,
121
+ external_file_links: () => external_file_links,
119
122
  filterItems: () => filterItems,
120
123
  formatBytes: () => formatBytes,
121
124
  formatCounter: () => formatCounter,
@@ -21090,6 +21093,51 @@ var AppFileServerProvider = class {
21090
21093
  return { ok: false, error: "write_denied", message: String(err) };
21091
21094
  }
21092
21095
  }
21096
+ async list(prefix) {
21097
+ const abs = this.resolve(prefix);
21098
+ if (!(0, import_node_fs.existsSync)(abs)) return [];
21099
+ const stat = (0, import_node_fs.statSync)(abs);
21100
+ if (!stat.isDirectory()) {
21101
+ return [{ path: prefix, name: (0, import_node_path.basename)(prefix), size: stat.size, isDirectory: false }];
21102
+ }
21103
+ const entries = [];
21104
+ this.walkDir(abs, prefix, entries);
21105
+ return entries;
21106
+ }
21107
+ walkDir(absDir, logicalDir, out) {
21108
+ const base = logicalDir.endsWith("/") ? logicalDir : `${logicalDir}/`;
21109
+ for (const item of (0, import_node_fs.readdirSync)(absDir, { withFileTypes: true })) {
21110
+ const logicalPath = `${base}${item.name}`;
21111
+ const absPath = (0, import_node_path.join)(absDir, item.name);
21112
+ if (item.isDirectory()) {
21113
+ out.push({ path: logicalPath, name: item.name, size: 0, isDirectory: true });
21114
+ this.walkDir(absPath, logicalPath, out);
21115
+ } else {
21116
+ const st = (0, import_node_fs.statSync)(absPath);
21117
+ out.push({ path: logicalPath, name: item.name, size: st.size, isDirectory: false });
21118
+ }
21119
+ }
21120
+ }
21121
+ async move(from, to) {
21122
+ const absFrom = this.resolve(from);
21123
+ const absTo = this.resolve(to);
21124
+ if (!(0, import_node_fs.existsSync)(absFrom)) throw new Error(`Not found: ${from}`);
21125
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(absTo), { recursive: true });
21126
+ try {
21127
+ (0, import_node_fs.renameSync)(absFrom, absTo);
21128
+ } catch (err) {
21129
+ const e = err;
21130
+ if (e.code === "EXDEV") {
21131
+ (0, import_node_fs.copyFileSync)(absFrom, absTo);
21132
+ (0, import_node_fs.rmSync)(absFrom, { force: true });
21133
+ } else {
21134
+ throw err;
21135
+ }
21136
+ }
21137
+ }
21138
+ async rename(from, to) {
21139
+ return this.move(from, to);
21140
+ }
21093
21141
  };
21094
21142
  async function streamToBuffer(s) {
21095
21143
  const chunks = [];
@@ -21128,6 +21176,39 @@ var InMemoryProvider = class {
21128
21176
  async probe() {
21129
21177
  return { ok: true };
21130
21178
  }
21179
+ async list(prefix) {
21180
+ const base = prefix.replace(/\/$/, "");
21181
+ const matchPrefix = `${base}/`;
21182
+ const entries = [];
21183
+ const seenDirs = /* @__PURE__ */ new Set();
21184
+ for (const [key, buf] of this.store.entries()) {
21185
+ if (key !== base && !key.startsWith(matchPrefix)) continue;
21186
+ const rel = key.startsWith(matchPrefix) ? key.slice(matchPrefix.length) : "";
21187
+ if (!rel) {
21188
+ entries.push({ path: key, name: key.split("/").pop(), size: buf.length, isDirectory: false });
21189
+ continue;
21190
+ }
21191
+ const parts = rel.split("/");
21192
+ for (let i = 1; i < parts.length; i++) {
21193
+ const dirPath = matchPrefix + parts.slice(0, i).join("/");
21194
+ if (!seenDirs.has(dirPath)) {
21195
+ seenDirs.add(dirPath);
21196
+ entries.push({ path: dirPath, name: parts[i - 1], size: 0, isDirectory: true });
21197
+ }
21198
+ }
21199
+ entries.push({ path: key, name: parts[parts.length - 1], size: buf.length, isDirectory: false });
21200
+ }
21201
+ return entries;
21202
+ }
21203
+ async move(from, to) {
21204
+ const buf = this.store.get(from);
21205
+ if (!buf) throw new Error(`Not found: ${from}`);
21206
+ this.store.set(to, buf);
21207
+ this.store.delete(from);
21208
+ }
21209
+ async rename(from, to) {
21210
+ return this.move(from, to);
21211
+ }
21131
21212
  /** Test-only escape hatch — exposes the internal store for assertions. */
21132
21213
  snapshot() {
21133
21214
  return new Map(this.store);
@@ -21223,6 +21304,60 @@ var GoogleDriveProvider = class {
21223
21304
  const id = await this.lookupFileId(path4);
21224
21305
  return `https://drive.google.com/uc?id=${id}&export=download`;
21225
21306
  }
21307
+ async list(prefix) {
21308
+ const segments = prefix.split("/").filter(Boolean);
21309
+ let parentId;
21310
+ try {
21311
+ parentId = await this.resolvePath(segments, { create: false });
21312
+ } catch {
21313
+ return [];
21314
+ }
21315
+ const res = await this.drive.files.list({
21316
+ q: `'${parentId}' in parents and trashed=false`,
21317
+ driveId: this.driveId,
21318
+ corpora: "drive",
21319
+ includeItemsFromAllDrives: true,
21320
+ fields: "files(id, name, mimeType, size)"
21321
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21322
+ });
21323
+ const files = res.data.files ?? [];
21324
+ const base = prefix.endsWith("/") ? prefix : `${prefix}/`;
21325
+ return files.map((f) => ({
21326
+ path: `${base}${f.name}`,
21327
+ name: f.name,
21328
+ size: Number(f.size ?? 0),
21329
+ isDirectory: f.mimeType === "application/vnd.google-apps.folder"
21330
+ }));
21331
+ }
21332
+ async move(from, to) {
21333
+ const id = await this.lookupFileId(from);
21334
+ const fromSegs = from.split("/").filter(Boolean);
21335
+ fromSegs.pop();
21336
+ const toSegs = to.split("/").filter(Boolean);
21337
+ const newName = toSegs.pop();
21338
+ const oldParentId = await this.resolvePath(fromSegs, { create: false });
21339
+ const newParentId = await this.resolvePath(toSegs, { create: true });
21340
+ await this.drive.files.update({
21341
+ fileId: id,
21342
+ addParents: newParentId,
21343
+ removeParents: oldParentId,
21344
+ requestBody: { name: newName },
21345
+ supportsAllDrives: true,
21346
+ fields: "id, parents"
21347
+ });
21348
+ await this.cache.invalidate(from);
21349
+ }
21350
+ async rename(from, to) {
21351
+ const id = await this.lookupFileId(from);
21352
+ const segs = to.split("/").filter(Boolean);
21353
+ const newName = segs[segs.length - 1];
21354
+ await this.drive.files.update({
21355
+ fileId: id,
21356
+ requestBody: { name: newName },
21357
+ supportsAllDrives: true,
21358
+ fields: "id, name"
21359
+ });
21360
+ }
21226
21361
  async resolvePath(segments, opts) {
21227
21362
  let parentId = this.driveId;
21228
21363
  const accumulated = [];
@@ -21340,6 +21475,132 @@ async function migrateToV3(executor, dbType, tableName) {
21340
21475
  }
21341
21476
  }
21342
21477
 
21478
+ // src/server/external-file-link.ts
21479
+ function assert_absolute_http(base) {
21480
+ if (!/^https?:\/\//i.test(base)) {
21481
+ throw new Error(`external_file_link: serve_base must be an absolute http(s) URL, got "${base}"`);
21482
+ }
21483
+ }
21484
+ async function external_file_link(file_id, deps, opts = {}) {
21485
+ const record2 = opts.metadata ?? await deps.metadataService.findById(file_id);
21486
+ if (!record2) throw new Error(`external_file_link: file not found (${file_id})`);
21487
+ const storage_type = record2.storage_type;
21488
+ if (storage_type === "local" || storage_type === "app_file_server") {
21489
+ const base = opts.serve_base;
21490
+ if (!base) {
21491
+ throw new Error(`external_file_link: serve_base is required for ${storage_type} storage (file ${file_id})`);
21492
+ }
21493
+ assert_absolute_http(base);
21494
+ const path4 = opts.serve_path ?? "/api/files/serve";
21495
+ const url2 = `${base.replace(/\/$/, "")}${path4}?file_id=${encodeURIComponent(file_id)}`;
21496
+ return { url: url2, link_type: "app_gated", storage_type };
21497
+ }
21498
+ if (storage_type === "google_drive") {
21499
+ const url2 = read_drive_link(record2);
21500
+ if (!url2) throw new Error(`external_file_link: no Google Drive link for file ${file_id}`);
21501
+ return { url: url2, link_type: "provider_managed", storage_type };
21502
+ }
21503
+ if (storage_type === "dropbox") {
21504
+ throw new Error("external_file_link: Dropbox storage is not yet supported");
21505
+ }
21506
+ throw new Error(`external_file_link: Unsupported storage_type "${storage_type}" (file ${file_id})`);
21507
+ }
21508
+ function read_drive_link(record2) {
21509
+ let data = {};
21510
+ try {
21511
+ data = JSON.parse(record2.file_data || "{}");
21512
+ } catch {
21513
+ data = {};
21514
+ }
21515
+ const nested = data.metadata ?? {};
21516
+ const webViewLink = data.webViewLink ?? nested.webViewLink;
21517
+ if (typeof webViewLink === "string" && webViewLink) return webViewLink;
21518
+ const driveId = data.driveId ?? nested.driveId;
21519
+ if (typeof driveId === "string" && driveId) return `https://drive.google.com/file/d/${driveId}/view`;
21520
+ return null;
21521
+ }
21522
+ async function external_file_links(file_ids, deps, opts = {}) {
21523
+ const out = {};
21524
+ await Promise.all(
21525
+ file_ids.map(async (id) => {
21526
+ try {
21527
+ out[id] = await external_file_link(id, deps, opts);
21528
+ } catch (err) {
21529
+ out[id] = { error: err instanceof Error ? err.message : String(err) };
21530
+ }
21531
+ })
21532
+ );
21533
+ return out;
21534
+ }
21535
+
21536
+ // src/server/file-serve-handler.ts
21537
+ var import_node_stream = require("stream");
21538
+ function default_get_file_id(request) {
21539
+ return new URL(request.url).searchParams.get("file_id");
21540
+ }
21541
+ function to_body(data) {
21542
+ if (Buffer.isBuffer(data)) return data;
21543
+ return import_node_stream.Readable.toWeb(data);
21544
+ }
21545
+ var INLINE_SAFE_TYPES = /* @__PURE__ */ new Set([
21546
+ "image/png",
21547
+ "image/jpeg",
21548
+ "image/jpg",
21549
+ "image/gif",
21550
+ "image/webp",
21551
+ "image/avif",
21552
+ "application/pdf",
21553
+ "text/plain"
21554
+ ]);
21555
+ function safe_content_headers(mime, filename) {
21556
+ const safe_name = filename.replace(/"/g, "");
21557
+ const inline_safe = INLINE_SAFE_TYPES.has(mime) || mime.startsWith("video/") || mime.startsWith("audio/");
21558
+ return {
21559
+ "Content-Type": inline_safe ? mime : "application/octet-stream",
21560
+ "Content-Disposition": `${inline_safe ? "inline" : "attachment"}; filename="${safe_name}"`,
21561
+ "X-Content-Type-Options": "nosniff",
21562
+ "Content-Security-Policy": "sandbox"
21563
+ };
21564
+ }
21565
+ function createFileServeHandler(opts) {
21566
+ const get_file_id = opts.get_file_id ?? default_get_file_id;
21567
+ return async function handle(request) {
21568
+ const file_id = get_file_id(request);
21569
+ if (!file_id) return new Response("Missing file_id", { status: 400 });
21570
+ if (opts.authenticate) {
21571
+ let user = null;
21572
+ try {
21573
+ user = await opts.authenticate({ request, file_id });
21574
+ } catch {
21575
+ user = null;
21576
+ }
21577
+ if (!user) return new Response("Unauthorized", { status: 401 });
21578
+ const metadata2 = await opts.metadataService.findById(file_id);
21579
+ if (!metadata2) return new Response("Not found", { status: 404 });
21580
+ if (opts.authorize) {
21581
+ const allowed = await opts.authorize({ request, file_id, user, metadata: metadata2 });
21582
+ if (!allowed) return new Response("Forbidden", { status: 403 });
21583
+ }
21584
+ return serve(opts.provider, metadata2);
21585
+ }
21586
+ const metadata = await opts.metadataService.findById(file_id);
21587
+ if (!metadata) return new Response("Not found", { status: 404 });
21588
+ return serve(opts.provider, metadata);
21589
+ };
21590
+ }
21591
+ async function serve(provider, metadata) {
21592
+ try {
21593
+ const data = await provider.get(metadata.file_path);
21594
+ const name = metadata.original_filename || metadata.filename || getBaseName(metadata.file_path);
21595
+ return new Response(to_body(data), {
21596
+ status: 200,
21597
+ headers: safe_content_headers(getMimeType(name), name)
21598
+ });
21599
+ } catch {
21600
+ return new Response("Internal error", { status: 500 });
21601
+ }
21602
+ }
21603
+
21343
21604
  // src/server/index.ts
21344
21605
  var log6 = (0, import_hazo_core8.createLogger)("hazo_files");
21345
21606
  try {
@@ -21417,6 +21678,7 @@ try {
21417
21678
  createFileManager,
21418
21679
  createFileMetadataService,
21419
21680
  createFileRef,
21681
+ createFileServeHandler,
21420
21682
  createFolderItem,
21421
21683
  createGoogleDriveAuth,
21422
21684
  createGoogleDriveModule,
@@ -21435,6 +21697,8 @@ try {
21435
21697
  createVariableSegment,
21436
21698
  deepMerge,
21437
21699
  errorResult,
21700
+ external_file_link,
21701
+ external_file_links,
21438
21702
  filterItems,
21439
21703
  formatBytes,
21440
21704
  formatCounter,