hazo_files 1.4.2 → 1.4.4

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.
@@ -33,6 +33,16 @@ function parseConfig(configContent) {
33
33
  rootFolderId: parsed.google_drive.root_folder_id || process.env.HAZO_GOOGLE_DRIVE_ROOT_FOLDER_ID
34
34
  };
35
35
  }
36
+ if (parsed.dropbox) {
37
+ config.dropbox = {
38
+ clientId: parsed.dropbox.client_id || process.env.HAZO_DROPBOX_CLIENT_ID || "",
39
+ clientSecret: parsed.dropbox.client_secret || process.env.HAZO_DROPBOX_CLIENT_SECRET || "",
40
+ redirectUri: parsed.dropbox.redirect_uri || process.env.HAZO_DROPBOX_REDIRECT_URI || "",
41
+ refreshToken: parsed.dropbox.refresh_token || process.env.HAZO_DROPBOX_REFRESH_TOKEN,
42
+ accessToken: parsed.dropbox.access_token || process.env.HAZO_DROPBOX_ACCESS_TOKEN,
43
+ rootPath: parsed.dropbox.root_path || process.env.HAZO_DROPBOX_ROOT_PATH
44
+ };
45
+ }
36
46
  return config;
37
47
  }
38
48
  function loadConfig(configPath) {
@@ -94,6 +104,18 @@ refresh_token =
94
104
  access_token =
95
105
  ; Optional: Root folder ID to use as base (empty = root of Drive)
96
106
  root_folder_id =
107
+
108
+ [dropbox]
109
+ ; Dropbox OAuth credentials
110
+ ; These can also be set via environment variables:
111
+ ; HAZO_DROPBOX_CLIENT_ID, HAZO_DROPBOX_CLIENT_SECRET, etc.
112
+ client_id =
113
+ client_secret =
114
+ redirect_uri = http://localhost:3000/api/auth/dropbox/callback
115
+ refresh_token =
116
+ access_token =
117
+ ; Optional: Root path to use as base (empty = root of Dropbox)
118
+ root_path =
97
119
  `;
98
120
  }
99
121
  async function saveConfig(config, configPath) {
@@ -120,6 +142,16 @@ async function saveConfig(config, configPath) {
120
142
  root_folder_id: config.google_drive.rootFolderId || ""
121
143
  };
122
144
  }
145
+ if (config.dropbox) {
146
+ iniConfig.dropbox = {
147
+ client_id: config.dropbox.clientId || "",
148
+ client_secret: config.dropbox.clientSecret || "",
149
+ redirect_uri: config.dropbox.redirectUri || "",
150
+ refresh_token: config.dropbox.refreshToken || "",
151
+ access_token: config.dropbox.accessToken || "",
152
+ root_path: config.dropbox.rootPath || ""
153
+ };
154
+ }
123
155
  const content = ini.stringify(iniConfig);
124
156
  await fs.promises.writeFile(resolvedPath, content, "utf-8");
125
157
  }
@@ -1657,10 +1689,641 @@ function createGoogleDriveModule() {
1657
1689
  return new GoogleDriveModule();
1658
1690
  }
1659
1691
 
1692
+ // src/modules/dropbox/index.ts
1693
+ import { Dropbox } from "dropbox";
1694
+
1695
+ // src/modules/dropbox/auth.ts
1696
+ var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
1697
+ var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
1698
+ var DROPBOX_REVOKE_URL = "https://api.dropboxapi.com/2/auth/token/revoke";
1699
+ var DropboxAuth = class {
1700
+ constructor(config, callbacks = {}) {
1701
+ this.tokens = null;
1702
+ this.config = config;
1703
+ this.callbacks = callbacks;
1704
+ }
1705
+ /**
1706
+ * Generate the authorization URL for OAuth consent
1707
+ */
1708
+ getAuthUrl(state) {
1709
+ const params = new URLSearchParams({
1710
+ client_id: this.config.clientId,
1711
+ redirect_uri: this.config.redirectUri,
1712
+ response_type: "code",
1713
+ token_access_type: "offline"
1714
+ });
1715
+ if (state) {
1716
+ params.set("state", state);
1717
+ }
1718
+ return `${DROPBOX_AUTH_URL}?${params.toString()}`;
1719
+ }
1720
+ /**
1721
+ * Exchange authorization code for tokens
1722
+ */
1723
+ async exchangeCodeForTokens(code) {
1724
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1725
+ method: "POST",
1726
+ headers: {
1727
+ "Content-Type": "application/x-www-form-urlencoded"
1728
+ },
1729
+ body: new URLSearchParams({
1730
+ code,
1731
+ grant_type: "authorization_code",
1732
+ client_id: this.config.clientId,
1733
+ client_secret: this.config.clientSecret,
1734
+ redirect_uri: this.config.redirectUri
1735
+ })
1736
+ });
1737
+ if (!response.ok) {
1738
+ const errorData = await response.text();
1739
+ throw new Error(`Failed to exchange code for tokens: ${errorData}`);
1740
+ }
1741
+ const data = await response.json();
1742
+ this.tokens = {
1743
+ accessToken: data.access_token,
1744
+ refreshToken: data.refresh_token,
1745
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1746
+ };
1747
+ if (this.callbacks.onTokensUpdated) {
1748
+ await this.callbacks.onTokensUpdated(this.tokens);
1749
+ }
1750
+ return this.tokens;
1751
+ }
1752
+ /**
1753
+ * Set tokens directly (e.g., from stored tokens)
1754
+ */
1755
+ async setTokens(tokens) {
1756
+ this.tokens = tokens;
1757
+ }
1758
+ /**
1759
+ * Load tokens from storage using callback
1760
+ */
1761
+ async loadStoredTokens() {
1762
+ if (!this.callbacks.getStoredTokens) {
1763
+ return false;
1764
+ }
1765
+ const tokens = await this.callbacks.getStoredTokens();
1766
+ if (tokens) {
1767
+ await this.setTokens(tokens);
1768
+ return true;
1769
+ }
1770
+ return false;
1771
+ }
1772
+ /**
1773
+ * Check if authenticated
1774
+ */
1775
+ isAuthenticated() {
1776
+ return this.tokens !== null && !!this.tokens.accessToken;
1777
+ }
1778
+ /**
1779
+ * Get current tokens
1780
+ */
1781
+ getTokens() {
1782
+ return this.tokens;
1783
+ }
1784
+ /**
1785
+ * Get current access token
1786
+ */
1787
+ getAccessToken() {
1788
+ return this.tokens?.accessToken ?? null;
1789
+ }
1790
+ /**
1791
+ * Refresh the access token
1792
+ */
1793
+ async refreshAccessToken() {
1794
+ if (!this.tokens?.refreshToken) {
1795
+ throw new Error("No refresh token available");
1796
+ }
1797
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1798
+ method: "POST",
1799
+ headers: {
1800
+ "Content-Type": "application/x-www-form-urlencoded"
1801
+ },
1802
+ body: new URLSearchParams({
1803
+ grant_type: "refresh_token",
1804
+ refresh_token: this.tokens.refreshToken,
1805
+ client_id: this.config.clientId,
1806
+ client_secret: this.config.clientSecret
1807
+ })
1808
+ });
1809
+ if (!response.ok) {
1810
+ const errorData = await response.text();
1811
+ throw new Error(`Failed to refresh token: ${errorData}`);
1812
+ }
1813
+ const data = await response.json();
1814
+ this.tokens = {
1815
+ ...this.tokens,
1816
+ accessToken: data.access_token,
1817
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1818
+ };
1819
+ if (this.callbacks.onTokensUpdated) {
1820
+ await this.callbacks.onTokensUpdated(this.tokens);
1821
+ }
1822
+ return this.tokens;
1823
+ }
1824
+ /**
1825
+ * Revoke access (disconnect)
1826
+ */
1827
+ async revokeAccess() {
1828
+ if (this.tokens?.accessToken) {
1829
+ try {
1830
+ await fetch(DROPBOX_REVOKE_URL, {
1831
+ method: "POST",
1832
+ headers: {
1833
+ Authorization: `Bearer ${this.tokens.accessToken}`
1834
+ }
1835
+ });
1836
+ } catch {
1837
+ }
1838
+ }
1839
+ this.tokens = null;
1840
+ }
1841
+ /**
1842
+ * Check if token is expired or will expire soon
1843
+ */
1844
+ isTokenExpired(bufferSeconds = 300) {
1845
+ if (!this.tokens?.expiryDate) {
1846
+ return false;
1847
+ }
1848
+ const now = Date.now();
1849
+ const expiry = this.tokens.expiryDate;
1850
+ return now >= expiry - bufferSeconds * 1e3;
1851
+ }
1852
+ /**
1853
+ * Ensure valid access token (refresh if needed)
1854
+ */
1855
+ async ensureValidToken() {
1856
+ if (this.isTokenExpired()) {
1857
+ await this.refreshAccessToken();
1858
+ }
1859
+ }
1860
+ };
1861
+ function createDropboxAuth(config, callbacks) {
1862
+ return new DropboxAuth(config, callbacks);
1863
+ }
1864
+
1865
+ // src/modules/dropbox/index.ts
1866
+ var MAX_UPLOAD_SIZE = 150 * 1024 * 1024;
1867
+ var DropboxModule = class extends BaseStorageModule {
1868
+ constructor() {
1869
+ super(...arguments);
1870
+ this.provider = "dropbox";
1871
+ this.auth = null;
1872
+ this.dbx = null;
1873
+ this.rootPath = "";
1874
+ this.authCallbacks = {};
1875
+ }
1876
+ /**
1877
+ * Set authentication callbacks for token persistence
1878
+ */
1879
+ setAuthCallbacks(callbacks) {
1880
+ this.authCallbacks = callbacks;
1881
+ }
1882
+ async initialize(config) {
1883
+ await super.initialize(config);
1884
+ const dropboxConfig = this.getProviderConfig();
1885
+ if (!dropboxConfig.clientId || !dropboxConfig.clientSecret) {
1886
+ throw new AuthenticationError("dropbox", "Missing client ID or client secret");
1887
+ }
1888
+ this.auth = createDropboxAuth(
1889
+ {
1890
+ clientId: dropboxConfig.clientId,
1891
+ clientSecret: dropboxConfig.clientSecret,
1892
+ redirectUri: dropboxConfig.redirectUri
1893
+ },
1894
+ this.authCallbacks
1895
+ );
1896
+ if (dropboxConfig.refreshToken || dropboxConfig.accessToken) {
1897
+ await this.auth.setTokens({
1898
+ accessToken: dropboxConfig.accessToken || "",
1899
+ refreshToken: dropboxConfig.refreshToken || ""
1900
+ });
1901
+ }
1902
+ this.rootPath = dropboxConfig.rootPath || "";
1903
+ this.createDropboxClient();
1904
+ }
1905
+ /**
1906
+ * Create/recreate the Dropbox SDK client with the current access token
1907
+ */
1908
+ createDropboxClient() {
1909
+ const accessToken = this.auth?.getAccessToken();
1910
+ if (accessToken) {
1911
+ this.dbx = new Dropbox({ accessToken });
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Get the auth instance for OAuth flow
1916
+ */
1917
+ getAuth() {
1918
+ if (!this.auth) {
1919
+ throw new AuthenticationError("dropbox", "Module not initialized");
1920
+ }
1921
+ return this.auth;
1922
+ }
1923
+ /**
1924
+ * Check if user is authenticated
1925
+ */
1926
+ isAuthenticated() {
1927
+ return this.auth?.isAuthenticated() ?? false;
1928
+ }
1929
+ /**
1930
+ * Authenticate with provided tokens
1931
+ */
1932
+ async authenticate(tokens) {
1933
+ if (!this.auth) {
1934
+ throw new AuthenticationError("dropbox", "Module not initialized");
1935
+ }
1936
+ await this.auth.setTokens(tokens);
1937
+ this.createDropboxClient();
1938
+ }
1939
+ /**
1940
+ * Ensure authenticated before operations
1941
+ */
1942
+ async ensureAuthenticated() {
1943
+ this.ensureInitialized();
1944
+ if (!this.isAuthenticated()) {
1945
+ throw new AuthenticationError("dropbox", "Not authenticated. Please connect your Dropbox.");
1946
+ }
1947
+ await this.auth.ensureValidToken();
1948
+ this.createDropboxClient();
1949
+ }
1950
+ /**
1951
+ * Convert virtual path to Dropbox path
1952
+ * Virtual: /folder/file.txt -> Dropbox: /folder/file.txt (or /rootPath/folder/file.txt)
1953
+ * Dropbox root is empty string "", not "/"
1954
+ */
1955
+ toDropboxPath(virtualPath) {
1956
+ const normalized = this.normalizePath(virtualPath);
1957
+ if (this.rootPath) {
1958
+ if (normalized === "/") {
1959
+ return `/${this.rootPath}`;
1960
+ }
1961
+ return `/${this.rootPath}${normalized}`;
1962
+ }
1963
+ if (normalized === "/") {
1964
+ return "";
1965
+ }
1966
+ return normalized;
1967
+ }
1968
+ /**
1969
+ * Convert Dropbox metadata to FileSystemItem
1970
+ */
1971
+ metadataToItem(entry, virtualPath) {
1972
+ const isFolder2 = entry[".tag"] === "folder";
1973
+ const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
1974
+ if (isFolder2) {
1975
+ return createFolderItem({
1976
+ id: entry.id,
1977
+ name: entry.name,
1978
+ path: path3,
1979
+ createdAt: /* @__PURE__ */ new Date(),
1980
+ modifiedAt: /* @__PURE__ */ new Date(),
1981
+ metadata: {
1982
+ dropboxId: entry.id,
1983
+ pathDisplay: entry.path_display
1984
+ }
1985
+ });
1986
+ }
1987
+ const fileEntry = entry;
1988
+ return createFileItem({
1989
+ id: fileEntry.id,
1990
+ name: fileEntry.name,
1991
+ path: path3,
1992
+ size: fileEntry.size,
1993
+ mimeType: getMimeType(fileEntry.name),
1994
+ createdAt: new Date(fileEntry.client_modified),
1995
+ modifiedAt: new Date(fileEntry.server_modified),
1996
+ metadata: {
1997
+ dropboxId: fileEntry.id,
1998
+ pathDisplay: fileEntry.path_display
1999
+ }
2000
+ });
2001
+ }
2002
+ /**
2003
+ * Convert a Dropbox path_display to virtual path
2004
+ */
2005
+ toVirtualPath(dropboxPath) {
2006
+ if (this.rootPath && dropboxPath.toLowerCase().startsWith(`/${this.rootPath.toLowerCase()}`)) {
2007
+ const stripped = dropboxPath.substring(this.rootPath.length + 1);
2008
+ return stripped || "/";
2009
+ }
2010
+ return dropboxPath || "/";
2011
+ }
2012
+ async createDirectory(virtualPath) {
2013
+ try {
2014
+ await this.ensureAuthenticated();
2015
+ const dbxPath = this.toDropboxPath(virtualPath);
2016
+ const response = await this.dbx.filesCreateFolderV2({
2017
+ path: dbxPath,
2018
+ autorename: false
2019
+ });
2020
+ const metadata = response.result.metadata;
2021
+ const item = this.metadataToItem(
2022
+ { ".tag": "folder", id: metadata.id, name: metadata.name, path_display: metadata.path_display, path_lower: metadata.path_lower },
2023
+ this.normalizePath(virtualPath)
2024
+ );
2025
+ return this.successResult(item);
2026
+ } catch (error) {
2027
+ if (error instanceof AuthenticationError) {
2028
+ return this.errorResult(error.message);
2029
+ }
2030
+ const errMsg = error.message || String(error);
2031
+ if (errMsg.includes("path/conflict")) {
2032
+ return this.errorResult(`Directory already exists: ${virtualPath}`);
2033
+ }
2034
+ return this.errorResult(`Failed to create directory: ${errMsg}`);
2035
+ }
2036
+ }
2037
+ async removeDirectory(virtualPath, recursive = false) {
2038
+ try {
2039
+ await this.ensureAuthenticated();
2040
+ const dbxPath = this.toDropboxPath(virtualPath);
2041
+ if (!recursive) {
2042
+ const listResponse = await this.dbx.filesListFolder({
2043
+ path: dbxPath,
2044
+ limit: 1
2045
+ });
2046
+ if (listResponse.result.entries.length > 0) {
2047
+ return this.errorResult(`Directory is not empty: ${virtualPath}`);
2048
+ }
2049
+ }
2050
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2051
+ return this.successResult();
2052
+ } catch (error) {
2053
+ const errMsg = error.message || String(error);
2054
+ if (errMsg.includes("path_lookup/not_found") || errMsg.includes("not_found")) {
2055
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2056
+ }
2057
+ return this.errorResult(`Failed to remove directory: ${errMsg}`);
2058
+ }
2059
+ }
2060
+ async uploadFile(source, remotePath, options = {}) {
2061
+ try {
2062
+ await this.ensureAuthenticated();
2063
+ const normalized = this.normalizePath(remotePath);
2064
+ const dbxPath = this.toDropboxPath(remotePath);
2065
+ let contents;
2066
+ if (typeof source === "string") {
2067
+ const fs3 = await import("fs");
2068
+ contents = await fs3.promises.readFile(source);
2069
+ } else if (Buffer.isBuffer(source)) {
2070
+ contents = source;
2071
+ } else {
2072
+ const chunks = [];
2073
+ const reader = source.getReader();
2074
+ let done = false;
2075
+ while (!done) {
2076
+ const result = await reader.read();
2077
+ done = result.done;
2078
+ if (result.value) {
2079
+ chunks.push(result.value);
2080
+ }
2081
+ }
2082
+ contents = Buffer.concat(chunks);
2083
+ }
2084
+ if (contents.length > MAX_UPLOAD_SIZE) {
2085
+ throw new FileTooLargeError(this.getBaseName(remotePath), contents.length, MAX_UPLOAD_SIZE);
2086
+ }
2087
+ if (!options.overwrite) {
2088
+ try {
2089
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2090
+ throw new FileExistsError(remotePath);
2091
+ } catch (err) {
2092
+ if (err instanceof FileExistsError) throw err;
2093
+ }
2094
+ }
2095
+ const response = await this.dbx.filesUpload({
2096
+ path: dbxPath,
2097
+ contents,
2098
+ mode: options.overwrite ? { ".tag": "overwrite" } : { ".tag": "add" },
2099
+ autorename: false
2100
+ });
2101
+ if (options.onProgress) {
2102
+ options.onProgress(100, contents.length, contents.length);
2103
+ }
2104
+ const metadata = response.result;
2105
+ const item = this.metadataToItem(
2106
+ {
2107
+ ".tag": "file",
2108
+ id: metadata.id,
2109
+ name: metadata.name,
2110
+ path_display: metadata.path_display,
2111
+ path_lower: metadata.path_lower,
2112
+ size: metadata.size,
2113
+ client_modified: metadata.client_modified,
2114
+ server_modified: metadata.server_modified
2115
+ },
2116
+ normalized
2117
+ );
2118
+ return this.successResult(item);
2119
+ } catch (error) {
2120
+ if (error instanceof AuthenticationError || error instanceof FileExistsError || error instanceof FileTooLargeError) {
2121
+ return this.errorResult(error.message);
2122
+ }
2123
+ return this.errorResult(`Failed to upload file: ${error.message}`);
2124
+ }
2125
+ }
2126
+ async downloadFile(remotePath, localPath, options = {}) {
2127
+ try {
2128
+ await this.ensureAuthenticated();
2129
+ const dbxPath = this.toDropboxPath(remotePath);
2130
+ const response = await this.dbx.filesDownload({ path: dbxPath });
2131
+ const result = response.result;
2132
+ const buffer = Buffer.from(result.fileBinary);
2133
+ if (options.onProgress) {
2134
+ options.onProgress(100, buffer.length, buffer.length);
2135
+ }
2136
+ if (localPath) {
2137
+ const fs3 = await import("fs");
2138
+ const path3 = await import("path");
2139
+ await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2140
+ await fs3.promises.writeFile(localPath, buffer);
2141
+ return this.successResult(localPath);
2142
+ }
2143
+ return this.successResult(buffer);
2144
+ } catch (error) {
2145
+ const errMsg = error.message || String(error);
2146
+ if (errMsg.includes("path/not_found") || errMsg.includes("path_lookup/not_found")) {
2147
+ return this.errorResult(`File not found: ${remotePath}`);
2148
+ }
2149
+ return this.errorResult(`Failed to download file: ${errMsg}`);
2150
+ }
2151
+ }
2152
+ async moveItem(sourcePath, destinationPath, _options = {}) {
2153
+ try {
2154
+ await this.ensureAuthenticated();
2155
+ const fromPath = this.toDropboxPath(sourcePath);
2156
+ const toPath = this.toDropboxPath(destinationPath);
2157
+ const response = await this.dbx.filesMoveV2({
2158
+ from_path: fromPath,
2159
+ to_path: toPath,
2160
+ autorename: false
2161
+ });
2162
+ const metadata = response.result.metadata;
2163
+ const item = this.metadataToItem(metadata, this.normalizePath(destinationPath));
2164
+ return this.successResult(item);
2165
+ } catch (error) {
2166
+ const errMsg = error.message || String(error);
2167
+ if (errMsg.includes("not_found")) {
2168
+ return this.errorResult(`Item not found: ${sourcePath}`);
2169
+ }
2170
+ return this.errorResult(`Failed to move item: ${errMsg}`);
2171
+ }
2172
+ }
2173
+ async deleteFile(virtualPath) {
2174
+ try {
2175
+ await this.ensureAuthenticated();
2176
+ const dbxPath = this.toDropboxPath(virtualPath);
2177
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2178
+ return this.successResult();
2179
+ } catch (error) {
2180
+ const errMsg = error.message || String(error);
2181
+ if (errMsg.includes("not_found")) {
2182
+ return this.errorResult(`File not found: ${virtualPath}`);
2183
+ }
2184
+ return this.errorResult(`Failed to delete file: ${errMsg}`);
2185
+ }
2186
+ }
2187
+ async renameFile(virtualPath, newName, _options = {}) {
2188
+ try {
2189
+ await this.ensureAuthenticated();
2190
+ const parentPath = this.getParentPath(virtualPath);
2191
+ const newVirtualPath = this.joinPath(parentPath, newName);
2192
+ const fromPath = this.toDropboxPath(virtualPath);
2193
+ const toPath = this.toDropboxPath(newVirtualPath);
2194
+ const response = await this.dbx.filesMoveV2({
2195
+ from_path: fromPath,
2196
+ to_path: toPath,
2197
+ autorename: false
2198
+ });
2199
+ const metadata = response.result.metadata;
2200
+ const item = this.metadataToItem(metadata, newVirtualPath);
2201
+ return this.successResult(item);
2202
+ } catch (error) {
2203
+ const errMsg = error.message || String(error);
2204
+ if (errMsg.includes("not_found")) {
2205
+ return this.errorResult(`File not found: ${virtualPath}`);
2206
+ }
2207
+ return this.errorResult(`Failed to rename file: ${errMsg}`);
2208
+ }
2209
+ }
2210
+ async renameFolder(virtualPath, newName, _options = {}) {
2211
+ try {
2212
+ await this.ensureAuthenticated();
2213
+ const parentPath = this.getParentPath(virtualPath);
2214
+ const newVirtualPath = this.joinPath(parentPath, newName);
2215
+ const fromPath = this.toDropboxPath(virtualPath);
2216
+ const toPath = this.toDropboxPath(newVirtualPath);
2217
+ const response = await this.dbx.filesMoveV2({
2218
+ from_path: fromPath,
2219
+ to_path: toPath,
2220
+ autorename: false
2221
+ });
2222
+ const metadata = response.result.metadata;
2223
+ const item = this.metadataToItem(metadata, newVirtualPath);
2224
+ return this.successResult(item);
2225
+ } catch (error) {
2226
+ const errMsg = error.message || String(error);
2227
+ if (errMsg.includes("not_found")) {
2228
+ return this.errorResult(`Folder not found: ${virtualPath}`);
2229
+ }
2230
+ return this.errorResult(`Failed to rename folder: ${errMsg}`);
2231
+ }
2232
+ }
2233
+ async listDirectory(virtualPath, options = {}) {
2234
+ try {
2235
+ await this.ensureAuthenticated();
2236
+ const dbxPath = this.toDropboxPath(virtualPath);
2237
+ const items = [];
2238
+ let hasMore = true;
2239
+ let cursor;
2240
+ const firstResponse = await this.dbx.filesListFolder({
2241
+ path: dbxPath,
2242
+ limit: 100
2243
+ });
2244
+ let entries = firstResponse.result.entries;
2245
+ hasMore = firstResponse.result.has_more;
2246
+ cursor = firstResponse.result.cursor;
2247
+ const processEntries = async (entryList) => {
2248
+ for (const entry of entryList) {
2249
+ if (!options.includeHidden && entry.name.startsWith(".")) {
2250
+ continue;
2251
+ }
2252
+ const itemPath = this.joinPath(virtualPath, entry.name);
2253
+ const item = this.metadataToItem(entry, itemPath);
2254
+ if (options.filter && !options.filter(item)) {
2255
+ continue;
2256
+ }
2257
+ items.push(item);
2258
+ if (options.recursive && entry[".tag"] === "folder") {
2259
+ const subResult = await this.listDirectory(itemPath, options);
2260
+ if (subResult.success && subResult.data) {
2261
+ items.push(...subResult.data);
2262
+ }
2263
+ }
2264
+ }
2265
+ };
2266
+ await processEntries(entries);
2267
+ while (hasMore && cursor) {
2268
+ const continueResponse = await this.dbx.filesListFolderContinue({ cursor });
2269
+ entries = continueResponse.result.entries;
2270
+ hasMore = continueResponse.result.has_more;
2271
+ cursor = continueResponse.result.cursor;
2272
+ await processEntries(entries);
2273
+ }
2274
+ return this.successResult(items);
2275
+ } catch (error) {
2276
+ const errMsg = error.message || String(error);
2277
+ if (errMsg.includes("path/not_found") || errMsg.includes("not_found")) {
2278
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2279
+ }
2280
+ return this.errorResult(`Failed to list directory: ${errMsg}`);
2281
+ }
2282
+ }
2283
+ async getItem(virtualPath) {
2284
+ try {
2285
+ await this.ensureAuthenticated();
2286
+ const dbxPath = this.toDropboxPath(virtualPath);
2287
+ const response = await this.dbx.filesGetMetadata({ path: dbxPath });
2288
+ const metadata = response.result;
2289
+ const item = this.metadataToItem(metadata, this.normalizePath(virtualPath));
2290
+ return this.successResult(item);
2291
+ } catch (error) {
2292
+ const errMsg = error.message || String(error);
2293
+ if (errMsg.includes("not_found")) {
2294
+ return this.errorResult(`Item not found: ${virtualPath}`);
2295
+ }
2296
+ return this.errorResult(`Failed to get item: ${errMsg}`);
2297
+ }
2298
+ }
2299
+ async exists(virtualPath) {
2300
+ try {
2301
+ await this.ensureAuthenticated();
2302
+ const dbxPath = this.toDropboxPath(virtualPath);
2303
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2304
+ return true;
2305
+ } catch {
2306
+ return false;
2307
+ }
2308
+ }
2309
+ async getFolderTree(path3 = "/", depth = 3) {
2310
+ try {
2311
+ await this.ensureAuthenticated();
2312
+ return super.getFolderTree(path3, depth);
2313
+ } catch (error) {
2314
+ return this.errorResult(`Failed to get folder tree: ${error.message}`);
2315
+ }
2316
+ }
2317
+ };
2318
+ function createDropboxModule() {
2319
+ return new DropboxModule();
2320
+ }
2321
+
1660
2322
  // src/modules/index.ts
1661
2323
  var moduleRegistry = {
1662
2324
  local: createLocalModule,
1663
- google_drive: createGoogleDriveModule
2325
+ google_drive: createGoogleDriveModule,
2326
+ dropbox: createDropboxModule
1664
2327
  };
1665
2328
  function getRegisteredProviders() {
1666
2329
  return Object.keys(moduleRegistry);
@@ -2226,6 +2889,7 @@ var FileMetadataService = class {
2226
2889
  if (input.scope_id !== void 0) record.scope_id = input.scope_id;
2227
2890
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
2228
2891
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
2892
+ if (input.content_tag !== void 0) record.content_tag = input.content_tag;
2229
2893
  const results = await this.crud.insert(record);
2230
2894
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
2231
2895
  return results[0] || null;
@@ -3603,10 +4267,18 @@ function createNamingConventionService(crudService, options) {
3603
4267
 
3604
4268
  // src/services/llm-extraction-service.ts
3605
4269
  var LLMExtractionService = class {
3606
- constructor(llmFactory, defaultProvider = "gemini") {
3607
- this.llmFactory = llmFactory;
4270
+ constructor(factoryConfig, defaultProvider = "gemini") {
4271
+ this.llmFactory = factoryConfig.create;
4272
+ this.cacheInvalidator = factoryConfig.invalidateCache;
3608
4273
  this.defaultProvider = defaultProvider;
3609
4274
  }
4275
+ /**
4276
+ * Invalidate the LLM prompt cache
4277
+ * Passthrough to hazo_llm_api's invalidate_prompt_cache when configured
4278
+ */
4279
+ invalidatePromptCache(area, key) {
4280
+ this.cacheInvalidator?.(area, key);
4281
+ }
3610
4282
  /**
3611
4283
  * Extract data from a document
3612
4284
  *
@@ -3733,8 +4405,8 @@ var LLMExtractionService = class {
3733
4405
  };
3734
4406
  }
3735
4407
  };
3736
- function createLLMExtractionService(llmFactory, defaultProvider) {
3737
- return new LLMExtractionService(llmFactory, defaultProvider);
4408
+ function createLLMExtractionService(factoryConfig, defaultProvider) {
4409
+ return new LLMExtractionService(factoryConfig, defaultProvider);
3738
4410
  }
3739
4411
 
3740
4412
  // src/common/naming-utils.ts
@@ -4060,10 +4732,11 @@ function generatePreviewName(pattern, userVariables, options = {}) {
4060
4732
 
4061
4733
  // src/services/upload-extract-service.ts
4062
4734
  var UploadExtractService = class {
4063
- constructor(fileManager, namingService, extractionService) {
4735
+ constructor(fileManager, namingService, extractionService, defaultContentTagConfig) {
4064
4736
  this.fileManager = fileManager;
4065
4737
  this.namingService = namingService;
4066
4738
  this.extractionService = extractionService;
4739
+ this.defaultContentTagConfig = defaultContentTagConfig;
4067
4740
  }
4068
4741
  /**
4069
4742
  * Upload a file with optional extraction and naming convention
@@ -4140,11 +4813,12 @@ var UploadExtractService = class {
4140
4813
  metadata.extraction_id = extractionData.id;
4141
4814
  metadata.extraction_source = extractionData.source;
4142
4815
  }
4816
+ const effectiveContentTagConfig = options.contentTagConfig ?? this.defaultContentTagConfig;
4817
+ const needsContentTagging = effectiveContentTagConfig?.content_tag_set_by_llm && this.extractionService && this.fileManager.isTrackingActive();
4143
4818
  const uploadResult = await this.fileManager.uploadFile(source, fullPath, {
4144
4819
  ...options,
4145
4820
  metadata,
4146
- awaitRecording: !!extractionData
4147
- // Await recording when extraction needs to be added
4821
+ awaitRecording: !!extractionData || !!needsContentTagging
4148
4822
  });
4149
4823
  if (!uploadResult.success) {
4150
4824
  return {
@@ -4168,13 +4842,23 @@ var UploadExtractService = class {
4168
4842
  );
4169
4843
  }
4170
4844
  }
4845
+ let contentTag;
4846
+ if (needsContentTagging && effectiveContentTagConfig) {
4847
+ contentTag = await this.performContentTagging(
4848
+ source,
4849
+ mimeType,
4850
+ effectiveContentTagConfig,
4851
+ fullPath
4852
+ );
4853
+ }
4171
4854
  return {
4172
4855
  success: true,
4173
4856
  file: uploadResult.data,
4174
4857
  extraction: extractionData,
4175
4858
  generatedPath: fullPath,
4176
4859
  generatedFolderPath: generatedFolderPath || void 0,
4177
- originalFileName
4860
+ originalFileName,
4861
+ contentTag
4178
4862
  };
4179
4863
  } catch (error) {
4180
4864
  const message = error instanceof Error ? error.message : String(error);
@@ -4272,6 +4956,76 @@ var UploadExtractService = class {
4272
4956
  folderPath: folderPath || void 0
4273
4957
  };
4274
4958
  }
4959
+ /**
4960
+ * Perform content tagging via LLM extraction.
4961
+ * Calls the LLM with the configured prompt, extracts the specified field,
4962
+ * and writes it to the content_tag column.
4963
+ */
4964
+ async performContentTagging(buffer, mimeType, config, filePath) {
4965
+ try {
4966
+ if (!this.extractionService) return void 0;
4967
+ const result = await this.extractionService.extract(buffer, mimeType, {
4968
+ promptArea: config.content_tag_prompt_area,
4969
+ promptKey: config.content_tag_prompt_key,
4970
+ promptVariables: config.content_tag_prompt_variables
4971
+ });
4972
+ if (!result.success || !result.data) return void 0;
4973
+ const tagValue = result.data[config.content_tag_prompt_return_fieldname];
4974
+ if (typeof tagValue !== "string" || !tagValue) return void 0;
4975
+ const metadataService = this.fileManager.getMetadataService();
4976
+ if (metadataService) {
4977
+ const storageType = this.fileManager.getProvider() || "local";
4978
+ const record = await metadataService.findByPath(filePath, storageType);
4979
+ if (record) {
4980
+ await metadataService.updateFields(record.id, { content_tag: tagValue });
4981
+ }
4982
+ }
4983
+ return tagValue;
4984
+ } catch {
4985
+ return void 0;
4986
+ }
4987
+ }
4988
+ /**
4989
+ * Manually tag a file's content via LLM.
4990
+ * Works with existing DB records, resolving the file path internally.
4991
+ *
4992
+ * @param fileId - Database record ID of the file
4993
+ * @param config - Content tag config (falls back to default if not provided)
4994
+ * @returns OperationResult with the tag value
4995
+ */
4996
+ async tagFileContent(fileId, config) {
4997
+ const effectiveConfig = config ?? this.defaultContentTagConfig;
4998
+ if (!effectiveConfig || !effectiveConfig.content_tag_set_by_llm) {
4999
+ return { success: false, error: "Content tagging is not configured or disabled" };
5000
+ }
5001
+ if (!this.extractionService) {
5002
+ return { success: false, error: "Extraction service not available" };
5003
+ }
5004
+ const metadataService = this.fileManager.getMetadataService();
5005
+ if (!metadataService) {
5006
+ return { success: false, error: "Metadata service not available (tracking not enabled)" };
5007
+ }
5008
+ const record = await metadataService.findById(fileId);
5009
+ if (!record) {
5010
+ return { success: false, error: `File record not found: ${fileId}` };
5011
+ }
5012
+ const downloadResult = await this.fileManager.downloadFile(record.file_path);
5013
+ if (!downloadResult.success || !downloadResult.data) {
5014
+ return { success: false, error: `Failed to download file: ${downloadResult.error}` };
5015
+ }
5016
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : Buffer.from(downloadResult.data);
5017
+ const mimeType = getMimeType(record.filename);
5018
+ const tagValue = await this.performContentTagging(
5019
+ buffer,
5020
+ mimeType,
5021
+ effectiveConfig,
5022
+ record.file_path
5023
+ );
5024
+ if (!tagValue) {
5025
+ return { success: false, error: "Content tagging did not produce a result" };
5026
+ }
5027
+ return { success: true, data: tagValue };
5028
+ }
4275
5029
  /**
4276
5030
  * Get the file manager
4277
5031
  */
@@ -4291,8 +5045,8 @@ var UploadExtractService = class {
4291
5045
  return this.extractionService;
4292
5046
  }
4293
5047
  };
4294
- function createUploadExtractService(fileManager, namingService, extractionService) {
4295
- return new UploadExtractService(fileManager, namingService, extractionService);
5048
+ function createUploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig) {
5049
+ return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
4296
5050
  }
4297
5051
 
4298
5052
  // src/server/factory.ts
@@ -4308,7 +5062,8 @@ async function createHazoFilesServer(options = {}) {
4308
5062
  metadataTableName = "hazo_files",
4309
5063
  namingTableName = "hazo_files_naming",
4310
5064
  enableTracking = !!crudService,
4311
- trackDownloads = true
5065
+ trackDownloads = true,
5066
+ defaultContentTagConfig
4312
5067
  } = options;
4313
5068
  const fileManagerOptions = {
4314
5069
  config,
@@ -4337,14 +5092,17 @@ async function createHazoFilesServer(options = {}) {
4337
5092
  const uploadExtractService = new UploadExtractService(
4338
5093
  fileManager,
4339
5094
  namingService || void 0,
4340
- extractionService || void 0
5095
+ extractionService || void 0,
5096
+ defaultContentTagConfig
4341
5097
  );
5098
+ const invalidatePromptCache = extractionService ? (area, key) => extractionService.invalidatePromptCache(area, key) : void 0;
4342
5099
  return {
4343
5100
  fileManager,
4344
5101
  metadataService,
4345
5102
  namingService,
4346
5103
  extractionService,
4347
- uploadExtractService
5104
+ uploadExtractService,
5105
+ invalidatePromptCache
4348
5106
  };
4349
5107
  }
4350
5108
  async function createBasicFileManager(config, crudService) {
@@ -4383,7 +5141,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4383
5141
  uploaded_by TEXT,
4384
5142
  storage_verified_at TEXT,
4385
5143
  deleted_at TEXT,
4386
- original_filename TEXT
5144
+ original_filename TEXT,
5145
+ content_tag TEXT
4387
5146
  )`,
4388
5147
  indexes: [
4389
5148
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4393,7 +5152,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4393
5152
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4394
5153
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4395
5154
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4396
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5155
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5156
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4397
5157
  ]
4398
5158
  },
4399
5159
  postgres: {
@@ -4416,7 +5176,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4416
5176
  uploaded_by UUID,
4417
5177
  storage_verified_at TIMESTAMP WITH TIME ZONE,
4418
5178
  deleted_at TIMESTAMP WITH TIME ZONE,
4419
- original_filename TEXT
5179
+ original_filename TEXT,
5180
+ content_tag TEXT
4420
5181
  )`,
4421
5182
  indexes: [
4422
5183
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4426,7 +5187,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4426
5187
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4427
5188
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4428
5189
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4429
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5190
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5191
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4430
5192
  ]
4431
5193
  },
4432
5194
  columns: [
@@ -4448,7 +5210,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4448
5210
  "uploaded_by",
4449
5211
  "storage_verified_at",
4450
5212
  "deleted_at",
4451
- "original_filename"
5213
+ "original_filename",
5214
+ "content_tag"
4452
5215
  ]
4453
5216
  };
4454
5217
  function getSchemaForTable(tableName, dbType) {
@@ -4589,6 +5352,45 @@ function getNamingSchemaForTable(tableName, dbType) {
4589
5352
  )
4590
5353
  };
4591
5354
  }
5355
+ var HAZO_FILES_MIGRATION_V3 = {
5356
+ tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
5357
+ sqlite: {
5358
+ alterStatements: [
5359
+ "ALTER TABLE hazo_files ADD COLUMN content_tag TEXT"
5360
+ ],
5361
+ indexes: [
5362
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5363
+ ],
5364
+ backfill: ""
5365
+ // No backfill needed — column is nullable, defaults to NULL
5366
+ },
5367
+ postgres: {
5368
+ alterStatements: [
5369
+ "ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS content_tag TEXT"
5370
+ ],
5371
+ indexes: [
5372
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5373
+ ],
5374
+ backfill: ""
5375
+ // No backfill needed — column is nullable, defaults to NULL
5376
+ },
5377
+ newColumns: [
5378
+ "content_tag"
5379
+ ]
5380
+ };
5381
+ function getMigrationV3ForTable(tableName, dbType) {
5382
+ const migration = HAZO_FILES_MIGRATION_V3[dbType];
5383
+ const defaultName = HAZO_FILES_MIGRATION_V3.tableName;
5384
+ return {
5385
+ alterStatements: migration.alterStatements.map(
5386
+ (stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
5387
+ ),
5388
+ indexes: migration.indexes.map(
5389
+ (idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
5390
+ ),
5391
+ backfill: migration.backfill
5392
+ };
5393
+ }
4592
5394
 
4593
5395
  // src/migrations/add-reference-tracking.ts
4594
5396
  async function migrateToV2(executor, dbType, tableName) {
@@ -4608,6 +5410,20 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4608
5410
  await executor.run(migration.backfill);
4609
5411
  }
4610
5412
 
5413
+ // src/migrations/add-content-tag.ts
5414
+ async function migrateToV3(executor, dbType, tableName) {
5415
+ const migration = tableName ? getMigrationV3ForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V3[dbType];
5416
+ for (const stmt of migration.alterStatements) {
5417
+ try {
5418
+ await executor.run(stmt);
5419
+ } catch {
5420
+ }
5421
+ }
5422
+ for (const idx of migration.indexes) {
5423
+ await executor.run(idx);
5424
+ }
5425
+ }
5426
+
4611
5427
  // src/server/index.ts
4612
5428
  try {
4613
5429
  __require("server-only");
@@ -4621,6 +5437,8 @@ export {
4621
5437
  DirectoryExistsError,
4622
5438
  DirectoryNotEmptyError,
4623
5439
  DirectoryNotFoundError,
5440
+ DropboxAuth,
5441
+ DropboxModule,
4624
5442
  FileExistsError,
4625
5443
  FileManager,
4626
5444
  FileMetadataService,
@@ -4630,6 +5448,7 @@ export {
4630
5448
  GoogleDriveModule,
4631
5449
  HAZO_FILES_DEFAULT_TABLE_NAME,
4632
5450
  HAZO_FILES_MIGRATION_V2,
5451
+ HAZO_FILES_MIGRATION_V3,
4633
5452
  HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
4634
5453
  HAZO_FILES_NAMING_TABLE_SCHEMA,
4635
5454
  HAZO_FILES_TABLE_SCHEMA,
@@ -4657,6 +5476,8 @@ export {
4657
5476
  computeFileInfo,
4658
5477
  createAndInitializeModule,
4659
5478
  createBasicFileManager,
5479
+ createDropboxAuth,
5480
+ createDropboxModule,
4660
5481
  createEmptyFileDataStructure,
4661
5482
  createEmptyNamingRuleSchema,
4662
5483
  createFileItem,
@@ -4701,6 +5522,7 @@ export {
4701
5522
  getFileMetadataValues,
4702
5523
  getMergedData,
4703
5524
  getMigrationForTable,
5525
+ getMigrationV3ForTable,
4704
5526
  getMimeType,
4705
5527
  getNameWithoutExtension,
4706
5528
  getNamingSchemaForTable,
@@ -4733,6 +5555,7 @@ export {
4733
5555
  loadConfig,
4734
5556
  loadConfigAsync,
4735
5557
  migrateToV2,
5558
+ migrateToV3,
4736
5559
  normalizePath,
4737
5560
  parseConfig,
4738
5561
  parseFileData,